From 801800511d0eaedc99f7d4935aa1c3bff5c3da54 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 7 Aug 2026 19:02:08 +0530 Subject: [PATCH 01/55] feat: added call-imports metadata --- .../components/callImports/AuditMetaChips.tsx | 175 +++ .../pages/callImports/CallImportDetail.tsx | 30 +- .../CallImportEvaluationDetail.tsx | 23 +- .../src/pages/callImports/CallImports.tsx | 1128 +++++++++-------- 4 files changed, 773 insertions(+), 583 deletions(-) create mode 100644 frontend/src/components/callImports/AuditMetaChips.tsx diff --git a/frontend/src/components/callImports/AuditMetaChips.tsx b/frontend/src/components/callImports/AuditMetaChips.tsx new file mode 100644 index 00000000..a7bae2e4 --- /dev/null +++ b/frontend/src/components/callImports/AuditMetaChips.tsx @@ -0,0 +1,175 @@ +import type { ReactNode } from 'react' + +/** Bordered metadata chips for call-import audit fields. */ + +const CHIP_CLASS = + 'inline-flex flex-col min-w-0 rounded-md border border-slate-200 bg-white px-2 py-1 text-[11px] leading-tight shadow-sm' + +export function formatMetaDateTime(iso: string | null | undefined): string { + if (!iso) return '—' + const parsed = new Date(iso) + if (Number.isNaN(parsed.getTime())) return '—' + return parsed.toLocaleString() +} + +type AuditMetaChipProps = { + label: string + value: string | null | undefined + wide?: boolean + /** Tighter chip for horizontal list rows */ + dense?: boolean + /** Fill a grid cell (2×2 activity column on list page) */ + stacked?: boolean + className?: string +} + +export function AuditMetaChip({ + label, + value, + wide, + dense, + stacked, + className = '', +}: AuditMetaChipProps) { + const display = value?.trim() || '—' + const sizeClass = dense + ? stacked + ? 'w-full min-w-0 px-1.5 py-0.5 text-[10px]' + : 'shrink-0 px-1.5 py-0.5 text-[10px] min-w-[4.75rem] max-w-[8.5rem]' + : wide + ? 'min-w-[7.5rem] max-w-[12rem]' + : 'min-w-[6.5rem] max-w-[10rem]' + return ( +
+ {label} + {display} +
+ ) +} + +type AuditMetaChipRowProps = { + className?: string + wide?: boolean + compact?: boolean + children: ReactNode +} + +function AuditMetaChipRow({ + className = '', + wide, + compact, + children, +}: AuditMetaChipRowProps) { + return ( +
+ {children} +
+ ) +} + +type CallImportAuditMetaProps = { + createdAt: string | null | undefined + updatedAt: string | null | undefined + createdByEmail?: string | null + lastUpdatedByEmail?: string | null + className?: string + wide?: boolean + compact?: boolean + /** 2×2 grid for table cells (no horizontal scroll) */ + stacked?: boolean +} + +/** Created / updated timestamps + actor emails for a call-import batch. */ +export function CallImportAuditMeta({ + createdAt, + updatedAt, + createdByEmail, + lastUpdatedByEmail, + className, + wide = true, + compact, + stacked, +}: CallImportAuditMetaProps) { + const chipCommon = { wide, dense: compact, stacked } + if (stacked) { + return ( +
+ + + + +
+ ) + } + return ( + + + + + + + ) +} + +type EvaluationAuditMetaProps = { + createdAt: string | null | undefined + updatedAt?: string | null | undefined + startedAt?: string | null + finishedAt?: string | null + /** User who started this evaluation run (API: created_by_email). */ + runByEmail?: string | null + /** @deprecated Use runByEmail */ + createdByEmail?: string | null + lastUpdatedByEmail?: string | null + formatDate?: (iso: string | null | undefined) => string + className?: string + wide?: boolean + compact?: boolean + showRunTimes?: boolean +} + +/** Evaluation run metadata (detail header or list card). */ +export function EvaluationAuditMeta({ + createdAt, + updatedAt, + startedAt, + finishedAt, + runByEmail, + createdByEmail, + lastUpdatedByEmail, + formatDate = formatMetaDateTime, + className, + wide = true, + compact, + showRunTimes = true, +}: EvaluationAuditMetaProps) { + const runner = runByEmail ?? createdByEmail + return ( + + + + + {updatedAt ? ( + + ) : null} + {showRunTimes && startedAt ? ( + + ) : null} + {showRunTimes && finishedAt ? ( + + ) : null} + + ) +} diff --git a/frontend/src/pages/callImports/CallImportDetail.tsx b/frontend/src/pages/callImports/CallImportDetail.tsx index bc8687b8..9270e14e 100644 --- a/frontend/src/pages/callImports/CallImportDetail.tsx +++ b/frontend/src/pages/callImports/CallImportDetail.tsx @@ -57,6 +57,10 @@ import Button from '../../components/Button' import ConfirmModal from '../../components/ConfirmModal' import Pagination from '../../components/Pagination' import StatusBadge from '../../components/shared/StatusBadge' +import { + CallImportAuditMeta, + EvaluationAuditMeta, +} from '../../components/callImports/AuditMetaChips' import DiariseStatusPill from '../../components/callImports/DiariseStatusPill' import ProviderModelPicker, { type ProviderModelValue, @@ -1680,12 +1684,12 @@ export default function CallImportDetail() { )} - - Created: {new Date(data.created_at).toLocaleString()} - - - Updated: {new Date(data.updated_at).toLocaleString()} - + @@ -3183,13 +3187,19 @@ export default function CallImportDetail() { to={`/call-imports/${id}/evaluations/${evaluation.id}`} className="flex-1 min-w-0 flex items-center justify-between gap-3" > -
+

{headerLabel}

-

- Created {new Date(evaluation.created_at).toLocaleString()} -

+
diff --git a/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx b/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx index 542fdd6c..0c42f91a 100644 --- a/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx +++ b/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx @@ -92,6 +92,7 @@ import ProviderModelPicker, { } from '../../components/providers/ProviderModelPicker' import { getActiveWorkspaceId, useWorkspaceStore } from '../../store/workspaceStore' import StatusBadge from '../../components/shared/StatusBadge' +import { EvaluationAuditMeta } from '../../components/callImports/AuditMetaChips' import DiariseStatusPill from '../../components/callImports/DiariseStatusPill' import CallImportProgressBar from './components/CallImportProgressBar' import MetricPromptImprovementsPanel from './components/MetricPromptImprovementsPanel' @@ -2499,19 +2500,15 @@ export default function CallImportEvaluationDetail() { ? 'Evaluated on Diarised transcript' : 'Evaluated on Production transcript'} - - Created: {formatDateTime(evaluation.created_at)} - - {evaluation.started_at && ( - - Started: {formatDateTime(evaluation.started_at)} - - )} - {evaluation.finished_at && ( - - Finished: {formatDateTime(evaluation.finished_at)} - - )} +
diff --git a/frontend/src/pages/callImports/CallImports.tsx b/frontend/src/pages/callImports/CallImports.tsx index c9ccaf26..b3d6295a 100644 --- a/frontend/src/pages/callImports/CallImports.tsx +++ b/frontend/src/pages/callImports/CallImports.tsx @@ -1,560 +1,568 @@ -import { useMemo, useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { - ChevronLeft, - ChevronRight, - FileAudio, - FileSpreadsheet, - Layers, - Phone, - RefreshCw, - Trash2, - Upload, -} from 'lucide-react' -import { Tag as TagIcon } from 'lucide-react' -import { apiClient } from '../../lib/api' -import { getApiErrorMessage } from '../../lib/apiErrors' -import { useToast } from '../../hooks/useToast' -import { useWorkspaceStore } from '../../store/workspaceStore' -import type { CallImport, CallImportStatus, CallImportTag } from '../../types/api' -import Button from '../../components/Button' -import ConfirmModal from '../../components/ConfirmModal' -import StatusBadge from '../../components/shared/StatusBadge' -import CallImportProgressBar from './components/CallImportProgressBar' -import UploadAudioModal from './components/UploadAudioModal' -import UploadCsvModal from './components/UploadCsvModal' - -const PAGE_SIZE = 20 - -const STATUS_OPTIONS: Array<{ label: string; value: '' | CallImportStatus }> = [ - { label: 'All statuses', value: '' }, - { label: 'Uploaded', value: 'uploaded' }, - { label: 'Mapped', value: 'mapped' }, - { label: 'Pending', value: 'pending' }, - { label: 'Processing', value: 'processing' }, - { label: 'Completed', value: 'completed' }, - { label: 'Partial', value: 'partial' }, - { label: 'Failed', value: 'failed' }, - { label: 'Deleting', value: 'deleting' }, -] - -type UploadTab = 'datasets' | 'audio' - -export default function CallImports() { - const navigate = useNavigate() - const queryClient = useQueryClient() - const { showToast, ToastContainer } = useToast() - // Active workspace is part of every workspace-scoped queryKey so a - // workspace switch produces a clean cache miss instead of leaking - // rows from the previously-active workspace. - const activeWorkspaceId = useWorkspaceStore((s) => s.activeWorkspaceId) - const [page, setPage] = useState(1) - const [statusFilter, setStatusFilter] = useState<'' | CallImportStatus>('') - const [datasetFilter, setDatasetFilter] = useState('') - const [tagFilter, setTagFilter] = useState([]) - const [activeTab, setActiveTab] = useState('datasets') - const [showUpload, setShowUpload] = useState(false) - const [showAudioUpload, setShowAudioUpload] = useState(false) - const [pendingDelete, setPendingDelete] = useState(null) - const [deleteError, setDeleteError] = useState(null) - - const { data: datasets = [] } = useQuery({ - queryKey: ['call-import-datasets', activeWorkspaceId], - queryFn: () => apiClient.listCallImportDatasets(), - }) - - const { data: allTags = [] } = useQuery({ - queryKey: ['call-import-tags', activeWorkspaceId], - queryFn: () => apiClient.listCallImportTags(), - }) - - const deleteMutation = useMutation({ - mutationFn: (id: string) => apiClient.deleteCallImport(id), - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey: ['call-imports'] }) - setPendingDelete(null) - setDeleteError(null) - if (result.status === 'accepted') { - showToast( - 'Deletion started — large imports may take a minute.', - 'success', - ) - } - }, - onError: (err: unknown) => { - const message = getApiErrorMessage(err, 'Failed to delete import.') - setDeleteError(message) - showToast(message, 'error') - }, - }) - - const queryParams = useMemo( - () => ({ - page, - page_size: PAGE_SIZE, - ...(statusFilter ? { status: statusFilter } : {}), - ...(datasetFilter ? { dataset: datasetFilter } : {}), - ...(tagFilter.length > 0 ? { tag_id: tagFilter } : {}), - source_format: activeTab === 'audio' ? 'audio' : '__non_audio__', - }), - [page, statusFilter, datasetFilter, tagFilter, activeTab], - ) - - const { data, isLoading, isFetching, refetch } = useQuery({ - queryKey: ['call-imports', activeWorkspaceId, queryParams], - queryFn: () => apiClient.listCallImports(queryParams), - refetchInterval: (query) => { - const items = query.state.data?.items ?? [] - const hasActive = items.some( - (i: CallImport) => i.status === 'pending' || i.status === 'processing', - ) - const hasDeleting = items.some( - (i: CallImport) => i.status === 'deleting', - ) - if (hasDeleting) return 3000 - return hasActive ? 5000 : false - }, - }) - - const items = data?.items ?? [] - const total = data?.total ?? 0 - const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) - - return ( -
- -
-
-

Call Imports

-

- Upload datasets from CSV / Excel, or add manual call recordings - directly and use the same diarisation and evaluation tools. -

-
-
- - - - - - - - -
-
- -
- - -
- - {/* - High-level dataset segregation lives at the top of the page so users can - scope all filtering/searching that follows to a specific dataset. We - intentionally render this above the main card to make it visually - distinct from the in-card status/tag filters. - */} -
- - - {datasetFilter && ( - - Showing imports tagged with dataset “{datasetFilter}”. - - )} -
- -
-
-
-
- - -
- {allTags.length > 0 && ( -
- Tags: - {allTags.map((tag: CallImportTag) => { - const active = tagFilter.includes(tag.id) - return ( - - ) - })} - {tagFilter.length > 0 && ( - - )} -
- )} -
-

- {total} {activeTab === 'audio' ? 'manual upload' : 'dataset import'} - {total === 1 ? '' : 's'} -

-
- - {isLoading ? ( -
- -

Loading imports...

-
- ) : items.length === 0 ? ( -
- -

- {statusFilter - ? 'No imports match this filter.' - : activeTab === 'audio' - ? 'No manual audio uploads yet.' - : 'No dataset uploads yet.'} -

- {!statusFilter && ( - - )} -
- ) : ( -
- - - - - - - - - - - - - - {items.map((item: CallImport) => { - const isDeleting = item.status === 'deleting' - return ( - { - if (isDeleting) return - navigate(`/call-imports/${item.id}`) - }} - > - - - - - - - - - ) - })} - -
- Filename - - Provider - - Dataset / Tags - - Progress - - Status - - Created - - Actions -
-
- {item.original_filename || '(unnamed)'} -
-
- {item.id.slice(0, 8)} -
-
- {item.source_format === 'audio' ? ( - - - Manual upload - - ) : item.provider || ( - - — - - )} - -
- {item.dataset ? ( - - {item.dataset} - - ) : ( - - no dataset - - )} - {item.tags.length > 0 && ( -
- {item.tags.map((tag) => ( - - {tag.name} - - ))} -
- )} -
-
- - - - - {new Date(item.created_at).toLocaleString()} - e.stopPropagation()} - > -
- - View - - -
-
- - {totalPages > 1 && ( -
-

- Page {page} of {totalPages} -

-
- - -
-
- )} -
- )} -
- - setShowUpload(false)} /> - setShowAudioUpload(false)} - /> - - { - if (!pendingDelete) return '' - const name = pendingDelete.original_filename || '(unnamed)' - const total = pendingDelete.total_rows - const completed = pendingDelete.completed_rows - const inFlight = - pendingDelete.status === 'pending' || - pendingDelete.status === 'processing' || - pendingDelete.status === 'deleting' - const lines = [ - `“${name}” will be permanently deleted, along with all ${total} row record${total === 1 ? '' : 's'} and ${completed} stored recording${completed === 1 ? '' : 's'} in S3.`, - inFlight - ? 'This batch is still processing — pending tasks will be revoked before deletion.' - : '', - 'This cannot be undone.', - deleteError ? `Error: ${deleteError}` : '', - ] - return lines.filter(Boolean).join('\n\n') - })()} - confirmLabel="Delete" - cancelLabel="Cancel" - variant="danger" - isLoading={deleteMutation.isPending} - onConfirm={() => { - if (pendingDelete) deleteMutation.mutate(pendingDelete.id) - }} - onCancel={() => { - if (deleteMutation.isPending) return - setPendingDelete(null) - setDeleteError(null) - }} - /> -
- ) -} +import { useMemo, useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + ChevronLeft, + ChevronRight, + FileAudio, + FileSpreadsheet, + Layers, + Phone, + RefreshCw, + Trash2, + Upload, +} from 'lucide-react' +import { Tag as TagIcon } from 'lucide-react' +import { apiClient } from '../../lib/api' +import { getApiErrorMessage } from '../../lib/apiErrors' +import { useToast } from '../../hooks/useToast' +import { useWorkspaceStore } from '../../store/workspaceStore' +import type { CallImport, CallImportStatus, CallImportTag } from '../../types/api' +import Button from '../../components/Button' +import ConfirmModal from '../../components/ConfirmModal' +import StatusBadge from '../../components/shared/StatusBadge' +import { CallImportAuditMeta } from '../../components/callImports/AuditMetaChips' +import CallImportProgressBar from './components/CallImportProgressBar' +import UploadAudioModal from './components/UploadAudioModal' +import UploadCsvModal from './components/UploadCsvModal' + +const PAGE_SIZE = 20 + +const STATUS_OPTIONS: Array<{ label: string; value: '' | CallImportStatus }> = [ + { label: 'All statuses', value: '' }, + { label: 'Uploaded', value: 'uploaded' }, + { label: 'Mapped', value: 'mapped' }, + { label: 'Pending', value: 'pending' }, + { label: 'Processing', value: 'processing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Partial', value: 'partial' }, + { label: 'Failed', value: 'failed' }, + { label: 'Deleting', value: 'deleting' }, +] + +type UploadTab = 'datasets' | 'audio' + +export default function CallImports() { + const navigate = useNavigate() + const queryClient = useQueryClient() + const { showToast, ToastContainer } = useToast() + // Active workspace is part of every workspace-scoped queryKey so a + // workspace switch produces a clean cache miss instead of leaking + // rows from the previously-active workspace. + const activeWorkspaceId = useWorkspaceStore((s) => s.activeWorkspaceId) + const [page, setPage] = useState(1) + const [statusFilter, setStatusFilter] = useState<'' | CallImportStatus>('') + const [datasetFilter, setDatasetFilter] = useState('') + const [tagFilter, setTagFilter] = useState([]) + const [activeTab, setActiveTab] = useState('datasets') + const [showUpload, setShowUpload] = useState(false) + const [showAudioUpload, setShowAudioUpload] = useState(false) + const [pendingDelete, setPendingDelete] = useState(null) + const [deleteError, setDeleteError] = useState(null) + + const { data: datasets = [] } = useQuery({ + queryKey: ['call-import-datasets', activeWorkspaceId], + queryFn: () => apiClient.listCallImportDatasets(), + }) + + const { data: allTags = [] } = useQuery({ + queryKey: ['call-import-tags', activeWorkspaceId], + queryFn: () => apiClient.listCallImportTags(), + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => apiClient.deleteCallImport(id), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['call-imports'] }) + setPendingDelete(null) + setDeleteError(null) + if (result.status === 'accepted') { + showToast( + 'Deletion started — large imports may take a minute.', + 'success', + ) + } + }, + onError: (err: unknown) => { + const message = getApiErrorMessage(err, 'Failed to delete import.') + setDeleteError(message) + showToast(message, 'error') + }, + }) + + const queryParams = useMemo( + () => ({ + page, + page_size: PAGE_SIZE, + ...(statusFilter ? { status: statusFilter } : {}), + ...(datasetFilter ? { dataset: datasetFilter } : {}), + ...(tagFilter.length > 0 ? { tag_id: tagFilter } : {}), + source_format: activeTab === 'audio' ? 'audio' : '__non_audio__', + }), + [page, statusFilter, datasetFilter, tagFilter, activeTab], + ) + + const { data, isLoading, isFetching, refetch } = useQuery({ + queryKey: ['call-imports', activeWorkspaceId, queryParams], + queryFn: () => apiClient.listCallImports(queryParams), + refetchInterval: (query) => { + const items = query.state.data?.items ?? [] + const hasActive = items.some( + (i: CallImport) => i.status === 'pending' || i.status === 'processing', + ) + const hasDeleting = items.some( + (i: CallImport) => i.status === 'deleting', + ) + if (hasDeleting) return 3000 + return hasActive ? 5000 : false + }, + }) + + const items = data?.items ?? [] + const total = data?.total ?? 0 + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) + + return ( +
+ +
+
+

Call Imports

+

+ Upload datasets from CSV / Excel, or add manual call recordings + directly and use the same diarisation and evaluation tools. +

+
+
+ + + + + + + + +
+
+ +
+ + +
+ + {/* + High-level dataset segregation lives at the top of the page so users can + scope all filtering/searching that follows to a specific dataset. We + intentionally render this above the main card to make it visually + distinct from the in-card status/tag filters. + */} +
+ + + {datasetFilter && ( + + Showing imports tagged with dataset “{datasetFilter}”. + + )} +
+ +
+
+
+
+ + +
+ {allTags.length > 0 && ( +
+ Tags: + {allTags.map((tag: CallImportTag) => { + const active = tagFilter.includes(tag.id) + return ( + + ) + })} + {tagFilter.length > 0 && ( + + )} +
+ )} +
+

+ {total} {activeTab === 'audio' ? 'manual upload' : 'dataset import'} + {total === 1 ? '' : 's'} +

+
+ + {isLoading ? ( +
+ +

Loading imports...

+
+ ) : items.length === 0 ? ( +
+ +

+ {statusFilter + ? 'No imports match this filter.' + : activeTab === 'audio' + ? 'No manual audio uploads yet.' + : 'No dataset uploads yet.'} +

+ {!statusFilter && ( + + )} +
+ ) : ( +
+ + + + + + + + + + + + + + {items.map((item: CallImport) => { + const isDeleting = item.status === 'deleting' + return ( + { + if (isDeleting) return + navigate(`/call-imports/${item.id}`) + }} + > + + + + + + + + + ) + })} + +
+ Filename + + Provider + + Dataset / Tags + + Progress + + Status + + Activity + + Actions +
+
+ {item.original_filename || '(unnamed)'} +
+
+ {item.id.slice(0, 8)} +
+
+ {item.source_format === 'audio' ? ( + + + Manual upload + + ) : item.provider || ( + + — + + )} + +
+ {item.dataset ? ( + + {item.dataset} + + ) : ( + + no dataset + + )} + {item.tags.length > 0 && ( +
+ {item.tags.map((tag) => ( + + {tag.name} + + ))} +
+ )} +
+
+ + + + + + e.stopPropagation()} + > +
+ + View + + +
+
+ + {totalPages > 1 && ( +
+

+ Page {page} of {totalPages} +

+
+ + +
+
+ )} +
+ )} +
+ + setShowUpload(false)} /> + setShowAudioUpload(false)} + /> + + { + if (!pendingDelete) return '' + const name = pendingDelete.original_filename || '(unnamed)' + const total = pendingDelete.total_rows + const completed = pendingDelete.completed_rows + const inFlight = + pendingDelete.status === 'pending' || + pendingDelete.status === 'processing' || + pendingDelete.status === 'deleting' + const lines = [ + `“${name}” will be permanently deleted, along with all ${total} row record${total === 1 ? '' : 's'} and ${completed} stored recording${completed === 1 ? '' : 's'} in S3.`, + inFlight + ? 'This batch is still processing — pending tasks will be revoked before deletion.' + : '', + 'This cannot be undone.', + deleteError ? `Error: ${deleteError}` : '', + ] + return lines.filter(Boolean).join('\n\n') + })()} + confirmLabel="Delete" + cancelLabel="Cancel" + variant="danger" + isLoading={deleteMutation.isPending} + onConfirm={() => { + if (pendingDelete) deleteMutation.mutate(pendingDelete.id) + }} + onCancel={() => { + if (deleteMutation.isPending) return + setPendingDelete(null) + setDeleteError(null) + }} + /> +
+ ) +} From 6d46894fd54222537ff8314df962a91223ba6705 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 7 Aug 2026 19:14:52 +0530 Subject: [PATCH 02/55] feat: added import-calls user metadata --- app/api/v1/routes/call_import_evaluations.py | 17804 ++++++++-------- app/api/v1/routes/call_imports.py | 7963 +++---- app/config.py | 2 +- app/migrations/059_call_import_audit_users.py | 69 + app/models/database.py | 5464 ++--- app/models/schemas.py | 4 + app/services/call_imports/audit.py | 99 + env.example | 2 +- frontend/src/types/api.ts | 4268 ++-- tests/test_api/test_call_import_audit.py | 78 + .../test_api/test_call_import_evaluations.py | 40 + 11 files changed, 18106 insertions(+), 17687 deletions(-) create mode 100644 app/migrations/059_call_import_audit_users.py create mode 100644 app/services/call_imports/audit.py create mode 100644 tests/test_api/test_call_import_audit.py diff --git a/app/api/v1/routes/call_import_evaluations.py b/app/api/v1/routes/call_import_evaluations.py index f73744ad..95222d6d 100644 --- a/app/api/v1/routes/call_import_evaluations.py +++ b/app/api/v1/routes/call_import_evaluations.py @@ -1,8877 +1,8927 @@ -"""Evaluation routes scoped to a Call Import batch.""" - -from __future__ import annotations - -import asyncio -import csv -import base64 -import io -import json -import math -import re -import statistics -from typing import Any, Dict, Iterator, List, Literal, Optional, Set, Tuple -from uuid import UUID - -from datetime import date, datetime, timedelta, timezone - -from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Query, Response, status -from fastapi.responses import StreamingResponse -from loguru import logger -from pydantic import BaseModel, Field, field_validator -from sqlalchemy import desc, func, or_, text -from sqlalchemy.orm import Session -from sqlalchemy.orm.attributes import flag_modified - -from app.core.auth import Principal, get_principal -from app.core.auth.capabilities import REPORTS_GENERATE, capability_denied_message -from app.database import get_db -from app.dependencies import ( - get_api_key, - get_organization_id, - get_workspace_id, - require_enterprise_feature, -) -from app.services.workspace_rbac import resolve_workspace_capabilities -from app.models.database import ( - AIProvider, - CallImport, - CallImportEvaluation, - CallImportEvaluationReportSnapshot, - CallImportEvaluationRow, - CallImportRow, - Metric, - PromptPartial, - Workspace, -) -from app.models.enums import CallImportRowStatus, ModelProvider -from app.models.schemas import ( - CallImportEvaluationAggregateResponse, - CallImportEvaluationBulkDelete, - CallImportEvaluationBulkActionResponse, - CallImportEvaluationCreate, - CallImportEvaluationListResponse, - CallImportEvaluationResponse, - CallImportEvaluationRetryRequest, - CallImportEvaluationRetryResponse, - CallImportEvaluationRetrySkippedItem, - CallImportEvaluationRowListResponse, - CallImportEvaluationRowResponse, - CallImportEvaluationUpdate, - CallImportMetricAggregate, - CallImportMetricHistogramBucket, - CallImportMetricLabelPair, - CallImportMetricSummary, - CallImportMetricValueCount, - DiscoveredLabelDeleteRequest, - DiscoveredLabelItem, - DiscoveredLabelMergeRequest, - DiscoveredLabelsResponse, - DiscoveredMetricDeleteRequest, - DiscoveredMetricItem, - DiscoveredMetricMergeRequest, - DiscoveredMetricsResponse, - EvaluationInsightsRequest, - EvaluationTldrSummary, - EvaluationMetricClustersRequest, - EvaluationMetricClustersState, - EvaluationPromptImprovementsRequest, - EvaluationPromptImprovementsState, - MetricFailurePoliciesResponse, - MetricFailurePoliciesSaveRequest, - MetricFailurePolicy, - MetricClusterEligibleRow, - MetricClusterEligibleRowsResponse, - EvaluationUserInsightsRequest, - EvaluationUserInsightsState, - MetricFlowEdge, - MetricPeriodDelta, - MetricFlowNode, - MetricFlowResponse, -) -from app.services.reporting.call_import_evaluation_pdf_report import ( - call_import_evaluation_pdf_report_service, -) -from app.services.call_import_metric_clusters import ( - METRIC_CLUSTERS_CANCELLED_BY_USER_ERROR, - estimate_metric_clusters_llm_calls, - filter_completed_row_pairs, - list_eligible_cluster_rows, - metric_clusters_raw_is_cancelled, - metric_clusters_state_from_raw, - metric_clusters_state_to_db, -) -from app.services.metric_failure_policy import ( - aggregate_primary_percent, - build_failure_policy_previews, - effective_policies, - failure_rate_percent_from_rows, - failure_policies_to_db, - has_clusterable_metrics, - merge_clustering_policies, - merge_failure_policies_into_raw, - policies_from_evaluation_raw, - validate_failure_policies_for_metrics, -) -from app.services.call_import_user_insights import ( - normalize_max_llm_calls, - total_llm_calls_for_rows, - user_insights_state_from_raw, -) - -router = APIRouter( - prefix="/call-imports/{call_import_id}/evaluations", - tags=["Call Import Evaluations"], - dependencies=[Depends(require_enterprise_feature("call_imports"))], -) - - -class CallImportEvaluationPdfReportRequest(BaseModel): - vendor_name: str = Field(..., min_length=1, max_length=120) - report_type: Literal["external", "internal"] = "external" - include_weekly_delta: bool = False - include_period_delta: bool = False - baseline_evaluation_id: Optional[str] = None - period_label: Optional[str] = Field(default=None, max_length=64) - use_case: Optional[str] = Field(default=None, max_length=120) - internal_brand_image_id: Optional[str] = None - external_brand_image_id: Optional[str] = None - report_config: Dict[str, Any] = Field(default_factory=dict) - platform_base_url: Optional[str] = Field( - default=None, - max_length=512, - description="Frontend origin for deep links to example calls in internal PDFs.", - ) - - @field_validator("vendor_name") - @classmethod - def _clean_vendor_name(cls, value: str) -> str: - cleaned = value.strip() - if not cleaned: - raise ValueError("Vendor name is required.") - return cleaned - - -class CallImportEvaluationBaselineCandidate(BaseModel): - evaluation_id: str - name: str - dataset: str - period_label: Optional[str] = None - period_start: Optional[date] = None - period_end: Optional[date] = None - period_display: str - completed_rows: int - created_at: datetime - is_default: bool = False - - -class CallImportEvaluationBaselineCandidatesResponse(BaseModel): - items: List[CallImportEvaluationBaselineCandidate] - default_evaluation_id: Optional[str] = None - - -def _require_import( - db: Session, - call_import_id: UUID, - organization_id: UUID, -) -> CallImport: - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException(status_code=404, detail="Call import not found") - return call_import - - -def require_call_import_capability(capability: str): - """Ensure the caller has *capability* in the call import's workspace (not just the header).""" - - def _dep( - call_import_id: UUID, - principal: Principal = Depends(get_principal), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), - ) -> CallImport: - call_import = _require_import(db, call_import_id, organization_id) - caps, _, role = resolve_workspace_capabilities( - db, - principal=principal, - workspace_id=call_import.workspace_id, - organization_id=organization_id, - ) - if capability not in caps: - raise HTTPException( - status_code=403, - detail=capability_denied_message( - capability, - role_name=role.name if role else None, - workspace_label="the active workspace", - ), - ) - return call_import - - return _dep - - -def _flatten_transcript(text: Optional[str]) -> str: - """Collapse a multi-line transcript onto a single line for spreadsheet export. - - The diarised transcript is stored as ``: `` lines joined - by ``\\n`` because the in-app ``TranscriptView`` parses those line - breaks to render chat bubbles. In Excel / Google Sheets that same - newline-per-turn formatting causes each cell to balloon vertically, - which the user reads as "lots of empty space on top of the cell". - Flattening at export time keeps the DB shape intact while giving the - spreadsheet a single-line cell per row. - """ - if not text: - return "" - parts = [ - segment.strip() - for segment in text.replace("\r\n", "\n").replace("\r", "\n").split("\n") - ] - return " ".join(p for p in parts if p) - - -def _evaluated_transcript_source_label( - evaluation: CallImportEvaluation, - source_row: CallImportRow, -) -> str: - """Label which transcript source this row was scored against.""" - source = (evaluation.transcript_source or "diarised").strip().lower() - if source == "production": - if not (source_row.transcript or "").strip(): - return "" - return "Production" - if not (source_row.diarised_transcript or "").strip(): - return "" - return "Diarised" - - -def _pick_evaluation_row_transcript( - source_row: Optional[CallImportRow], - evaluation: Optional[CallImportEvaluation] = None, -) -> Optional[str]: - """Transcript shown in evaluation row detail for the run's source.""" - if source_row is None: - return None - source = ( - (evaluation.transcript_source or "diarised").strip().lower() - if evaluation is not None - else "diarised" - ) - if source == "production": - raw = (source_row.transcript or "").strip() - return raw or None - diarised = (source_row.diarised_transcript or "").strip() - if diarised: - return diarised - raw = (source_row.transcript or "").strip() - return raw or None - - -def _to_evaluation_row_response( - eval_row_obj: CallImportEvaluationRow, - source_row: Optional[CallImportRow], - evaluation: Optional[CallImportEvaluation] = None, -) -> CallImportEvaluationRowResponse: - """Serialize one evaluation row plus joined source-row metadata.""" - return CallImportEvaluationRowResponse( - id=eval_row_obj.id, - evaluation_id=eval_row_obj.evaluation_id, - call_import_row_id=eval_row_obj.call_import_row_id, - row_index=source_row.row_index if source_row else None, - conversation_id=source_row.conversation_id if source_row else None, - transcript=_pick_evaluation_row_transcript(source_row, evaluation), - raw_columns=source_row.raw_columns if source_row else None, - recording_url=source_row.recording_url if source_row else None, - recording_date=source_row.recording_date if source_row else None, - recording_s3_key=source_row.recording_s3_key if source_row else None, - diarised_transcript_status=( - source_row.diarised_transcript_status if source_row else None - ), - diarised_transcript_error=( - source_row.diarised_transcript_error if source_row else None - ), - status=eval_row_obj.status, - metric_scores=eval_row_obj.metric_scores or {}, - error_message=eval_row_obj.error_message, - started_at=eval_row_obj.started_at, - finished_at=eval_row_obj.finished_at, - created_at=eval_row_obj.created_at, - updated_at=eval_row_obj.updated_at, - ) - - -def _serialize_selected_metric_ids(value) -> List[UUID]: - result: List[UUID] = [] - if not isinstance(value, list): - return result - for item in value: - try: - result.append(UUID(str(item))) - except (TypeError, ValueError): - continue - return result - - -def _metrics_for_ids(db: Session, org_id: UUID, ids: List[UUID]) -> List[Metric]: - if not ids: - return [] - rows = ( - db.query(Metric) - .filter( - Metric.organization_id == org_id, - Metric.id.in_(ids), - ) - .all() - ) - by_id = {row.id: row for row in rows} - return [by_id[mid] for mid in ids if mid in by_id] - - -def _expand_metric_selection( - db: Session, - org_id: UUID, - selected_ids: List[UUID], -) -> Tuple[List[Metric], Dict[UUID, List[Metric]]]: - """Resolve user-supplied metric ids into actual leaves + parent grouping. - - Rules: - * If a parent id is in ``selected_ids`` and no specific children of - that parent are also listed, include EVERY enabled child of that - parent. - * If a parent id AND some of its children are listed, include only - the listed children (treat the parent selection as the - "container" so users can deselect labels). - * Standalone metrics (no parent, no children) pass through - unchanged. - * Disabled metrics are filtered out at this layer so the caller - doesn't have to repeat the check. - - Returns: - (effective_metrics, parent_to_children) - - ``effective_metrics`` is the deduplicated list of metrics the - worker will actually score (children + standalone). Order is - preserved from ``selected_ids`` for display stability. - - ``parent_to_children`` maps each parent metric id (UUID) to the - list of its selected children. Useful for grouping in the LLM - prompt builder. - """ - if not selected_ids: - return [], {} - - requested = list(selected_ids) - initial_rows = ( - db.query(Metric) - .filter( - Metric.organization_id == org_id, - Metric.id.in_(requested), - ) - .all() - ) - initial_by_id = {row.id: row for row in initial_rows} - - parent_ids_requested = { - m.id for m in initial_rows if m.selection_mode and not m.parent_metric_id - } - # Map parent id -> children explicitly requested by the user. - explicit_children_by_parent: Dict[UUID, List[Metric]] = {} - for m in initial_rows: - if m.parent_metric_id and m.parent_metric_id in parent_ids_requested: - explicit_children_by_parent.setdefault( - m.parent_metric_id, [] - ).append(m) - - # For parents without explicit children, hydrate every enabled child. - parents_needing_full_expansion = [ - pid - for pid in parent_ids_requested - if pid not in explicit_children_by_parent - ] - auto_expanded_children: Dict[UUID, List[Metric]] = {} - if parents_needing_full_expansion: - for pid in parents_needing_full_expansion: - child_rows = ( - db.query(Metric) - .filter( - Metric.organization_id == org_id, - Metric.parent_metric_id == pid, - Metric.enabled.is_(True), - ) - .order_by(Metric.created_at.asc()) - .all() - ) - auto_expanded_children[pid] = child_rows - - parent_to_children: Dict[UUID, List[Metric]] = {} - for pid in parent_ids_requested: - children = explicit_children_by_parent.get( - pid - ) or auto_expanded_children.get(pid, []) - # Drop disabled children so the worker doesn't waste a slot on - # them. Empty parents (no enabled children) are still tracked - # because the UI may want to show "0 of 0" rather than swallow - # them silently. - parent_to_children[pid] = [c for c in children if c.enabled] - - effective: List[Metric] = [] - seen: set[UUID] = set() - for mid in requested: - m = initial_by_id.get(mid) - if m is None: - continue - if m.selection_mode and not m.parent_metric_id: - # Parent row itself is not scored — only its children. - for child in parent_to_children.get(m.id, []): - if child.id in seen or not child.enabled: - continue - seen.add(child.id) - effective.append(child) - continue - if m.parent_metric_id and m.parent_metric_id in parent_ids_requested: - # Already accounted for via the parent expansion above. - continue - if not m.enabled: - continue - if m.id in seen: - continue - seen.add(m.id) - effective.append(m) - - return effective, parent_to_children - - -def _evaluation_bulk_operation_for_response( - evaluation_id: UUID, -) -> Optional[str]: - from app.services.call_imports.evaluation_bulk_op import ( - get_evaluation_bulk_operation, - ) - - return get_evaluation_bulk_operation(evaluation_id) - - -def _serialize_eval( - db: Session, - row: CallImportEvaluation, - *, - sibling_evaluation_ids: Optional[List[UUID]] = None, -) -> CallImportEvaluationResponse: - selected_ids = _serialize_selected_metric_ids(row.selected_metric_ids) - - # Pull every metric referenced anywhere in the run's grouping (leaves, - # standalone, AND parents from selected_metric_groups) so the UI can - # render parent labels even when only children were materialized into - # selected_metric_ids. - groups_raw: Dict[str, List[str]] = {} - if isinstance(row.selected_metric_groups, dict): - for parent_str, children in row.selected_metric_groups.items(): - if not isinstance(children, list): - continue - cleaned: List[str] = [] - for c in children: - try: - UUID(str(c)) - cleaned.append(str(c)) - except (TypeError, ValueError): - continue - try: - UUID(parent_str) - groups_raw[parent_str] = cleaned - except (TypeError, ValueError): - continue - - metric_ids_for_lookup: List[UUID] = list(selected_ids) - for parent_str in groups_raw.keys(): - try: - pid = UUID(parent_str) - if pid not in metric_ids_for_lookup: - metric_ids_for_lookup.append(pid) - except (TypeError, ValueError): - continue - - metrics = _metrics_for_ids( - db, row.organization_id, metric_ids_for_lookup - ) - - from app.services.call_imports.progress_counters import merge_eval_counters_for_ui - - ui_completed_raw, ui_failed_raw = merge_eval_counters_for_ui(row) - total = int(row.total_rows or 0) - ui_completed = ( - min(ui_completed_raw, total) if total else ui_completed_raw - ) - ui_failed = min(ui_failed_raw, total) if total else ui_failed_raw - - return CallImportEvaluationResponse( - id=row.id, - call_import_id=row.call_import_id, - organization_id=row.organization_id, - name=row.name, - selected_metric_ids=selected_ids, - selected_metric_groups=groups_raw or None, - metrics=[ - CallImportMetricSummary( - id=metric.id, - name=metric.name, - metric_type=metric.metric_type, - description=metric.description, - parent_metric_id=metric.parent_metric_id, - selection_mode=metric.selection_mode, - # Required by the Flow tab to know whether a parent - # opted into discovery; without it the - # DiscoveredLabelsPanel stays hidden even when the - # worker is actively producing discovered_labels. - allow_discovery=bool( - getattr(metric, "allow_discovery", False) - ), - ) - for metric in metrics - ], - status=row.status, - total_rows=row.total_rows, - completed_rows=ui_completed, - failed_rows=ui_failed, - error_message=row.error_message, - llm_provider=row.llm_provider, - llm_model=row.llm_model, - llm_credential_id=row.llm_credential_id, - llm_config=( - row.llm_config if isinstance(getattr(row, "llm_config", None), dict) else None - ), - metric_llm_overrides=( - row.metric_llm_overrides - if isinstance(row.metric_llm_overrides, dict) - else None - ), - stt_provider=row.stt_provider, - stt_model=row.stt_model, - stt_credential_id=row.stt_credential_id, - diarisation_llm_provider=getattr(row, "diarisation_llm_provider", None), - diarisation_llm_model=getattr(row, "diarisation_llm_model", None), - diarisation_llm_credential_id=getattr( - row, "diarisation_llm_credential_id", None - ), - diarisation_prompt=getattr(row, "diarisation_prompt", None), - transcribe_mode=( - (getattr(row, "transcribe_mode", None) or "stt_llm") - ), - transcript_source=(row.transcript_source or "diarised"), - sibling_evaluation_ids=list(sibling_evaluation_ids or []), - started_at=row.started_at, - finished_at=row.finished_at, - created_at=row.created_at, - updated_at=row.updated_at, - tldr_summary=_tldr_summary_payload(row), - user_insights=_user_insights_payload(row), - metric_clusters=_metric_clusters_payload(row), - discover_new_metrics=bool( - getattr(row, "discover_new_metrics", False) - ), - bulk_operation=_evaluation_bulk_operation_for_response(row.id), - ) - - -def _normalize_name(value: Optional[str]) -> Optional[str]: - """Trim user-supplied name; empty string becomes ``NULL``.""" - if value is None: - return None - trimmed = value.strip() - return trimmed or None - - -def _rollup_evaluation_status(evaluation: CallImportEvaluation, db: Session) -> None: - """Recompute counters + terminal status after rows are added/removed. - - Uses a single aggregate query instead of loading every row status. - """ - from app.workers.tasks.evaluate_call_import_row_core import ( - _apply_parent_status_from_counters, - reconcile_evaluation_counters, - ) - - reconcile_evaluation_counters(db, evaluation) - _apply_parent_status_from_counters(evaluation) - db.flush() - - if evaluation.status in {"completed", "failed", "partial"}: - from app.models.database import CallImport - from app.services.call_imports.bulk_ops import rollup_call_import_batch_status - - call_import = ( - db.query(CallImport) - .filter(CallImport.id == evaluation.call_import_id) - .first() - ) - if call_import is not None: - rollup_call_import_batch_status(db, call_import) - - -@router.post( - "", - response_model=CallImportEvaluationResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="createCallImportEvaluation", -) -async def create_call_import_evaluation( - call_import_id: UUID, - payload: CallImportEvaluationCreate, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationResponse: - del api_key - call_import = _require_import(db, call_import_id, organization_id) - - metric_ids = payload.metric_ids - if not metric_ids: - raise HTTPException( - status_code=400, - detail="Select at least one metric to run the evaluation against.", - ) - - org_metrics = ( - db.query(Metric) - .filter( - Metric.organization_id == organization_id, - Metric.id.in_(metric_ids), - ) - .all() - ) - by_id = {metric.id: metric for metric in org_metrics} - unknown_ids = [mid for mid in metric_ids if mid not in by_id] - if unknown_ids: - raise HTTPException( - status_code=400, - detail=( - "These metric ids do not exist in your organization: " - f"{', '.join(str(mid) for mid in unknown_ids)}. " - "Refresh the metrics list and try again." - ), - ) - # Parents themselves are containers, not scored rows, so a disabled - # parent shouldn't block the run as long as it has enabled children. - # We only reject disabled rows that the worker will actually try to - # evaluate (children + standalone leaves). - disabled_leaves = [ - metric - for metric in org_metrics - if not metric.enabled - and not (metric.selection_mode and not metric.parent_metric_id) - ] - if disabled_leaves: - names = ", ".join(metric.name for metric in disabled_leaves) - raise HTTPException( - status_code=400, - detail=( - f"These metrics are disabled and cannot be evaluated: {names}. " - "Enable them on the Metrics page (or pick different ones) and " - "try again." - ), - ) - - # Expand hierarchical selection: parents auto-include their enabled - # children, mixed parent+child selections respect the user's subset. - effective_metrics, parent_to_children = _expand_metric_selection( - db, organization_id, metric_ids - ) - if not effective_metrics: - raise HTTPException( - status_code=400, - detail=( - "None of the selected metrics yielded an enabled leaf to " - "evaluate. Check that parent categories have enabled " - "children, then try again." - ), - ) - - # The effective list (children + standalone leaves) is what gets - # persisted to ``selected_metric_ids`` and scored by the worker. - # The original parents are preserved in ``selected_metric_groups`` - # so the UI can rebuild the tree later. - leaf_metric_ids: List[UUID] = [m.id for m in effective_metrics] - selected_metric_groups: Dict[str, List[str]] = { - str(pid): [str(c.id) for c in children] - for pid, children in parent_to_children.items() - } - metric_rows = effective_metrics - valid_metric_id_strs = {str(m.id) for m in metric_rows} - - # ----- Validate run-level + per-metric LLM config ----- - llm_provider_norm: Optional[str] = None - llm_model_norm: Optional[str] = None - if payload.llm_provider or payload.llm_model: - if not (payload.llm_provider and payload.llm_model): - raise HTTPException( - status_code=400, - detail="Both llm_provider and llm_model are required when overriding the run LLM.", - ) - try: - llm_provider_norm = ModelProvider( - payload.llm_provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=( - f"Unknown LLM provider '{payload.llm_provider}'. " - "Valid keys are documented in ModelProvider." - ), - ) - llm_model_norm = payload.llm_model.strip() or None - if not llm_model_norm: - raise HTTPException( - status_code=400, detail="llm_model cannot be empty." - ) - - if payload.llm_credential_id is not None: - cred = ( - db.query(AIProvider) - .filter( - AIProvider.id == payload.llm_credential_id, - AIProvider.organization_id == organization_id, - ) - .first() - ) - if not cred: - raise HTTPException( - status_code=400, - detail=( - "The provided llm_credential_id does not exist in this " - "organization." - ), - ) - - # Per-metric overrides: keys can be either a leaf metric id (applies - # to that metric only) or a parent metric id (applies to every - # child of that parent). Parent keys are expanded to their - # children so the worker only sees concrete leaf ids. - metric_overrides_payload: Optional[Dict[str, Dict[str, Any]]] = None - if payload.metric_llm_overrides: - metric_overrides_payload = {} - for metric_id, override in payload.metric_llm_overrides.items(): - target_leaf_ids: List[str] = [] - if metric_id in valid_metric_id_strs: - target_leaf_ids = [metric_id] - else: - # Maybe it's a parent id — expand to the children that - # are part of THIS run. - try: - parent_uuid = UUID(metric_id) - except (TypeError, ValueError): - raise HTTPException( - status_code=400, - detail=( - "metric_llm_overrides references metric " - f"{metric_id} which is not a valid UUID." - ), - ) - children_for_parent = parent_to_children.get(parent_uuid) - if not children_for_parent: - raise HTTPException( - status_code=400, - detail=( - "metric_llm_overrides references metric " - f"{metric_id} which is not in metric_ids." - ), - ) - target_leaf_ids = [str(c.id) for c in children_for_parent] - - override_dict: Dict[str, Any] = {} - if override.provider is not None: - if not override.model: - raise HTTPException( - status_code=400, - detail=( - f"Override for metric {metric_id} has a provider " - "but no model." - ), - ) - try: - override_dict["provider"] = ModelProvider( - override.provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=( - f"Override for metric {metric_id} uses unknown " - f"provider '{override.provider}'." - ), - ) - override_dict["model"] = override.model.strip() - elif override.model: - # Model without provider doesn't make sense — treat as 400 - # so the UI can fix it instead of silently falling back. - raise HTTPException( - status_code=400, - detail=( - f"Override for metric {metric_id} has a model but " - "no provider." - ), - ) - if override.credential_id is not None: - override_dict["credential_id"] = str(override.credential_id) - if override.llm_config is not None: - override_dict["llm_config"] = override.llm_config - if override_dict: - for leaf_id in target_leaf_ids: - metric_overrides_payload[leaf_id] = override_dict - - # ----- Validate auto-transcribe settings ----- - # Diarised runs auto-diarise rows missing a diarised transcript and - # require STT + diariser LLM config. Production runs score the CSV - # transcript directly and skip diarisation entirely. - use_diarised = payload.transcript_sources[0] == "diarised" - auto_transcribe = use_diarised - - transcribe_mode_norm: Optional[str] = None - stt_provider_norm: Optional[str] = None - stt_model_norm: Optional[str] = None - diarisation_llm_provider_norm: Optional[str] = None - diarisation_llm_model_norm: Optional[str] = None - diarisation_prompt_norm: Optional[str] = None - - if use_diarised: - transcribe_mode_norm = (payload.transcribe_mode or "stt_llm").strip().lower() - if transcribe_mode_norm not in {"stt_llm", "llm_only"}: - raise HTTPException( - status_code=400, - detail=( - f"Unknown transcribe_mode '{payload.transcribe_mode}'. " - "Expected 'stt_llm' or 'llm_only'." - ), - ) - - if transcribe_mode_norm == "stt_llm": - if not payload.stt_provider: - raise HTTPException( - status_code=400, - detail=( - "stt_provider is required when " - "transcribe_mode='stt_llm': every evaluation run " - "auto-diarises rows that are missing a diarised " - "transcript." - ), - ) - if not payload.stt_model: - raise HTTPException( - status_code=400, - detail=( - "stt_model is required when transcribe_mode='stt_llm'." - ), - ) - try: - stt_provider_norm = ModelProvider( - payload.stt_provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=f"Unknown STT provider '{payload.stt_provider}'.", - ) - stt_model_norm = payload.stt_model.strip() or None - if not stt_model_norm: - raise HTTPException( - status_code=400, detail="stt_model cannot be empty." - ) - else: - # llm_only — explicitly reject lingering STT inputs so the - # contract is unambiguous (the worker would ignore them but - # silent acceptance hides accidental misconfiguration). - if (payload.stt_provider or "").strip() or ( - payload.stt_model or "" - ).strip(): - raise HTTPException( - status_code=400, - detail=( - "stt_provider / stt_model must be omitted when " - "transcribe_mode='llm_only'; the LLM consumes the " - "audio directly." - ), - ) - - # --- Validate LLM diariser settings ----- - if not payload.diarization_llm_provider: - raise HTTPException( - status_code=400, - detail=( - "diarization_llm_provider is required: every evaluation " - "run diarises STT output with an LLM." - ), - ) - if not payload.diarization_llm_model: - raise HTTPException( - status_code=400, - detail=( - "diarization_llm_model is required: every evaluation " - "run diarises STT output with an LLM." - ), - ) - try: - diarisation_llm_provider_norm = ModelProvider( - payload.diarization_llm_provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=( - f"Unknown diarisation LLM provider " - f"'{payload.diarization_llm_provider}'." - ), - ) - diarisation_llm_model_norm = ( - payload.diarization_llm_model.strip() or None - ) - if not diarisation_llm_model_norm: - raise HTTPException( - status_code=400, - detail="diarization_llm_model cannot be empty.", - ) - diarisation_prompt_norm = ( - payload.diarization_prompt.strip() - if isinstance(payload.diarization_prompt, str) - else None - ) or None - - from app.models.enums import CallImportParameterType, CallImportStatus - from app.services.call_imports.bulk_ops import ( - count_all_source_rows, - count_completed_source_rows, - count_source_rows_with_production_transcript, - ) - - starting_from_mapped = False - if call_import.status == CallImportStatus.MAPPED: - if not call_import.source_s3_key or not call_import.source_format: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "This batch has no staged source file. Upload and map " - "a CSV/Excel file before running evaluation." - ), - ) - if not call_import.schema_id: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Cannot run evaluation without a mapped schema.", - ) - from app.api.v1.routes.call_imports import ( - _ensure_blob_storage_enabled, - _resolve_schema, - _resolve_telephony_integration, - _validate_direct_url_import_ready, - ) - - workspace_id = call_import.workspace_id - schema = _resolve_schema( - db, organization_id, workspace_id, call_import.schema_id - ) - parameters = list(schema.parameters) - if not use_diarised: - transcript_mapped = any( - param.type == CallImportParameterType.TRANSCRIPT - and (call_import.parameter_mapping or {}).get(param.name) - for param in parameters - ) - if not transcript_mapped: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "No transcript column is mapped in this batch. " - "Map a schema transcript parameter to a CSV column, " - "or choose 'Diarize then evaluate'." - ), - ) - if payload.telephony_integration_id is not None: - integration = _resolve_telephony_integration( - db, - organization_id, - payload.telephony_integration_id, - payload.provider or "", - ) - else: - _validate_direct_url_import_ready( - parameters, dict(call_import.parameter_mapping or {}) - ) - integration = None - - _ensure_blob_storage_enabled() - - if integration is not None: - call_import.provider = integration.provider - call_import.telephony_integration_id = integration.id - else: - call_import.provider = None - call_import.telephony_integration_id = None - - call_import.total_rows = 0 - call_import.completed_rows = 0 - call_import.failed_rows = 0 - call_import.error_message = None - call_import.status = CallImportStatus.PROCESSING - db.commit() - db.refresh(call_import) - starting_from_mapped = True - - if use_diarised: - total_row_count = count_completed_source_rows(db, call_import.id) - else: - # Production runs score CSV text — rows need not wait for - # recording fetch to finish before they are evaluable. - total_row_count = count_source_rows_with_production_transcript( - db, call_import.id - ) - - requested_sources: List[str] = list(payload.transcript_sources) - - if ( - not use_diarised - and not starting_from_mapped - and count_all_source_rows(db, call_import.id) > 0 - and total_row_count == 0 - ): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "No rows have a production transcript. " - "Choose 'Diarize then evaluate' or import rows with " - "a transcript column." - ), - ) - - base_name = _normalize_name(payload.name) - - def _name_for_source(source: str) -> Optional[str]: - # Single-source runs preserve the user's chosen name verbatim. - del source - return base_name - - created_evaluations: List[CallImportEvaluation] = [] - - for source in requested_sources: - evaluation = CallImportEvaluation( - call_import_id=call_import.id, - organization_id=organization_id, - # Mirror the parent CallImport's workspace so listings can - # filter on workspace_id directly without joining. - workspace_id=call_import.workspace_id, - name=_name_for_source(source), - selected_metric_ids=[ - str(metric_id) for metric_id in leaf_metric_ids - ], - selected_metric_groups=selected_metric_groups or None, - status="pending", - total_rows=total_row_count, - completed_rows=0, - failed_rows=0, - llm_provider=llm_provider_norm, - llm_model=llm_model_norm, - llm_credential_id=payload.llm_credential_id, - llm_config=payload.llm_config, - metric_llm_overrides=metric_overrides_payload, - stt_provider=stt_provider_norm, - stt_model=stt_model_norm, - stt_credential_id=( - payload.stt_credential_id if auto_transcribe else None - ), - diarisation_llm_provider=diarisation_llm_provider_norm, - diarisation_llm_model=diarisation_llm_model_norm, - diarisation_llm_credential_id=( - payload.diarization_llm_credential_id if auto_transcribe else None - ), - diarisation_prompt=diarisation_prompt_norm, - transcribe_mode=transcribe_mode_norm, - transcript_source=source, - discover_new_metrics=bool( - getattr(payload, "discover_new_metrics", False) - ), - ) - db.add(evaluation) - db.flush() - created_evaluations.append(evaluation) - - db.commit() - for evaluation in created_evaluations: - db.refresh(evaluation) - - primary_evaluation = created_evaluations[0] - sibling_ids = [e.id for e in created_evaluations[1:]] - - if not total_row_count and not starting_from_mapped: - for evaluation in created_evaluations: - evaluation.status = "completed" - db.commit() - for evaluation in created_evaluations: - db.refresh(evaluation) - return _serialize_eval( - db, primary_evaluation, sibling_evaluation_ids=sibling_ids - ) - - if starting_from_mapped: - from app.workers.tasks.call_import_bulk_ops import ( - materialize_mapped_call_import_evaluation_task, - ) - - for evaluation in created_evaluations: - materialize_mapped_call_import_evaluation_task.delay( - str(call_import.id), - str(organization_id), - str(call_import.workspace_id), - str(evaluation.id), - transcribe_overwrite=payload.transcribe_overwrite, - ) - else: - from app.workers.tasks.call_import_bulk_ops import ( - materialize_call_import_evaluation_task, - ) - - for evaluation in created_evaluations: - materialize_call_import_evaluation_task.delay( - str(evaluation.id), - transcribe_overwrite=payload.transcribe_overwrite, - ) - - for evaluation in created_evaluations: - db.refresh(evaluation) - - return _serialize_eval( - db, primary_evaluation, sibling_evaluation_ids=sibling_ids - ) - - -@router.get( - "", - response_model=CallImportEvaluationListResponse, - operation_id="listCallImportEvaluations", -) -async def list_call_import_evaluations( - call_import_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationListResponse: - del api_key - _require_import(db, call_import_id, organization_id) - rows = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .order_by(desc(CallImportEvaluation.created_at)) - .all() - ) - return CallImportEvaluationListResponse( - items=[_serialize_eval(db, row) for row in rows], - total=len(rows), - ) - - -@router.get( - "/{eval_id}", - response_model=CallImportEvaluationResponse, - operation_id="getCallImportEvaluation", -) -async def get_call_import_evaluation( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationResponse: - del api_key - _require_import(db, call_import_id, organization_id) - row = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not row: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - return _serialize_eval(db, row) - - -@router.get( - "/{eval_id}/rows", - response_model=CallImportEvaluationRowListResponse, - operation_id="listCallImportEvaluationRows", -) -async def list_call_import_evaluation_rows( - call_import_id: UUID, - eval_id: UUID, - page: int = Query(1, ge=1), - page_size: int = Query(100, ge=1, le=500), - q: Optional[str] = Query( - None, - description=( - "Free-text search across conversation_id and transcript " - "(case-insensitive substring match)." - ), - ), - metric_id: Optional[UUID] = Query( - None, - description=( - "If set, only return rows whose ``metric_scores[metric_id].value`` " - "exactly matches ``metric_value`` (string-compared). " - "Use together with ``metric_value``." - ), - ), - metric_value: Optional[str] = Query( - None, - description="Value to match against metric_id (string compare).", - ), - status_filter: Optional[str] = Query( - None, - alias="status", - description="Restrict to rows with this evaluation row status.", - ), - flow_parent_id: Optional[UUID] = Query( - None, - description=( - "Parent (category) metric whose ``sequence`` array should be " - "checked against ``flow_node`` and ``flow_edge_target``. Used " - "to drill into the calls behind a flow-chart node or edge." - ), - ), - flow_node: Optional[str] = Query( - None, - description=( - "If set together with ``flow_parent_id``, only return rows " - "whose sequence under that parent contains this step. Accepts " - "either a child metric UUID (resolved to slug(name)), a " - "``disc:`` discovered-label id, or a raw slug." - ), - ), - flow_edge_target: Optional[str] = Query( - None, - description=( - "Optional companion to ``flow_node``: when set, restrict to " - "rows whose sequence contains the directed transition " - "``flow_node -> flow_edge_target`` (immediately adjacent). " - "Same id format as ``flow_node``." - ), - ), - discovered_parent_id: Optional[UUID] = Query( - None, - description=( - "Parent (category) metric that defines the discovery scope " - "for ``discovered_label_key`` / ``has_discovered``." - ), - ), - discovered_label_key: Optional[str] = Query( - None, - description=( - "If set together with ``discovered_parent_id``, only return " - "rows whose ``metric_scores[parent].discovered_labels`` " - "list contains an entry with this slug (after applying " - "evaluation-level merge aliases)." - ), - ), - has_discovered: Optional[bool] = Query( - None, - description=( - "If true together with ``discovered_parent_id``, only return " - "rows that have at least one LLM-discovered label for the " - "parent. Useful to triage which calls produced novel labels." - ), - ), - sort_by: Optional[str] = Query( - None, - description=( - "Column to sort by. Accepted values: ``row_index`` (default " - "when omitted), ``conversation_id``, ``status`` (the " - "evaluation-row status), or ``metric:`` to sort " - "by ``metric_scores[].value``. Metric sorts compare " - "the extracted JSON text — adequate for booleans, enum " - "labels, and 0-1 ratings; large integer values may sort " - "lexicographically (10 before 2)." - ), - ), - sort_dir: Optional[str] = Query( - "asc", - description="Sort direction: ``asc`` (default) or ``desc``.", - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationRowListResponse: - del api_key - _require_import(db, call_import_id, organization_id) - - eval_row = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not eval_row: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - query = ( - db.query(CallImportEvaluationRow, CallImportRow) - .join(CallImportRow, CallImportRow.id == CallImportEvaluationRow.call_import_row_id) - .filter(CallImportEvaluationRow.evaluation_id == eval_id) - ) - - # --- Filters ---------------------------------------------------------- - if q and q.strip(): - needle = f"%{q.strip()}%" - # Search across both transcript columns so a hit in either the - # production or the diarised version surfaces the row, - # independent of which source the evaluation actually scored. - query = query.filter( - or_( - CallImportRow.conversation_id.ilike(needle), - CallImportRow.transcript.ilike(needle), - CallImportRow.diarised_transcript.ilike(needle), - ) - ) - - if status_filter: - # The CallImportEvaluationRow.status column is a string in PG so a - # plain == filter works; we lowercase to match the stored values. - query = query.filter( - CallImportEvaluationRow.status == status_filter.strip().lower() - ) - - if metric_id is not None and metric_value is not None: - # ``metric_scores`` is a JSONB column shaped like - # ``{"": {"value": , "type": "boolean", ...}}``. We - # extract the nested ``value`` as text and compare to the user - # input as a string — that handles bool/int/enum without needing - # per-type casts. ``metric_value`` is matched case-insensitively - # so chart clicks on labels like "True" survive any casing drift - # between worker output and the chart label. - path_value = func.json_extract_path_text( - CallImportEvaluationRow.metric_scores, - str(metric_id), - "value", - ) - query = query.filter(func.lower(path_value) == metric_value.strip().lower()) - - # --- Flow chart drilldown filter ------------------------------------- - # Translates a clicked node (or edge) on the flow chart into a - # SQL filter against ``metric_scores[].sequence``. The - # frontend sends either a child UUID, a ``disc:`` discovered - # node id, or a raw slug — we normalize all three to the slug that - # actually appears in stored ``sequence`` arrays. - if flow_parent_id is not None and flow_node and flow_node.strip(): - parent_id_str_local = str(flow_parent_id) - alias_map_flow = _alias_map_for_parent(eval_row, flow_parent_id) - - def _flow_node_to_slug(raw: str) -> Optional[str]: - raw_clean = raw.strip() - if not raw_clean: - return None - if raw_clean == _FLOW_START_NODE_ID: - # The synthetic START node isn't a real sequence entry; - # filtering on it is meaningless so we skip silently. - return None - if raw_clean.startswith(_DISCOVERED_NODE_PREFIX): - return _resolve_alias( - alias_map_flow, - _slug_label(raw_clean[len(_DISCOVERED_NODE_PREFIX) :]), - ) - # Try to interpret as a child metric UUID first; fall back - # to treating it as a slug. - try: - child_uuid = UUID(raw_clean) - except (TypeError, ValueError): - return _resolve_alias(alias_map_flow, _slug_label(raw_clean)) - child = ( - db.query(Metric.name) - .filter( - Metric.id == child_uuid, - Metric.organization_id == organization_id, - ) - .first() - ) - if child and child[0]: - return _resolve_alias(alias_map_flow, _slug_label(child[0])) - return _resolve_alias(alias_map_flow, _slug_label(raw_clean)) - - from_slug = _flow_node_to_slug(flow_node) - target_slug: Optional[str] = None - if flow_edge_target and flow_edge_target.strip(): - target_slug = _flow_node_to_slug(flow_edge_target) - - if from_slug: - # The ``metric_scores`` column is declared as ``Column(JSON)`` - # in the model so on databases where the table was created - # from the model (rather than the migration) the physical - # type is ``json``, not ``jsonb``. The JSONB-only operators - # below (``jsonb_exists``, ``jsonb_array_elements_text``, - # ``@>``) require a JSONB input — we cast once up front so - # the same SQL works regardless of which path created the - # table. - scores_jsonb = ( - "(call_import_evaluation_rows.metric_scores)::jsonb" - ) - if target_slug: - # Edge filter: rows whose sequence under this parent - # contains ``from_slug`` immediately followed by - # ``target_slug``. Implemented as a correlated EXISTS - # over ``jsonb_array_elements_text`` with ORDINALITY, - # which is the portable way to express "next array - # index" against a JSONB array in Postgres. - edge_filter_sql = text( - f""" - EXISTS ( - SELECT 1 - FROM jsonb_array_elements_text( - COALESCE( - {scores_jsonb} -> :p_id -> 'sequence', - '[]'::jsonb - ) - ) WITH ORDINALITY AS s1(elem, ord) - JOIN jsonb_array_elements_text( - COALESCE( - {scores_jsonb} -> :p_id -> 'sequence', - '[]'::jsonb - ) - ) WITH ORDINALITY AS s2(elem, ord) - ON s2.ord = s1.ord + 1 - WHERE s1.elem = :from_slug - AND s2.elem = :to_slug - ) - """ - ).bindparams( - p_id=parent_id_str_local, - from_slug=from_slug, - to_slug=target_slug, - ) - query = query.filter(edge_filter_sql) - else: - # Node filter: rows whose ``metric_scores -> parent -> - # 'sequence'`` array contains ``from_slug``. We use the - # function form ``jsonb_exists`` rather than the ``?`` - # operator to avoid psycopg2 mistaking the question - # mark for a parameter placeholder. - node_filter_sql = text( - f""" - jsonb_exists( - COALESCE( - {scores_jsonb} -> :p_id -> 'sequence', - '[]'::jsonb - ), - :slug - ) - """ - ).bindparams(p_id=parent_id_str_local, slug=from_slug) - query = query.filter(node_filter_sql) - - # --- Discovered label filters --------------------------------------- - # Surfaces "which calls produced THIS LLM-discovered label" and the - # broader "which calls produced ANY LLM-discovered label". Both - # operate on ``metric_scores[].discovered_labels`` (a list - # of dicts) plus the same ``sequence`` array — covering both legacy - # rows where the slug only made it into ``sequence`` and newer - # rows where it landed in both. - if discovered_parent_id is not None and ( - discovered_label_key or has_discovered - ): - d_parent_str = str(discovered_parent_id) - alias_map_disc = _alias_map_for_parent(eval_row, discovered_parent_id) - # See note above: cast once so the JSONB operators don't reject - # the column when it's typed as ``json`` in the database. - scores_jsonb = "(call_import_evaluation_rows.metric_scores)::jsonb" - if discovered_label_key and discovered_label_key.strip(): - target = _resolve_alias( - alias_map_disc, _slug_label(discovered_label_key) - ) - if target: - # Match rows whose discovered_labels list has an entry - # ``{"key": }`` OR whose sequence array still - # contains the slug. The latter covers older rows that - # were rewritten by a merge in the discovered_labels - # blob but whose sequence may have lagged. - contains_json = json.dumps( - {d_parent_str: {"discovered_labels": [{"key": target}]}} - ) - disc_filter_sql = text( - f""" - ( - {scores_jsonb} @> CAST(:contains AS JSONB) - OR - jsonb_exists( - COALESCE( - {scores_jsonb} -> :p_id -> 'sequence', - '[]'::jsonb - ), - :slug - ) - ) - """ - ).bindparams( - contains=contains_json, - p_id=d_parent_str, - slug=target, - ) - query = query.filter(disc_filter_sql) - elif has_discovered: - # No specific slug — just rows that surfaced any candidate - # under this parent. We coalesce missing paths to ``[]`` so - # ``jsonb_array_length`` always sees an array (it raises on - # non-array inputs, but our shape guarantees a list when - # the key is present). - has_disc_sql = text( - f""" - jsonb_array_length( - COALESCE( - {scores_jsonb} -> :p_id -> 'discovered_labels', - '[]'::jsonb - ) - ) > 0 - """ - ).bindparams(p_id=d_parent_str) - query = query.filter(has_disc_sql) - - # --- Sorting ---------------------------------------------------------- - # Column-click sorting from the UI. Falls back to ``row_index`` so - # paging stays stable when the user clears the sort. We always add a - # secondary ``row_index`` tiebreaker so duplicate sort keys (e.g. - # many rows with ``status = 'completed'``) keep a deterministic - # order across page boundaries — without this, pagination can - # double-show or skip rows when Postgres picks a different physical - # order on each query. - direction_desc = (sort_dir or "asc").strip().lower() == "desc" - - def _apply_direction(column_expr): - return column_expr.desc() if direction_desc else column_expr.asc() - - # Whether the caller's ``sort_by`` resolved to a known column. We - # use this flag to decide whether ``sort_dir`` is honoured on the - # fallback path: unrecognized columns (typos, stale UI state) fall - # back to the implicit ``row_index ASC`` default and intentionally - # ignore ``sort_dir`` so users don't get a surprise reverse order - # from a typo'd column name. - sort_recognized = False - sort_by_clean = (sort_by or "").strip() - primary_sort = None - metric_uuid: Optional[UUID] = None - if sort_by_clean == "row_index": - sort_recognized = True - # Falls through to the default ``order_by`` below with - # ``primary_sort`` still None — but ``sort_recognized=True`` - # tells the fallback branch to apply the requested direction. - elif sort_by_clean == "conversation_id": - sort_recognized = True - primary_sort = _apply_direction(CallImportRow.conversation_id) - elif sort_by_clean == "status": - sort_recognized = True - primary_sort = _apply_direction(CallImportEvaluationRow.status) - elif sort_by_clean.startswith("metric:"): - raw_metric_id = sort_by_clean.split(":", 1)[1].strip() - try: - metric_uuid = UUID(raw_metric_id) - except (TypeError, ValueError): - metric_uuid = None - if metric_uuid is not None: - sort_recognized = True - # ``metric_scores`` is JSON-typed but the helper functions - # for path extraction differ between Postgres (production) - # and SQLite (default test backend). Branch on the active - # dialect so we can use the right primitive: - # * Postgres → ``json_extract_path_text(col, key, "value")`` - # which returns the value as TEXT for both ``json`` and - # ``jsonb`` columns. - # * SQLite → ``json_extract(col, '$."".value')`` - # using JSONPath syntax. ``metric_uuid`` is already - # validated above (``UUID(raw_metric_id)``), so the - # interpolated path is safe from injection. - # NULL values (rows where the metric wasn't scored) sort - # to the END regardless of direction so un-scored rows - # don't crowd the top of an ascending sort. - dialect_name = ( - db.bind.dialect.name if db.bind is not None else "postgresql" - ) - if dialect_name == "sqlite": - json_path = f'$."{metric_uuid}".value' - path_value = func.json_extract( - CallImportEvaluationRow.metric_scores, - json_path, - ) - else: - path_value = func.json_extract_path_text( - CallImportEvaluationRow.metric_scores, - str(metric_uuid), - "value", - ) - primary_sort = ( - path_value.desc().nullslast() - if direction_desc - else path_value.asc().nullslast() - ) - - if primary_sort is not None: - query = query.order_by(primary_sort, CallImportRow.row_index.asc()) - elif sort_recognized: - # Explicit ``sort_by=row_index`` request — honour direction. - query = query.order_by(_apply_direction(CallImportRow.row_index)) - else: - # No sort requested OR unrecognized column — safe default of - # ``row_index ASC``. We deliberately ignore ``sort_dir`` here - # so a typo'd / stale ``sort_by`` doesn't quietly invert the - # default order. - query = query.order_by(CallImportRow.row_index.asc()) - from app.db_sharding.eval_rows import fetch_evaluation_row_pairs_page - from app.db_sharding.sessions import is_sharding_enabled - - def _pair_row_index( - pair: Tuple[CallImportEvaluationRow, CallImportRow], - ) -> int: - return int(pair[1].row_index or 0) - - def _directed_string(value: Optional[str], desc: bool) -> Tuple[int, ...]: - text = value or "" - if not desc: - return (0, *text.encode("utf-8")) - return (1, *(-byte for byte in text.encode("utf-8"))) - - if sort_by_clean == "conversation_id": - def _pair_sort_key( - pair: Tuple[CallImportEvaluationRow, CallImportRow], - ) -> Tuple[Any, ...]: - return ( - _directed_string(pair[1].conversation_id, direction_desc), - _pair_row_index(pair), - ) - elif sort_by_clean == "status": - def _pair_sort_key( - pair: Tuple[CallImportEvaluationRow, CallImportRow], - ) -> Tuple[Any, ...]: - return ( - _directed_string(pair[0].status, direction_desc), - _pair_row_index(pair), - ) - elif sort_by_clean.startswith("metric:") and metric_uuid is not None: - metric_id_str = str(metric_uuid) - - def _pair_sort_key( - pair: Tuple[CallImportEvaluationRow, CallImportRow], - ) -> Tuple[Any, ...]: - scores = pair[0].metric_scores or {} - entry = scores.get(metric_id_str, {}) - raw_value = entry.get("value") if isinstance(entry, dict) else None - null_rank = 1 if raw_value is None else 0 - return ( - null_rank, - _directed_string( - str(raw_value) if raw_value is not None else None, - direction_desc, - ), - _pair_row_index(pair), - ) - elif sort_recognized and sort_by_clean == "row_index": - def _pair_sort_key( - pair: Tuple[CallImportEvaluationRow, CallImportRow], - ) -> Tuple[int, ...]: - idx = _pair_row_index(pair) - return (-idx,) if direction_desc else (idx,) - else: - def _pair_sort_key( - pair: Tuple[CallImportEvaluationRow, CallImportRow], - ) -> Tuple[int, ...]: - return (_pair_row_index(pair),) - - if is_sharding_enabled(): - def _build_query(session: Session): - return query.with_session(session) - - total, rows = fetch_evaluation_row_pairs_page( - db, - _build_query, - page=page, - page_size=page_size, - sort_key=_pair_sort_key, - bounded_shard_fetch=( - not sort_recognized or sort_by_clean == "row_index" - ), - ) - else: - total = query.count() - rows = query.offset((page - 1) * page_size).limit(page_size).all() - - # Row detail shows the transcript for this run's chosen source. - items: List[CallImportEvaluationRowResponse] = [ - _to_evaluation_row_response(eval_row_obj, source_row, eval_row) - for eval_row_obj, source_row in rows - ] - - return CallImportEvaluationRowListResponse( - items=items, - total=total, - page=page, - page_size=page_size, - ) - - -@router.get( - "/{eval_id}/export", - operation_id="exportCallImportEvaluationCsv", -) -async def export_call_import_evaluation_csv( - call_import_id: UUID, - eval_id: UUID, - format: Literal["csv", "xlsx"] = Query( - "csv", - description=( - "Output format. ``csv`` returns a UTF-8 BOM CSV; ``xlsx`` " - "returns a native Excel workbook (single sheet)." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> StreamingResponse: - del api_key - call_import = _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - selected_metric_ids = _serialize_selected_metric_ids(evaluation.selected_metric_ids) - # Include parent metric ids referenced in selected_metric_groups so - # the export shows a parent "Chosen Label" column next to its - # children's true/false columns. - lookup_ids: List[UUID] = list(selected_metric_ids) - groups_raw = ( - evaluation.selected_metric_groups - if isinstance(evaluation.selected_metric_groups, dict) - else {} - ) - for parent_str in groups_raw.keys(): - try: - pid = UUID(parent_str) - if pid not in lookup_ids: - lookup_ids.append(pid) - except (TypeError, ValueError): - continue - metrics = _metrics_for_ids(db, organization_id, lookup_ids) - metric_names = {str(metric.id): metric.name for metric in metrics} - metrics_by_id = {str(metric.id): metric for metric in metrics} - - # Two export-time modes depending on how the batch was uploaded: - # - # * Schema-driven (new): ``call_imports.schema_id`` is set, - # ``parameter_mapping`` records which CSV header fed each - # parameter, and ``raw_columns`` on each row is keyed by - # parameter NAME. Export headers are the parameter names. - # * Legacy (pre-schema): ``column_mapping`` / ``extra_columns`` / - # ``custom_column_mapping`` drive the columns and - # ``raw_columns`` is keyed by the original CSV header. - # - # We bucket entries into ``standard_export_headers`` (raw_columns - # key == export header) and ``custom_export`` (export header - # differs from the raw_columns key) so the row-projection loop - # below stays mode-agnostic. - standard_export_headers: List[str] = [] - custom_export: List[tuple[str, str]] = [] # [(export_header, raw_columns_key)] - - if call_import.schema_id is not None: - # Use the live schema parameter list for column ordering. Falls - # back to whatever's in ``parameter_mapping`` if the schema was - # deleted (defensive - the FK is ON DELETE RESTRICT, but tests - # / future cascades may still hit this branch). - from app.models.database import CallImportSchema as _ImportSchema - - schema_obj = ( - db.query(_ImportSchema) - .filter(_ImportSchema.id == call_import.schema_id) - .first() - ) - if schema_obj is not None: - params_sorted = sorted( - schema_obj.parameters, key=lambda p: p.ordering or 0 - ) - for param in params_sorted: - if param.name and param.name not in standard_export_headers: - standard_export_headers.append(param.name) - else: - for param_name in (call_import.parameter_mapping or {}).keys(): - if param_name and param_name not in standard_export_headers: - standard_export_headers.append(param_name) - else: - mapping = call_import.column_mapping or {} - mapped_headers = [ - mapping.get("external_call_id"), - mapping.get("transcript"), - mapping.get("recording_url"), - ] - for header in [*mapped_headers, *(call_import.extra_columns or [])]: - if ( - isinstance(header, str) - and header - and header not in standard_export_headers - ): - standard_export_headers.append(header) - - custom_mapping = call_import.custom_column_mapping or {} - if isinstance(custom_mapping, dict): - for name, csv_header in custom_mapping.items(): - if not isinstance(name, str) or not isinstance(csv_header, str): - continue - if not name or not csv_header: - continue - if name in standard_export_headers: - continue # would clobber a real column - custom_export.append((name, csv_header)) - - if ( - call_import.source_format == "audio" - and "conversation_id" not in standard_export_headers - ): - standard_export_headers.insert(0, "conversation_id") - - # Build the metric columns: each parent (if any) gets a value column - # and (when capture_rationale=true) a " - LLM Rationale" - # column. The per-child boolean columns are intentionally suppressed - # — categorization metrics now collapse to exactly two columns in - # the export, mirroring the in-app table. - child_ids_in_groups: set[str] = set() - for parent_str, child_strs in groups_raw.items(): - for child_str in child_strs: - if isinstance(child_str, str): - child_ids_in_groups.add(child_str) - - metric_headers: List[str] = [] - rationale_headers: Dict[str, str] = {} # metric_id_str -> rationale column name - seen_metric_ids: set[str] = set() - - def _add_metric_column(metric: Metric) -> None: - mid_str = str(metric.id) - if mid_str in seen_metric_ids: - return - # Skip any child whose parent is part of this run — the parent - # column above already shows the chosen child name as its - # value. - if mid_str in child_ids_in_groups: - return - seen_metric_ids.add(mid_str) - header = metric_names[mid_str] - metric_headers.append(header) - if bool(getattr(metric, "capture_rationale", False)): - rationale_header = f"{header} - LLM Rationale" - metric_headers.append(rationale_header) - rationale_headers[mid_str] = rationale_header - - for parent_str in groups_raw.keys(): - parent = metrics_by_id.get(parent_str) - if parent: - _add_metric_column(parent) - # Children of an in-run parent are deliberately not emitted — - # the ``child_ids_in_groups`` guard inside ``_add_metric_column`` - # is what enforces this. We still iterate the keys above (not - # ``.items()``) so the parent-only emission is explicit. - # Append anything left over (standalone metrics not in any group, or - # legacy runs without ``selected_metric_groups``). - for metric in metrics: - if metric.selection_mode and not metric.parent_metric_id: - continue # already handled above - if str(metric.id) in seen_metric_ids: - continue - _add_metric_column(metric) - - # Three new fixed columns surface the two transcript fields and the - # evaluation's transcript_source as live values pulled from the - # ``CallImportRow`` (not from the frozen ``raw_columns`` snapshot). - # The user can now compare "what was in the CSV" vs "what the - # diarisation worker produced" without round-tripping through the - # UI, and downstream tools can verify which transcript the metrics - # were computed against. - PRODUCTION_TRANSCRIPT_HEADER = "Production Transcript" - DIARISED_TRANSCRIPT_HEADER = "Diarised Transcript" - EVAL_SOURCE_HEADER = "Evaluated Transcript Source" - - fieldnames = [ - *standard_export_headers, - *[h for h, _ in custom_export], - PRODUCTION_TRANSCRIPT_HEADER, - DIARISED_TRANSCRIPT_HEADER, - EVAL_SOURCE_HEADER, - *metric_headers, - ] - - from app.db_sharding.scatter_gather import load_evaluation_row_pairs - from app.db_sharding.sessions import is_sharding_enabled - - if is_sharding_enabled(): - rows = sorted( - load_evaluation_row_pairs(db, eval_id), - key=lambda pair: int(pair[1].row_index or 0), - ) - else: - rows = ( - db.query(CallImportEvaluationRow, CallImportRow) - .join( - CallImportRow, - CallImportRow.id == CallImportEvaluationRow.call_import_row_id, - ) - .filter(CallImportEvaluationRow.evaluation_id == eval_id) - .order_by(CallImportRow.row_index.asc()) - .all() - ) - - def _project_rows() -> Iterator[Dict[str, str]]: - for eval_row, source_row in rows: - row_out: Dict[str, str] = {} - raw = ( - source_row.raw_columns - if isinstance(source_row.raw_columns, dict) - else {} - ) - for header in standard_export_headers: - value = raw.get(header) - if value is None and header == "conversation_id": - value = source_row.conversation_id - row_out[header] = "" if value is None else str(value) - for export_header, csv_header in custom_export: - value = raw.get(csv_header) - row_out[export_header] = "" if value is None else str(value) - - # Live transcripts pulled from the row, NOT from raw_columns, - # so re-diarised values are always reflected in the export. - # Both transcript columns are flattened to a single line so the - # spreadsheet cell doesn't balloon vertically — the in-app - # ``TranscriptView`` still has the DB copy with line breaks - # intact for chat-bubble rendering. - row_out[PRODUCTION_TRANSCRIPT_HEADER] = _flatten_transcript( - source_row.transcript - ) - row_out[DIARISED_TRANSCRIPT_HEADER] = _flatten_transcript( - source_row.diarised_transcript - ) - row_out[EVAL_SOURCE_HEADER] = _evaluated_transcript_source_label( - evaluation, - source_row, - ) - - scores = ( - eval_row.metric_scores - if isinstance(eval_row.metric_scores, dict) - else {} - ) - for metric in metrics: - metric_score = ( - scores.get(str(metric.id)) - if isinstance(scores, dict) - else None - ) - value = ( - metric_score.get("value") - if isinstance(metric_score, dict) - else None - ) - # Parent metrics (selection_mode set) render the chosen - # child name for single_choice or the ";"-joined list of - # true child names for multi_label. - if ( - metric.selection_mode - and not metric.parent_metric_id - and isinstance(metric_score, dict) - ): - if metric.selection_mode == "multi_label": - selected = metric_score.get("selected_child_names") - if isinstance(selected, list): - value = ";".join(str(s) for s in selected) - else: - value = ( - metric_score.get("chosen_child_name") - or metric_score.get("value") - ) - row_out[metric.name] = "" if value is None else str(value) - rationale_header = rationale_headers.get(str(metric.id)) - if rationale_header is not None: - rationale = ( - metric_score.get("rationale") - if isinstance(metric_score, dict) - else None - ) - row_out[rationale_header] = ( - "" if rationale is None else str(rationale) - ) - yield row_out - - base_filename = f"call-import-{call_import_id}-evaluation-{eval_id}" - - if format == "xlsx": - # xlsx is unicode-native (Hindi/Devanagari, emoji, etc.) so the - # UTF-8-BOM dance isn't needed here. ``write_only`` mode keeps - # peak memory bounded for large evaluations because openpyxl - # only buffers the current row. - try: - from openpyxl import Workbook # type: ignore - from openpyxl.cell import WriteOnlyCell # type: ignore - from openpyxl.styles import Font # type: ignore - except ImportError as exc: # pragma: no cover - exercised by pyproject lock - raise HTTPException( - status_code=500, - detail=( - "Excel export requires the 'openpyxl' package which is " - "not installed." - ), - ) from exc - - workbook = Workbook(write_only=True) - worksheet = workbook.create_sheet(title="Evaluation") - - bold_font = Font(bold=True) - header_cells = [] - for header in fieldnames: - cell = WriteOnlyCell(worksheet, value=header) - cell.font = bold_font - header_cells.append(cell) - worksheet.append(header_cells) - - for row_dict in _project_rows(): - worksheet.append([row_dict.get(h, "") for h in fieldnames]) - - buffer = io.BytesIO() - workbook.save(buffer) - xlsx_bytes = buffer.getvalue() - filename = f"{base_filename}.xlsx" - return StreamingResponse( - iter([xlsx_bytes]), - media_type=( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - headers={"Content-Disposition": f'attachment; filename="{filename}"'}, - ) - - output = io.StringIO() - writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore") - writer.writeheader() - for row_dict in _project_rows(): - writer.writerow(row_dict) - - # Excel on Windows defaults to the system ANSI codepage (Windows-1252) - # when a CSV has no encoding marker, which turns UTF-8 Hindi/Devanagari - # / any non-ASCII text into mojibake (e.g. ``ठीक`` → ``ठीक``). - # A UTF-8 BOM tells Excel to switch to UTF-8 decoding and is silently - # skipped by every other UTF-8-aware reader (pandas, LibreOffice, - # Google Sheets, etc.), so the data round-trips correctly everywhere. - csv_text = output.getvalue() - # ``utf-8-sig`` adds the UTF-8 BOM so Excel on Windows decodes the file - # as UTF-8 instead of the system codepage. We also declare the same - # codec in the Content-Type header so well-behaved HTTP clients (incl. - # ``httpx`` / ``requests`` in our tests) strip the BOM during decode. - csv_bytes = csv_text.encode("utf-8-sig") - filename = f"{base_filename}.csv" - return StreamingResponse( - iter([csv_bytes]), - media_type="text/csv; charset=utf-8-sig", - headers={"Content-Disposition": f'attachment; filename="{filename}"'}, - ) - - -def _report_filename_slug(value: str) -> str: - slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-") - return slug or "client" - - -def _report_branding_for_import_workspace( - db: Session, - organization_id: UUID, - workspace_id: UUID, - *, - internal_brand_image_id: Optional[str] = None, - external_brand_image_id: Optional[str] = None, -) -> tuple[dict[str, str] | list[str], Optional[str]]: - workspace = ( - db.query(Workspace) - .filter( - Workspace.id == workspace_id, - Workspace.organization_id == organization_id, - ) - .first() - ) - raw = workspace.report_branding if workspace and isinstance(workspace.report_branding, dict) else {} - images = raw.get("images") if isinstance(raw.get("images"), list) else [] - loaded_images: list[dict[str, str]] = [] - for item in images: - if not isinstance(item, dict) or not item.get("s3_key"): - continue - content_type = str(item.get("content_type") or "image/png") - try: - from app.services.storage.s3_service import s3_service - - image_bytes = s3_service.download_file_by_key(str(item["s3_key"])) - except Exception as exc: # noqa: BLE001 - logger.warning( - "Unable to load report branding image for workspace {}: {}", - workspace_id, - exc, - ) - continue - encoded = base64.b64encode(image_bytes).decode("ascii") - role = str(item.get("role") or "generic") - if role not in {"internal", "external", "generic"}: - role = "generic" - loaded_images.append( - { - "id": str(item.get("id") or ""), - "role": role, - "data_uri": f"data:{content_type};base64,{encoded}", - } - ) - - def _pick(role: str, selected_id: Optional[str]) -> Optional[str]: - if selected_id: - for loaded in loaded_images: - if loaded["id"] == selected_id: - return loaded["data_uri"] - for loaded in loaded_images: - if loaded["role"] == role: - return loaded["data_uri"] - return None - - logo_data_uris: dict[str, str] = {} - internal_uri = _pick("internal", internal_brand_image_id) - external_uri = _pick("external", external_brand_image_id) - if internal_uri: - logo_data_uris["internal"] = internal_uri - if external_uri: - logo_data_uris["external"] = external_uri - if ( - not logo_data_uris - and not internal_brand_image_id - and not external_brand_image_id - ): - # Backward compatibility for workspaces that only had a generic logo - # library before the two-slot report header existed. - generic_uris = [ - loaded["data_uri"] - for loaded in loaded_images - if loaded.get("data_uri") - ] - if generic_uris: - heading = raw.get("heading") if isinstance(raw.get("heading"), str) else None - return generic_uris[:4], heading - heading = raw.get("heading") if isinstance(raw.get("heading"), str) else None - return logo_data_uris, heading - - -def _display_metrics_for_pdf_report( - db: Session, - organization_id: UUID, - evaluation: CallImportEvaluation, -) -> list[Metric]: - selected_metric_ids = _serialize_selected_metric_ids(evaluation.selected_metric_ids) - lookup_ids: List[UUID] = list(selected_metric_ids) - groups_raw = ( - evaluation.selected_metric_groups - if isinstance(evaluation.selected_metric_groups, dict) - else {} - ) - for parent_str in groups_raw.keys(): - try: - parent_id = UUID(parent_str) - except (TypeError, ValueError): - continue - if parent_id not in lookup_ids: - lookup_ids.append(parent_id) - - metrics = _metrics_for_ids(db, organization_id, lookup_ids) - child_ids_in_groups: set[str] = set() - for child_strs in groups_raw.values(): - if not isinstance(child_strs, list): - continue - child_ids_in_groups.update(str(child_id) for child_id in child_strs) - - metrics_by_id = {str(metric.id): metric for metric in metrics} - display: list[Metric] = [] - seen: set[str] = set() - - for parent_str in groups_raw.keys(): - parent = metrics_by_id.get(str(parent_str)) - if parent and str(parent.id) not in seen: - display.append(parent) - seen.add(str(parent.id)) - - for metric in metrics: - metric_id = str(metric.id) - if metric_id in seen or metric_id in child_ids_in_groups: - continue - if metric.selection_mode and not metric.parent_metric_id: - continue - display.append(metric) - seen.add(metric_id) - - return display - - -def _metrics_for_clustering( - db: Session, - evaluation: CallImportEvaluation, - eval_rows: List[CallImportEvaluationRow], -) -> List[Metric]: - """All enabled quality metrics scored in this run, normalized for clustering. - - Hierarchical children are collapsed to their parent metric so cluster - groups render at the category level (e.g. ``AI reveal``) instead of the - child label level (e.g. ``Yes`` / ``No``). - """ - aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) - aggregate_metric_ids: List[UUID] = [] - for agg in aggregates: - if (agg.metric_category or "quality") == "user_insight": - continue - try: - aggregate_metric_ids.append(UUID(agg.metric_id)) - except (TypeError, ValueError): - continue - if not aggregate_metric_ids: - return [] - - aggregate_metrics = _metrics_for_ids( - db, evaluation.organization_id, aggregate_metric_ids - ) - by_id = {metric.id: metric for metric in aggregate_metrics} - - normalized_ids: List[UUID] = [] - seen: set[UUID] = set() - for metric_id in aggregate_metric_ids: - metric = by_id.get(metric_id) - target_id = ( - metric.parent_metric_id - if metric is not None and metric.parent_metric_id - else metric_id - ) - if target_id in seen: - continue - seen.add(target_id) - normalized_ids.append(target_id) - - metrics = _metrics_for_ids(db, evaluation.organization_id, normalized_ids) - return [ - metric - for metric in metrics - if getattr(metric, "enabled", True) and not _metric_is_user_insight(metric) - ] - - -def _metric_is_user_insight(metric: Metric) -> bool: - if (getattr(metric, "metric_category", "quality") or "quality") == "user_insight": - return True - text_value = " ".join( - str(part or "").lower() - for part in (getattr(metric, "name", ""), getattr(metric, "description", "")) - ) - normalized = text_value.replace("-", " ").replace("_", " ") - phrases = ( - "call context", - "caller context", - "product identification", - "out of scope", - "identity match", - "user identity", - "caller identity", - "frustration trigger", - "video call offer", - "video call reception", - ) - return any(phrase in normalized for phrase in phrases) - - -def _evaluation_rows_for_period( - db: Session, - evaluation_id: UUID, -) -> list[tuple[CallImportEvaluationRow, CallImportRow]]: - return ( - db.query(CallImportEvaluationRow, CallImportRow) - .join(CallImportRow, CallImportRow.id == CallImportEvaluationRow.call_import_row_id) - .filter(CallImportEvaluationRow.evaluation_id == evaluation_id) - .order_by(CallImportRow.row_index.asc()) - .all() - ) - - -def _baseline_candidate_evaluations( - db: Session, - organization_id: UUID, - workspace_id: UUID, - current_evaluation: CallImportEvaluation, - current_period_start: Optional[date], - *, - limit: int = 20, -) -> list[dict[str, Any]]: - candidates = ( - db.query(CallImportEvaluation, CallImport) - .join(CallImport, CallImport.id == CallImportEvaluation.call_import_id) - .filter( - CallImportEvaluation.organization_id == organization_id, - CallImport.workspace_id == workspace_id, - CallImportEvaluation.id != current_evaluation.id, - CallImportEvaluation.status == "completed", - CallImportEvaluation.completed_rows > 0, - ) - .order_by(desc(CallImportEvaluation.created_at)) - .limit(limit * 3) - .all() - ) - items: list[dict[str, Any]] = [] - for candidate_eval, candidate_import in candidates: - rows = _evaluation_rows_for_period(db, candidate_eval.id) - period_start, period_end, period_label, period_display = _report_period_from_rows(rows) - if current_period_start and period_start and period_start >= current_period_start: - continue - dataset = ( - (candidate_import.dataset or "").strip() - or (candidate_import.original_filename or candidate_import.filename or "").strip() - or "Unknown dataset" - ) - evaluation_name = ( - (candidate_eval.name or "").strip() - or str(candidate_eval.id)[:8] - ) - items.append( - { - "evaluation_id": str(candidate_eval.id), - "name": evaluation_name, - "dataset": dataset, - "period_label": period_label, - "period_start": period_start, - "period_end": period_end, - "period_display": period_display, - "completed_rows": int(candidate_eval.completed_rows or 0), - "created_at": candidate_eval.created_at, - "is_default": False, - } - ) - if len(items) >= limit: - break - items.sort( - key=lambda item: ( - item["period_start"] or date.min, - item["created_at"] or datetime.min.replace(tzinfo=timezone.utc), - ), - reverse=True, - ) - if items: - items[0]["is_default"] = True - return items - - -def _resolve_baseline_evaluation( - db: Session, - organization_id: UUID, - workspace_id: UUID, - current_evaluation: CallImportEvaluation, - current_period_start: Optional[date], - baseline_evaluation_id: Optional[str], -) -> Optional[CallImportEvaluation]: - candidates = _baseline_candidate_evaluations( - db, - organization_id, - workspace_id, - current_evaluation, - current_period_start, - ) - allowed_ids = {item["evaluation_id"] for item in candidates} - if baseline_evaluation_id: - baseline_id = str(baseline_evaluation_id).strip() - if baseline_id not in allowed_ids: - raise HTTPException( - status_code=400, - detail="Selected baseline evaluation is not a valid prior run for this report.", - ) - return ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == UUID(baseline_id), - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not candidates: - return None - default_id = candidates[0]["evaluation_id"] - return ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == UUID(default_id), - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - - -def _benchmark_context_for_evaluation( - db: Session, - baseline_evaluation: Optional[CallImportEvaluation], -) -> Optional[dict[str, str]]: - if baseline_evaluation is None: - return None - baseline_import = ( - db.query(CallImport) - .filter(CallImport.id == baseline_evaluation.call_import_id) - .first() - ) - rows = _evaluation_rows_for_period(db, baseline_evaluation.id) - period_start, _period_end, period_label, _period_display = _report_period_from_rows(rows) - dataset = ( - (baseline_import.dataset or "").strip() - if baseline_import and baseline_import.dataset - else None - ) - filename = ( - (baseline_import.original_filename or baseline_import.filename or "").strip() - if baseline_import - else None - ) - evaluation_label = ( - (baseline_evaluation.name or "").strip() - if baseline_evaluation.name - else str(baseline_evaluation.id)[:8] - ) - period = period_label or ( - period_start.isoformat() if period_start else "previous report" - ) - return { - "dataset": dataset or filename or "Unknown dataset", - "evaluation": evaluation_label, - "evaluation_id": str(baseline_evaluation.id), - "period": period, - } - - -def _period_deltas_from_evaluation( - db: Session, - baseline_evaluation: CallImportEvaluation, - current_metric_aggregates: list[dict[str, Any]], - current_evaluation: CallImportEvaluation, - current_eval_rows: List[CallImportEvaluationRow], -) -> dict[str, dict[str, str]]: - baseline_rows = _evaluation_rows_for_period(db, baseline_evaluation.id) - baseline_eval_rows = [eval_row for eval_row, _source_row in baseline_rows] - baseline_aggregate_models = _compute_metric_aggregates( - db, - baseline_evaluation, - baseline_eval_rows, - ) - baseline_metric_aggregates = [ - _aggregate_to_dict(aggregate) for aggregate in baseline_aggregate_models - ] - _metrics, _aggs, policies, _source, _child_map = _clustering_context( - db, current_evaluation, current_eval_rows - ) - metric_by_id = {str(m.id): m for m in _metrics} - current_by_id = { - str(item.get("metric_id")): item for item in current_metric_aggregates - } - previous_by_id = { - str(item.get("metric_id")): item - for item in baseline_metric_aggregates - if isinstance(item, dict) - } - deltas: dict[str, dict[str, str]] = {} - for metric_id, current in current_by_id.items(): - metric = metric_by_id.get(metric_id) - policy = policies.get(metric_id) - previous_raw = previous_by_id.get(metric_id) - if metric is None or policy is None: - deltas[metric_id] = { - "label": "No previous-week baseline", - "detail": "No comparable prior report snapshot was found.", - } - continue - current_pct = failure_rate_percent_from_rows( - current_eval_rows, metric, policy - ) - previous_pct = failure_rate_percent_from_rows( - baseline_eval_rows, metric, policy - ) - if current_pct is None or previous_pct is None: - current_pct = current_pct or _aggregate_primary_percent(current, policy) - previous_pct = ( - previous_pct or _aggregate_primary_percent(previous_raw, policy) - if previous_raw - else None - ) - if current_pct is None or previous_pct is None: - deltas[metric_id] = { - "label": "No previous-week baseline", - "detail": "No comparable prior report snapshot was found.", - } - continue - delta = current_pct - previous_pct - sign = "+" if delta >= 0 else "" - deltas[metric_id] = { - "label": f"{sign}{delta:.1f} pp", - "detail": ( - f"Current report {current_pct:.1f}% vs previous report " - f"{previous_pct:.1f}%" - ), - } - return deltas - - -_DELTA_EXPLANATION_SYSTEM_PROMPT = ( - "You are a senior conversation-analytics reviewer. You will receive " - "week-over-week metric failure-rate deltas plus reconciled failure " - "cluster context per metric.\n\n" - "Return STRICT JSON only:\n" - "{\n" - ' "explanations": {"": "<1-2 sentence explanation of why the delta likely occurred>"}\n' - "}\n\n" - "Constraints:\n" - "- Only include metrics supplied in the prompt.\n" - "- Cluster labels are generated independently each run and are NOT stable " - "IDs. Never compare an unmatched current label to 0% baseline.\n" - "- Use matched_theme_shifts for label-aligned comparisons, " - "gap_label_shifts for structural shifts, and new_themes_current_period " - "for themes that emerged without a baseline match.\n" - "- If reconciliation is uncertain, explain using the numeric delta and " - "gap_label_shifts only.\n" - "- Keep each explanation to 1-2 short sentences (~220 chars).\n" - "- Vendor-safe, factual language; no markdown." -) - - -def _period_delta_explanation_cache_key( - baseline_evaluation_id: UUID, - *, - completed_rows: int, - baseline_completed_rows: int, -) -> str: - return ( - f"{baseline_evaluation_id}:{completed_rows}:" - f"{baseline_completed_rows}:reconciled-v2" - ) - - -def _normalize_cluster_label(label: str) -> str: - return re.sub(r"[^a-z0-9]+", " ", (label or "").lower()).strip() - - -_CLUSTER_LABEL_STOPWORDS = frozenset( - { - "a", - "an", - "the", - "and", - "or", - "during", - "while", - "with", - "for", - "from", - "into", - "general", - "user", - "bot", - "agent", - } -) - - -def _cluster_label_tokens(label: str) -> set[str]: - return { - token - for token in _normalize_cluster_label(label).split() - if token and token not in _CLUSTER_LABEL_STOPWORDS and len(token) > 2 - } - - -def _cluster_label_similarity(left: str, right: str) -> float: - tokens_left = _cluster_label_tokens(left) - tokens_right = _cluster_label_tokens(right) - if not tokens_left or not tokens_right: - return 0.0 - intersection = tokens_left & tokens_right - if not intersection: - return 0.0 - union = tokens_left | tokens_right - jaccard = len(intersection) / len(union) - smaller = tokens_left if len(tokens_left) <= len(tokens_right) else tokens_right - overlap_ratio = len(intersection) / len(smaller) - return max(jaccard, overlap_ratio * 0.85) - - -def _group_clusters_by_gap_label( - clusters: list[dict[str, Any]], -) -> dict[str, list[dict[str, Any]]]: - grouped: dict[str, list[dict[str, Any]]] = {} - for cluster in clusters: - gap_label = str(cluster.get("gap_label") or "UNKNOWN") - grouped.setdefault(gap_label, []).append(cluster) - return grouped - - -def _append_matched_cluster_pair( - matched: list[dict[str, Any]], - current: dict[str, Any], - baseline: dict[str, Any], - *, - match_confidence: float, - match_method: str, -) -> None: - matched.append( - { - "current_label": current.get("label"), - "baseline_label": baseline.get("label"), - "gap_label": current.get("gap_label") or baseline.get("gap_label"), - "current_share_pct": current.get("share_pct"), - "baseline_share_pct": baseline.get("share_pct"), - "share_delta_pp": round( - float(current.get("share_pct") or 0.0) - - float(baseline.get("share_pct") or 0.0), - 1, - ), - "match_confidence": round(match_confidence, 2), - "match_method": match_method, - } - ) - - -def _aggregate_share_by_gap_label( - clusters: list[dict[str, Any]], -) -> dict[str, float]: - totals: dict[str, float] = {} - for cluster in clusters: - gap_label = str(cluster.get("gap_label") or "UNKNOWN") - totals[gap_label] = totals.get(gap_label, 0.0) + float( - cluster.get("share_pct") or 0.0 - ) - return {gap: round(share, 1) for gap, share in totals.items()} - - -def _reconcile_cluster_periods( - current_clusters: list[dict[str, Any]], - baseline_clusters: list[dict[str, Any]], - *, - similarity_threshold: float = 0.35, -) -> dict[str, Any]: - """Align independently-generated cluster labels before delta explanation.""" - matched: list[dict[str, Any]] = [] - current_unmatched = list(current_clusters) - remaining_baseline = list(baseline_clusters) - - current_by_gap = _group_clusters_by_gap_label(current_unmatched) - baseline_by_gap = _group_clusters_by_gap_label(remaining_baseline) - for gap_label in list(current_by_gap): - current_group = current_by_gap.get(gap_label) or [] - baseline_group = baseline_by_gap.get(gap_label) or [] - if len(current_group) != 1 or len(baseline_group) != 1: - continue - current = current_group[0] - baseline = baseline_group[0] - _append_matched_cluster_pair( - matched, - current, - baseline, - match_confidence=0.75, - match_method="single_cluster_per_gap_label", - ) - current_unmatched.remove(current) - remaining_baseline.remove(baseline) - current_by_gap[gap_label] = [] - baseline_by_gap[gap_label] = [] - - for current in list(current_unmatched): - best_idx: Optional[int] = None - best_score = 0.0 - for idx, baseline in enumerate(remaining_baseline): - score = _cluster_label_similarity( - str(current.get("label") or ""), - str(baseline.get("label") or ""), - ) - if current.get("gap_label") == baseline.get("gap_label"): - score += 0.1 - if score > best_score: - best_score = score - best_idx = idx - - if best_idx is not None and best_score >= similarity_threshold: - baseline = remaining_baseline.pop(best_idx) - _append_matched_cluster_pair( - matched, - current, - baseline, - match_confidence=best_score, - match_method="label_similarity", - ) - - matched_current_labels = { - str(item.get("current_label") or "") for item in matched - } - matched_baseline_labels = { - str(item.get("baseline_label") or "") for item in matched - } - current_unmatched = [ - cluster - for cluster in current_clusters - if str(cluster.get("label") or "") not in matched_current_labels - ] - remaining_baseline = [ - cluster - for cluster in baseline_clusters - if str(cluster.get("label") or "") not in matched_baseline_labels - ] - - new_themes = [ - { - "label": cluster.get("label"), - "gap_label": cluster.get("gap_label"), - "share_pct": cluster.get("share_pct"), - "note": "New theme in current period (no close baseline match).", - } - for cluster in current_unmatched - ] - - retired_themes = [ - { - "label": baseline.get("label"), - "gap_label": baseline.get("gap_label"), - "share_pct": baseline.get("share_pct"), - "note": "Theme present in baseline only (retired or renamed).", - } - for baseline in remaining_baseline - ] - - current_gap = _aggregate_share_by_gap_label(current_clusters) - baseline_gap = _aggregate_share_by_gap_label(baseline_clusters) - gap_label_shifts: dict[str, dict[str, float]] = {} - for gap_label in set(current_gap) | set(baseline_gap): - current_share = current_gap.get(gap_label, 0.0) - baseline_share = baseline_gap.get(gap_label, 0.0) - if abs(current_share - baseline_share) >= 0.5: - gap_label_shifts[gap_label] = { - "current_share_pct": current_share, - "baseline_share_pct": baseline_share, - "share_delta_pp": round(current_share - baseline_share, 1), - } - - return { - "matched_theme_shifts": matched, - "new_themes_current_period": new_themes, - "retired_themes_baseline_period": retired_themes, - "gap_label_shifts": gap_label_shifts, - "reconciliation_note": ( - "Cluster labels are generated independently each run and may " - "rename the same failure mode. Do not treat unmatched current " - "labels as 0% in the baseline period." - ), - } - - -def _load_period_delta_explanations_cache( - evaluation: CallImportEvaluation, - cache_key: str, -) -> Optional[dict[str, str]]: - raw = getattr(evaluation, "period_delta_explanations", None) - if not isinstance(raw, dict): - return None - entry = raw.get(cache_key) - if not isinstance(entry, dict): - return None - explanations_raw = entry.get("explanations") - if not isinstance(explanations_raw, dict): - return None - return { - str(metric_id): str(why).strip() - for metric_id, why in explanations_raw.items() - if str(metric_id).strip() and isinstance(why, str) and why.strip() - } - - -def _save_period_delta_explanations_cache( - db: Session, - evaluation: CallImportEvaluation, - cache_key: str, - explanations: dict[str, str], -) -> None: - raw = evaluation.period_delta_explanations - if not isinstance(raw, dict): - raw = {} - updated = dict(raw) - updated[cache_key] = { - "explanations": explanations, - "generated_at": datetime.now(timezone.utc).isoformat(), - } - evaluation.period_delta_explanations = updated - flag_modified(evaluation, "period_delta_explanations") - db.commit() - - -def _cluster_summary_for_metric( - state: Optional[EvaluationMetricClustersState], - metric_id: str, -) -> list[dict[str, Any]]: - if state is None or state.status != "completed": - return [] - for group in state.groups: - if str(group.metric_id) != metric_id: - continue - return [ - { - "label": cluster.label, - "gap_label": cluster.gap_label, - "share_pct": round(cluster.share_pct, 1), - "count": cluster.count, - } - for cluster in group.clusters[:5] - ] - return [] - - -def _merge_delta_why( - raw_deltas: dict[str, dict[str, str]], - explanations: dict[str, str], -) -> dict[str, dict[str, str]]: - if not explanations: - return raw_deltas - merged: dict[str, dict[str, str]] = {} - for metric_id, delta in raw_deltas.items(): - updated = dict(delta) - why = explanations.get(metric_id) - if why: - updated["why"] = why - merged[metric_id] = updated - return merged - - -def _explain_period_deltas( - db: Session, - organization_id: UUID, - evaluation: CallImportEvaluation, - baseline_evaluation: CallImportEvaluation, - raw_deltas: dict[str, dict[str, str]], - *, - min_delta_pp: float = 0.5, -) -> dict[str, dict[str, str]]: - """Attach ``why`` explanations to period deltas using cached LLM output.""" - if not raw_deltas: - return raw_deltas - - cache_key = _period_delta_explanation_cache_key( - baseline_evaluation.id, - completed_rows=evaluation.completed_rows, - baseline_completed_rows=baseline_evaluation.completed_rows, - ) - cached = _load_period_delta_explanations_cache(evaluation, cache_key) - if cached is not None: - return _merge_delta_why(raw_deltas, cached) - - current_clusters = _metric_clusters_payload(evaluation) - baseline_clusters = _metric_clusters_payload(baseline_evaluation) - metrics_for_prompt: list[dict[str, Any]] = [] - for metric_id, delta in raw_deltas.items(): - label = delta.get("label") or "" - if "No previous-week baseline" in label: - continue - match = re.search(r"([+-]?\d+(?:\.\d+)?)\s*pp", label) - if match and abs(float(match.group(1))) < min_delta_pp: - continue - current_summary = _cluster_summary_for_metric(current_clusters, metric_id) - baseline_summary = _cluster_summary_for_metric(baseline_clusters, metric_id) - if not current_summary and not baseline_summary: - continue - cluster_reconciliation = _reconcile_cluster_periods( - current_summary, - baseline_summary, - ) - metrics_for_prompt.append( - { - "metric_id": metric_id, - "delta_label": label, - "delta_detail": delta.get("detail") or "", - "cluster_reconciliation": cluster_reconciliation, - } - ) - - if not metrics_for_prompt: - return raw_deltas - - provider_hint: Optional[str] = None - model_hint: Optional[str] = None - tldr_raw = evaluation.tldr_summary - if isinstance(tldr_raw, dict): - if isinstance(tldr_raw.get("provider"), str): - provider_hint = tldr_raw["provider"] - if isinstance(tldr_raw.get("model"), str): - model_hint = tldr_raw["model"] - - from app.services.ai.llm_resolver import get_llm_provider_and_model - from app.services.call_import_user_insights import _call_llm, _parse_json_object - - provider_enum, model_str = get_llm_provider_and_model( - organization_id, db, provider_hint, model_hint - ) - try: - text = _call_llm( - db, - organization_id, - provider_enum, - model_str, - [ - {"role": "system", "content": _DELTA_EXPLANATION_SYSTEM_PROMPT}, - { - "role": "user", - "content": json.dumps( - {"metrics": metrics_for_prompt}, - ensure_ascii=False, - default=str, - ), - }, - ], - temperature=0.3, - max_tokens=900, - ) - except Exception as exc: - logger.warning("[PeriodDeltaExplain] LLM call failed: {}", exc) - return raw_deltas - - parsed = _parse_json_object(text) - explanations_raw = parsed.get("explanations") - explanations: dict[str, str] = {} - if isinstance(explanations_raw, dict): - for metric_id, why in explanations_raw.items(): - if isinstance(why, str) and why.strip(): - explanations[str(metric_id)] = why.strip() - - if explanations: - _save_period_delta_explanations_cache( - db, evaluation, cache_key, explanations - ) - return _merge_delta_why(raw_deltas, explanations) - - -def _period_deltas_with_explanations( - db: Session, - organization_id: UUID, - evaluation: CallImportEvaluation, - baseline_evaluation: CallImportEvaluation, - raw_deltas: dict[str, dict[str, str]], -) -> dict[str, dict[str, str]]: - return _explain_period_deltas( - db, - organization_id, - evaluation, - baseline_evaluation, - raw_deltas, - ) - - -def _benchmark_context_for_snapshot( - db: Session, - previous_snapshot: Optional[CallImportEvaluationReportSnapshot], -) -> Optional[dict[str, str]]: - if previous_snapshot is None: - return None - previous_import = ( - db.query(CallImport) - .filter(CallImport.id == previous_snapshot.call_import_id) - .first() - ) - previous_eval = ( - db.query(CallImportEvaluation) - .filter(CallImportEvaluation.id == previous_snapshot.evaluation_id) - .first() - ) - dataset = ( - (previous_import.dataset or "").strip() - if previous_import and previous_import.dataset - else None - ) - filename = ( - (previous_import.original_filename or previous_import.filename or "").strip() - if previous_import - else None - ) - evaluation_label = ( - (previous_eval.name or "").strip() - if previous_eval and previous_eval.name - else str(previous_snapshot.evaluation_id)[:8] - ) - period = previous_snapshot.period_label or ( - previous_snapshot.period_start.isoformat() - if previous_snapshot.period_start - else "previous report" - ) - return { - "dataset": dataset or filename or "Unknown dataset", - "evaluation": evaluation_label, - "evaluation_id": str(previous_snapshot.evaluation_id), - "period": period, - } - - -def _clamp_prose_to_sentences( - text: str, - *, - max_sentences: int = 3, - max_chars: int = 300, -) -> str: - """Keep concise audit/TLDR prose within sentence and character limits.""" - cleaned = (text or "").strip() - if not cleaned: - return cleaned - cleaned = re.sub(r"\s*\n+\s*", " ", cleaned).strip() - sentences = [ - sentence.strip() - for sentence in re.split(r"(?<=[.!?])\s+", cleaned) - if sentence.strip() - ] - if sentences: - result = " ".join(sentences[:max_sentences]).strip() - else: - result = cleaned - if len(result) > max_chars: - trimmed = result[: max_chars - 3].rsplit(" ", 1)[0].rstrip(".,;:") - result = f"{trimmed}..." if trimmed else result[:max_chars] - return result - - -def _audit_summary_text_from_tldr( - summary: Optional[EvaluationTldrSummary], -) -> Optional[str]: - if summary is None: - return None - narrative = _clamp_prose_to_sentences(summary.narrative.strip()) - return narrative or None - - -def _metric_insights_from_tldr( - summary: Optional[EvaluationTldrSummary], -) -> dict[str, str]: - if summary is None: - return {} - return { - str(metric_id): insight.strip() - for metric_id, insight in summary.metric_insights.items() - if str(metric_id).strip() and insight.strip() - } - - -def _report_period_from_rows( - rows: list[tuple[CallImportEvaluationRow, CallImportRow]], -) -> tuple[Optional[date], Optional[date], Optional[str], str]: - dates = [ - source_row.recording_date - for eval_row, source_row in rows - if eval_row.status == "completed" and source_row.recording_date - ] - if not dates: - return None, None, None, "Not specified" - start = min(dates) - end = max(dates) - week_anchor = max(dates) - week_start = week_anchor - timedelta(days=week_anchor.weekday()) - week_end = week_start + timedelta(days=6) - iso_year, iso_week, _ = week_anchor.isocalendar() - label = f"{iso_year}-W{iso_week:02d}" - if week_start.year == week_end.year: - week_range = f"{week_start.strftime('%b %d')}–{week_end.strftime('%b %d, %Y')}" - else: - week_range = ( - f"{week_start.strftime('%b %d, %Y')}–{week_end.strftime('%b %d, %Y')}" - ) - display = f"W{iso_week:02d} · {week_range}" - return start, end, label, display - - -def _aggregate_to_dict(aggregate: CallImportMetricAggregate) -> dict[str, Any]: - if hasattr(aggregate, "model_dump"): - return aggregate.model_dump(mode="json") - return aggregate.dict() - - -def _aggregate_primary_percent( - raw: dict[str, Any], - policy: Optional[MetricFailurePolicy] = None, -) -> Optional[float]: - return aggregate_primary_percent(raw, policy) - - -def _child_names_by_parent( - db: Session, - organization_id: UUID, - parent_metric_ids: Sequence[UUID], -) -> Dict[str, List[str]]: - if not parent_metric_ids: - return {} - children = ( - db.query(Metric) - .filter( - Metric.organization_id == organization_id, - Metric.parent_metric_id.in_(list(parent_metric_ids)), - ) - .all() - ) - out: Dict[str, List[str]] = {} - for child in children: - pid = str(child.parent_metric_id) - out.setdefault(pid, []).append(child.name) - return out - - -def _clustering_context( - db: Session, - evaluation: CallImportEvaluation, - eval_rows: List[CallImportEvaluationRow], -) -> Tuple[ - List[Metric], - List[CallImportMetricAggregate], - Dict[str, MetricFailurePolicy], - Literal["inferred", "user"], - Dict[str, List[str]], -]: - metrics = _metrics_for_clustering(db, evaluation, eval_rows) - aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) - parent_ids = [ - m.id - for m in metrics - if getattr(m, "selection_mode", None) and not getattr(m, "parent_metric_id", None) - ] - child_names_by_parent = _child_names_by_parent( - db, evaluation.organization_id, parent_ids - ) - policies, source = effective_policies( - evaluation, - metrics, - aggregates, - child_names_by_parent=child_names_by_parent, - ) - return metrics, aggregates, policies, source, child_names_by_parent - - -def _period_deltas_from_aggregates( - previous_metric_aggregates: list[dict[str, Any]], - current_metric_aggregates: list[dict[str, Any]], - policies: Optional[Dict[str, MetricFailurePolicy]] = None, -) -> dict[str, dict[str, str]]: - current_by_id = {str(item.get("metric_id")): item for item in current_metric_aggregates} - previous_by_id = { - str(item.get("metric_id")): item - for item in previous_metric_aggregates - if isinstance(item, dict) - } - deltas: dict[str, dict[str, str]] = {} - for metric_id, current in current_by_id.items(): - previous_raw = previous_by_id.get(metric_id) - policy = (policies or {}).get(metric_id) - current_pct = _aggregate_primary_percent(current, policy) - previous_pct = ( - _aggregate_primary_percent(previous_raw, policy) - if previous_raw - else None - ) - if current_pct is None or previous_pct is None: - deltas[metric_id] = { - "label": "No previous-week baseline", - "detail": "No comparable prior report snapshot was found.", - } - continue - delta = current_pct - previous_pct - sign = "+" if delta >= 0 else "" - deltas[metric_id] = { - "label": f"{sign}{delta:.1f} pp", - "detail": f"Current report {current_pct:.1f}% vs previous report {previous_pct:.1f}%", - } - return deltas - - -def _period_deltas_from_snapshot( - previous: Optional[CallImportEvaluationReportSnapshot], - current_metric_aggregates: list[dict[str, Any]], -) -> dict[str, dict[str, str]]: - previous_items = ( - previous.metric_aggregates - if previous and isinstance(previous.metric_aggregates, list) - else [] - ) - return _period_deltas_from_aggregates(previous_items, current_metric_aggregates) - - -def _sample_evidence_for_metrics( - rows: list[tuple[CallImportEvaluationRow, CallImportRow]], - metric_ids: set[str], -) -> dict[str, list[dict[str, str]]]: - samples: dict[str, list[dict[str, str]]] = {metric_id: [] for metric_id in metric_ids} - for eval_row, source_row in rows: - scores = eval_row.metric_scores if isinstance(eval_row.metric_scores, dict) else {} - for metric_id in metric_ids: - if len(samples.get(metric_id, [])) >= 4: - continue - score = scores.get(metric_id) - if not isinstance(score, dict): - continue - rationale = score.get("rationale") - transcript = source_row.diarised_transcript or source_row.transcript or "" - quote = rationale if isinstance(rationale, str) and rationale.strip() else transcript[:350] - if quote: - samples.setdefault(metric_id, []).append( - { - "conversation_id": source_row.conversation_id, - "quote": str(quote).strip()[:500], - } - ) - return samples - - -def _fallback_report_narrative( - insight_aggregates: list[dict[str, Any]], - evidence_samples: dict[str, list[dict[str, str]]], -) -> dict[str, Any]: - observations: dict[str, str] = {} - evidence: dict[str, dict[str, str]] = {} - design_notes: list[str] = [] - for aggregate in insight_aggregates: - metric_id = str(aggregate.get("metric_id") or "") - name = str(aggregate.get("metric_name") or "Insight") - counts = aggregate.get("value_counts") if isinstance(aggregate.get("value_counts"), list) else [] - if counts: - top = counts[0] - total = int(aggregate.get("count") or 0) or sum( - int(item.get("count") or 0) for item in counts if isinstance(item, dict) - ) - pct = (int(top.get("count") or 0) / total) * 100 if total else 0 - observations[metric_id] = ( - f"{top.get('label')} is the dominant {name.lower()} category at {pct:.1f}% of classified calls." - ) - design_notes.append( - f"{name}: {top.get('label')} is the largest segment and should be reviewed for workflow or prompt improvements." - ) - sample = (evidence_samples.get(metric_id) or [{}])[0] - if sample: - evidence[metric_id] = sample - return { - "observations": observations, - "evidence": evidence, - "design_notes": design_notes[:7], - "audit_summary": None, - } - - -def _generate_report_narrative( - db: Session, - organization_id: UUID, - *, - metric_aggregates: list[dict[str, Any]], - insight_aggregates: list[dict[str, Any]], - period_delta_by_metric: dict[str, dict[str, str]], - evidence_samples: dict[str, list[dict[str, str]]], - report_config: dict[str, Any], -) -> dict[str, Any]: - if not insight_aggregates: - return {"observations": {}, "evidence": {}, "design_notes": [], "audit_summary": None} - try: - from app.services.ai.llm_resolver import get_llm_provider_and_model - from app.services.ai.llm_service import llm_service - - provider_enum, model_str = get_llm_provider_and_model(organization_id, db, None, None) - prompt = ( - "You are writing a vendor-safe external call quality audit report. " - "Return strict JSON with keys observations (object keyed by metric_id), " - "evidence (object keyed by metric_id with conversation_id and quote), " - "design_notes (array of concise numbered-note strings), and audit_summary (string). " - "Use only the supplied aggregates and evidence samples.\n\n" - + json.dumps( - { - "metric_aggregates": metric_aggregates[:30], - "insight_aggregates": insight_aggregates, - "period_deltas": period_delta_by_metric, - "evidence_samples": evidence_samples, - "report_config": report_config, - }, - default=str, - ) - ) - llm_result = llm_service.generate_response( - messages=[ - {"role": "system", "content": "Return JSON only. No markdown."}, - {"role": "user", "content": prompt}, - ], - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - temperature=0.2, - max_tokens=1200, - ) - parsed = json.loads(str(llm_result.content or "{}")) - if isinstance(parsed, dict): - fallback = _fallback_report_narrative(insight_aggregates, evidence_samples) - return { - "observations": parsed.get("observations") or fallback["observations"], - "evidence": parsed.get("evidence") or fallback["evidence"], - "design_notes": parsed.get("design_notes") or fallback["design_notes"], - "audit_summary": parsed.get("audit_summary") or fallback["audit_summary"], - } - except Exception as exc: # noqa: BLE001 - logger.warning("Report narrative LLM generation fell back to deterministic text: {}", exc) - return _fallback_report_narrative(insight_aggregates, evidence_samples) - - -@router.get( - "/{eval_id}/baseline-candidates", - response_model=CallImportEvaluationBaselineCandidatesResponse, - operation_id="listCallImportEvaluationBaselineCandidates", -) -async def list_call_import_evaluation_baseline_candidates( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationBaselineCandidatesResponse: - del api_key - call_import = _require_import(db, call_import_id, organization_id) - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - rows = _evaluation_rows_for_period(db, evaluation.id) - period_start, _period_end, _derived_period_label, _period_display = _report_period_from_rows( - rows - ) - candidates = _baseline_candidate_evaluations( - db, - organization_id, - call_import.workspace_id, - evaluation, - period_start, - ) - default_evaluation_id = next( - (item["evaluation_id"] for item in candidates if item.get("is_default")), - None, - ) - return CallImportEvaluationBaselineCandidatesResponse( - items=[CallImportEvaluationBaselineCandidate(**item) for item in candidates], - default_evaluation_id=default_evaluation_id, - ) - - -@router.post( - "/{eval_id}/pdf-report", - operation_id="generateCallImportEvaluationPdfReport", - dependencies=[Depends(require_call_import_capability(REPORTS_GENERATE))], -) -async def generate_call_import_evaluation_pdf_report( - call_import_id: UUID, - eval_id: UUID, - payload: CallImportEvaluationPdfReportRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> StreamingResponse: - del api_key - call_import = _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - is_internal = payload.report_type == "internal" - from app.db_sharding.scatter_gather import load_evaluation_row_pairs - from app.db_sharding.sessions import is_sharding_enabled - - if is_sharding_enabled(): - rows = sorted( - load_evaluation_row_pairs(db, eval_id), - key=lambda pair: int(pair[1].row_index or 0), - ) - else: - rows = ( - db.query(CallImportEvaluationRow, CallImportRow) - .join( - CallImportRow, - CallImportRow.id == CallImportEvaluationRow.call_import_row_id, - ) - .filter(CallImportEvaluationRow.evaluation_id == eval_id) - .order_by(CallImportRow.row_index.asc()) - .all() - ) - report_config = payload.report_config if isinstance(payload.report_config, dict) else {} - metrics = _display_metrics_for_pdf_report(db, organization_id, evaluation) - configured_quality_ids = { - str(item) - for item in report_config.get("quality_metric_ids", []) - if item - } - configured_insight_ids = { - str(item.get("metric_id") or item) - for item in report_config.get("insights", []) - if item - } - if configured_quality_ids or configured_insight_ids: - allowed_ids = configured_quality_ids | configured_insight_ids - metrics = [metric for metric in metrics if str(metric.id) in allowed_ids] - - eval_rows = [eval_row for eval_row, _source_row in rows] - aggregate_models = _compute_metric_aggregates(db, evaluation, eval_rows) - selected_report_metric_ids = {str(metric.id) for metric in metrics} - aggregate_dicts = [ - _aggregate_to_dict(aggregate) - for aggregate in aggregate_models - if aggregate.metric_id in selected_report_metric_ids - ] - insight_metric_ids = { - str(metric.id) - for metric in metrics - if _metric_is_user_insight(metric) - } - metric_aggregates = [ - item for item in aggregate_dicts if str(item.get("metric_id")) not in insight_metric_ids - ] - insight_aggregates = [ - item for item in aggregate_dicts if str(item.get("metric_id")) in insight_metric_ids - ] - period_start, period_end, derived_period_label, period_display = _report_period_from_rows(rows) - period_label = (payload.period_label or derived_period_label or "").strip() or None - include_period_delta = ( - payload.include_period_delta or payload.include_weekly_delta - ) - previous_snapshot = None - period_delta_by_metric: dict[str, dict[str, str]] = {} - baseline_evaluation: Optional[CallImportEvaluation] = None - if include_period_delta and period_start: - baseline_evaluation = _resolve_baseline_evaluation( - db, - organization_id, - call_import.workspace_id, - evaluation, - period_start, - payload.baseline_evaluation_id, - ) - if baseline_evaluation: - period_delta_by_metric = _period_deltas_from_evaluation( - db, - baseline_evaluation, - metric_aggregates, - evaluation, - [eval_row for eval_row, _ in rows], - ) - period_delta_by_metric = _period_deltas_with_explanations( - db, - organization_id, - evaluation, - baseline_evaluation, - period_delta_by_metric, - ) - benchmark_context = _benchmark_context_for_evaluation(db, baseline_evaluation) - evidence_samples = _sample_evidence_for_metrics(rows, insight_metric_ids) - cached_tldr_summary = _tldr_summary_payload(evaluation) - cached_user_insights = _user_insights_payload(evaluation) - cached_metric_clusters = _metric_clusters_payload(evaluation) - cached_prompt_improvements = _prompt_improvements_payload(evaluation) - generated_insights_for_pdf = _selected_generated_user_insights( - cached_user_insights, - report_config, - ) - metric_clusters_for_pdf = _selected_metric_clusters_for_pdf( - cached_metric_clusters, - report_config, - ) - prompt_improvements_for_pdf = _selected_prompt_improvements_for_pdf( - cached_prompt_improvements, - report_config, - ) - narrative = _generate_report_narrative( - db, - organization_id, - metric_aggregates=metric_aggregates, - insight_aggregates=insight_aggregates if is_internal else [], - period_delta_by_metric=period_delta_by_metric, - evidence_samples=evidence_samples if is_internal else {}, - report_config=report_config, - ) - - generated_at = datetime.now(timezone.utc) - branding_images, custom_heading = _report_branding_for_import_workspace( - db, - organization_id, - call_import.workspace_id, - internal_brand_image_id=payload.internal_brand_image_id, - external_brand_image_id=payload.external_brand_image_id, - ) - eval_row_list = [eval_row for eval_row, _ in rows] - pdf_aggregates = _compute_metric_aggregates(db, evaluation, eval_row_list) - pdf_parent_ids = [ - m.id - for m in metrics - if getattr(m, "selection_mode", None) - and not getattr(m, "parent_metric_id", None) - ] - pdf_child_map = _child_names_by_parent( - db, evaluation.organization_id, pdf_parent_ids - ) - failure_policies_for_pdf, _fp_source = effective_policies( - evaluation, - metrics, - pdf_aggregates, - child_names_by_parent=pdf_child_map, - ) - try: - pdf_started = datetime.now(timezone.utc) - pdf_bytes = await asyncio.to_thread( - call_import_evaluation_pdf_report_service.render_pdf, - vendor_name=payload.vendor_name, - call_import=call_import, - evaluation=evaluation, - metrics=metrics, - rows=rows, - failure_policies=failure_policies_for_pdf, - generated_at=generated_at, - internal=is_internal, - logo_data_uris=branding_images, - custom_heading=custom_heading, - include_weekly_delta=include_period_delta, - period_delta_by_metric=period_delta_by_metric, - use_case=payload.use_case, - period_display=period_display, - total_metric_count=db.query(Metric) - .filter(Metric.organization_id == organization_id, Metric.enabled.is_(True)) - .count(), - report_config=report_config, - narrative=narrative, - audit_summary=_audit_summary_text_from_tldr(cached_tldr_summary), - metric_insights=_metric_insights_from_tldr(cached_tldr_summary), - benchmark_context=benchmark_context, - generated_user_insights=generated_insights_for_pdf, - user_insights_overview=( - cached_user_insights.overview if cached_user_insights else None - ), - metric_clusters=metric_clusters_for_pdf, - metric_clusters_overview=( - cached_metric_clusters.overview if cached_metric_clusters else None - ), - prompt_improvements=prompt_improvements_for_pdf, - platform_base_url=payload.platform_base_url, - ) - logger.info( - "PDF report render finished in {:.1f}s for evaluation {}", - (datetime.now(timezone.utc) - pdf_started).total_seconds(), - eval_id, - ) - except Exception as exc: # noqa: BLE001 - logger.exception( - "Failed to generate PDF report for call import {} evaluation {}", - call_import_id, - eval_id, - ) - raise HTTPException( - status_code=500, - detail=f"Failed to generate PDF report: {exc}", - ) from exc - - snapshot = CallImportEvaluationReportSnapshot( - evaluation_id=evaluation.id, - call_import_id=call_import.id, - organization_id=organization_id, - workspace_id=call_import.workspace_id, - period_label=period_label, - period_start=period_start, - period_end=period_end, - report_config=report_config, - selected_metric_ids=[str(metric.id) for metric in metrics], - metric_aggregates=metric_aggregates, - insight_aggregates=insight_aggregates, - narrative=narrative, - total_calls=evaluation.total_rows, - selected_metric_count=len(metrics), - total_metric_count=db.query(Metric) - .filter(Metric.organization_id == organization_id, Metric.enabled.is_(True)) - .count(), - ) - db.add(snapshot) - db.commit() - - filename = ( - f"{_report_filename_slug(payload.vendor_name)}-" - f"{payload.report_type}-quality-metric-audit-{eval_id}.pdf" - ) - return StreamingResponse( - iter([pdf_bytes]), - media_type="application/pdf", - headers={"Content-Disposition": f'attachment; filename="{filename}"'}, - ) - - -@router.patch( - "/{eval_id}", - response_model=CallImportEvaluationResponse, - operation_id="updateCallImportEvaluation", -) -async def update_call_import_evaluation( - call_import_id: UUID, - eval_id: UUID, - payload: CallImportEvaluationUpdate, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationResponse: - """Edit metadata on an existing evaluation run (currently just ``name``).""" - - del api_key - _require_import(db, call_import_id, organization_id) - - row = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not row: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - # Treat unset vs explicit ``None`` differently: unset = leave alone, - # explicit ``None`` or empty string = clear the name. - payload_data = payload.model_dump(exclude_unset=True) - if "name" in payload_data: - row.name = _normalize_name(payload_data["name"]) - - db.commit() - db.refresh(row) - return _serialize_eval(db, row) - - -def _revoke_pending_tasks(evaluation: CallImportEvaluation) -> None: - """Best-effort cancel of any in-flight Celery tasks for an evaluation.""" - - if not evaluation.celery_group_id and not any( - r.celery_task_id for r in evaluation.row_results - ): - return - try: - from app.workers.celery_app import celery_app - - pending_task_ids = [ - eval_row.celery_task_id - for eval_row in evaluation.row_results - if eval_row.celery_task_id - and eval_row.status in {"pending", "running"} - ] - if pending_task_ids: - celery_app.control.revoke(pending_task_ids, terminate=False) - except Exception: - # Best effort — DB delete remains the source of truth. - pass - - -# --------------------------------------------------------------------------- -# User-initiated cancel for in-flight evaluation rows -# --------------------------------------------------------------------------- -# -# Evaluation rows can sit in ``running`` for many minutes when the underlying -# LLM / audio metric call is slow or wedged (the worker carries an 8 min -# soft / 10 min hard time limit). Without a cancel affordance the operator's -# only recourse is to wait for Celery's time limit to fire — or to manually -# mutate the DB. These helpers + the two endpoints below give the UI a -# first-class "Abort" button mirroring the diarisation cancel pattern at -# ``app.api.v1.routes.call_imports`` (``_apply_diarisation_cancel`` etc.). -# -# Why ``terminate=True``: the legacy ``_revoke_pending_tasks`` above uses -# ``terminate=False`` because it's called from delete-flow paths where the -# task may simply not get to run (a worker pulls it off the queue and drops -# it). For a user-initiated cancel we want SIGTERM to interrupt the worker -# mid-LLM/audio call so the in-flight HTTP request actually aborts. -# ``terminate=True`` routes the signal to the executing process; we spell -# ``signal="SIGTERM"`` out for clarity even though it's the default. - -# Sentinel error message stamped on cancelled rows. Read by the eval worker's -# ``_was_cancelled_externally`` guard (see -# :mod:`app.workers.tasks.evaluate_call_import_row`) so a worker that's already -# past its slowest operation can't overwrite the cancelled state with its own -# terminal status. Touching either copy means touching both. -EVAL_CANCELLED_BY_USER_ERROR: str = "Evaluation cancelled by user" - - -def _cancellable_eval_states() -> Tuple[str, ...]: - """States that an evaluation row can be cancelled from. - - Kept as a tiny helper so adding a future ``"queued"`` / ``"retrying"`` - state only needs one edit. - """ - return ("pending", "running") - - -def _revoke_eval_task(eval_row: CallImportEvaluationRow) -> None: - """Best-effort revoke of a single eval row's Celery task. - - Always swallows control-plane exceptions — Celery's control bus is - inherently best-effort and a missed revoke is not catastrophic - because the DB row is already flipped to ``failed`` by the caller - before this runs (so the UI immediately reflects the cancel; if - the task happens to finish anyway, the worker's finaliser skips - over the row via :data:`EVAL_CANCELLED_BY_USER_ERROR`). - """ - task_id = (eval_row.celery_task_id or "").strip() - if not task_id: - return - try: - from app.workers.celery_app import celery_app - - celery_app.control.revoke( - task_id, terminate=True, signal="SIGTERM" - ) - logger.info( - "Revoked evaluation task {} for eval row {}", - task_id, - eval_row.id, - ) - except Exception as exc: # noqa: BLE001 — revoke is best-effort - logger.warning( - "Failed to revoke evaluation task {} for eval row {}: {}", - task_id, - eval_row.id, - exc, - ) - - -def _apply_evaluation_cancel( - eval_rows: List[CallImportEvaluationRow], -) -> Tuple[int, int]: - """Cancel every cancellable row in ``eval_rows``. - - Returns ``(cancelled, skipped)`` so the caller can build a typed - response without re-querying the DB. The caller is responsible for - ``db.commit()`` after this returns — we deliberately don't commit - here so a batch endpoint can flush all rows in one transaction. - """ - cancellable_states = _cancellable_eval_states() - cancelled = 0 - skipped = 0 - now = datetime.now(timezone.utc) - for eval_row in eval_rows: - if (eval_row.status or "").lower() not in cancellable_states: - skipped += 1 - continue - # Flip the row state BEFORE we revoke so the UI's next poll - # already shows the cancel, even if Celery's control plane is - # slow to ack. - eval_row.status = "failed" - eval_row.error_message = EVAL_CANCELLED_BY_USER_ERROR - eval_row.finished_at = now - _revoke_eval_task(eval_row) - # Drop the task id so a follow-up retry (or a stale poll) can't - # accidentally re-revoke or get confused. - eval_row.celery_task_id = None - cancelled += 1 - return cancelled, skipped - - -def _claim_evaluation_bulk_operation( - evaluation_id: UUID, - operation: str, -) -> None: - """Reserve the run for a single bulk worker pass; 409 if one is active.""" - from app.services.call_imports.evaluation_bulk_op import ( - get_evaluation_bulk_operation, - try_set_evaluation_bulk_operation, - ) - - if try_set_evaluation_bulk_operation(evaluation_id, operation): # type: ignore[arg-type] - return - existing = get_evaluation_bulk_operation(evaluation_id) or operation - raise HTTPException( - status_code=409, - detail=( - f"A bulk {existing.replace('_', ' ')} operation is already in " - "progress for this evaluation. Wait for it to finish before " - "starting another action." - ), - ) - - -def _require_no_evaluation_bulk_operation(evaluation_id: UUID) -> None: - from app.services.call_imports.evaluation_bulk_op import ( - get_evaluation_bulk_operation, - ) - - existing = get_evaluation_bulk_operation(evaluation_id) - if existing: - raise HTTPException( - status_code=409, - detail=( - f"A bulk {existing.replace('_', ' ')} operation is already in " - "progress for this evaluation. Wait for it to finish before " - "starting another action." - ), - ) - - -@router.post( - "/{eval_id}/cancel", - response_model=CallImportEvaluationBulkActionResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="cancelCallImportEvaluation", -) -async def cancel_call_import_evaluation( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationBulkActionResponse: - """Abort all in-flight (or queued) rows in a single evaluation run. - - Idempotent: calling on a run whose rows are already terminal returns - ``target_count=0`` with 202 so the UI can fire this from an - "Abort" button without having to pre-check the state. - - Heavy row resets and Celery revokes run in a background worker so - large batches do not block the API thread. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - from app.services.call_imports.bulk_ops import count_evaluation_cancel_targets - - target_count = count_evaluation_cancel_targets(db, eval_id, mode="abort") - if target_count == 0: - return CallImportEvaluationBulkActionResponse( - accepted=True, - target_count=0, - evaluation_id=eval_id, - ) - - _claim_evaluation_bulk_operation(eval_id, "abort") - evaluation.status = "cancelled" - db.commit() - - from app.workers.tasks.call_import_bulk_ops import ( - cancel_call_import_evaluation_task, - ) - - cancel_call_import_evaluation_task.delay(str(eval_id), mode="abort") - return CallImportEvaluationBulkActionResponse( - accepted=True, - target_count=target_count, - evaluation_id=eval_id, - ) - - -@router.post( - "/{eval_id}/force-fail-pending", - response_model=CallImportEvaluationBulkActionResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="forceFailCallImportEvaluationPending", -) -async def force_fail_pending_call_import_evaluation_rows( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationBulkActionResponse: - """Force-fail only rows currently in ``pending`` for a single run. - - This is narrower than :func:`cancel_call_import_evaluation`: it leaves - ``running`` rows untouched so operators can clear permanently queued rows - without interrupting in-flight evaluations. - - Row updates run in a background worker so large batches do not block - the API thread. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - from app.services.call_imports.bulk_ops import count_evaluation_cancel_targets - - target_count = count_evaluation_cancel_targets( - db, eval_id, mode="force_fail_pending" - ) - if target_count == 0: - return CallImportEvaluationBulkActionResponse( - accepted=True, - target_count=0, - evaluation_id=eval_id, - ) - - _claim_evaluation_bulk_operation(eval_id, "force_fail_pending") - - from app.workers.tasks.call_import_bulk_ops import ( - cancel_call_import_evaluation_task, - ) - - cancel_call_import_evaluation_task.delay( - str(eval_id), mode="force_fail_pending" - ) - return CallImportEvaluationBulkActionResponse( - accepted=True, - target_count=target_count, - evaluation_id=eval_id, - ) - - -@router.post( - "/{eval_id}/rows/{eval_row_id}/cancel", - response_model=CallImportEvaluationRowResponse, - status_code=status.HTTP_200_OK, - operation_id="cancelCallImportEvaluationRow", -) -async def cancel_call_import_evaluation_row( - call_import_id: UUID, - eval_id: UUID, - eval_row_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationRowResponse: - """Abort an in-flight (or queued) evaluation for a single row. - - Idempotent: calling on a row that's already terminal (``completed`` - / ``failed``) returns the row unchanged with a 200 so the UI can - wire this to a "Stop" button without having to pre-check the - state. Updates the parent run's rollup so its counters reflect - the cancel immediately. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - _require_no_evaluation_bulk_operation(eval_id) - - from app.db_sharding.eval_rows import evaluation_row_session - from app.db_sharding.sessions import is_sharding_enabled - - if is_sharding_enabled(): - try: - with evaluation_row_session(eval_row_id) as ( - row_db, - _catalog_db, - eval_row, - source_row, - _shard_id, - ): - if eval_row.evaluation_id != eval_id: - raise HTTPException( - status_code=404, - detail="Evaluation row not found in this run", - ) - _apply_evaluation_cancel([eval_row]) - row_db.commit() - _rollup_evaluation_status(evaluation, db) - db.commit() - row_db.refresh(eval_row) - return _to_evaluation_row_response(eval_row, source_row, evaluation) - except LookupError as exc: - raise HTTPException( - status_code=404, detail="Evaluation row not found in this run" - ) from exc - - eval_row = ( - db.query(CallImportEvaluationRow) - .filter( - CallImportEvaluationRow.id == eval_row_id, - CallImportEvaluationRow.evaluation_id == eval_id, - ) - .first() - ) - if not eval_row: - raise HTTPException( - status_code=404, detail="Evaluation row not found in this run" - ) - - _apply_evaluation_cancel([eval_row]) - db.flush() - _rollup_evaluation_status(evaluation, db) - db.commit() - db.refresh(eval_row) - - source_row = ( - db.query(CallImportRow) - .filter(CallImportRow.id == eval_row.call_import_row_id) - .first() - ) - - return _to_evaluation_row_response(eval_row, source_row, evaluation) - - -@router.delete( - "/{eval_id}", - status_code=status.HTTP_204_NO_CONTENT, - operation_id="deleteCallImportEvaluation", -) -async def delete_call_import_evaluation( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Response: - del api_key - _require_import(db, call_import_id, organization_id) - - row = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not row: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - _revoke_pending_tasks(row) - - db.delete(row) - db.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - -@router.post( - "/bulk-delete", - status_code=status.HTTP_200_OK, - operation_id="bulkDeleteCallImportEvaluations", -) -async def bulk_delete_call_import_evaluations( - call_import_id: UUID, - payload: CallImportEvaluationBulkDelete, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Dict[str, int]: - """Delete multiple evaluation runs scoped to one call import. - - Mirrors :func:`delete_call_import_evaluation` but in bulk so the UI - can clear out a multi-select. Unknown ids (already deleted, or - belonging to a different org/import) are silently skipped — the - response just reports how many actually went away. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - if not payload.evaluation_ids: - return {"deleted": 0} - - rows = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id.in_(payload.evaluation_ids), - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .all() - ) - deleted = 0 - for row in rows: - _revoke_pending_tasks(row) - db.delete(row) - deleted += 1 - db.commit() - return {"deleted": deleted} - - -# --------------------------------------------------------------------------- -# Aggregation: turns per-row metric scores into histograms / value counts. -# -# Designed to be cheap enough to call on every page load: we read each -# evaluation row once, bucket numeric values into a fixed 10-bin -# histogram, and tally the top categorical values. Scaling concerns -# (millions of rows) are deferred — at that point we'd push this into a -# Postgres aggregate query, but for typical CSV imports (<10k rows) the -# Python pass is fast enough and dramatically simpler. -# --------------------------------------------------------------------------- - - -_HISTOGRAM_BUCKETS = 10 -_TOP_VALUE_COUNTS = 10 - - -def _coerce_numeric(value: Any) -> Optional[float]: - """Return ``value`` as ``float`` when it's numeric; ``None`` otherwise.""" - if isinstance(value, bool): - # Booleans are ints in Python; treat them as categorical so - # pass/fail metrics show up in value_counts instead of becoming - # a degenerate {0,1} histogram. - return None - if isinstance(value, (int, float)) and math.isfinite(value): - return float(value) - if isinstance(value, str): - try: - f = float(value) - if math.isfinite(f): - return f - except ValueError: - return None - return None - - -def _coerce_category(value: Any) -> Optional[str]: - """Render ``value`` as a label suitable for a value_counts bucket.""" - if value is None: - return None - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, (int, float)): - return str(value) - if isinstance(value, str): - text = value.strip() - return text or None - # Lists / dicts: stringify so they still group sensibly without - # exploding the cardinality (worst case: everything is "[…]" once). - return str(value) - - -def _build_histogram( - values: List[float], -) -> List[CallImportMetricHistogramBucket]: - """Fixed-bin histogram over ``values``; returns [] for <2 values.""" - if len(values) < 2: - return [] - lo = min(values) - hi = max(values) - if lo == hi: - # All values identical — render a single bucket so the UI shows a - # spike rather than empty space. - return [ - CallImportMetricHistogramBucket(x0=lo, x1=hi, count=len(values)) - ] - width = (hi - lo) / _HISTOGRAM_BUCKETS - buckets: List[List[float]] = [[] for _ in range(_HISTOGRAM_BUCKETS)] - for v in values: - # Right-edge inclusive on the last bucket so ``hi`` doesn't fall - # off into a non-existent bucket index. - idx = int((v - lo) / width) - if idx >= _HISTOGRAM_BUCKETS: - idx = _HISTOGRAM_BUCKETS - 1 - buckets[idx].append(v) - return [ - CallImportMetricHistogramBucket( - x0=lo + i * width, - x1=lo + (i + 1) * width, - count=len(bucket), - ) - for i, bucket in enumerate(buckets) - ] - - -def _percentile(values: List[float], pct: float) -> Optional[float]: - """Linear-interpolated percentile compatible with NumPy default.""" - if not values: - return None - sorted_vals = sorted(values) - if len(sorted_vals) == 1: - return sorted_vals[0] - rank = (pct / 100.0) * (len(sorted_vals) - 1) - lo = int(math.floor(rank)) - hi = int(math.ceil(rank)) - if lo == hi: - return sorted_vals[lo] - frac = rank - lo - return sorted_vals[lo] + (sorted_vals[hi] - sorted_vals[lo]) * frac - - -def _compute_metric_aggregates( - db: Session, - evaluation: CallImportEvaluation, - eval_rows: List[CallImportEvaluationRow], -) -> List[CallImportMetricAggregate]: - """Collapse per-row ``metric_scores`` into one aggregate per metric. - - Selected metrics are read fresh from the DB so the response always - surfaces the current ``metric.name`` / ``metric_type`` even when a - metric was renamed after the run finished. - """ - - selected_ids = _serialize_selected_metric_ids(evaluation.selected_metric_ids) - # Include parent metrics from selected_metric_groups so they appear - # alongside their children in the aggregate response. Use ``getattr`` - # with a default so the helper still works for callers that pass - # lightweight objects (tests, in-memory shims) that don't carry the - # attribute at all. - groups_raw_candidate = getattr(evaluation, "selected_metric_groups", None) - groups_raw = ( - groups_raw_candidate if isinstance(groups_raw_candidate, dict) else {} - ) - for parent_str in groups_raw.keys(): - try: - pid = UUID(parent_str) - if pid not in selected_ids: - selected_ids.append(pid) - except (TypeError, ValueError): - continue - - metrics = _metrics_for_ids(db, evaluation.organization_id, selected_ids) - metric_meta: Dict[str, Metric] = {str(m.id): m for m in metrics} - - # Default to selected metrics, but also include any metric ids that - # surface in row scores even if missing from the metric registry — - # otherwise renaming/deleting a metric mid-run would silently drop - # results from the chart. - discovered_ids: List[str] = list(metric_meta.keys()) - for row in eval_rows: - scores = row.metric_scores if isinstance(row.metric_scores, dict) else {} - for metric_id_str in scores.keys(): - if metric_id_str not in metric_meta and metric_id_str not in discovered_ids: - discovered_ids.append(metric_id_str) - - results: List[CallImportMetricAggregate] = [] - - for metric_id_str in discovered_ids: - meta = metric_meta.get(metric_id_str) - numeric_values: List[float] = [] - category_counts: Dict[str, int] = {} - # For multi-label parents we still need to know how many rows - # were scored (each row votes for >=1 label) so the n-badge in - # the UI shows "n=50" instead of the misleading "n=208" sum. - multi_label_rows_scored = 0 - # Unordered pair tally for the co-occurrence heatmap. Keys are - # ``(label_a, label_b)`` with ``a < b`` so we never double-count - # the same unordered pair. Only populated for multi-label - # parents — every other metric leaves this empty. - multi_label_pair_counts: Dict[Tuple[str, str], int] = {} - skipped = 0 - errored = 0 - observed_metric_type: Optional[str] = None - observed_name: Optional[str] = None - - # ``meta`` is a real ``Metric`` row in production, but tests - # frequently pass a lightweight stub. Pull the two attributes - # we need via ``getattr`` so a stub that only sets ``id`` / - # ``name`` / ``metric_type`` doesn't blow up here. - is_multi_label_parent = bool( - meta - and getattr(meta, "selection_mode", None) == "multi_label" - and not getattr(meta, "parent_metric_id", None) - ) - - for row in eval_rows: - scores = ( - row.metric_scores - if isinstance(row.metric_scores, dict) - else {} - ) - entry = scores.get(metric_id_str) - if not isinstance(entry, dict): - continue - if entry.get("metric_name"): - observed_name = entry.get("metric_name") - if entry.get("type"): - observed_metric_type = entry.get("type") - if entry.get("skipped"): - skipped += 1 - continue - if entry.get("error"): - errored += 1 - continue - - # Multi-label parents store a comma-joined value that - # isn't useful as a single category; instead tally each - # selected child individually so the chart shows per-label - # counts that mirror the children's own boolean histograms. - if is_multi_label_parent: - selected = entry.get("selected_child_names") - if isinstance(selected, list) and selected: - multi_label_rows_scored += 1 - cleaned: List[str] = [] - for label in selected: - text_label = str(label).strip() or None - if text_label: - cleaned.append(text_label) - category_counts[text_label] = ( - category_counts.get(text_label, 0) + 1 - ) - # Emit one increment per unordered pair of distinct - # labels that fired together on this row. ``cleaned`` - # is deduplicated first because the LLM occasionally - # repeats a label inside ``selected_child_names``. - distinct = sorted(set(cleaned)) - for i in range(len(distinct)): - for j in range(i + 1, len(distinct)): - pair = (distinct[i], distinct[j]) - multi_label_pair_counts[pair] = ( - multi_label_pair_counts.get(pair, 0) + 1 - ) - continue - - value = entry.get("value") - numeric = _coerce_numeric(value) - if numeric is not None: - numeric_values.append(numeric) - continue - category = _coerce_category(value) - if category is not None: - category_counts[category] = category_counts.get(category, 0) + 1 - - # ``count`` is "rows scored". For numeric / single-choice - # metrics that's the same as ``len(numeric) + sum(categories)`` - # because each scored row contributes exactly one observation. - # Multi-label parents however contribute one observation per - # selected child, so summing ``category_counts`` over-counts — - # we tracked rows-scored separately above and use it here. - rows_scored = ( - multi_label_rows_scored - if is_multi_label_parent - else len(numeric_values) + sum(category_counts.values()) - ) - - # Build numeric stats first, then categorical (both can coexist). - agg = CallImportMetricAggregate( - metric_id=metric_id_str, - metric_name=( - (meta.name if meta else observed_name) or "Unknown metric" - ), - metric_type=( - meta.metric_type if meta else observed_metric_type - ), - metric_category=( - "user_insight" - if meta is not None and _metric_is_user_insight(meta) - else "quality" - ) - or "quality", - is_multi_label_parent=is_multi_label_parent, - count=rows_scored, - skipped_count=skipped, - error_count=errored, - ) - if numeric_values: - agg.mean = float(statistics.fmean(numeric_values)) - agg.median = float(statistics.median(numeric_values)) - agg.min = min(numeric_values) - agg.max = max(numeric_values) - agg.stddev = ( - float(statistics.pstdev(numeric_values)) - if len(numeric_values) > 1 - else 0.0 - ) - agg.p25 = _percentile(numeric_values, 25) - agg.p75 = _percentile(numeric_values, 75) - agg.p95 = _percentile(numeric_values, 95) - agg.histogram_buckets = _build_histogram(numeric_values) - if category_counts: - sorted_counts = sorted( - category_counts.items(), key=lambda kv: kv[1], reverse=True - ) - agg.value_counts = [ - CallImportMetricValueCount(label=label, count=count) - for label, count in sorted_counts[:_TOP_VALUE_COUNTS] - ] - # Restrict the heatmap to pairs of labels we actually - # rendered above so the frontend never has to match - # against truncated/missing rows. Sorted desc by pair - # count to keep the most informative cells in the - # response when ``_TOP_VALUE_COUNTS`` clipped the matrix. - if is_multi_label_parent and multi_label_pair_counts: - kept_labels = { - label for label, _ in sorted_counts[:_TOP_VALUE_COUNTS] - } - pair_items = [ - (a, b, count) - for (a, b), count in multi_label_pair_counts.items() - if a in kept_labels and b in kept_labels - ] - pair_items.sort(key=lambda t: t[2], reverse=True) - agg.co_occurrence = [ - CallImportMetricLabelPair(a=a, b=b, count=count) - for a, b, count in pair_items - ] - - results.append(agg) - - # Sort so each parent metric immediately precedes its children. - # The Visualizations grid renders metrics top-to-bottom in this - # order, so multi-label parents (the "summary" chart) sit above - # the per-child boolean histograms that drill into them. Metrics - # whose ``meta`` row was deleted mid-run (``meta is None``) sink - # to the bottom but keep their relative order. - enumerated = list(enumerate(results)) - - def _sort_key(item: Tuple[int, CallImportMetricAggregate]): - original_idx, agg = item - meta = metric_meta.get(agg.metric_id) - if meta is None: - return (1, "", 1, "", original_idx) - parent_id = getattr(meta, "parent_metric_id", None) - # Group key: a child shares its parent's UUID; a parent - # uses its own UUID. Within a group, depth=0 (parent) sorts - # before depth=1 (child); ties break alphabetically by name - # so children render in a stable order regardless of which - # row scored which label first. - if parent_id is None: - group_key = str(meta.id) - depth = 0 - else: - group_key = str(parent_id) - depth = 1 - return ( - 0, - group_key, - depth, - (getattr(meta, "name", "") or "").lower(), - original_idx, - ) - - enumerated.sort(key=_sort_key) - return [agg for _idx, agg in enumerated] - - -@router.get( - "/{eval_id}/aggregate", - response_model=CallImportEvaluationAggregateResponse, - operation_id="getCallImportEvaluationAggregate", -) -async def get_call_import_evaluation_aggregate( - call_import_id: UUID, - eval_id: UUID, - baseline_evaluation_id: Optional[UUID] = Query(None), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationAggregateResponse: - """Return per-metric distributions for the Visualizations tab. - - The shape is intentionally chart-friendly: histograms for numeric - metrics, top-N value counts for categorical/text metrics, plus - summary stats (mean/p50/p95) so the UI can render summary cards - without recomputing on the client. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - eval_rows = _load_eval_rows(db, eval_id) - - metrics = _compute_metric_aggregates(db, evaluation, eval_rows) - - period_deltas: dict[str, MetricPeriodDelta] = {} - resolved_baseline_id: Optional[UUID] = None - if baseline_evaluation_id is not None: - call_import = _require_import(db, call_import_id, organization_id) - from app.db_sharding.scatter_gather import load_evaluation_row_pairs - - rows = load_evaluation_row_pairs(db, eval_id) - period_start, _, _, _ = _report_period_from_rows(rows) - baseline_evaluation = _resolve_baseline_evaluation( - db, - organization_id, - call_import.workspace_id, - evaluation, - period_start, - str(baseline_evaluation_id), - ) - if baseline_evaluation: - resolved_baseline_id = baseline_evaluation.id - metric_aggregates_dicts = [ - _aggregate_to_dict(agg) for agg in metrics - ] - raw_deltas = _period_deltas_from_evaluation( - db, - baseline_evaluation, - metric_aggregates_dicts, - evaluation, - eval_rows, - ) - raw_deltas = _period_deltas_with_explanations( - db, - organization_id, - evaluation, - baseline_evaluation, - raw_deltas, - ) - period_deltas = { - metric_id: MetricPeriodDelta( - label=delta.get("label") or "", - detail=delta.get("detail") or "", - why=(delta.get("why") or "").strip() or None, - ) - for metric_id, delta in raw_deltas.items() - } - - _fp_stored, failure_policies_source = policies_from_evaluation_raw( - evaluation.metric_clusters - ) - return CallImportEvaluationAggregateResponse( - evaluation_id=eval_id, - total_rows=evaluation.total_rows, - completed_rows=evaluation.completed_rows, - failed_rows=evaluation.failed_rows, - metrics=metrics, - period_deltas=period_deltas, - baseline_evaluation_id=resolved_baseline_id, - failure_policies_source=failure_policies_source, - ) - - -# --------------------------------------------------------------------------- -# TLDR insights: LLM-generated narrative + bullet patterns rendered above -# the Visualizations charts. Cached on ``CallImportEvaluation.tldr_summary`` -# so the page never auto-burns LLM tokens; the user explicitly clicks -# "Generate summary" or "Regenerate" from the empty-state CTA. -# --------------------------------------------------------------------------- - - -_INSIGHTS_SYSTEM_PROMPT = ( - "You are a senior conversation-analytics reviewer. You will be " - "given aggregated metric statistics + a sample of rationales for " - "the rows of a single call-import evaluation. Identify the most " - "useful PATTERNS that hold ACROSS the calls -- not just per-metric " - "numbers. Look for combinations (e.g. `when X happens, Y also " - "tends to happen`), notable outliers, frequent failure modes, and " - "any signal that would change how a reviewer triages the run.\n\n" - "Return STRICT JSON only, with this shape and no extra keys:\n" - "{\n" - ' "narrative": "",\n' - ' "patterns": ["", "", ...],\n' - ' "metric_insights": {"": "<2-3 line business meaning>"}\n' - "}\n\n" - "Constraints:\n" - "- narrative is the ONLY text shown in the external audit summary and " - "Visualizations TLDR; keep it to at most 3 short sentences (~300 chars).\n" - "- patterns are optional supporting notes and are NOT rendered in the " - "audit summary; keep 0 to 3 bullets if supplied, each <= 120 characters.\n" - "- metric_insights must include one entry for each top-level metric id supplied.\n" - "- Each metric insight should explain what the metric means for the business and what the current distribution suggests, not restate the metric rubric.\n" - "- Avoid restating raw counts unless they reveal a pattern.\n" - "- Use neutral, factual language ('frustration appeared in...') " - "rather than judgemental ('the agents failed to...')." -) - - -def _tldr_summary_payload( - evaluation: CallImportEvaluation, -) -> Optional[EvaluationTldrSummary]: - """Return the cached TLDR (with ``is_stale`` set) or ``None``. - - ``CallImportEvaluation.tldr_summary`` is a ``JSON`` column so we - have to validate shape defensively -- a half-written or hand-edited - blob should not break the aggregate response. Returns ``None`` when - no cached summary exists. - """ - raw = evaluation.tldr_summary - if not isinstance(raw, dict): - return None - narrative = raw.get("narrative") - if not isinstance(narrative, str) or not narrative.strip(): - return None - patterns_raw = raw.get("patterns") - patterns = ( - [str(p) for p in patterns_raw if isinstance(p, str) and p.strip()] - if isinstance(patterns_raw, list) - else [] - ) - metric_insights_raw = raw.get("metric_insights") - metric_insights = ( - { - str(metric_id): str(insight).strip() - for metric_id, insight in metric_insights_raw.items() - if str(metric_id).strip() - and isinstance(insight, str) - and insight.strip() - } - if isinstance(metric_insights_raw, dict) - else {} - ) - generated_at_raw = raw.get("generated_at") - try: - generated_at = ( - datetime.fromisoformat(generated_at_raw) - if isinstance(generated_at_raw, str) - else evaluation.updated_at or datetime.now(timezone.utc) - ) - except ValueError: - generated_at = evaluation.updated_at or datetime.now(timezone.utc) - snapshot = raw.get("generated_at_completed_rows") - snapshot_int = int(snapshot) if isinstance(snapshot, (int, float)) else 0 - return EvaluationTldrSummary( - narrative=_clamp_prose_to_sentences(narrative.strip()), - patterns=patterns, - metric_insights=metric_insights, - generated_at=generated_at, - generated_at_completed_rows=snapshot_int, - provider=raw.get("provider") if isinstance(raw.get("provider"), str) else None, - model=raw.get("model") if isinstance(raw.get("model"), str) else None, - is_stale=evaluation.completed_rows > snapshot_int, - ) - - -def _sample_rationales_per_metric( - eval_rows: List[CallImportEvaluationRow], - *, - per_metric_cap: int = 3, - rationale_char_cap: int = 600, -) -> Dict[str, List[str]]: - """Collect up to ``per_metric_cap`` distinct rationales per metric. - - Distinctness is case- and whitespace-insensitive. We truncate each - rationale to ``rationale_char_cap`` so a few unusually verbose rows - can't dominate the prompt budget. Empty / non-string rationales are - skipped. - """ - out: Dict[str, List[str]] = {} - seen: Dict[str, set[str]] = {} - for row in eval_rows: - scores = row.metric_scores if isinstance(row.metric_scores, dict) else {} - for metric_id, entry in scores.items(): - if not isinstance(entry, dict): - continue - rationale = entry.get("rationale") - if not isinstance(rationale, str): - continue - text = rationale.strip() - if not text: - continue - bucket = out.setdefault(metric_id, []) - if len(bucket) >= per_metric_cap: - continue - key = " ".join(text.lower().split()) - seen_set = seen.setdefault(metric_id, set()) - if key in seen_set: - continue - seen_set.add(key) - bucket.append(text[:rationale_char_cap]) - return out - - -def _build_insights_messages( - evaluation: CallImportEvaluation, - aggregate: List[CallImportMetricAggregate], - rationale_samples: Dict[str, List[str]], - metric_meta: Dict[str, Metric], -) -> List[Dict[str, str]]: - """Render the user prompt fed to the LLM. - - The shape is plain markdown-ish text instead of JSON so the LLM can - skim it without us spending tokens on verbose schema delimiters. - Parent metrics surface their child metrics nested underneath so the - model sees the hierarchy and can talk about "X often co-occurred - with Y" rather than treating sub-labels as standalone metrics. - """ - name = evaluation.name or f"Run {str(evaluation.id)[:8]}" - lines: List[str] = [ - f"Evaluation: {name}", - ( - f"Rows: total={evaluation.total_rows} " - f"completed={evaluation.completed_rows} " - f"failed={evaluation.failed_rows}" - ), - "", - "## Per-metric aggregate", - ] - - # Group metrics by parent so the prompt mirrors the hierarchy. Any - # aggregate row whose ``metric_id`` is missing from ``metric_meta`` - # is rendered as a leaf at the top-level list (handles renamed / - # deleted parents). - children_by_parent: Dict[str, List[CallImportMetricAggregate]] = {} - top_level: List[CallImportMetricAggregate] = [] - for agg in aggregate: - meta = metric_meta.get(agg.metric_id) - parent_id = ( - str(meta.parent_metric_id) - if meta is not None and getattr(meta, "parent_metric_id", None) - else None - ) - if parent_id: - children_by_parent.setdefault(parent_id, []).append(agg) - else: - top_level.append(agg) - - def _format_metric_block(agg: CallImportMetricAggregate, indent: int) -> List[str]: - prefix = " " * indent + "- " - bits: List[str] = [f"{prefix}{agg.metric_name} [id={agg.metric_id}] (n={agg.count}"] - if agg.skipped_count: - bits.append(f", skipped={agg.skipped_count}") - if agg.error_count: - bits.append(f", errors={agg.error_count}") - bits.append(")") - meta = metric_meta.get(agg.metric_id) - description = (meta.description or "").strip() if meta else "" - if description: - bits.append(f" | definition={description[:500]}") - if agg.mean is not None: - mean_s = f"{agg.mean:.2f}" - stddev_s = f"{agg.stddev:.2f}" if agg.stddev is not None else "-" - bits.append(f" | mean={mean_s} stddev={stddev_s}") - if agg.min is not None and agg.max is not None: - bits.append(f" range=[{agg.min:.2f}, {agg.max:.2f}]") - if agg.value_counts: - total = sum(v.count for v in agg.value_counts) or 1 - top = agg.value_counts[:3] - shares = ", ".join( - f'"{v.label}"={v.count}/{total}' for v in top - ) - bits.append(f" | top={shares}") - result = ["".join(bits)] - rationales = rationale_samples.get(agg.metric_id, []) - for r in rationales: - result.append(" " * (indent + 1) + f"- rationale: {r}") - return result - - for agg in top_level: - lines.extend(_format_metric_block(agg, indent=0)) - meta = metric_meta.get(agg.metric_id) - children = children_by_parent.get(str(meta.id), []) if meta else [] - for child in children: - lines.extend(_format_metric_block(child, indent=1)) - - lines.append("") - top_level_ids = [agg.metric_id for agg in top_level] - if top_level_ids: - lines.append( - "metric_insights keys must exactly use these top-level metric ids: " - + ", ".join(top_level_ids) - ) - lines.append("") - lines.append( - "Write the JSON object as instructed. Do not include " - "preamble, code fences, or trailing commentary." - ) - - return [ - {"role": "system", "content": _INSIGHTS_SYSTEM_PROMPT}, - {"role": "user", "content": "\n".join(lines)}, - ] - - -def _parse_insights_response(text: str) -> EvaluationTldrSummary: - """Coerce the LLM response into ``narrative`` + ``patterns``. - - Matches the JSON-with-fallback pattern used by - ``app.api.v1.routes.metrics._parse_metric_generation_response``: try - ``json.loads`` first, then fall back to regex extraction of the - first ``{...}`` block. Raises ``HTTPException`` with a 502 when the - response can't be parsed at all. - """ - cleaned = (text or "").strip() - if not cleaned: - raise HTTPException( - status_code=502, detail="LLM returned an empty insights response" - ) - try: - parsed = json.loads(cleaned) - except json.JSONDecodeError: - import re - - match = re.search(r"\{.*\}", cleaned, re.DOTALL) - if not match: - raise HTTPException( - status_code=502, - detail="Could not parse LLM insights response as JSON", - ) - try: - parsed = json.loads(match.group(0)) - except json.JSONDecodeError as e: - raise HTTPException( - status_code=502, - detail=f"Could not parse LLM insights response: {e}", - ) - - if not isinstance(parsed, dict): - raise HTTPException( - status_code=502, detail="LLM insights JSON was not an object" - ) - - narrative = parsed.get("narrative") - if not isinstance(narrative, str) or not narrative.strip(): - raise HTTPException( - status_code=502, - detail="LLM insights JSON missing 'narrative' string", - ) - - patterns_raw = parsed.get("patterns") - if patterns_raw is None: - patterns: List[str] = [] - elif isinstance(patterns_raw, list): - patterns = [ - str(p).strip() - for p in patterns_raw - if isinstance(p, str) and p.strip() - ] - else: - raise HTTPException( - status_code=502, - detail="LLM insights JSON 'patterns' must be a list of strings", - ) - metric_insights_raw = parsed.get("metric_insights") - if metric_insights_raw is None: - metric_insights: Dict[str, str] = {} - elif isinstance(metric_insights_raw, dict): - metric_insights = { - str(metric_id): str(insight).strip() - for metric_id, insight in metric_insights_raw.items() - if str(metric_id).strip() - and isinstance(insight, str) - and insight.strip() - } - else: - raise HTTPException( - status_code=502, - detail="LLM insights JSON 'metric_insights' must be an object", - ) - - return EvaluationTldrSummary( - narrative=_clamp_prose_to_sentences(narrative.strip()), - patterns=patterns, - metric_insights=metric_insights, - generated_at=datetime.now(timezone.utc), - generated_at_completed_rows=0, # filled in by caller - is_stale=False, - ) - - -def _generate_and_persist_tldr_summary( - db: Session, - evaluation: CallImportEvaluation, - *, - organization_id: UUID, - provider: Optional[str] = None, - model: Optional[str] = None, -) -> EvaluationTldrSummary: - """LLM TLDR generation used by the imports-queue Celery worker.""" - eval_id = evaluation.id - from app.db_sharding.scatter_gather import load_evaluation_row_pairs - - pairs = load_evaluation_row_pairs(db, eval_id) - eval_rows = [eval_row for eval_row, _ in pairs] - aggregate = _compute_metric_aggregates(db, evaluation, eval_rows) - if not aggregate: - raise HTTPException( - status_code=400, - detail=( - "No metric data yet. Wait for at least one row to " - "finish scoring before generating a summary." - ), - ) - - metric_ids: List[UUID] = [] - for agg in aggregate: - try: - metric_ids.append(UUID(agg.metric_id)) - except (TypeError, ValueError): - continue - metrics = _metrics_for_ids(db, organization_id, metric_ids) - metric_meta: Dict[str, Metric] = {str(m.id): m for m in metrics} - - rationale_samples = _sample_rationales_per_metric(eval_rows) - messages = _build_insights_messages( - evaluation, aggregate, rationale_samples, metric_meta - ) - - from app.services.ai.llm_resolver import get_llm_provider_and_model - from app.services.ai.llm_service import llm_service - - provider_enum, model_str = get_llm_provider_and_model( - organization_id, db, provider, model - ) - - try: - llm_result = llm_service.generate_response( - messages=messages, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - temperature=0.4, - max_tokens=1400, - ) - except Exception as e: - logger.error(f"[CallImportInsights] LLM call failed: {e}") - raise HTTPException( - status_code=502, detail=f"LLM call failed: {e}" - ) from e - - summary = _parse_insights_response(llm_result.get("text", "")) - total = int(evaluation.total_rows or 0) - ui_completed = min(int(evaluation.completed_rows or 0), total) if total else int( - evaluation.completed_rows or 0 - ) - summary.generated_at_completed_rows = ui_completed - summary.provider = provider_enum.value - summary.model = model_str - summary.is_stale = False - - evaluation.tldr_summary = { - "narrative": summary.narrative, - "patterns": summary.patterns, - "metric_insights": summary.metric_insights, - "generated_at": summary.generated_at.isoformat(), - "generated_at_completed_rows": summary.generated_at_completed_rows, - "provider": summary.provider, - "model": summary.model, - } - flag_modified(evaluation, "tldr_summary") - db.commit() - db.refresh(evaluation) - return summary - - -@router.get( - "/{eval_id}/insights", - response_model=Optional[EvaluationTldrSummary], - operation_id="getCallImportEvaluationInsights", -) -async def get_call_import_evaluation_insights( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Optional[EvaluationTldrSummary]: - """Return the cached TLDR (or ``null``) without contacting the LLM. - - Used by the Visualizations tab on first paint so the empty-state - CTA can show up before the user opts into generation. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - return _tldr_summary_payload(evaluation) - - -@router.post( - "/{eval_id}/insights", - response_model=EvaluationTldrSummary, - operation_id="generateCallImportEvaluationInsights", -) -async def generate_call_import_evaluation_insights( - call_import_id: UUID, - eval_id: UUID, - body: EvaluationInsightsRequest = Body(default_factory=EvaluationInsightsRequest), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> EvaluationTldrSummary: - """Generate (or return-cached) the LLM TLDR for an evaluation run. - - Behavior: - - * ``body.regenerate=False`` and a cached summary at the current - ``completed_rows`` watermark exists -> return it as-is. - * ``body.regenerate=False`` and a stale cached summary exists - (``generated_at_completed_rows < completed_rows``) -> return it - with ``is_stale=True``; the UI prompts the user to regenerate. - * Otherwise -> resolve provider+model (auto-detect when omitted), - call the LLM, persist the new summary, return it. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - if not body.regenerate: - cached = _tldr_summary_payload(evaluation) - if cached is not None: - return cached - - # Run the TLDR LLM on the imports worker (not the default worker or API). - from app.workers.tasks.generate_evaluation_tldr_insights import ( - generate_evaluation_tldr_insights_task, - ) - - try: - task_result = generate_evaluation_tldr_insights_task.apply_async( - kwargs={ - "evaluation_id": str(eval_id), - "call_import_id": str(call_import_id), - "organization_id": str(organization_id), - "provider": body.provider, - "model": body.model, - }, - ).get(timeout=25 * 60) - except Exception as exc: - logger.error( - "[CallImportInsights] TLDR task failed for evaluation {}: {}", - eval_id, - exc, - ) - raise HTTPException( - status_code=502, - detail=f"Summary generation failed: {exc}", - ) from exc - - if isinstance(task_result, dict) and task_result.get("error"): - status_code = int(task_result.get("status_code") or 502) - raise HTTPException( - status_code=status_code, - detail=str(task_result["error"]), - ) - - summary = EvaluationTldrSummary.model_validate(task_result) - db.refresh(evaluation) - - from app.services.ai.llm_resolver import get_llm_provider_and_model - - provider_enum, model_str = get_llm_provider_and_model( - organization_id, db, body.provider, body.model, body.credential_id - ) - - _enqueue_user_insights_job( - evaluation, - provider=summary.provider or provider_enum.value, - model=summary.model or model_str, - force=body.regenerate, - max_llm_calls=body.max_llm_calls, - db=db, - ) - - return summary - - -def _user_insights_payload( - evaluation: CallImportEvaluation, -) -> Optional[EvaluationUserInsightsState]: - raw = getattr(evaluation, "user_insights", None) - if raw is None: - return None - return user_insights_state_from_raw( - raw, - completed_rows=evaluation.completed_rows, - ) - - -def _selected_generated_user_insights( - state: Optional[EvaluationUserInsightsState], - report_config: dict[str, Any], -) -> list[dict[str, Any]]: - """Filter and order generated insights for PDF section 03.""" - if state is None or state.status != "completed" or not state.insights: - return [] - - selected_ids = report_config.get("user_insight_ids") - if isinstance(selected_ids, list) and selected_ids: - allowed = {str(item) for item in selected_ids if item} - items = [item for item in state.insights if item.id in allowed] - else: - items = list(state.insights) - - order_raw = report_config.get("order") - order_ids: list[str] = [] - if isinstance(order_raw, dict): - user_order = order_raw.get("user_insights") - if isinstance(user_order, list): - order_ids = [str(item) for item in user_order if item] - - if order_ids: - by_id = {item.id: item for item in items} - ordered = [by_id[iid] for iid in order_ids if iid in by_id] - seen = set(order_ids) - ordered.extend(item for item in items if item.id not in seen) - items = ordered - - return [item.model_dump(mode="json") for item in items] - - -def _enqueue_user_insights_job( - evaluation: CallImportEvaluation, - *, - provider: Optional[str] = None, - model: Optional[str] = None, - force: bool = False, - max_llm_calls: Optional[int] = None, - db: Optional[Session] = None, -) -> None: - """Enqueue background user-insights generation unless already running.""" - current = _user_insights_payload(evaluation) - if current is not None and current.status == "running" and not force: - return - - llm_budget = normalize_max_llm_calls(max_llm_calls) - - completed_count = ( - _count_completed_eval_rows(db, evaluation.id) - if db is not None - else evaluation.completed_rows - ) - total_calls = total_llm_calls_for_rows(completed_count, max_llm_calls=llm_budget) - evaluation.user_insights = { - "status": "running", - "insights": ( - (evaluation.user_insights or {}).get("insights", []) - if isinstance(evaluation.user_insights, dict) - else [] - ), - "generated_at": datetime.now(timezone.utc).isoformat(), - "generated_at_completed_rows": evaluation.completed_rows, - "progress": {"completed_llm_calls": 0, "total_llm_calls": total_calls}, - "provider": provider, - "model": model, - "max_llm_calls": llm_budget, - "llm_calls_used": 0, - "error_message": None, - } - if db is not None: - flag_modified(evaluation, "user_insights") - db.commit() - - from app.workers.tasks.generate_evaluation_user_insights import ( - generate_evaluation_user_insights_task, - ) - - generate_evaluation_user_insights_task.delay( - str(evaluation.id), - provider=provider, - model=model, - max_llm_calls=llm_budget, - ) - - -@router.get( - "/{eval_id}/user-insights", - response_model=Optional[EvaluationUserInsightsState], - operation_id="getCallImportEvaluationUserInsights", -) -async def get_call_import_evaluation_user_insights( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Optional[EvaluationUserInsightsState]: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - return _user_insights_payload(evaluation) - - -@router.post( - "/{eval_id}/user-insights", - response_model=EvaluationUserInsightsState, - operation_id="generateCallImportEvaluationUserInsights", -) -async def generate_call_import_evaluation_user_insights( - call_import_id: UUID, - eval_id: UUID, - body: EvaluationUserInsightsRequest = Body( - default_factory=EvaluationUserInsightsRequest - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> EvaluationUserInsightsState: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - if not body.regenerate and not body.force: - cached = _user_insights_payload(evaluation) - if cached is not None and cached.status in {"running", "completed"}: - return cached - - eval_rows = _load_eval_rows(db, eval_id) - if not any(row.status == "completed" for row in eval_rows): - raise HTTPException( - status_code=400, - detail=( - "No completed rows yet. Wait for at least one row to " - "finish scoring before generating user insights." - ), - ) - - from app.services.ai.llm_resolver import get_llm_provider_and_model - - provider_enum, model_str = get_llm_provider_and_model( - organization_id, db, body.provider, body.model, body.credential_id - ) - - _enqueue_user_insights_job( - evaluation, - provider=provider_enum.value, - model=model_str, - force=body.force or body.regenerate, - max_llm_calls=body.max_llm_calls, - db=db, - ) - - db.refresh(evaluation) - return _user_insights_payload(evaluation) or EvaluationUserInsightsState( - status="running" - ) - - -def _metric_clusters_payload( - evaluation: CallImportEvaluation, -) -> Optional[EvaluationMetricClustersState]: - raw = getattr(evaluation, "metric_clusters", None) - if raw is None: - return None - return metric_clusters_state_from_raw( - raw, - completed_rows=evaluation.completed_rows, - ) - - -def _selected_metric_clusters_for_pdf( - state: Optional[EvaluationMetricClustersState], - report_config: dict[str, Any], -) -> dict[str, Any]: - if state is None or state.status != "completed": - return {} - sections = report_config.get("sections") - if isinstance(sections, dict) and sections.get("failure_diagnostics") is False: - return {} - payload: dict[str, Any] = { - "groups": [g.model_dump(mode="json") for g in state.groups], - "discovered_problems": [ - d.model_dump(mode="json") for d in state.discovered_problems - ], - } - if state.rca_summary is not None: - payload["rca_summary"] = state.rca_summary.model_dump(mode="json") - return payload - - -def _prompt_improvements_payload( - evaluation: CallImportEvaluation, -) -> Optional[EvaluationPromptImprovementsState]: - from app.services.call_import_prompt_improvements import ( - prompt_improvements_state_from_raw, - ) - - raw = getattr(evaluation, "prompt_improvements", None) - if raw is None: - return None - return prompt_improvements_state_from_raw( - raw, - completed_rows=evaluation.completed_rows, - ) - - -def _selected_prompt_improvements_for_pdf( - state: Optional[EvaluationPromptImprovementsState], - report_config: dict[str, Any], -) -> dict[str, Any]: - if state is None or state.status != "completed": - return {} - sections = report_config.get("sections") - if isinstance(sections, dict) and sections.get("prompt_improvements") is False: - return {} - return { - "imported_agent_id": state.imported_agent_id, - "imported_agent_name": state.imported_agent_name, - "overview": state.overview, - "suggestions": [s.model_dump(mode="json") for s in state.suggestions], - } - - -def _enqueue_prompt_improvements_job( - evaluation: CallImportEvaluation, - *, - imported_agent_id: UUID, - imported_agent_name: str, - provider: Optional[str] = None, - model: Optional[str] = None, - credential_id: Optional[UUID] = None, - force: bool = False, - db: Optional[Session] = None, -) -> None: - current = _prompt_improvements_payload(evaluation) - if current is not None and current.status == "running" and not force: - return - - evaluation.prompt_improvements = { - "status": "running", - "imported_agent_id": str(imported_agent_id), - "imported_agent_name": imported_agent_name, - "suggestions": [], - "generated_at": datetime.now(timezone.utc).isoformat(), - "generated_at_completed_rows": evaluation.completed_rows, - "provider": provider, - "model": model, - "error_message": None, - } - if db is not None: - flag_modified(evaluation, "prompt_improvements") - db.commit() - - from app.workers.tasks.generate_evaluation_prompt_improvements import ( - generate_evaluation_prompt_improvements_task, - ) - - async_result = generate_evaluation_prompt_improvements_task.apply_async( - kwargs={ - "evaluation_id": str(evaluation.id), - "imported_agent_id": str(imported_agent_id), - "provider": provider, - "model": model, - "credential_id": str(credential_id) if credential_id else None, - }, - queue="imports", - ) - if db is not None and isinstance(evaluation.prompt_improvements, dict): - evaluation.prompt_improvements["celery_task_id"] = async_result.id - flag_modified(evaluation, "prompt_improvements") - db.commit() - - -def _load_eval_rows(db: Session, evaluation_id: UUID) -> List[CallImportEvaluationRow]: - from app.db_sharding.eval_rows import load_evaluation_rows_for_run - - return load_evaluation_rows_for_run(db, evaluation_id) - - -def _count_completed_eval_rows(db: Session, evaluation_id: UUID) -> int: - from app.db_sharding.eval_rows import count_evaluation_rows_for_run - - return count_evaluation_rows_for_run( - db, evaluation_id, statuses=["completed"] - ) - - -def _completed_row_pairs_for_evaluation( - db: Session, - evaluation_id: UUID, -) -> List[Tuple[CallImportEvaluationRow, CallImportRow]]: - from app.db_sharding.scatter_gather import load_evaluation_row_pairs - - row_pairs = load_evaluation_row_pairs(db, evaluation_id) - return [ - (eval_row, source_row) - for eval_row, source_row in row_pairs - if eval_row.status == "completed" - ] - - -def _resolve_metric_cluster_row_selection( - db: Session, - evaluation: CallImportEvaluation, - eval_rows: List[CallImportEvaluationRow], - evaluation_row_ids: Optional[List[UUID]], - *, - row_limit: Optional[int] = None, - policies: Optional[Dict[str, MetricFailurePolicy]] = None, -) -> Tuple[List[Tuple[CallImportEvaluationRow, CallImportRow]], List[str]]: - """Return filtered completed row pairs and the selected row id strings.""" - completed_pairs = _completed_row_pairs_for_evaluation(db, evaluation.id) - metrics = _metrics_for_clustering(db, evaluation, eval_rows) - if policies is None: - aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) - parent_ids = [ - m.id - for m in metrics - if getattr(m, "selection_mode", None) - and not getattr(m, "parent_metric_id", None) - ] - child_names_by_parent = _child_names_by_parent( - db, evaluation.organization_id, parent_ids - ) - policies, _ = effective_policies( - evaluation, - metrics, - aggregates, - child_names_by_parent=child_names_by_parent, - ) - eligible = list_eligible_cluster_rows( - evaluation, completed_pairs, metrics, policies - ) - eligible_ordered_ids = [str(item["evaluation_row_id"]) for item in eligible] - eligible_id_set = set(eligible_ordered_ids) - - if evaluation_row_ids is None and row_limit is not None: - selected_ids = eligible_ordered_ids[:row_limit] - filtered = filter_completed_row_pairs( - completed_pairs, - [UUID(rid) for rid in selected_ids], - ) - return filtered, selected_ids - - if evaluation_row_ids is None: - selected_ids = eligible_ordered_ids - filtered = filter_completed_row_pairs( - completed_pairs, - [UUID(rid) for rid in selected_ids], - ) - return filtered, selected_ids - - requested = {str(rid) for rid in evaluation_row_ids} - completed_id_set = {str(eval_row.id) for eval_row, _ in completed_pairs} - unknown = sorted(requested - completed_id_set) - if unknown: - raise HTTPException( - status_code=400, - detail=( - "One or more evaluation_row_ids are missing or not completed: " - + ", ".join(unknown[:5]) - + ("…" if len(unknown) > 5 else "") - ), - ) - not_eligible = sorted(requested - eligible_id_set) - if not_eligible: - raise HTTPException( - status_code=400, - detail=( - "Each selected row must have at least one flagged quality metric. " - "Ineligible row(s): " - + ", ".join(not_eligible[:5]) - + ("…" if len(not_eligible) > 5 else "") - ), - ) - selected_ids = sorted(requested) - filtered = filter_completed_row_pairs(completed_pairs, evaluation_row_ids) - return filtered, selected_ids - - -def _enqueue_metric_clusters_job( - evaluation: CallImportEvaluation, - *, - provider: Optional[str] = None, - model: Optional[str] = None, - credential_id: Optional[UUID] = None, - force: bool = False, - max_llm_calls: Optional[int] = None, - evaluation_row_ids: Optional[List[UUID]] = None, - selected_evaluation_row_ids: Optional[List[str]] = None, - failure_policies: Optional[Dict[str, MetricFailurePolicy]] = None, - db: Optional[Session] = None, -) -> None: - current = _metric_clusters_payload(evaluation) - if current is not None and current.status == "running" and not force: - return - - llm_budget = normalize_max_llm_calls(max_llm_calls) - total_calls = 1 - row_ids_for_task: Optional[List[str]] = None - if db is not None: - eval_rows = _load_eval_rows(db, evaluation.id) - if selected_evaluation_row_ids is None: - _, selected_evaluation_row_ids = _resolve_metric_cluster_row_selection( - db, - evaluation, - eval_rows, - evaluation_row_ids, - ) - completed_pairs = filter_completed_row_pairs( - _completed_row_pairs_for_evaluation(db, evaluation.id), - [UUID(rid) for rid in selected_evaluation_row_ids], - ) - metrics = _metrics_for_clustering(db, evaluation, eval_rows) - policies_for_estimate = failure_policies - if policies_for_estimate is None: - aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) - parent_ids = [ - m.id - for m in metrics - if getattr(m, "selection_mode", None) - and not getattr(m, "parent_metric_id", None) - ] - child_names_by_parent = _child_names_by_parent( - db, evaluation.organization_id, parent_ids - ) - policies_for_estimate, _ = effective_policies( - evaluation, - metrics, - aggregates, - child_names_by_parent=child_names_by_parent, - ) - _, total_calls = estimate_metric_clusters_llm_calls( - evaluation, - metrics, - completed_pairs, - policies_for_estimate, - max_llm_calls=llm_budget, - ) - row_ids_for_task = list(selected_evaluation_row_ids) - - prior_raw = ( - evaluation.metric_clusters - if isinstance(evaluation.metric_clusters, dict) - else {} - ) - policy_blob: Dict[str, Any] = {} - if failure_policies: - policy_blob = failure_policies_to_db(failure_policies, source="user") - - evaluation.metric_clusters = { - "status": "running", - "groups": prior_raw.get("groups", []) if isinstance(prior_raw, dict) else [], - "discovered_problems": ( - prior_raw.get("discovered_problems", []) - if isinstance(prior_raw, dict) - else [] - ), - "generated_at": datetime.now(timezone.utc).isoformat(), - "generated_at_completed_rows": evaluation.completed_rows, - "progress": {"completed_llm_calls": 0, "total_llm_calls": total_calls}, - "provider": provider, - "model": model, - "max_llm_calls": llm_budget, - "llm_calls_used": 0, - "error_message": None, - "selected_evaluation_row_ids": selected_evaluation_row_ids or [], - **policy_blob, - } - if db is not None: - flag_modified(evaluation, "metric_clusters") - db.commit() - - from app.workers.tasks.generate_evaluation_metric_clusters import ( - generate_evaluation_metric_clusters_task, - ) - - async_result = generate_evaluation_metric_clusters_task.apply_async( - kwargs={ - "evaluation_id": str(evaluation.id), - "provider": provider, - "model": model, - "credential_id": str(credential_id) if credential_id else None, - "max_llm_calls": llm_budget, - "evaluation_row_ids": row_ids_for_task, - }, - queue="imports", - ) - if db is not None and isinstance(evaluation.metric_clusters, dict): - evaluation.metric_clusters["celery_task_id"] = async_result.id - flag_modified(evaluation, "metric_clusters") - db.commit() - - -def _revoke_metric_clusters_task(evaluation: CallImportEvaluation) -> None: - """Best-effort SIGTERM revoke of the in-flight clustering Celery task.""" - raw = evaluation.metric_clusters - if not isinstance(raw, dict): - return - task_id = str(raw.get("celery_task_id") or "").strip() - if not task_id: - return - try: - from app.workers.celery_app import celery_app - - celery_app.control.revoke(task_id, terminate=True, signal="SIGTERM") - logger.info( - "Revoked metric-clusters task {} for evaluation {}", - task_id, - evaluation.id, - ) - except Exception as exc: # noqa: BLE001 - logger.warning( - "Failed to revoke metric-clusters task {} for evaluation {}: {}", - task_id, - evaluation.id, - exc, - ) - - -def _apply_metric_clusters_cancel(evaluation: CallImportEvaluation) -> bool: - """Mark clustering as cancelled and revoke the worker task. - - Returns True if a running job was cancelled, False if already terminal. - """ - raw = evaluation.metric_clusters - if not isinstance(raw, dict): - return False - if (raw.get("status") or "").lower() != "running": - return False - - _revoke_metric_clusters_task(evaluation) - progress = raw.get("progress") if isinstance(raw.get("progress"), dict) else {} - evaluation.metric_clusters = { - **raw, - "status": "cancelled", - "error_message": METRIC_CLUSTERS_CANCELLED_BY_USER_ERROR, - "progress": progress, - "celery_task_id": None, - } - return True - - -@router.get( - "/{eval_id}/metric-clusters/failure-policies", - response_model=MetricFailurePoliciesResponse, - operation_id="getCallImportEvaluationMetricClusterFailurePolicies", -) -async def get_call_import_evaluation_metric_cluster_failure_policies( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> MetricFailurePoliciesResponse: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - eval_rows = _load_eval_rows(db, eval_id) - metrics, aggregates, policies, source, child_names_by_parent = _clustering_context( - db, evaluation, eval_rows - ) - previews = build_failure_policy_previews( - metrics, - aggregates, - child_names_by_parent=child_names_by_parent, - effective=policies, - ) - updated_at = None - raw_mc = evaluation.metric_clusters - if isinstance(raw_mc, dict) and raw_mc.get("failure_policies_updated_at"): - try: - updated_at = datetime.fromisoformat( - str(raw_mc["failure_policies_updated_at"]) - ) - except ValueError: - updated_at = None - return MetricFailurePoliciesResponse( - previews=previews, - policies=policies, - source=source, - updated_at=updated_at, - ) - - -@router.put( - "/{eval_id}/metric-clusters/failure-policies", - response_model=MetricFailurePoliciesResponse, - operation_id="saveCallImportEvaluationMetricClusterFailurePolicies", -) -async def save_call_import_evaluation_metric_cluster_failure_policies( - call_import_id: UUID, - eval_id: UUID, - body: MetricFailurePoliciesSaveRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> MetricFailurePoliciesResponse: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - eval_rows = _load_eval_rows(db, eval_id) - metrics, aggregates, _existing, _source, child_names_by_parent = _clustering_context( - db, evaluation, eval_rows - ) - try: - validate_failure_policies_for_metrics(body.policies, metrics) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - prior = ( - evaluation.metric_clusters - if isinstance(evaluation.metric_clusters, dict) - else {} - ) - evaluation.metric_clusters = merge_failure_policies_into_raw( - prior, - body.policies, - source="user", - ) - flag_modified(evaluation, "metric_clusters") - db.commit() - db.refresh(evaluation) - - policies, source = policies_from_evaluation_raw(evaluation.metric_clusters) - if source != "user": - source = "user" - previews = build_failure_policy_previews( - metrics, - aggregates, - child_names_by_parent=child_names_by_parent, - effective=policies, - ) - updated_at = None - raw_mc = evaluation.metric_clusters - if isinstance(raw_mc, dict) and raw_mc.get("failure_policies_updated_at"): - try: - updated_at = datetime.fromisoformat( - str(raw_mc["failure_policies_updated_at"]) - ) - except ValueError: - updated_at = None - return MetricFailurePoliciesResponse( - previews=previews, - policies=policies, - source="user", - updated_at=updated_at, - ) - - -@router.get( - "/{eval_id}/metric-clusters/eligible-rows", - response_model=MetricClusterEligibleRowsResponse, - operation_id="listCallImportEvaluationMetricClusterEligibleRows", -) -async def list_call_import_evaluation_metric_cluster_eligible_rows( - call_import_id: UUID, - eval_id: UUID, - limit: Optional[int] = Query(default=None, ge=1), - count_only: bool = Query(default=False), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> MetricClusterEligibleRowsResponse: - """Completed rows that have at least one flagged quality metric.""" - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - eval_rows = _load_eval_rows(db, eval_id) - completed_pairs = _completed_row_pairs_for_evaluation(db, eval_id) - metrics, _aggregates, policies, _source, _child_map = _clustering_context( - db, evaluation, eval_rows - ) - all_eligible = list_eligible_cluster_rows( - evaluation, completed_pairs, metrics, policies - ) - total = len(all_eligible) - if count_only: - return MetricClusterEligibleRowsResponse(items=[], total=total) - raw_items = all_eligible if limit is None else all_eligible[:limit] - items = [MetricClusterEligibleRow.model_validate(item) for item in raw_items] - return MetricClusterEligibleRowsResponse(items=items, total=total) - - -@router.get( - "/{eval_id}/metric-clusters", - response_model=Optional[EvaluationMetricClustersState], - operation_id="getCallImportEvaluationMetricClusters", -) -async def get_call_import_evaluation_metric_clusters( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Optional[EvaluationMetricClustersState]: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - return _metric_clusters_payload(evaluation) - - -@router.post( - "/{eval_id}/metric-clusters", - response_model=EvaluationMetricClustersState, - operation_id="generateCallImportEvaluationMetricClusters", -) -async def generate_call_import_evaluation_metric_clusters( - call_import_id: UUID, - eval_id: UUID, - body: EvaluationMetricClustersRequest = Body( - default_factory=EvaluationMetricClustersRequest - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> EvaluationMetricClustersState: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - if not body.regenerate and not body.force: - cached = _metric_clusters_payload(evaluation) - if cached is not None and cached.status in {"running", "completed"}: - return cached - - eval_rows = _load_eval_rows(db, eval_id) - if not any(row.status == "completed" for row in eval_rows): - raise HTTPException( - status_code=400, - detail=( - "No completed rows yet. Wait for at least one row to " - "finish scoring before generating metric clusters." - ), - ) - - if body.evaluation_row_ids and body.row_limit is not None: - raise HTTPException( - status_code=400, - detail="Specify either evaluation_row_ids or row_limit, not both.", - ) - - if body.evaluation_row_ids: - completed_pairs = _completed_row_pairs_for_evaluation(db, evaluation.id) - completed_id_set = {str(eval_row.id) for eval_row, _ in completed_pairs} - requested = {str(rid) for rid in body.evaluation_row_ids} - unknown = sorted(requested - completed_id_set) - if unknown: - raise HTTPException( - status_code=400, - detail=( - "One or more evaluation_row_ids are missing or not completed: " - + ", ".join(unknown[:5]) - + ("…" if len(unknown) > 5 else "") - ), - ) - - from app.services.ai.llm_resolver import get_llm_provider_and_model - - provider_enum, model_str = get_llm_provider_and_model( - organization_id, db, body.provider, body.model, body.credential_id - ) - - metrics, aggregates, _inferred, _source, child_names_by_parent = _clustering_context( - db, evaluation, eval_rows - ) - merged_policies = merge_clustering_policies( - body.failure_policies, - evaluation, - metrics, - aggregates, - child_names_by_parent=child_names_by_parent, - ) - try: - validate_failure_policies_for_metrics( - body.failure_policies or merged_policies, metrics - ) - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - - if not has_clusterable_metrics(metrics, merged_policies, eval_rows): - raise HTTPException( - status_code=400, - detail=( - "No calls match any failure policy. Select failure values on " - "metrics that have matching rows, or leave metrics with no " - "failures unchecked — they are skipped automatically." - ), - ) - - filtered_pairs, selected_row_ids = _resolve_metric_cluster_row_selection( - db, - evaluation, - eval_rows, - body.evaluation_row_ids, - row_limit=body.row_limit, - policies=merged_policies, - ) - if not selected_row_ids: - raise HTTPException( - status_code=400, - detail=( - "No eligible rows to cluster. Select completed calls that match " - "at least one configured failure policy." - ), - ) - if not filtered_pairs: - raise HTTPException( - status_code=400, - detail="No completed rows match the selected evaluation_row_ids.", - ) - - _enqueue_metric_clusters_job( - evaluation, - provider=provider_enum.value, - model=model_str, - credential_id=body.credential_id, - force=body.force or body.regenerate, - max_llm_calls=body.max_llm_calls, - evaluation_row_ids=body.evaluation_row_ids, - selected_evaluation_row_ids=selected_row_ids, - failure_policies=merged_policies, - db=db, - ) - - db.refresh(evaluation) - return _metric_clusters_payload(evaluation) or EvaluationMetricClustersState( - status="running" - ) - - -@router.post( - "/{eval_id}/metric-clusters/cancel", - response_model=EvaluationMetricClustersState, - operation_id="cancelCallImportEvaluationMetricClusters", -) -async def cancel_call_import_evaluation_metric_clusters( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> EvaluationMetricClustersState: - """Abort in-flight failure-diagnostics clustering. - - Idempotent: if clustering is not ``running``, returns the current state - unchanged. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - _apply_metric_clusters_cancel(evaluation) - flag_modified(evaluation, "metric_clusters") - db.commit() - db.refresh(evaluation) - - return _metric_clusters_payload(evaluation) or EvaluationMetricClustersState( - status="idle" - ) - - -@router.get( - "/{eval_id}/prompt-improvements", - response_model=Optional[EvaluationPromptImprovementsState], - operation_id="getCallImportEvaluationPromptImprovements", -) -async def get_call_import_evaluation_prompt_improvements( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Optional[EvaluationPromptImprovementsState]: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - return _prompt_improvements_payload(evaluation) - - -@router.post( - "/{eval_id}/prompt-improvements", - response_model=EvaluationPromptImprovementsState, - operation_id="generateCallImportEvaluationPromptImprovements", -) -async def generate_call_import_evaluation_prompt_improvements( - call_import_id: UUID, - eval_id: UUID, - body: EvaluationPromptImprovementsRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> EvaluationPromptImprovementsState: - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - clusters = _metric_clusters_payload(evaluation) - if clusters is None or clusters.status != "completed": - raise HTTPException( - status_code=400, - detail=( - "Metric clusters must be completed before generating prompt " - "improvements. Run failure diagnostics first." - ), - ) - - from app.services.call_import_prompt_improvements import is_imported_agent - from app.services.ai.llm_resolver import get_llm_provider_and_model - - imported_agent = ( - db.query(PromptPartial) - .filter( - PromptPartial.id == body.imported_agent_id, - PromptPartial.organization_id == organization_id, - PromptPartial.workspace_id == workspace_id, - ) - .first() - ) - if imported_agent is None or not is_imported_agent(imported_agent): - raise HTTPException( - status_code=404, - detail="Imported agent not found in the active workspace", - ) - - if not body.regenerate and not body.force: - cached = _prompt_improvements_payload(evaluation) - if ( - cached is not None - and cached.status in {"running", "completed"} - and cached.imported_agent_id == str(body.imported_agent_id) - ): - return cached - - provider_enum, model_str = get_llm_provider_and_model( - organization_id, db, body.provider, body.model, body.credential_id - ) - - _enqueue_prompt_improvements_job( - evaluation, - imported_agent_id=body.imported_agent_id, - imported_agent_name=imported_agent.name, - provider=provider_enum.value, - model=model_str, - credential_id=body.credential_id, - force=body.force or body.regenerate, - db=db, - ) - - db.refresh(evaluation) - return _prompt_improvements_payload(evaluation) or EvaluationPromptImprovementsState( - status="running", - imported_agent_id=str(body.imported_agent_id), - imported_agent_name=imported_agent.name, - ) - - -# --------------------------------------------------------------------------- -# Flow chart: turns per-row LLM-inferred ``sequence`` arrays into a -# directed graph of (label -> label) transitions across the whole run. -# Powers the aggregate Sankey-style React Flow chart on the evaluation -# overview; per-call flow charts are built client-side from the same -# ``sequence`` field on a single row's metric_scores entry. -# --------------------------------------------------------------------------- - - -_FLOW_TERMINAL_THRESHOLD = 0.2 # Mark as terminal when >=20% of sequences end here. -_FLOW_START_NODE_ID = "__START__" -_DISCOVERED_NODE_PREFIX = "disc:" - - -def _slug_label(value: Any) -> str: - """Lowercase + whitespace-collapse + underscore-join. - - Used everywhere we need a stable key for a metric/label name — - matching the same convention the worker uses when emitting - ``sequence`` entries and discovered keys. - """ - if value is None: - return "" - return "_".join(str(value).strip().lower().split()) - - -def _resolve_alias(alias_map: Dict[str, str], key: str) -> str: - """Walk the alias map until we hit a slug that doesn't redirect. - - The merge endpoint stores ``from_slug -> to_slug`` pairs. The delete - endpoint stores ``from_slug -> ""`` (empty string sentinel) to mark - a slug as tombstoned. Chains can accumulate when the user merges - A→B and later merges B→C; this helper collapses them so callers - always land on the final canonical slug. - - Returns: - * the canonical slug if it still resolves to a real label, - * an empty string if the slug has been tombstoned (callers MUST - treat an empty result as "drop this entry entirely"), - * the input ``key`` if it isn't aliased. - - Cycles are guarded by a hard step limit since the alias map is - user-driven. - """ - if not key: - return "" - if not alias_map: - return key - current = key - seen: set[str] = set() - for _ in range(16): - if current in seen: - return current - seen.add(current) - if current not in alias_map: - return current - nxt = alias_map[current] - if nxt == current: - return current - if nxt == "": - # Deletion sentinel — the user has explicitly retired this - # slug. Propagate the empty string up so callers drop it. - return "" - current = nxt - return current - - -# Reserved JSON key under which the worker stores top-level metric -# discoveries on each row's ``metric_scores`` dict. Mirrors the constant -# in ``app/workers/tasks/helpers/llm_evaluation.py`` — kept local here to -# avoid a worker import cycle from the routes module. -DISCOVERED_METRICS_KEY = "__discovered_metrics__" - -# Allowed values for an LLM-suggested top-level metric type. Kept in -# sync with ``DiscoveredMetricSuggestedType`` in -# ``app/models/schemas.py``. -_DISCOVERED_METRIC_TYPES = ("boolean", "rating", "category") - - -def normalize_scores_with_aliases( - metric_scores: Dict[str, Any], - evaluation: CallImportEvaluation, - db: Session, - organization_id: UUID, -) -> Dict[str, Any]: - """Rewrite per-row ``metric_scores`` to honor merges + promotions. - - Called by the worker right after ``evaluate_with_llm`` returns so - every row that finishes AFTER a user has merged or promoted a - discovered label persists data already reflecting that decision. - Without this hook, a worker holding a stale prompt could re-emit a - ``from_key`` slug long after the user merged it away. - - For every parent entry (``selection_mode != null`` and a - ``discovered_labels`` / ``sequence`` field) we: - - * resolve discovered slugs through the evaluation's - ``discovered_label_aliases`` map (transitively), - * drop any discovered_labels entry whose canonical slug now - matches a real promoted child of the parent (merging them out - of the panel for free), and - * collapse adjacent duplicate sequence entries that result. - - Returns ``metric_scores`` (mutated in place) for chaining. - """ - if not isinstance(metric_scores, dict): - return metric_scores - - aliases_top = ( - evaluation.discovered_label_aliases - if isinstance(evaluation.discovered_label_aliases, dict) - else {} - ) - - # Identify the parent entries inside metric_scores. They're the - # dicts that carry a ``selection_mode`` key (set by the LLM - # hierarchy parser) and either a ``sequence`` or a - # ``discovered_labels`` list. - for key, entry in list(metric_scores.items()): - if not isinstance(entry, dict): - continue - if entry.get("type") != "category" and not entry.get("selection_mode"): - continue - try: - parent_uuid = UUID(str(key)) - except (TypeError, ValueError): - continue - - alias_map = {} - sub = aliases_top.get(str(parent_uuid)) - if isinstance(sub, dict): - alias_map = { - str(k): str(v) - for k, v in sub.items() - if isinstance(k, str) and isinstance(v, str) - } - promoted = _promoted_child_slugs(db, parent_uuid, organization_id) - - # Rewrite discovered_labels: alias-resolve keys, drop duplicates - # post-resolution, and drop entries that have been promoted. - discovered = entry.get("discovered_labels") - if isinstance(discovered, list): - kept_disc: List[Dict[str, Any]] = [] - seen: set[str] = set() - for d in discovered: - if not isinstance(d, dict): - continue - slug = _slug_label(d.get("key") or d.get("name")) - slug = _resolve_alias(alias_map, slug) - if not slug or slug in promoted or slug in seen: - continue - seen.add(slug) - new_entry = dict(d) - new_entry["key"] = slug - kept_disc.append(new_entry) - entry["discovered_labels"] = kept_disc - - # Rewrite sequence: alias-resolve every entry; collapse adjacent - # duplicates that result. We DON'T drop slugs that match - # promoted children — the promoted child slug is still a valid - # sequence entry; the flow chart will resolve it to the real - # child node. - seq = entry.get("sequence") - if isinstance(seq, list): - new_seq: List[str] = [] - last: Optional[str] = None - for item in seq: - if not isinstance(item, str): - continue - slug = _resolve_alias(alias_map, _slug_label(item)) - if not slug or slug == last: - continue - new_seq.append(slug) - last = slug - entry["sequence"] = new_seq - - # Top-level metric discoveries live alongside the parent entries - # under the reserved ``DISCOVERED_METRICS_KEY`` slot. Apply the - # flat evaluation-level alias/tombstone map + suppress slugs that - # already correspond to a real top-level Metric so workers that - # finish AFTER the user has merged / deleted / promoted can't - # resurrect a retired candidate. - discovered_metrics_payload = metric_scores.get(DISCOVERED_METRICS_KEY) - if isinstance(discovered_metrics_payload, list): - flat_alias_map = ( - evaluation.discovered_metric_aliases - if isinstance(evaluation.discovered_metric_aliases, dict) - else {} - ) - promoted_metric_slugs = _promoted_top_level_metric_slugs( - db, organization_id - ) - kept_metrics: List[Dict[str, Any]] = [] - seen_metrics: set[str] = set() - for d in discovered_metrics_payload: - if not isinstance(d, dict): - continue - slug = _slug_label(d.get("key") or d.get("name")) - slug = _resolve_alias(flat_alias_map, slug) - if ( - not slug - or slug in promoted_metric_slugs - or slug in seen_metrics - ): - continue - seen_metrics.add(slug) - new_entry = dict(d) - new_entry["key"] = slug - kept_metrics.append(new_entry) - if kept_metrics: - metric_scores[DISCOVERED_METRICS_KEY] = kept_metrics - else: - # No survivors — drop the empty array so empty-discovery rows - # keep their pre-feature payload shape. - metric_scores.pop(DISCOVERED_METRICS_KEY, None) - - return metric_scores - - -def _alias_map_for_parent( - evaluation: CallImportEvaluation, parent_metric_id: UUID -) -> Dict[str, str]: - """Pull ``{from_slug: to_slug}`` for one parent out of the eval's blob. - - Stored shape on the evaluation row is - ``{parent_id_str: {from_slug: to_slug, ...}}``. Returns an empty - dict for parents that have never had a merge applied. - """ - raw = getattr(evaluation, "discovered_label_aliases", None) - if not isinstance(raw, dict): - return {} - submap = raw.get(str(parent_metric_id)) - if not isinstance(submap, dict): - return {} - return { - str(k): str(v) - for k, v in submap.items() - if isinstance(k, str) and isinstance(v, str) - } - - -def _promoted_child_slugs( - db: Session, parent_metric_id: UUID, organization_id: UUID -) -> set[str]: - """Slugs of every real child currently sitting under the parent. - - The Discovered Labels panel hides any candidate whose slug already - matches a real child — that covers both freshly-promoted candidates - and legacy children the LLM happened to re-discover. We pull from - the live ``metrics`` table rather than the eval's - ``selected_metric_groups`` snapshot so newly-promoted children take - effect immediately, even on evaluations that ran before the - promotion. - """ - children = ( - db.query(Metric.name) - .filter( - Metric.parent_metric_id == parent_metric_id, - Metric.organization_id == organization_id, - ) - .all() - ) - out: set[str] = set() - for (name,) in children: - slug = _slug_label(name) - if slug: - out.add(slug) - return out - - -def _promoted_top_level_metric_slugs( - db: Session, organization_id: UUID -) -> set[str]: - """Slugs of every top-level (non-child) Metric in the organization. - - Used to suppress discovered-metric candidates whose slug already - matches a real standalone metric. We intentionally include both - standalone metrics AND parent category metrics — a top-level - discovery that collides with either name is a duplicate by - definition. - """ - rows = ( - db.query(Metric.name) - .filter( - Metric.organization_id == organization_id, - Metric.parent_metric_id.is_(None), - ) - .all() - ) - out: set[str] = set() - for (name,) in rows: - slug = _slug_label(name) - if slug: - out.add(slug) - return out - - -def _get_running_discovered_labels( - db: Session, - eval_id: UUID, - parent_metric_id: UUID, - organization_id: Optional[UUID] = None, - alias_map: Optional[Dict[str, str]] = None, -) -> List[Dict[str, Any]]: - """Slug-deduped view of every discovered label seen in this eval so far. - - Walks each ``call_import_evaluation_rows`` row's - ``metric_scores[parent_id]["discovered_labels"]`` and folds entries - that share the same slug. Returns a list ordered by descending - count and stable on label key, shaped like:: - - [{"key": "customer_on_hold", "name": "Customer put on hold", - "description": "...", "sample_rationale": "...", "count": 12}] - - Powers two callers: - * The worker prompt builder ("REUSE the existing key if it fits") - — invoked just before each row's LLM call to feed the model the - running list of previously-discovered labels in this evaluation. - * The ``/discovered-labels`` API surface used by the frontend - Discovered Labels panel to render candidates with counts + - sample rationales. - - Non-completed rows are skipped: an in-flight row's discoveries are - not yet reliable (the row could fail and never produce final - metric_scores). We accept the tradeoff that rows running - concurrently won't see each other's labels — slug-collision dedup - catches identical re-inventions, and near-paraphrases surface in - the UI panel where the user can manually merge. - """ - - parent_id_str = str(parent_metric_id) - from app.db_sharding.eval_rows import load_evaluation_rows_for_run - - eval_rows = load_evaluation_rows_for_run(db, eval_id) - rows = [ - (row.metric_scores,) - for row in eval_rows - if row.status == CallImportRowStatus.COMPLETED.value - ] - - # Suppress slugs that have either: - # * been promoted to a real child of the parent (so the panel doesn't - # keep nagging the user about a candidate they've already - # accepted), or - # * been merged INTO another slug (the "from" side of a merge) — - # those occurrences fold into the canonical target instead. - promoted_slugs: set[str] = set() - if organization_id is not None: - promoted_slugs = _promoted_child_slugs( - db, parent_metric_id, organization_id - ) - aliases = alias_map or {} - - by_key: Dict[str, Dict[str, Any]] = {} - for (scores,) in rows: - if not isinstance(scores, dict): - continue - parent_entry = scores.get(parent_id_str) - if not isinstance(parent_entry, dict): - continue - discovered = parent_entry.get("discovered_labels") - if not isinstance(discovered, list): - continue - for entry in discovered: - if not isinstance(entry, dict): - continue - raw_key = entry.get("key") or entry.get("name") - key = _slug_label(raw_key) - if not key: - continue - # Apply user merges + deletions first, THEN drop anything - # that ended up on a real child slug. Order matters: a - # candidate that was merged into a slug which has since - # been promoted should disappear, not show up at the - # canonical slug. An empty resolved key means the slug was - # tombstoned via the delete endpoint. - key = _resolve_alias(aliases, key) - if not key or key in promoted_slugs: - continue - name = (entry.get("name") or "").strip() or key.replace("_", " ") - description = (entry.get("description") or "").strip() or None - sample = (entry.get("rationale") or "").strip() or None - - existing = by_key.get(key) - if existing is None: - # Track up to N=3 distinct rationales per candidate so - # the Promote-to-child flow can pre-fill the new - # sub-metric's rubric with concrete LLM examples - # without the user copy-pasting from the row table. - # ``sample_rationale`` is preserved for back-compat - # with older clients; ``examples`` is the new field. - examples = [sample] if sample else [] - by_key[key] = { - "key": key, - "name": name, - "description": description, - "sample_rationale": sample, - "examples": examples, - "count": 1, - } - continue - - existing["count"] += 1 - if not existing["description"] and description: - existing["description"] = description - if not existing["sample_rationale"] and sample: - existing["sample_rationale"] = sample - # Append distinct rationales (case-insensitive trim) up - # to a small cap. Headroom is intentionally one above - # what the UI surfaces (2) so we have a backup when the - # first rationale is unhelpful. - if sample: - ex_list: List[str] = existing.setdefault("examples", []) - if len(ex_list) < 3 and not any( - s.strip().lower() == sample.strip().lower() for s in ex_list - ): - ex_list.append(sample) - - return sorted( - by_key.values(), - key=lambda item: (-item["count"], item["key"]), - ) - - -def _get_running_discovered_metrics( - db: Session, - eval_id: UUID, - organization_id: Optional[UUID] = None, - alias_map: Optional[Dict[str, str]] = None, -) -> List[Dict[str, Any]]: - """Slug-deduped view of every discovered top-level metric in this eval. - - Mirrors :func:`_get_running_discovered_labels` but is keyed at the - evaluation level (no ``parent_metric_id``). Walks each completed - row's ``metric_scores[DISCOVERED_METRICS_KEY]`` list, folds entries - that share the same slug (post-alias resolution), and suppresses - slugs that already correspond to a real top-level :class:`Metric` - in the organization. - - Each returned entry is shaped:: - - {"key": "customer_satisfaction", - "name": "Customer Satisfaction", - "description": "...", - "suggested_type": "boolean" | "rating" | "category", - "sample_rationale": "...", - "examples": ["..."], - "count": 12} - """ - - from app.db_sharding.eval_rows import load_evaluation_rows_for_run - - eval_rows = load_evaluation_rows_for_run(db, eval_id) - rows = [ - (row.metric_scores,) - for row in eval_rows - if row.status == CallImportRowStatus.COMPLETED.value - ] - - promoted_slugs: set[str] = set() - if organization_id is not None: - promoted_slugs = _promoted_top_level_metric_slugs( - db, organization_id - ) - aliases = alias_map or {} - - by_key: Dict[str, Dict[str, Any]] = {} - for (scores,) in rows: - if not isinstance(scores, dict): - continue - discovered = scores.get(DISCOVERED_METRICS_KEY) - if not isinstance(discovered, list): - continue - for entry in discovered: - if not isinstance(entry, dict): - continue - raw_key = entry.get("key") or entry.get("name") - key = _slug_label(raw_key) - if not key: - continue - # Apply user merges + deletions first, THEN drop anything - # that ended up on an already-existing top-level metric - # slug. Empty resolved key = tombstoned. - key = _resolve_alias(aliases, key) - if not key or key in promoted_slugs: - continue - name = (entry.get("name") or "").strip() or key.replace( - "_", " " - ) - description = (entry.get("description") or "").strip() or None - sample = (entry.get("rationale") or "").strip() or None - raw_type = str(entry.get("suggested_type") or "").strip().lower() - if raw_type not in _DISCOVERED_METRIC_TYPES: - raw_type = "boolean" - - existing = by_key.get(key) - if existing is None: - examples = [sample] if sample else [] - by_key[key] = { - "key": key, - "name": name, - "description": description, - "suggested_type": raw_type, - "sample_rationale": sample, - "examples": examples, - "count": 1, - } - continue - - existing["count"] += 1 - if not existing["description"] and description: - existing["description"] = description - if not existing["sample_rationale"] and sample: - existing["sample_rationale"] = sample - # Keep the most-frequently-suggested type. We don't track - # per-type frequency yet; defer to the first non-default - # type encountered when the existing entry has the default. - if existing.get("suggested_type") == "boolean" and raw_type != "boolean": - existing["suggested_type"] = raw_type - if sample: - ex_list: List[str] = existing.setdefault("examples", []) - if len(ex_list) < 3 and not any( - s.strip().lower() == sample.strip().lower() for s in ex_list - ): - ex_list.append(sample) - - return sorted( - by_key.values(), - key=lambda item: (-item["count"], item["key"]), - ) - - -def _build_flow_graph( - eval_rows: List[CallImportEvaluationRow], - parent_metric: Metric, - children: List[Metric], - alias_map: Optional[Dict[str, str]] = None, - extra_children: Optional[List[Metric]] = None, -) -> MetricFlowResponse: - """Walk per-row ``sequence`` arrays and produce aggregate nodes/edges. - - A synthetic ``START`` node is prepended to every sequence so the - diagram has a single origin. Children that never appear in any - sequence are still emitted as nodes (count=0) so the UI can render - them in the legend. - - ``alias_map`` lets callers fold merged-out discovered slugs into - their canonical target before building the graph; ``extra_children`` - are children of the parent that aren't in the legend list (e.g. - children promoted *after* the evaluation was created and therefore - missing from ``selected_metric_groups``) but should still resolve in - sequences so the slug doesn't get redrawn as a discovered candidate. - """ - parent_id_str = str(parent_metric.id) - aliases = alias_map or {} - # Build a fast lookup keyed by both the lower_snake child key (what the - # LLM emits in ``sequence``) and the child UUID (what some clients may - # store) so legacy / drifted payloads still resolve. - child_lookup: Dict[str, Metric] = {} - for child in children: - slug = _slug_label(child.name) - child_lookup[slug] = child - child_lookup[str(child.id)] = child - # ``extra_children`` are resolved-only — they shouldn't add legend - # nodes (those come from the explicit ``children`` argument), but - # they need to be in ``child_lookup`` so a sequence step that - # matches a freshly-promoted child resolves to the real child UUID - # instead of falling through to ``discovered_lookup`` and rendering - # as a "discovered" node. - if extra_children: - for child in extra_children: - slug = _slug_label(child.name) - if slug and slug not in child_lookup: - child_lookup[slug] = child - cid = str(child.id) - child_lookup.setdefault(cid, child) - - # Discovered labels: walk every row's discovered_labels first so we - # know which discovered slugs are valid before resolving sequences. - # Discovered nodes get a ``disc:`` prefixed id so they can't collide - # with real child UUIDs in the node/edge graph. We apply - # ``alias_map`` first so merged-out source slugs fold into their - # canonical target — preserving the user's "merge" intent on still- - # in-flight rows whose JSON wasn't rewritten by the merge endpoint. - discovered_lookup: Dict[str, Dict[str, Any]] = {} - for row in eval_rows: - scores = ( - row.metric_scores if isinstance(row.metric_scores, dict) else {} - ) - parent_entry = scores.get(parent_id_str) - if not isinstance(parent_entry, dict): - continue - raw_discovered = parent_entry.get("discovered_labels") - if not isinstance(raw_discovered, list): - continue - for entry in raw_discovered: - if not isinstance(entry, dict): - continue - slug = _slug_label(entry.get("key") or entry.get("name")) - slug = _resolve_alias(aliases, slug) - if not slug or slug in child_lookup: - continue - name = (entry.get("name") or "").strip() or slug.replace("_", " ") - existing = discovered_lookup.get(slug) - if existing is None: - discovered_lookup[slug] = { - "id": f"{_DISCOVERED_NODE_PREFIX}{slug}", - "name": name, - } - - node_counts: Dict[str, int] = {} - edge_counts: Dict[tuple[str, str], int] = {} - terminal_counts: Dict[str, int] = {} - - total_rows = len(eval_rows) - rows_with_sequence = 0 - - for row in eval_rows: - scores = ( - row.metric_scores if isinstance(row.metric_scores, dict) else {} - ) - parent_entry = scores.get(parent_id_str) - if not isinstance(parent_entry, dict): - continue - raw_sequence = parent_entry.get("sequence") - if not isinstance(raw_sequence, list): - continue - - resolved_ids: List[str] = [] - last_resolved: Optional[str] = None - for item in raw_sequence: - if not isinstance(item, str): - continue - normalized = _resolve_alias(aliases, _slug_label(item)) - child = child_lookup.get(normalized) or child_lookup.get(item) - if child is not None: - cid = str(child.id) - # Adjacent dedupe AFTER alias resolution so two - # different raw slugs that fold to the same target - # don't draw a self-edge through the chart. - if cid == last_resolved: - continue - resolved_ids.append(cid) - last_resolved = cid - continue - disc = discovered_lookup.get(normalized) - if disc is not None: - if disc["id"] == last_resolved: - continue - resolved_ids.append(disc["id"]) - last_resolved = disc["id"] - - if not resolved_ids: - continue - - rows_with_sequence += 1 - for nid in resolved_ids: - node_counts[nid] = node_counts.get(nid, 0) + 1 - - edge_counts[(_FLOW_START_NODE_ID, resolved_ids[0])] = ( - edge_counts.get((_FLOW_START_NODE_ID, resolved_ids[0]), 0) + 1 - ) - for src, tgt in zip(resolved_ids, resolved_ids[1:]): - if src == tgt: - continue - edge_counts[(src, tgt)] = edge_counts.get((src, tgt), 0) + 1 - - terminal_id = resolved_ids[-1] - terminal_counts[terminal_id] = terminal_counts.get(terminal_id, 0) + 1 - - nodes: List[MetricFlowNode] = [] - # Always include a START node so the UI has a stable entry point. - nodes.append( - MetricFlowNode( - id=_FLOW_START_NODE_ID, - label="Start", - count=rows_with_sequence, - is_terminal=False, - ) - ) - - def _emit_child_node(child: Metric) -> None: - cid = str(child.id) - count = node_counts.get(cid, 0) - terminal_count = terminal_counts.get(cid, 0) - is_terminal = False - if rows_with_sequence > 0: - is_terminal = ( - terminal_count / rows_with_sequence - ) >= _FLOW_TERMINAL_THRESHOLD - nodes.append( - MetricFlowNode( - id=cid, - label=child.name, - count=count, - is_terminal=is_terminal, - ) - ) - - emitted_child_ids: set[str] = set() - for child in children: - cid = str(child.id) - if cid in emitted_child_ids: - continue - emitted_child_ids.add(cid) - _emit_child_node(child) - # Extra children (promoted after the eval was created) only get - # legend nodes if they actually appear in the data — otherwise we'd - # pollute the diagram with every standalone promotion the user has - # ever made under this parent. - if extra_children: - for child in extra_children: - cid = str(child.id) - if cid in emitted_child_ids: - continue - if node_counts.get(cid, 0) == 0: - continue - emitted_child_ids.add(cid) - _emit_child_node(child) - # Append discovered nodes after the real children so legend ordering - # keeps user-defined labels first. - for slug, info in discovered_lookup.items(): - nid = info["id"] - count = node_counts.get(nid, 0) - terminal_count = terminal_counts.get(nid, 0) - is_terminal = False - if rows_with_sequence > 0: - is_terminal = ( - terminal_count / rows_with_sequence - ) >= _FLOW_TERMINAL_THRESHOLD - nodes.append( - MetricFlowNode( - id=nid, - label=info["name"], - count=count, - is_terminal=is_terminal, - is_discovered=True, - ) - ) - - edges: List[MetricFlowEdge] = [ - MetricFlowEdge(source=src, target=tgt, count=count) - for (src, tgt), count in sorted( - edge_counts.items(), key=lambda kv: kv[1], reverse=True - ) - ] - - return MetricFlowResponse( - parent_metric_id=parent_id_str, - parent_metric_name=parent_metric.name, - selection_mode=parent_metric.selection_mode, - nodes=nodes, - edges=edges, - total_rows=total_rows, - rows_with_sequence=rows_with_sequence, - ) - - -@router.get( - "/{eval_id}/flow", - response_model=MetricFlowResponse, - operation_id="getCallImportEvaluationFlow", -) -async def get_call_import_evaluation_flow( - call_import_id: UUID, - eval_id: UUID, - parent_metric_id: UUID = Query( - ..., - description=( - "Parent (category) metric whose children's sequences should be " - "aggregated into a flow graph." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> MetricFlowResponse: - """Aggregate the LLM-inferred per-row sequences into one flow graph. - - Returns ``nodes`` (one per child of the parent metric, plus a - synthetic ``START`` node) and ``edges`` (counts of consecutive - label transitions across every row that produced a sequence). The - frontend feeds this directly into a React Flow / xyflow canvas; - edge thickness should scale with ``count / total_rows`` and - ``is_terminal`` nodes should be styled as outcomes. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - parent = ( - db.query(Metric) - .filter( - Metric.id == parent_metric_id, - Metric.organization_id == organization_id, - ) - .first() - ) - if not parent: - raise HTTPException( - status_code=404, - detail="Parent metric not found in this organization.", - ) - if not parent.selection_mode: - raise HTTPException( - status_code=400, - detail=( - "Flow charts are only meaningful for parent metrics " - "(selection_mode set). This metric is standalone." - ), - ) - - # Children are taken from selected_metric_groups when present so the - # flow chart reflects exactly the subset that ran in this - # evaluation; otherwise fall back to every enabled child of the - # parent. - groups_raw = ( - evaluation.selected_metric_groups - if isinstance(evaluation.selected_metric_groups, dict) - else {} - ) - parent_id_str = str(parent.id) - children: List[Metric] = [] - if parent_id_str in groups_raw and isinstance( - groups_raw[parent_id_str], list - ): - child_ids: List[UUID] = [] - for c in groups_raw[parent_id_str]: - try: - child_ids.append(UUID(str(c))) - except (TypeError, ValueError): - continue - if child_ids: - children = ( - db.query(Metric) - .filter( - Metric.organization_id == organization_id, - Metric.id.in_(child_ids), - ) - .order_by(Metric.created_at.asc()) - .all() - ) - if not children: - children = ( - db.query(Metric) - .filter( - Metric.organization_id == organization_id, - Metric.parent_metric_id == parent.id, - ) - .order_by(Metric.created_at.asc()) - .all() - ) - - # Children promoted AFTER this evaluation was created aren't in - # ``selected_metric_groups`` but their slugs still appear in already- - # scored rows' sequences. Pass them as ``extra_children`` so those - # sequence entries resolve against the real (now promoted) child - # instead of being redrawn as discovered candidates. - extra_children: List[Metric] = [] - if children: - existing_ids = {child.id for child in children} - all_children = ( - db.query(Metric) - .filter( - Metric.organization_id == organization_id, - Metric.parent_metric_id == parent.id, - ) - .all() - ) - extra_children = [c for c in all_children if c.id not in existing_ids] - - eval_rows = _load_eval_rows(db, eval_id) - - alias_map = _alias_map_for_parent(evaluation, parent.id) - return _build_flow_graph( - eval_rows, - parent, - children, - alias_map=alias_map, - extra_children=extra_children, - ) - - -@router.get( - "/{eval_id}/discovered-labels", - response_model=DiscoveredLabelsResponse, - operation_id="getCallImportEvaluationDiscoveredLabels", -) -async def get_call_import_evaluation_discovered_labels( - call_import_id: UUID, - eval_id: UUID, - parent_metric_id: UUID = Query( - ..., - description=( - "Parent (category) metric whose LLM-discovered candidate " - "sub-labels should be aggregated across rows." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> DiscoveredLabelsResponse: - """Aggregate candidate sub-labels the LLM discovered during this eval. - - Only meaningful for parents with ``allow_discovery=true``; for other - parents we just return an empty ``items`` list rather than 400-ing - so the frontend can call the endpoint unconditionally for every - parent on the Flow tab without branching. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - parent = ( - db.query(Metric) - .filter( - Metric.id == parent_metric_id, - Metric.organization_id == organization_id, - ) - .first() - ) - if not parent: - raise HTTPException( - status_code=404, - detail="Parent metric not found in this organization.", - ) - - alias_map = _alias_map_for_parent(evaluation, parent_metric_id) - items_raw = _get_running_discovered_labels( - db, - eval_id, - parent_metric_id, - organization_id=organization_id, - alias_map=alias_map, - ) - items = [DiscoveredLabelItem(**item) for item in items_raw] - return DiscoveredLabelsResponse( - parent_metric_id=str(parent.id), items=items - ) - - -@router.post( - "/{eval_id}/discovered-labels/merge", - response_model=DiscoveredLabelsResponse, - operation_id="mergeCallImportEvaluationDiscoveredLabels", -) -async def merge_call_import_evaluation_discovered_labels( - call_import_id: UUID, - eval_id: UUID, - body: DiscoveredLabelMergeRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> DiscoveredLabelsResponse: - """Rewrite every row's ``discovered_labels`` entry from from_key -> to_key. - - Idempotent — re-merging the same pair is a no-op. Discovered slugs - inside per-row ``sequence`` arrays are also rewritten so the flow - chart stays consistent with the panel. When a row already has - ``to_key`` and we're merging ``from_key`` into it, we drop the - ``from_key`` entry instead of producing two entries with the same - slug. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - parent = ( - db.query(Metric) - .filter( - Metric.id == body.parent_metric_id, - Metric.organization_id == organization_id, - ) - .first() - ) - if not parent: - raise HTTPException( - status_code=404, - detail="Parent metric not found in this organization.", - ) - - from_key = _slug_label(body.from_key) - to_key = _slug_label(body.to_key) - if not from_key or not to_key: - raise HTTPException( - status_code=400, - detail="from_key and to_key must be non-empty slugs.", - ) - if from_key == to_key: - # No-op; just return the current aggregate so the client can - # refresh its view. - alias_map_existing = _alias_map_for_parent(evaluation, parent.id) - items_raw = _get_running_discovered_labels( - db, - eval_id, - body.parent_metric_id, - organization_id=organization_id, - alias_map=alias_map_existing, - ) - return DiscoveredLabelsResponse( - parent_metric_id=str(parent.id), - items=[DiscoveredLabelItem(**item) for item in items_raw], - ) - - parent_id_str = str(parent.id) - from app.db_sharding.eval_rows import foreach_evaluation_row_mutating - - def _merge_discovered_label_row(row: CallImportEvaluationRow) -> bool: - scores = ( - row.metric_scores - if isinstance(row.metric_scores, dict) - else None - ) - if not scores: - return False - parent_entry = scores.get(parent_id_str) - if not isinstance(parent_entry, dict): - return False - - mutated = False - discovered = parent_entry.get("discovered_labels") - if isinstance(discovered, list): - kept: List[Dict[str, Any]] = [] - existing_to = next( - ( - e - for e in discovered - if isinstance(e, dict) - and _slug_label(e.get("key") or e.get("name")) == to_key - ), - None, - ) - for entry in discovered: - if not isinstance(entry, dict): - kept.append(entry) - continue - key = _slug_label(entry.get("key") or entry.get("name")) - if key == from_key: - if existing_to is not None: - mutated = True - continue - new_entry = dict(entry) - new_entry["key"] = to_key - kept.append(new_entry) - mutated = True - else: - kept.append(entry) - if mutated: - parent_entry["discovered_labels"] = kept - - seq = parent_entry.get("sequence") - if isinstance(seq, list): - new_seq: List[str] = [] - seq_changed = False - last_added: Optional[str] = None - for item in seq: - if isinstance(item, str) and _slug_label(item) == from_key: - seq_changed = True - if last_added == to_key: - continue - new_seq.append(to_key) - last_added = to_key - else: - new_seq.append(item) - last_added = ( - _slug_label(item) if isinstance(item, str) else None - ) - if seq_changed: - parent_entry["sequence"] = new_seq - mutated = True - - if mutated: - row.metric_scores = dict(scores) - return mutated - - foreach_evaluation_row_mutating(db, eval_id, _merge_discovered_label_row) - - # Persist the merge at the evaluation level too. This is what makes - # the merge survive future scoring: rows that finish AFTER this - # call (e.g. retries, in-flight workers) will go through the - # alias map in the API surface even if the per-row JSON they - # write still mentions ``from_key``. We chain through any existing - # alias so merging A→B and then B→C resolves A→C in the panel. - raw_aliases = ( - evaluation.discovered_label_aliases - if isinstance(evaluation.discovered_label_aliases, dict) - else {} - ) - aliases_top = dict(raw_aliases) - parent_aliases = dict(aliases_top.get(parent_id_str) or {}) - # Resolve transitively: if to_key itself was previously merged into - # something else, point from_key at the canonical end-of-chain. - canonical_to = _resolve_alias(parent_aliases, to_key) - parent_aliases[from_key] = canonical_to - # Re-target any earlier aliases that pointed AT from_key — without - # this, A→B and then B→C would leave A still pointing to B (now a - # broken pointer because B is gone). Rewriting them keeps the - # alias map self-consistent. - for k, v in list(parent_aliases.items()): - if v == from_key: - parent_aliases[k] = canonical_to - aliases_top[parent_id_str] = parent_aliases - evaluation.discovered_label_aliases = aliases_top - - db.commit() - - alias_map_after = _alias_map_for_parent(evaluation, parent.id) - items_raw = _get_running_discovered_labels( - db, - eval_id, - body.parent_metric_id, - organization_id=organization_id, - alias_map=alias_map_after, - ) - return DiscoveredLabelsResponse( - parent_metric_id=str(parent.id), - items=[DiscoveredLabelItem(**item) for item in items_raw], - ) - - -@router.post( - "/{eval_id}/discovered-labels/delete", - response_model=DiscoveredLabelsResponse, - operation_id="deleteCallImportEvaluationDiscoveredLabel", -) -async def delete_call_import_evaluation_discovered_label( - call_import_id: UUID, - eval_id: UUID, - body: DiscoveredLabelDeleteRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> DiscoveredLabelsResponse: - """Tombstone a single LLM-discovered candidate for this evaluation. - - Symmetric with the merge endpoint, but instead of redirecting the - slug at another candidate we mark it as deleted. After this call: - - * the slug is stripped from every row's - ``metric_scores[parent].discovered_labels`` list, and from - every row's ``sequence`` array (so the flow chart no longer - draws a node for it); - * the slug is recorded in - ``evaluation.discovered_label_aliases[parent][slug] = ""`` - so any worker that finishes a row AFTER this call (e.g. a row - still in flight when the user clicked Delete) silently drops - the slug instead of resurrecting it. - - Idempotent: deleting an already-deleted slug is a no-op. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - parent = ( - db.query(Metric) - .filter( - Metric.id == body.parent_metric_id, - Metric.organization_id == organization_id, - ) - .first() - ) - if not parent: - raise HTTPException( - status_code=404, - detail="Parent metric not found in this organization.", - ) - - target_key = _slug_label(body.key) - if not target_key: - raise HTTPException( - status_code=400, - detail="key must be a non-empty slug.", - ) - - parent_id_str = str(parent.id) - from app.db_sharding.eval_rows import foreach_evaluation_row_mutating - - def _delete_discovered_label_row(row: CallImportEvaluationRow) -> bool: - scores = ( - row.metric_scores - if isinstance(row.metric_scores, dict) - else None - ) - if not scores: - return False - parent_entry = scores.get(parent_id_str) - if not isinstance(parent_entry, dict): - return False - - mutated = False - discovered = parent_entry.get("discovered_labels") - if isinstance(discovered, list): - kept = [ - e - for e in discovered - if not ( - isinstance(e, dict) - and _slug_label(e.get("key") or e.get("name")) - == target_key - ) - ] - if len(kept) != len(discovered): - parent_entry["discovered_labels"] = kept - mutated = True - - seq = parent_entry.get("sequence") - if isinstance(seq, list): - new_seq: List[str] = [] - seq_changed = False - last_added: Optional[str] = None - for item in seq: - if isinstance(item, str) and _slug_label(item) == target_key: - seq_changed = True - continue - if isinstance(item, str): - norm = _slug_label(item) - if norm == last_added: - seq_changed = True - continue - last_added = norm - new_seq.append(item) - if seq_changed: - parent_entry["sequence"] = new_seq - mutated = True - - if mutated: - row.metric_scores = dict(scores) - return mutated - - foreach_evaluation_row_mutating(db, eval_id, _delete_discovered_label_row) - - # 3. Persist the tombstone on the evaluation so workers that finish - # later don't re-surface the deleted slug. We also retarget any - # existing aliases whose ``to_key`` was the deleted slug — without - # this, a previous merge that pointed at this slug would leave a - # dangling pointer. - raw_aliases = ( - evaluation.discovered_label_aliases - if isinstance(evaluation.discovered_label_aliases, dict) - else {} - ) - aliases_top = dict(raw_aliases) - parent_aliases = dict(aliases_top.get(parent_id_str) or {}) - parent_aliases[target_key] = "" # deletion sentinel - for k, v in list(parent_aliases.items()): - if v == target_key: - parent_aliases[k] = "" - aliases_top[parent_id_str] = parent_aliases - evaluation.discovered_label_aliases = aliases_top - - db.commit() - - alias_map_after = _alias_map_for_parent(evaluation, parent.id) - items_raw = _get_running_discovered_labels( - db, - eval_id, - body.parent_metric_id, - organization_id=organization_id, - alias_map=alias_map_after, - ) - return DiscoveredLabelsResponse( - parent_metric_id=str(parent.id), - items=[DiscoveredLabelItem(**item) for item in items_raw], - ) - - -# --------------------------------------------------------------------------- -# Discovered TOP-LEVEL METRICS (per-evaluation discovery) -# -# These endpoints are the parallel of the discovered-labels trio above but -# scoped to the evaluation as a whole instead of to a parent metric. They -# all live under ``/{eval_id}/discovered-metrics`` and operate on the -# reserved ``DISCOVERED_METRICS_KEY`` slot of each per-row -# ``metric_scores`` plus the flat ``CallImportEvaluation.discovered_metric_aliases`` -# map (no parent-id nesting). -# --------------------------------------------------------------------------- - - -def _flat_metric_aliases( - evaluation: CallImportEvaluation, -) -> Dict[str, str]: - """Pull the flat ``{from_slug: to_slug}`` map for an evaluation.""" - raw = getattr(evaluation, "discovered_metric_aliases", None) - if not isinstance(raw, dict): - return {} - return { - str(k): str(v) - for k, v in raw.items() - if isinstance(k, str) and isinstance(v, str) - } - - -@router.get( - "/{eval_id}/discovered-metrics", - response_model=DiscoveredMetricsResponse, - operation_id="getCallImportEvaluationDiscoveredMetrics", -) -async def get_call_import_evaluation_discovered_metrics( - call_import_id: UUID, - eval_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> DiscoveredMetricsResponse: - """Aggregate top-level metric candidates the LLM discovered during this eval. - - Returns an empty ``items`` list when the evaluation did not opt - into top-level metric discovery; this keeps the frontend able to - call the endpoint unconditionally without branching on the - evaluation's ``discover_new_metrics`` flag. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - if not bool(getattr(evaluation, "discover_new_metrics", False)): - return DiscoveredMetricsResponse(evaluation_id=evaluation.id, items=[]) - - items_raw = _get_running_discovered_metrics( - db, - eval_id, - organization_id=organization_id, - alias_map=_flat_metric_aliases(evaluation), - ) - return DiscoveredMetricsResponse( - evaluation_id=evaluation.id, - items=[DiscoveredMetricItem(**item) for item in items_raw], - ) - - -@router.post( - "/{eval_id}/discovered-metrics/merge", - response_model=DiscoveredMetricsResponse, - operation_id="mergeCallImportEvaluationDiscoveredMetrics", -) -async def merge_call_import_evaluation_discovered_metrics( - call_import_id: UUID, - eval_id: UUID, - body: DiscoveredMetricMergeRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> DiscoveredMetricsResponse: - """Rewrite every row's ``__discovered_metrics__`` entry from→to. - - Mirrors the discovered-labels merge endpoint but operates on the - flat top-level metric list. Idempotent — re-merging is a no-op. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - from_key = _slug_label(body.from_key) - to_key = _slug_label(body.to_key) - if not from_key or not to_key: - raise HTTPException( - status_code=400, - detail="from_key and to_key must be non-empty slugs.", - ) - if from_key == to_key: - items_raw = _get_running_discovered_metrics( - db, - eval_id, - organization_id=organization_id, - alias_map=_flat_metric_aliases(evaluation), - ) - return DiscoveredMetricsResponse( - evaluation_id=evaluation.id, - items=[DiscoveredMetricItem(**item) for item in items_raw], - ) - - from app.db_sharding.eval_rows import foreach_evaluation_row_mutating - - def _merge_discovered_metric_row(row: CallImportEvaluationRow) -> bool: - scores = ( - row.metric_scores - if isinstance(row.metric_scores, dict) - else None - ) - if not scores: - return False - discovered = scores.get(DISCOVERED_METRICS_KEY) - if not isinstance(discovered, list): - return False - - kept: List[Dict[str, Any]] = [] - mutated = False - existing_to = next( - ( - e - for e in discovered - if isinstance(e, dict) - and _slug_label(e.get("key") or e.get("name")) == to_key - ), - None, - ) - for entry in discovered: - if not isinstance(entry, dict): - kept.append(entry) - continue - key = _slug_label(entry.get("key") or entry.get("name")) - if key == from_key: - if existing_to is not None: - mutated = True - continue - new_entry = dict(entry) - new_entry["key"] = to_key - kept.append(new_entry) - mutated = True - else: - kept.append(entry) - if mutated: - scores[DISCOVERED_METRICS_KEY] = kept - row.metric_scores = dict(scores) - return mutated - - foreach_evaluation_row_mutating(db, eval_id, _merge_discovered_metric_row) - - raw_aliases = ( - evaluation.discovered_metric_aliases - if isinstance(evaluation.discovered_metric_aliases, dict) - else {} - ) - aliases = dict(raw_aliases) - canonical_to = _resolve_alias(aliases, to_key) - aliases[from_key] = canonical_to - for k, v in list(aliases.items()): - if v == from_key: - aliases[k] = canonical_to - evaluation.discovered_metric_aliases = aliases - - db.commit() - - items_raw = _get_running_discovered_metrics( - db, - eval_id, - organization_id=organization_id, - alias_map=_flat_metric_aliases(evaluation), - ) - return DiscoveredMetricsResponse( - evaluation_id=evaluation.id, - items=[DiscoveredMetricItem(**item) for item in items_raw], - ) - - -@router.post( - "/{eval_id}/discovered-metrics/delete", - response_model=DiscoveredMetricsResponse, - operation_id="deleteCallImportEvaluationDiscoveredMetric", -) -async def delete_call_import_evaluation_discovered_metric( - call_import_id: UUID, - eval_id: UUID, - body: DiscoveredMetricDeleteRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> DiscoveredMetricsResponse: - """Tombstone a single LLM-discovered top-level metric candidate.""" - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - target_key = _slug_label(body.key) - if not target_key: - raise HTTPException( - status_code=400, - detail="key must be a non-empty slug.", - ) - - from app.db_sharding.eval_rows import foreach_evaluation_row_mutating - - def _delete_discovered_metric_row(row: CallImportEvaluationRow) -> bool: - scores = ( - row.metric_scores - if isinstance(row.metric_scores, dict) - else None - ) - if not scores: - return False - discovered = scores.get(DISCOVERED_METRICS_KEY) - if not isinstance(discovered, list): - return False - kept = [ - e - for e in discovered - if not ( - isinstance(e, dict) - and _slug_label(e.get("key") or e.get("name")) - == target_key - ) - ] - if len(kept) == len(discovered): - return False - if kept: - scores[DISCOVERED_METRICS_KEY] = kept - else: - scores.pop(DISCOVERED_METRICS_KEY, None) - row.metric_scores = dict(scores) - return True - - foreach_evaluation_row_mutating(db, eval_id, _delete_discovered_metric_row) - - raw_aliases = ( - evaluation.discovered_metric_aliases - if isinstance(evaluation.discovered_metric_aliases, dict) - else {} - ) - aliases = dict(raw_aliases) - aliases[target_key] = "" # tombstone - for k, v in list(aliases.items()): - if v == target_key: - aliases[k] = "" - evaluation.discovered_metric_aliases = aliases - - db.commit() - - items_raw = _get_running_discovered_metrics( - db, - eval_id, - organization_id=organization_id, - alias_map=_flat_metric_aliases(evaluation), - ) - return DiscoveredMetricsResponse( - evaluation_id=evaluation.id, - items=[DiscoveredMetricItem(**item) for item in items_raw], - ) - - -@router.delete( - "/{eval_id}/rows/{eval_row_id}", - status_code=status.HTTP_204_NO_CONTENT, - operation_id="deleteCallImportEvaluationRow", -) -async def delete_call_import_evaluation_row( - call_import_id: UUID, - eval_id: UUID, - eval_row_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Response: - """Delete a single per-row scoring entry within an evaluation run. - - Useful when the user wants to drop a noisy row before re-exporting - the CSV — e.g. a row whose audio was corrupt and skewed the - aggregate. Counters on the parent are recomputed so the rolled-up - status stays accurate. - """ - - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException(status_code=404, detail="Call import evaluation not found") - - from app.db_sharding.sessions import is_sharding_enabled - - if is_sharding_enabled(): - from app.db_sharding.eval_rows import delete_evaluation_row_on_shards - - if not delete_evaluation_row_on_shards(eval_row_id, eval_id): - raise HTTPException( - status_code=404, detail="Evaluation row not found in this run" - ) - _rollup_evaluation_status(evaluation, db) - db.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - eval_row = ( - db.query(CallImportEvaluationRow) - .filter( - CallImportEvaluationRow.id == eval_row_id, - CallImportEvaluationRow.evaluation_id == eval_id, - ) - .first() - ) - if not eval_row: - raise HTTPException( - status_code=404, detail="Evaluation row not found in this run" - ) - - # If the row was still in flight, best-effort revoke the worker task - # so it doesn't try to write into a deleted DB row mid-execution. - if eval_row.celery_task_id and eval_row.status in {"pending", "running"}: - try: - from app.workers.celery_app import celery_app - - celery_app.control.revoke(eval_row.celery_task_id, terminate=False) - except Exception: - pass - - db.delete(eval_row) - db.flush() - _rollup_evaluation_status(evaluation, db) - db.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - -# --------------------------------------------------------------------------- -# Retry endpoints -# --------------------------------------------------------------------------- -# -# The create endpoint enqueues every row of a fresh run; these endpoints -# let the user re-enqueue a *subset* of rows in an existing run — most -# commonly the ones that failed. We keep the worker contract identical -# (``evaluate_call_import_row_task(eval_row_id)``), so the retry path -# only has to reset row state and re-fan-out. When a row is missing its -# diarised transcript and the run was configured for diarised -# transcripts, we chain through ``transcribe_call_import_row_task`` the -# same way the create endpoint does — that's what makes "retry" feel -# like "just fix it" instead of "fail again immediately". - - -def _prepare_source_row_for_retry( - source_row: CallImportRow, - *, - transcribe_overwrite: bool, -) -> None: - """Clear stale diarisation markers so retry dispatch can re-run the pipeline.""" - source_row.celery_task_id = None - - # Re-fetch recordings when a prior import failed or stalled without S3 audio. - # Mirrors retry_failed_call_import_rows so eval retry can re-enqueue imports. - if ( - source_row.status - in (CallImportRowStatus.FAILED, CallImportRowStatus.PROCESSING) - and not (source_row.recording_s3_key or "").strip() - ): - source_row.status = CallImportRowStatus.PENDING - source_row.error_message = None - - if transcribe_overwrite and (source_row.diarised_transcript or "").strip(): - source_row.diarised_transcript = None - - has_dia = bool((source_row.diarised_transcript or "").strip()) - dia_status = (source_row.diarised_transcript_status or "").strip().lower() - - if has_dia and not transcribe_overwrite: - source_row.diarised_transcript_status = "completed" - source_row.diarised_transcript_error = None - return - - if dia_status in {"failed", "pending", "running", "idle"}: - source_row.diarised_transcript_status = "idle" - source_row.diarised_transcript_error = None - - -def _reset_eval_row_for_retry( - eval_row: CallImportEvaluationRow, - *, - metric_ids: Optional[List[UUID]] = None, - skip_revoke: bool = False, -) -> None: - """Wipe per-row state so the worker can re-run it cleanly. - - Mirrors the initial state used by ``create_call_import_evaluation`` - when it first inserts a row, with the addition of revoking any - lingering Celery task id. - - When ``metric_ids`` is provided, this is a **metric-subset retry**: - only the scores for those metrics are removed from - ``metric_scores`` (other metrics' previously-computed values are - preserved so the worker's partial-merge write keeps them intact). - Otherwise the entire ``metric_scores`` dict is reset, matching the - legacy behaviour. - """ - if ( - not skip_revoke - and eval_row.celery_task_id - and eval_row.status in {"pending", "running"} - ): - try: - from app.workers.celery_app import celery_app - - celery_app.control.revoke(eval_row.celery_task_id, terminate=False) - except Exception: # noqa: BLE001 — revoke is best-effort - pass - eval_row.status = "pending" - eval_row.error_message = None - if metric_ids: - # Strip ONLY the targeted metric keys. Both string and UUID - # forms can appear in ``metric_scores`` depending on which - # code path wrote the dict, so we normalise to lower-case - # strings for the comparison. - existing = ( - eval_row.metric_scores if isinstance(eval_row.metric_scores, dict) else {} - ) - target_keys = {str(mid).lower() for mid in metric_ids} - eval_row.metric_scores = { - key: value - for key, value in existing.items() - if str(key).lower() not in target_keys - } - else: - eval_row.metric_scores = {} - eval_row.started_at = None - eval_row.finished_at = None - eval_row.celery_task_id = None - - -def _enqueue_eval_rows_with_optional_transcribe( - db: Session, - evaluation: CallImportEvaluation, - eval_rows_with_source: List[ - Tuple[CallImportEvaluationRow, CallImportRow] - ], - *, - transcribe_overwrite: bool = False, - restricted_metric_ids: Optional[List[UUID]] = None, -) -> Tuple[int, int]: - """Schedule throttled evaluation dispatch for pending eval rows. - - Returns ``(evaluate_only_count, transcribe_then_evaluate_count)`` for - logging/UI compatibility. Actual Celery fan-out is handled by - :func:`dispatch_evaluation_rows_task` under Redis fair-share limits. - """ - from app.workers.concurrency.eval_dispatch import _needs_transcribe_for_eval - from app.workers.concurrency.fair_dispatch import ( - schedule_fair_dispatch, - store_evaluation_transcribe_overwrite, - store_row_restricted_metrics, - ) - - eval_only_count = 0 - transcribe_count = 0 - if eval_rows_with_source: - for eval_row, source_row in eval_rows_with_source: - if _needs_transcribe_for_eval( - evaluation, - source_row, - transcribe_overwrite=transcribe_overwrite, - ): - transcribe_count += 1 - else: - eval_only_count += 1 - - restricted_metric_ids_str: Optional[List[str]] = ( - [str(mid) for mid in restricted_metric_ids] - if restricted_metric_ids - else None - ) - if restricted_metric_ids_str: - for eval_row, _ in eval_rows_with_source: - store_row_restricted_metrics(eval_row.id, restricted_metric_ids_str) - else: - restricted_metric_ids_str = ( - [str(mid) for mid in restricted_metric_ids] if restricted_metric_ids else None - ) - store_evaluation_transcribe_overwrite( - evaluation.id, - overwrite=transcribe_overwrite, - ) - schedule_fair_dispatch(max_workspace_turns=999) - return eval_only_count, transcribe_count - - -def _apply_telephony_retry_overrides( - db: Session, - *, - call_import: CallImport, - organization_id: UUID, - payload: CallImportEvaluationRetryRequest, -) -> None: - """Pin or clear telephony credentials on the batch for this retry pass.""" - fields_set = payload.model_fields_set - if ( - "provider" not in fields_set - and "telephony_integration_id" not in fields_set - ): - return - - from app.api.v1.routes.call_imports import _resolve_telephony_integration - - if payload.telephony_integration_id is not None: - integration = _resolve_telephony_integration( - db, - organization_id, - payload.telephony_integration_id, - payload.provider or "", - ) - call_import.provider = integration.provider - call_import.telephony_integration_id = integration.id - else: - call_import.provider = None - call_import.telephony_integration_id = None - db.flush() - - -def _apply_retry_overrides( - db: Session, - evaluation: CallImportEvaluation, - organization_id: UUID, - payload: CallImportEvaluationRetryRequest, -) -> None: - """Validate + persist the LLM/STT override fields on the run. - - Mirrors the validation in ``create_call_import_evaluation`` but - only touches the fields the caller actually sent — leaving any - field ``None`` preserves the run's existing value. Raises - ``HTTPException(400)`` on bad input so the route handler can let - FastAPI turn it into a clean 400 response. - """ - # --- LLM provider + model (must be sent together) --- - if payload.llm_provider is not None or payload.llm_model is not None: - if not (payload.llm_provider and payload.llm_model): - raise HTTPException( - status_code=400, - detail=( - "Both llm_provider and llm_model are required when " - "overriding the run LLM on retry." - ), - ) - try: - evaluation.llm_provider = ModelProvider( - payload.llm_provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=( - f"Unknown LLM provider '{payload.llm_provider}'. " - "Valid keys are documented in ModelProvider." - ), - ) - new_model = payload.llm_model.strip() or None - if not new_model: - raise HTTPException( - status_code=400, detail="llm_model cannot be empty." - ) - evaluation.llm_model = new_model - - # --- LLM credential pin --- - if payload.llm_credential_id is not None: - cred = ( - db.query(AIProvider) - .filter( - AIProvider.id == payload.llm_credential_id, - AIProvider.organization_id == organization_id, - ) - .first() - ) - if not cred: - raise HTTPException( - status_code=400, - detail=( - "The provided llm_credential_id does not exist in " - "this organization." - ), - ) - evaluation.llm_credential_id = payload.llm_credential_id - - if payload.llm_config is not None: - evaluation.llm_config = payload.llm_config - - # --- Per-metric LLM overrides --- - # We accept the same dict shape as the create endpoint but - # constrain keys to leaf metrics that are actually in this run. - # Passing an empty dict explicitly clears existing overrides. - if payload.metric_llm_overrides is not None: - valid_leaf_ids = { - str(mid) for mid in (evaluation.selected_metric_ids or []) - } - overrides_payload: Dict[str, Dict[str, Any]] = {} - for metric_id, override in payload.metric_llm_overrides.items(): - if metric_id not in valid_leaf_ids: - raise HTTPException( - status_code=400, - detail=( - "metric_llm_overrides references metric " - f"{metric_id} which is not a leaf metric in " - "this run." - ), - ) - override_dict: Dict[str, Any] = {} - if override.provider is not None: - if not override.model: - raise HTTPException( - status_code=400, - detail=( - f"Override for metric {metric_id} has a " - "provider but no model." - ), - ) - try: - override_dict["provider"] = ModelProvider( - override.provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=( - f"Override for metric {metric_id} uses " - f"unknown provider '{override.provider}'." - ), - ) - override_dict["model"] = override.model.strip() - elif override.model: - raise HTTPException( - status_code=400, - detail=( - f"Override for metric {metric_id} has a model " - "but no provider." - ), - ) - if override.credential_id is not None: - override_dict["credential_id"] = str(override.credential_id) - if override.llm_config is not None: - override_dict["llm_config"] = override.llm_config - if override_dict: - overrides_payload[metric_id] = override_dict - evaluation.metric_llm_overrides = overrides_payload or None - - # --- STT provider + model (must be sent together) --- - if payload.stt_provider is not None or payload.stt_model is not None: - if not (payload.stt_provider and payload.stt_model): - raise HTTPException( - status_code=400, - detail=( - "Both stt_provider and stt_model are required " - "when overriding the run STT on retry." - ), - ) - try: - evaluation.stt_provider = ModelProvider( - payload.stt_provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=f"Unknown STT provider '{payload.stt_provider}'.", - ) - new_stt_model = payload.stt_model.strip() or None - if not new_stt_model: - raise HTTPException( - status_code=400, detail="stt_model cannot be empty." - ) - evaluation.stt_model = new_stt_model - - # --- STT credential pin --- - if payload.stt_credential_id is not None: - evaluation.stt_credential_id = payload.stt_credential_id - - # --- LLM diariser provider + model (must be sent together) --- - if ( - payload.diarization_llm_provider is not None - or payload.diarization_llm_model is not None - ): - if not ( - payload.diarization_llm_provider - and payload.diarization_llm_model - ): - raise HTTPException( - status_code=400, - detail=( - "Both diarization_llm_provider and " - "diarization_llm_model are required when overriding " - "the run diariser on retry." - ), - ) - try: - evaluation.diarisation_llm_provider = ModelProvider( - payload.diarization_llm_provider.lower() - ).value - except ValueError: - raise HTTPException( - status_code=400, - detail=( - "Unknown diarisation LLM provider " - f"'{payload.diarization_llm_provider}'." - ), - ) - new_diariser_model = ( - payload.diarization_llm_model.strip() or None - ) - if not new_diariser_model: - raise HTTPException( - status_code=400, - detail="diarization_llm_model cannot be empty.", - ) - evaluation.diarisation_llm_model = new_diariser_model - - if payload.diarization_llm_credential_id is not None: - evaluation.diarisation_llm_credential_id = ( - payload.diarization_llm_credential_id - ) - - # ``diarization_prompt`` semantics: None = leave untouched; - # empty string = clear (fall back to the canonical default at - # worker time); anything else = persist verbatim. - if payload.diarization_prompt is not None: - cleaned = payload.diarization_prompt.strip() - evaluation.diarisation_prompt = cleaned or None - - if payload.transcribe_mode is not None: - mode = payload.transcribe_mode.strip().lower() - if mode not in {"stt_llm", "llm_only"}: - raise HTTPException( - status_code=400, - detail=( - f"Unknown transcribe_mode '{payload.transcribe_mode}'. " - "Valid values are 'stt_llm' and 'llm_only'." - ), - ) - evaluation.transcribe_mode = mode - - -def _gather_retry_targets( - db: Session, - evaluation: CallImportEvaluation, - requested_ids: Optional[List[UUID]], - *, - include_completed: bool = False, -) -> Tuple[ - List[Tuple[CallImportEvaluationRow, CallImportRow]], - List[CallImportEvaluationRetrySkippedItem], -]: - """Resolve which rows to retry + reasons for any we refuse. - - When ``requested_ids`` is None we retry every row whose status is - ``failed`` (or every row when ``include_completed`` is also set — - used by the metric-subset retry path which legitimately wants to - recompute a metric on already-successful rows). When the caller - passes ids explicitly we still filter out rows that are currently - in flight; ``include_completed`` controls whether previously- - successful rows are eligible. - """ - from app.db_sharding.sessions import is_sharding_enabled - - if is_sharding_enabled(): - from app.db_sharding.eval_rows import gather_retry_targets_sharded - - return gather_retry_targets_sharded( - db, - evaluation, - requested_ids, - include_completed=include_completed, - ) - - eval_rows_query = db.query(CallImportEvaluationRow).filter( - CallImportEvaluationRow.evaluation_id == evaluation.id - ) - - targets: List[Tuple[CallImportEvaluationRow, CallImportRow]] = [] - skipped: List[CallImportEvaluationRetrySkippedItem] = [] - - if requested_ids is None: - if include_completed: - # "Retry everything" path used by the metric-subset re-run - # UI. Still skip in-flight rows below so we don't trample - # work the worker is actively doing. - candidate_rows = eval_rows_query.filter( - CallImportEvaluationRow.status.in_(["failed", "completed"]) - ).all() - else: - candidate_rows = eval_rows_query.filter( - CallImportEvaluationRow.status == "failed" - ).all() - else: - requested_set = set(requested_ids) - candidate_rows = eval_rows_query.filter( - CallImportEvaluationRow.id.in_(requested_set) - ).all() - found_ids = {row.id for row in candidate_rows} - for missing in requested_set - found_ids: - skipped.append( - CallImportEvaluationRetrySkippedItem( - eval_row_id=missing, - reason="unknown", - ) - ) - - if not candidate_rows: - return targets, skipped - - source_row_ids = [row.call_import_row_id for row in candidate_rows] - source_rows = ( - db.query(CallImportRow) - .filter(CallImportRow.id.in_(source_row_ids)) - .all() - ) - source_by_id = {row.id: row for row in source_rows} - - for eval_row in candidate_rows: - if eval_row.status in {"pending", "running"}: - skipped.append( - CallImportEvaluationRetrySkippedItem( - eval_row_id=eval_row.id, - reason="in_progress", - ) - ) - continue - if eval_row.status == "completed" and not include_completed: - skipped.append( - CallImportEvaluationRetrySkippedItem( - eval_row_id=eval_row.id, - reason="completed", - ) - ) - continue - source_row = source_by_id.get(eval_row.call_import_row_id) - if source_row is None: - skipped.append( - CallImportEvaluationRetrySkippedItem( - eval_row_id=eval_row.id, - reason="source_row_missing", - ) - ) - continue - targets.append((eval_row, source_row)) - - return targets, skipped - - -@router.post( - "/{eval_id}/retry", - response_model=CallImportEvaluationRetryResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="retryCallImportEvaluation", -) -async def retry_call_import_evaluation( - call_import_id: UUID, - eval_id: UUID, - payload: Optional[CallImportEvaluationRetryRequest] = Body(default=None), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationRetryResponse: - """Re-enqueue failed rows in an evaluation run. - - Default behavior (no body) is "retry every row that failed". Pass - ``eval_row_ids`` to scope the retry to a specific subset (e.g. the - single row a user clicked in the UI). Rows that are still - in-flight or already completed are returned in ``skipped`` rather - than re-enqueued, so this endpoint is always safe to call. - - When ``metric_ids`` is set in the payload, this is a **metric- - subset retry**: only the listed metrics are recomputed (and merged - into the row's existing ``metric_scores`` — other metrics' values - are preserved). The route auto-flips ``include_completed=True`` in - that case so previously-successful rows are eligible for re- - scoring; without it the call would no-op because every row would - be skipped as ``completed``. - - The worker contract is the same as the create endpoint: - ``evaluate_call_import_row_task(eval_row_id, [restricted_metric_ids])``. - When the run is configured for diarised transcripts and the row's - diarised transcript is missing, we chain through - ``transcribe_call_import_row_task`` first — matching the - auto-transcribe behavior of POST ``/evaluations``. - """ - del api_key - call_import = _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - requested_ids = payload.eval_row_ids if payload else None - # Metric-subset retry: validate that every metric is something this - # run actually scored. Empty list is rejected too — callers that - # want a full re-run should omit the field entirely. - # - # ``selected_metric_ids`` holds the LEAVES only (children for - # hierarchical / category metrics, standalone metrics otherwise) — - # see ``leaf_metric_ids`` in :func:`create_call_import_evaluation`. - # Parent IDs for hierarchical metrics live separately in - # ``selected_metric_groups`` (``{parent_id: [child_ids]}``) so the - # UI can reconstruct the tree without round-tripping through the - # metric table. - # - # The Re-run-metrics modal surfaces PARENTS for hierarchical - # metrics (it suppresses individual children via - # ``childrenInGroups`` in ``CallImportEvaluationDetail.tsx``), so a - # naive ``metric_ids ⊆ selected_metric_ids`` check rejects every - # parent-ID request with a misleading "unknown ids" 400. We accept - # both shapes here and then EXPAND any parent IDs into - # ``{parent_id, *child_ids}`` so the downstream helpers see the - # full set of keys that need clearing + the full set of leaves - # that need re-scoring. - metric_ids: Optional[List[UUID]] = ( - payload.metric_ids if payload else None - ) - if metric_ids is not None: - if not metric_ids: - raise HTTPException( - status_code=400, - detail=( - "metric_ids must be a non-empty list. Omit the " - "field to re-run all metrics." - ), - ) - - leaf_set: Set[str] = { - str(item).lower() - for item in (evaluation.selected_metric_ids or []) - } - # ``selected_metric_groups`` is a dict ``{parent_id_str: - # [child_id_str, ...]}`` (see line ~487 in - # ``create_call_import_evaluation``). We tolerate stale data - # (string / UUID / non-dict) without crashing the retry path — - # if it's malformed we just treat it as "no parents" and fall - # back to the leaf-only check. - groups_raw = ( - evaluation.selected_metric_groups - if isinstance(evaluation.selected_metric_groups, dict) - else {} - ) - parent_to_children_str: Dict[str, List[str]] = {} - for parent_key, children_raw in groups_raw.items(): - if not isinstance(children_raw, (list, tuple)): - continue - children_norm = [ - str(c).lower() for c in children_raw if c is not None - ] - parent_to_children_str[str(parent_key).lower()] = children_norm - parent_set = set(parent_to_children_str.keys()) - - unknown = [ - mid for mid in metric_ids - if str(mid).lower() not in leaf_set - and str(mid).lower() not in parent_set - ] - if unknown: - raise HTTPException( - status_code=400, - detail=( - "metric_ids must be a subset of this evaluation's " - f"selected metrics; unknown ids: {[str(u) for u in unknown]}." - ), - ) - - # Expand parent IDs into ``{parent, *children}`` so: - # * ``_reset_eval_row_for_retry`` strips BOTH the parent - # entry (with ``chosen_child_id`` / rationale) AND every - # per-child boolean entry that the LLM evaluator wrote - # under each child's ID (see - # ``app/workers/tasks/helpers/llm_evaluation.py`` lines - # 1584 and 1649). - # * ``_enqueue_eval_rows_with_optional_transcribe`` → - # ``evaluate_call_import_row_task`` filters the work-list - # off ``selected_metric_ids`` (leaves), so we MUST hand it - # the child IDs for the parent to actually get re-scored. - # Leaves pass through unchanged. - expanded: List[UUID] = [] - seen: Set[str] = set() - for mid in metric_ids: - mid_norm = str(mid).lower() - children_str = parent_to_children_str.get(mid_norm) - if children_str is not None: - # Parent: include the parent ID itself (so the parent - # entry in ``metric_scores`` is also cleared) and all - # of its children. - candidates = [mid_norm, *children_str] - else: - candidates = [mid_norm] - for candidate in candidates: - if candidate in seen: - continue - try: - expanded.append(UUID(candidate)) - except (TypeError, ValueError): - # Defensive: skip junk values rather than 500. - continue - seen.add(candidate) - metric_ids = expanded - - # ``include_completed`` is auto-enabled when the caller asked for a - # metric subset (otherwise the metric-subset retry would always - # no-op on a green run, which is the whole reason this feature - # exists). The explicit payload flag wins for full-row retries. - include_completed = bool( - (payload.include_completed if payload else False) - or (metric_ids is not None) - ) - - transcribe_overwrite = bool( - payload.transcribe_overwrite if payload else False - ) - - skipped: List[CallImportEvaluationRetrySkippedItem] = [] - if requested_ids is None: - from app.db_sharding.eval_rows import count_evaluation_rows_for_run - from app.db_sharding.sessions import is_sharding_enabled - - if is_sharding_enabled(): - statuses = ( - ["failed", "completed"] if include_completed else ["failed"] - ) - target_count = count_evaluation_rows_for_run( - db, eval_id, statuses=statuses - ) - else: - from sqlalchemy import func - - count_query = db.query(func.count(CallImportEvaluationRow.id)).filter( - CallImportEvaluationRow.evaluation_id == eval_id - ) - if include_completed: - count_query = count_query.filter( - CallImportEvaluationRow.status.in_(["failed", "completed"]) - ) - else: - count_query = count_query.filter( - CallImportEvaluationRow.status == "failed" - ) - target_count = int(count_query.scalar() or 0) - if target_count == 0: - return CallImportEvaluationRetryResponse( - requeued=0, - transcribe_requeued=0, - skipped=skipped, - ) - else: - targets, skipped = _gather_retry_targets( - db, - evaluation, - requested_ids, - include_completed=include_completed, - ) - if not targets: - return CallImportEvaluationRetryResponse( - requeued=0, - transcribe_requeued=0, - skipped=skipped, - ) - target_count = len(targets) - - # Apply LLM / STT overrides BEFORE enqueueing so the persisted run - # config is correct by the time the worker reads it. - if payload is not None: - _apply_retry_overrides(db, evaluation, organization_id, payload) - _apply_telephony_retry_overrides( - db, - call_import=call_import, - organization_id=organization_id, - payload=payload, - ) - - evaluation.error_message = None - evaluation.finished_at = None - evaluation.status = "running" - if not evaluation.started_at: - from datetime import datetime, timezone - - evaluation.started_at = datetime.now(timezone.utc) - - _claim_evaluation_bulk_operation(eval_id, "retry") - db.commit() - - from app.workers.tasks.call_import_bulk_ops import ( - retry_call_import_evaluation_task, - ) - - retry_call_import_evaluation_task.delay( - str(eval_id), - { - "eval_row_ids": [str(rid) for rid in requested_ids] - if requested_ids - else None, - "metric_ids": [str(mid) for mid in metric_ids] if metric_ids else None, - "include_completed": include_completed, - "transcribe_overwrite": transcribe_overwrite, - }, - ) - - return CallImportEvaluationRetryResponse( - requeued=target_count, - transcribe_requeued=0, - skipped=skipped, - ) - - -@router.post( - "/{eval_id}/rows/{eval_row_id}/retry", - response_model=CallImportEvaluationRowResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="retryCallImportEvaluationRow", -) -async def retry_call_import_evaluation_row( - call_import_id: UUID, - eval_id: UUID, - eval_row_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportEvaluationRowResponse: - """Re-enqueue a single failed evaluation row. - - Convenience wrapper around ``retry_call_import_evaluation`` for the - "Retry this row" affordance in the row table. Returns the - refreshed row so the UI can update its badge immediately, without - waiting for the next polling tick. - """ - del api_key - _require_import(db, call_import_id, organization_id) - - evaluation = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.id == eval_id, - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .first() - ) - if not evaluation: - raise HTTPException( - status_code=404, detail="Call import evaluation not found" - ) - - _require_no_evaluation_bulk_operation(eval_id) - - from app.db_sharding.eval_rows import ( - evaluation_row_session, - find_evaluation_row_in_run, - ) - from app.db_sharding.sessions import is_sharding_enabled - - eval_row, _source_stub = find_evaluation_row_in_run(db, eval_id, eval_row_id) - if eval_row is None: - raise HTTPException( - status_code=404, detail="Evaluation row not found in this run" - ) - - if eval_row.status in {"pending", "running"}: - raise HTTPException( - status_code=409, - detail=( - "This row is still in progress — wait for it to finish " - "before retrying." - ), - ) - - targets, _ = _gather_retry_targets(db, evaluation, [eval_row.id]) - if not targets: - raise HTTPException( - status_code=409, - detail=( - "This row cannot be retried in its current state " - f"(status={eval_row.status})." - ), - ) - - if is_sharding_enabled(): - with evaluation_row_session(eval_row_id) as ( - row_db, - _catalog_db, - eval_row, - source_row, - _shard_id, - ): - _prepare_source_row_for_retry(source_row, transcribe_overwrite=False) - _reset_eval_row_for_retry(eval_row) - row_db.commit() - targets = [(eval_row, source_row)] - else: - for er, source_row in targets: - _prepare_source_row_for_retry(source_row, transcribe_overwrite=False) - _reset_eval_row_for_retry(er) - - evaluation.error_message = None - evaluation.finished_at = None - evaluation.status = "running" - if not evaluation.started_at: - from datetime import datetime, timezone - - evaluation.started_at = datetime.now(timezone.utc) - db.flush() - _rollup_evaluation_status(evaluation, db) - db.commit() - - try: - _enqueue_eval_rows_with_optional_transcribe(db, evaluation, targets) - except Exception as exc: # noqa: BLE001 - logger.exception( - "Failed to re-enqueue retry for evaluation row {}", eval_row_id - ) - if is_sharding_enabled(): - with evaluation_row_session(eval_row_id) as ( - row_db, - _catalog_db, - eval_row, - _source_row, - _shard_id, - ): - eval_row.status = "failed" - eval_row.error_message = f"Failed to re-enqueue retry: {exc}" - row_db.commit() - else: - eval_row.status = "failed" - eval_row.error_message = f"Failed to re-enqueue retry: {exc}" - _rollup_evaluation_status(evaluation, db) - db.commit() - raise HTTPException( - status_code=500, - detail=f"Failed to re-enqueue retry: {exc}", - ) - - if is_sharding_enabled(): - with evaluation_row_session(eval_row_id) as ( - _row_db, - _catalog_db, - eval_row, - source_row, - _shard_id, - ): - return _to_evaluation_row_response(eval_row, source_row, evaluation) - - db.refresh(eval_row) - source_row = targets[0][1] - return _to_evaluation_row_response(eval_row, source_row, evaluation) - - -from app.core.auth.capabilities import EVALS_RUN, EVALS_VIEW, REPORTS_GENERATE -from app.core.auth.workspace_route_capabilities import apply_workspace_route_capabilities - -apply_workspace_route_capabilities( - router, - view_capability=EVALS_VIEW, - manage_capability=EVALS_RUN, - run_capability=EVALS_RUN, - report_capability=REPORTS_GENERATE, -) +"""Evaluation routes scoped to a Call Import batch.""" + +from __future__ import annotations + +import asyncio +import csv +import base64 +import io +import json +import math +import re +import statistics +from typing import Any, Dict, Iterator, List, Literal, Optional, Set, Tuple +from uuid import UUID + +from datetime import date, datetime, timedelta, timezone + +from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Query, Response, status +from fastapi.responses import StreamingResponse +from loguru import logger +from pydantic import BaseModel, Field, field_validator +from sqlalchemy import desc, func, or_, text +from sqlalchemy.orm import Session +from sqlalchemy.orm.attributes import flag_modified + +from app.core.auth import Principal, get_principal +from app.core.auth.capabilities import REPORTS_GENERATE, capability_denied_message +from app.database import get_db +from app.dependencies import ( + get_api_key, + get_organization_id, + get_workspace_id, + require_enterprise_feature, +) +from app.services.call_imports.audit import ( + actor_emails_for_evaluation, + emails_for_user_ids, + stamp_evaluation_actor, + user_ids_from_evaluations, +) +from app.services.workspace_rbac import resolve_workspace_capabilities +from app.models.database import ( + AIProvider, + CallImport, + CallImportEvaluation, + CallImportEvaluationReportSnapshot, + CallImportEvaluationRow, + CallImportRow, + Metric, + PromptPartial, + Workspace, +) +from app.models.enums import CallImportRowStatus, ModelProvider +from app.models.schemas import ( + CallImportEvaluationAggregateResponse, + CallImportEvaluationBulkDelete, + CallImportEvaluationBulkActionResponse, + CallImportEvaluationCreate, + CallImportEvaluationListResponse, + CallImportEvaluationResponse, + CallImportEvaluationRetryRequest, + CallImportEvaluationRetryResponse, + CallImportEvaluationRetrySkippedItem, + CallImportEvaluationRowListResponse, + CallImportEvaluationRowResponse, + CallImportEvaluationUpdate, + CallImportMetricAggregate, + CallImportMetricHistogramBucket, + CallImportMetricLabelPair, + CallImportMetricSummary, + CallImportMetricValueCount, + DiscoveredLabelDeleteRequest, + DiscoveredLabelItem, + DiscoveredLabelMergeRequest, + DiscoveredLabelsResponse, + DiscoveredMetricDeleteRequest, + DiscoveredMetricItem, + DiscoveredMetricMergeRequest, + DiscoveredMetricsResponse, + EvaluationInsightsRequest, + EvaluationTldrSummary, + EvaluationMetricClustersRequest, + EvaluationMetricClustersState, + EvaluationPromptImprovementsRequest, + EvaluationPromptImprovementsState, + MetricFailurePoliciesResponse, + MetricFailurePoliciesSaveRequest, + MetricFailurePolicy, + MetricClusterEligibleRow, + MetricClusterEligibleRowsResponse, + EvaluationUserInsightsRequest, + EvaluationUserInsightsState, + MetricFlowEdge, + MetricPeriodDelta, + MetricFlowNode, + MetricFlowResponse, +) +from app.services.reporting.call_import_evaluation_pdf_report import ( + call_import_evaluation_pdf_report_service, +) +from app.services.call_import_metric_clusters import ( + METRIC_CLUSTERS_CANCELLED_BY_USER_ERROR, + estimate_metric_clusters_llm_calls, + filter_completed_row_pairs, + list_eligible_cluster_rows, + metric_clusters_raw_is_cancelled, + metric_clusters_state_from_raw, + metric_clusters_state_to_db, +) +from app.services.metric_failure_policy import ( + aggregate_primary_percent, + build_failure_policy_previews, + effective_policies, + failure_rate_percent_from_rows, + failure_policies_to_db, + has_clusterable_metrics, + merge_clustering_policies, + merge_failure_policies_into_raw, + policies_from_evaluation_raw, + validate_failure_policies_for_metrics, +) +from app.services.call_import_user_insights import ( + normalize_max_llm_calls, + total_llm_calls_for_rows, + user_insights_state_from_raw, +) + +router = APIRouter( + prefix="/call-imports/{call_import_id}/evaluations", + tags=["Call Import Evaluations"], + dependencies=[Depends(require_enterprise_feature("call_imports"))], +) + + +class CallImportEvaluationPdfReportRequest(BaseModel): + vendor_name: str = Field(..., min_length=1, max_length=120) + report_type: Literal["external", "internal"] = "external" + include_weekly_delta: bool = False + include_period_delta: bool = False + baseline_evaluation_id: Optional[str] = None + period_label: Optional[str] = Field(default=None, max_length=64) + use_case: Optional[str] = Field(default=None, max_length=120) + internal_brand_image_id: Optional[str] = None + external_brand_image_id: Optional[str] = None + report_config: Dict[str, Any] = Field(default_factory=dict) + platform_base_url: Optional[str] = Field( + default=None, + max_length=512, + description="Frontend origin for deep links to example calls in internal PDFs.", + ) + + @field_validator("vendor_name") + @classmethod + def _clean_vendor_name(cls, value: str) -> str: + cleaned = value.strip() + if not cleaned: + raise ValueError("Vendor name is required.") + return cleaned + + +class CallImportEvaluationBaselineCandidate(BaseModel): + evaluation_id: str + name: str + dataset: str + period_label: Optional[str] = None + period_start: Optional[date] = None + period_end: Optional[date] = None + period_display: str + completed_rows: int + created_at: datetime + is_default: bool = False + + +class CallImportEvaluationBaselineCandidatesResponse(BaseModel): + items: List[CallImportEvaluationBaselineCandidate] + default_evaluation_id: Optional[str] = None + + +def _require_import( + db: Session, + call_import_id: UUID, + organization_id: UUID, +) -> CallImport: + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException(status_code=404, detail="Call import not found") + return call_import + + +def require_call_import_capability(capability: str): + """Ensure the caller has *capability* in the call import's workspace (not just the header).""" + + def _dep( + call_import_id: UUID, + principal: Principal = Depends(get_principal), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), + ) -> CallImport: + call_import = _require_import(db, call_import_id, organization_id) + caps, _, role = resolve_workspace_capabilities( + db, + principal=principal, + workspace_id=call_import.workspace_id, + organization_id=organization_id, + ) + if capability not in caps: + raise HTTPException( + status_code=403, + detail=capability_denied_message( + capability, + role_name=role.name if role else None, + workspace_label="the active workspace", + ), + ) + return call_import + + return _dep + + +def _flatten_transcript(text: Optional[str]) -> str: + """Collapse a multi-line transcript onto a single line for spreadsheet export. + + The diarised transcript is stored as ``: `` lines joined + by ``\\n`` because the in-app ``TranscriptView`` parses those line + breaks to render chat bubbles. In Excel / Google Sheets that same + newline-per-turn formatting causes each cell to balloon vertically, + which the user reads as "lots of empty space on top of the cell". + Flattening at export time keeps the DB shape intact while giving the + spreadsheet a single-line cell per row. + """ + if not text: + return "" + parts = [ + segment.strip() + for segment in text.replace("\r\n", "\n").replace("\r", "\n").split("\n") + ] + return " ".join(p for p in parts if p) + + +def _evaluated_transcript_source_label( + evaluation: CallImportEvaluation, + source_row: CallImportRow, +) -> str: + """Label which transcript source this row was scored against.""" + source = (evaluation.transcript_source or "diarised").strip().lower() + if source == "production": + if not (source_row.transcript or "").strip(): + return "" + return "Production" + if not (source_row.diarised_transcript or "").strip(): + return "" + return "Diarised" + + +def _pick_evaluation_row_transcript( + source_row: Optional[CallImportRow], + evaluation: Optional[CallImportEvaluation] = None, +) -> Optional[str]: + """Transcript shown in evaluation row detail for the run's source.""" + if source_row is None: + return None + source = ( + (evaluation.transcript_source or "diarised").strip().lower() + if evaluation is not None + else "diarised" + ) + if source == "production": + raw = (source_row.transcript or "").strip() + return raw or None + diarised = (source_row.diarised_transcript or "").strip() + if diarised: + return diarised + raw = (source_row.transcript or "").strip() + return raw or None + + +def _to_evaluation_row_response( + eval_row_obj: CallImportEvaluationRow, + source_row: Optional[CallImportRow], + evaluation: Optional[CallImportEvaluation] = None, +) -> CallImportEvaluationRowResponse: + """Serialize one evaluation row plus joined source-row metadata.""" + return CallImportEvaluationRowResponse( + id=eval_row_obj.id, + evaluation_id=eval_row_obj.evaluation_id, + call_import_row_id=eval_row_obj.call_import_row_id, + row_index=source_row.row_index if source_row else None, + conversation_id=source_row.conversation_id if source_row else None, + transcript=_pick_evaluation_row_transcript(source_row, evaluation), + raw_columns=source_row.raw_columns if source_row else None, + recording_url=source_row.recording_url if source_row else None, + recording_date=source_row.recording_date if source_row else None, + recording_s3_key=source_row.recording_s3_key if source_row else None, + diarised_transcript_status=( + source_row.diarised_transcript_status if source_row else None + ), + diarised_transcript_error=( + source_row.diarised_transcript_error if source_row else None + ), + status=eval_row_obj.status, + metric_scores=eval_row_obj.metric_scores or {}, + error_message=eval_row_obj.error_message, + started_at=eval_row_obj.started_at, + finished_at=eval_row_obj.finished_at, + created_at=eval_row_obj.created_at, + updated_at=eval_row_obj.updated_at, + ) + + +def _serialize_selected_metric_ids(value) -> List[UUID]: + result: List[UUID] = [] + if not isinstance(value, list): + return result + for item in value: + try: + result.append(UUID(str(item))) + except (TypeError, ValueError): + continue + return result + + +def _metrics_for_ids(db: Session, org_id: UUID, ids: List[UUID]) -> List[Metric]: + if not ids: + return [] + rows = ( + db.query(Metric) + .filter( + Metric.organization_id == org_id, + Metric.id.in_(ids), + ) + .all() + ) + by_id = {row.id: row for row in rows} + return [by_id[mid] for mid in ids if mid in by_id] + + +def _expand_metric_selection( + db: Session, + org_id: UUID, + selected_ids: List[UUID], +) -> Tuple[List[Metric], Dict[UUID, List[Metric]]]: + """Resolve user-supplied metric ids into actual leaves + parent grouping. + + Rules: + * If a parent id is in ``selected_ids`` and no specific children of + that parent are also listed, include EVERY enabled child of that + parent. + * If a parent id AND some of its children are listed, include only + the listed children (treat the parent selection as the + "container" so users can deselect labels). + * Standalone metrics (no parent, no children) pass through + unchanged. + * Disabled metrics are filtered out at this layer so the caller + doesn't have to repeat the check. + + Returns: + (effective_metrics, parent_to_children) + + ``effective_metrics`` is the deduplicated list of metrics the + worker will actually score (children + standalone). Order is + preserved from ``selected_ids`` for display stability. + + ``parent_to_children`` maps each parent metric id (UUID) to the + list of its selected children. Useful for grouping in the LLM + prompt builder. + """ + if not selected_ids: + return [], {} + + requested = list(selected_ids) + initial_rows = ( + db.query(Metric) + .filter( + Metric.organization_id == org_id, + Metric.id.in_(requested), + ) + .all() + ) + initial_by_id = {row.id: row for row in initial_rows} + + parent_ids_requested = { + m.id for m in initial_rows if m.selection_mode and not m.parent_metric_id + } + # Map parent id -> children explicitly requested by the user. + explicit_children_by_parent: Dict[UUID, List[Metric]] = {} + for m in initial_rows: + if m.parent_metric_id and m.parent_metric_id in parent_ids_requested: + explicit_children_by_parent.setdefault( + m.parent_metric_id, [] + ).append(m) + + # For parents without explicit children, hydrate every enabled child. + parents_needing_full_expansion = [ + pid + for pid in parent_ids_requested + if pid not in explicit_children_by_parent + ] + auto_expanded_children: Dict[UUID, List[Metric]] = {} + if parents_needing_full_expansion: + for pid in parents_needing_full_expansion: + child_rows = ( + db.query(Metric) + .filter( + Metric.organization_id == org_id, + Metric.parent_metric_id == pid, + Metric.enabled.is_(True), + ) + .order_by(Metric.created_at.asc()) + .all() + ) + auto_expanded_children[pid] = child_rows + + parent_to_children: Dict[UUID, List[Metric]] = {} + for pid in parent_ids_requested: + children = explicit_children_by_parent.get( + pid + ) or auto_expanded_children.get(pid, []) + # Drop disabled children so the worker doesn't waste a slot on + # them. Empty parents (no enabled children) are still tracked + # because the UI may want to show "0 of 0" rather than swallow + # them silently. + parent_to_children[pid] = [c for c in children if c.enabled] + + effective: List[Metric] = [] + seen: set[UUID] = set() + for mid in requested: + m = initial_by_id.get(mid) + if m is None: + continue + if m.selection_mode and not m.parent_metric_id: + # Parent row itself is not scored — only its children. + for child in parent_to_children.get(m.id, []): + if child.id in seen or not child.enabled: + continue + seen.add(child.id) + effective.append(child) + continue + if m.parent_metric_id and m.parent_metric_id in parent_ids_requested: + # Already accounted for via the parent expansion above. + continue + if not m.enabled: + continue + if m.id in seen: + continue + seen.add(m.id) + effective.append(m) + + return effective, parent_to_children + + +def _evaluation_bulk_operation_for_response( + evaluation_id: UUID, +) -> Optional[str]: + from app.services.call_imports.evaluation_bulk_op import ( + get_evaluation_bulk_operation, + ) + + return get_evaluation_bulk_operation(evaluation_id) + + +def _serialize_eval( + db: Session, + row: CallImportEvaluation, + *, + sibling_evaluation_ids: Optional[List[UUID]] = None, + user_emails: Optional[Dict[UUID, str]] = None, +) -> CallImportEvaluationResponse: + selected_ids = _serialize_selected_metric_ids(row.selected_metric_ids) + + # Pull every metric referenced anywhere in the run's grouping (leaves, + # standalone, AND parents from selected_metric_groups) so the UI can + # render parent labels even when only children were materialized into + # selected_metric_ids. + groups_raw: Dict[str, List[str]] = {} + if isinstance(row.selected_metric_groups, dict): + for parent_str, children in row.selected_metric_groups.items(): + if not isinstance(children, list): + continue + cleaned: List[str] = [] + for c in children: + try: + UUID(str(c)) + cleaned.append(str(c)) + except (TypeError, ValueError): + continue + try: + UUID(parent_str) + groups_raw[parent_str] = cleaned + except (TypeError, ValueError): + continue + + metric_ids_for_lookup: List[UUID] = list(selected_ids) + for parent_str in groups_raw.keys(): + try: + pid = UUID(parent_str) + if pid not in metric_ids_for_lookup: + metric_ids_for_lookup.append(pid) + except (TypeError, ValueError): + continue + + metrics = _metrics_for_ids( + db, row.organization_id, metric_ids_for_lookup + ) + + from app.services.call_imports.progress_counters import merge_eval_counters_for_ui + + ui_completed_raw, ui_failed_raw = merge_eval_counters_for_ui(row) + total = int(row.total_rows or 0) + ui_completed = ( + min(ui_completed_raw, total) if total else ui_completed_raw + ) + ui_failed = min(ui_failed_raw, total) if total else ui_failed_raw + + if user_emails is None: + user_emails = emails_for_user_ids(db, user_ids_from_evaluations([row])) + created_email, updated_email = actor_emails_for_evaluation(row, user_emails) + + return CallImportEvaluationResponse( + id=row.id, + call_import_id=row.call_import_id, + organization_id=row.organization_id, + name=row.name, + selected_metric_ids=selected_ids, + selected_metric_groups=groups_raw or None, + metrics=[ + CallImportMetricSummary( + id=metric.id, + name=metric.name, + metric_type=metric.metric_type, + description=metric.description, + parent_metric_id=metric.parent_metric_id, + selection_mode=metric.selection_mode, + # Required by the Flow tab to know whether a parent + # opted into discovery; without it the + # DiscoveredLabelsPanel stays hidden even when the + # worker is actively producing discovered_labels. + allow_discovery=bool( + getattr(metric, "allow_discovery", False) + ), + ) + for metric in metrics + ], + status=row.status, + total_rows=row.total_rows, + completed_rows=ui_completed, + failed_rows=ui_failed, + error_message=row.error_message, + llm_provider=row.llm_provider, + llm_model=row.llm_model, + llm_credential_id=row.llm_credential_id, + llm_config=( + row.llm_config if isinstance(getattr(row, "llm_config", None), dict) else None + ), + metric_llm_overrides=( + row.metric_llm_overrides + if isinstance(row.metric_llm_overrides, dict) + else None + ), + stt_provider=row.stt_provider, + stt_model=row.stt_model, + stt_credential_id=row.stt_credential_id, + diarisation_llm_provider=getattr(row, "diarisation_llm_provider", None), + diarisation_llm_model=getattr(row, "diarisation_llm_model", None), + diarisation_llm_credential_id=getattr( + row, "diarisation_llm_credential_id", None + ), + diarisation_prompt=getattr(row, "diarisation_prompt", None), + transcribe_mode=( + (getattr(row, "transcribe_mode", None) or "stt_llm") + ), + transcript_source=(row.transcript_source or "diarised"), + sibling_evaluation_ids=list(sibling_evaluation_ids or []), + started_at=row.started_at, + finished_at=row.finished_at, + created_at=row.created_at, + updated_at=row.updated_at, + created_by_email=created_email, + last_updated_by_email=updated_email, + tldr_summary=_tldr_summary_payload(row), + user_insights=_user_insights_payload(row), + metric_clusters=_metric_clusters_payload(row), + discover_new_metrics=bool( + getattr(row, "discover_new_metrics", False) + ), + bulk_operation=_evaluation_bulk_operation_for_response(row.id), + ) + + +def _normalize_name(value: Optional[str]) -> Optional[str]: + """Trim user-supplied name; empty string becomes ``NULL``.""" + if value is None: + return None + trimmed = value.strip() + return trimmed or None + + +def _rollup_evaluation_status(evaluation: CallImportEvaluation, db: Session) -> None: + """Recompute counters + terminal status after rows are added/removed. + + Uses a single aggregate query instead of loading every row status. + """ + from app.workers.tasks.evaluate_call_import_row_core import ( + _apply_parent_status_from_counters, + reconcile_evaluation_counters, + ) + + reconcile_evaluation_counters(db, evaluation) + _apply_parent_status_from_counters(evaluation) + db.flush() + + if evaluation.status in {"completed", "failed", "partial"}: + from app.models.database import CallImport + from app.services.call_imports.bulk_ops import rollup_call_import_batch_status + + call_import = ( + db.query(CallImport) + .filter(CallImport.id == evaluation.call_import_id) + .first() + ) + if call_import is not None: + rollup_call_import_batch_status(db, call_import) + + +@router.post( + "", + response_model=CallImportEvaluationResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="createCallImportEvaluation", +) +async def create_call_import_evaluation( + call_import_id: UUID, + payload: CallImportEvaluationCreate, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationResponse: + del api_key + call_import = _require_import(db, call_import_id, organization_id) + + metric_ids = payload.metric_ids + if not metric_ids: + raise HTTPException( + status_code=400, + detail="Select at least one metric to run the evaluation against.", + ) + + org_metrics = ( + db.query(Metric) + .filter( + Metric.organization_id == organization_id, + Metric.id.in_(metric_ids), + ) + .all() + ) + by_id = {metric.id: metric for metric in org_metrics} + unknown_ids = [mid for mid in metric_ids if mid not in by_id] + if unknown_ids: + raise HTTPException( + status_code=400, + detail=( + "These metric ids do not exist in your organization: " + f"{', '.join(str(mid) for mid in unknown_ids)}. " + "Refresh the metrics list and try again." + ), + ) + # Parents themselves are containers, not scored rows, so a disabled + # parent shouldn't block the run as long as it has enabled children. + # We only reject disabled rows that the worker will actually try to + # evaluate (children + standalone leaves). + disabled_leaves = [ + metric + for metric in org_metrics + if not metric.enabled + and not (metric.selection_mode and not metric.parent_metric_id) + ] + if disabled_leaves: + names = ", ".join(metric.name for metric in disabled_leaves) + raise HTTPException( + status_code=400, + detail=( + f"These metrics are disabled and cannot be evaluated: {names}. " + "Enable them on the Metrics page (or pick different ones) and " + "try again." + ), + ) + + # Expand hierarchical selection: parents auto-include their enabled + # children, mixed parent+child selections respect the user's subset. + effective_metrics, parent_to_children = _expand_metric_selection( + db, organization_id, metric_ids + ) + if not effective_metrics: + raise HTTPException( + status_code=400, + detail=( + "None of the selected metrics yielded an enabled leaf to " + "evaluate. Check that parent categories have enabled " + "children, then try again." + ), + ) + + # The effective list (children + standalone leaves) is what gets + # persisted to ``selected_metric_ids`` and scored by the worker. + # The original parents are preserved in ``selected_metric_groups`` + # so the UI can rebuild the tree later. + leaf_metric_ids: List[UUID] = [m.id for m in effective_metrics] + selected_metric_groups: Dict[str, List[str]] = { + str(pid): [str(c.id) for c in children] + for pid, children in parent_to_children.items() + } + metric_rows = effective_metrics + valid_metric_id_strs = {str(m.id) for m in metric_rows} + + # ----- Validate run-level + per-metric LLM config ----- + llm_provider_norm: Optional[str] = None + llm_model_norm: Optional[str] = None + if payload.llm_provider or payload.llm_model: + if not (payload.llm_provider and payload.llm_model): + raise HTTPException( + status_code=400, + detail="Both llm_provider and llm_model are required when overriding the run LLM.", + ) + try: + llm_provider_norm = ModelProvider( + payload.llm_provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Unknown LLM provider '{payload.llm_provider}'. " + "Valid keys are documented in ModelProvider." + ), + ) + llm_model_norm = payload.llm_model.strip() or None + if not llm_model_norm: + raise HTTPException( + status_code=400, detail="llm_model cannot be empty." + ) + + if payload.llm_credential_id is not None: + cred = ( + db.query(AIProvider) + .filter( + AIProvider.id == payload.llm_credential_id, + AIProvider.organization_id == organization_id, + ) + .first() + ) + if not cred: + raise HTTPException( + status_code=400, + detail=( + "The provided llm_credential_id does not exist in this " + "organization." + ), + ) + + # Per-metric overrides: keys can be either a leaf metric id (applies + # to that metric only) or a parent metric id (applies to every + # child of that parent). Parent keys are expanded to their + # children so the worker only sees concrete leaf ids. + metric_overrides_payload: Optional[Dict[str, Dict[str, Any]]] = None + if payload.metric_llm_overrides: + metric_overrides_payload = {} + for metric_id, override in payload.metric_llm_overrides.items(): + target_leaf_ids: List[str] = [] + if metric_id in valid_metric_id_strs: + target_leaf_ids = [metric_id] + else: + # Maybe it's a parent id — expand to the children that + # are part of THIS run. + try: + parent_uuid = UUID(metric_id) + except (TypeError, ValueError): + raise HTTPException( + status_code=400, + detail=( + "metric_llm_overrides references metric " + f"{metric_id} which is not a valid UUID." + ), + ) + children_for_parent = parent_to_children.get(parent_uuid) + if not children_for_parent: + raise HTTPException( + status_code=400, + detail=( + "metric_llm_overrides references metric " + f"{metric_id} which is not in metric_ids." + ), + ) + target_leaf_ids = [str(c.id) for c in children_for_parent] + + override_dict: Dict[str, Any] = {} + if override.provider is not None: + if not override.model: + raise HTTPException( + status_code=400, + detail=( + f"Override for metric {metric_id} has a provider " + "but no model." + ), + ) + try: + override_dict["provider"] = ModelProvider( + override.provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Override for metric {metric_id} uses unknown " + f"provider '{override.provider}'." + ), + ) + override_dict["model"] = override.model.strip() + elif override.model: + # Model without provider doesn't make sense — treat as 400 + # so the UI can fix it instead of silently falling back. + raise HTTPException( + status_code=400, + detail=( + f"Override for metric {metric_id} has a model but " + "no provider." + ), + ) + if override.credential_id is not None: + override_dict["credential_id"] = str(override.credential_id) + if override.llm_config is not None: + override_dict["llm_config"] = override.llm_config + if override_dict: + for leaf_id in target_leaf_ids: + metric_overrides_payload[leaf_id] = override_dict + + # ----- Validate auto-transcribe settings ----- + # Diarised runs auto-diarise rows missing a diarised transcript and + # require STT + diariser LLM config. Production runs score the CSV + # transcript directly and skip diarisation entirely. + use_diarised = payload.transcript_sources[0] == "diarised" + auto_transcribe = use_diarised + + transcribe_mode_norm: Optional[str] = None + stt_provider_norm: Optional[str] = None + stt_model_norm: Optional[str] = None + diarisation_llm_provider_norm: Optional[str] = None + diarisation_llm_model_norm: Optional[str] = None + diarisation_prompt_norm: Optional[str] = None + + if use_diarised: + transcribe_mode_norm = (payload.transcribe_mode or "stt_llm").strip().lower() + if transcribe_mode_norm not in {"stt_llm", "llm_only"}: + raise HTTPException( + status_code=400, + detail=( + f"Unknown transcribe_mode '{payload.transcribe_mode}'. " + "Expected 'stt_llm' or 'llm_only'." + ), + ) + + if transcribe_mode_norm == "stt_llm": + if not payload.stt_provider: + raise HTTPException( + status_code=400, + detail=( + "stt_provider is required when " + "transcribe_mode='stt_llm': every evaluation run " + "auto-diarises rows that are missing a diarised " + "transcript." + ), + ) + if not payload.stt_model: + raise HTTPException( + status_code=400, + detail=( + "stt_model is required when transcribe_mode='stt_llm'." + ), + ) + try: + stt_provider_norm = ModelProvider( + payload.stt_provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=f"Unknown STT provider '{payload.stt_provider}'.", + ) + stt_model_norm = payload.stt_model.strip() or None + if not stt_model_norm: + raise HTTPException( + status_code=400, detail="stt_model cannot be empty." + ) + else: + # llm_only — explicitly reject lingering STT inputs so the + # contract is unambiguous (the worker would ignore them but + # silent acceptance hides accidental misconfiguration). + if (payload.stt_provider or "").strip() or ( + payload.stt_model or "" + ).strip(): + raise HTTPException( + status_code=400, + detail=( + "stt_provider / stt_model must be omitted when " + "transcribe_mode='llm_only'; the LLM consumes the " + "audio directly." + ), + ) + + # --- Validate LLM diariser settings ----- + if not payload.diarization_llm_provider: + raise HTTPException( + status_code=400, + detail=( + "diarization_llm_provider is required: every evaluation " + "run diarises STT output with an LLM." + ), + ) + if not payload.diarization_llm_model: + raise HTTPException( + status_code=400, + detail=( + "diarization_llm_model is required: every evaluation " + "run diarises STT output with an LLM." + ), + ) + try: + diarisation_llm_provider_norm = ModelProvider( + payload.diarization_llm_provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Unknown diarisation LLM provider " + f"'{payload.diarization_llm_provider}'." + ), + ) + diarisation_llm_model_norm = ( + payload.diarization_llm_model.strip() or None + ) + if not diarisation_llm_model_norm: + raise HTTPException( + status_code=400, + detail="diarization_llm_model cannot be empty.", + ) + diarisation_prompt_norm = ( + payload.diarization_prompt.strip() + if isinstance(payload.diarization_prompt, str) + else None + ) or None + + from app.models.enums import CallImportParameterType, CallImportStatus + from app.services.call_imports.bulk_ops import ( + count_all_source_rows, + count_completed_source_rows, + count_source_rows_with_production_transcript, + ) + + starting_from_mapped = False + if call_import.status == CallImportStatus.MAPPED: + if not call_import.source_s3_key or not call_import.source_format: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "This batch has no staged source file. Upload and map " + "a CSV/Excel file before running evaluation." + ), + ) + if not call_import.schema_id: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Cannot run evaluation without a mapped schema.", + ) + from app.api.v1.routes.call_imports import ( + _ensure_blob_storage_enabled, + _resolve_schema, + _resolve_telephony_integration, + _validate_direct_url_import_ready, + ) + + workspace_id = call_import.workspace_id + schema = _resolve_schema( + db, organization_id, workspace_id, call_import.schema_id + ) + parameters = list(schema.parameters) + if not use_diarised: + transcript_mapped = any( + param.type == CallImportParameterType.TRANSCRIPT + and (call_import.parameter_mapping or {}).get(param.name) + for param in parameters + ) + if not transcript_mapped: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "No transcript column is mapped in this batch. " + "Map a schema transcript parameter to a CSV column, " + "or choose 'Diarize then evaluate'." + ), + ) + if payload.telephony_integration_id is not None: + integration = _resolve_telephony_integration( + db, + organization_id, + payload.telephony_integration_id, + payload.provider or "", + ) + else: + _validate_direct_url_import_ready( + parameters, dict(call_import.parameter_mapping or {}) + ) + integration = None + + _ensure_blob_storage_enabled() + + if integration is not None: + call_import.provider = integration.provider + call_import.telephony_integration_id = integration.id + else: + call_import.provider = None + call_import.telephony_integration_id = None + + call_import.total_rows = 0 + call_import.completed_rows = 0 + call_import.failed_rows = 0 + call_import.error_message = None + call_import.status = CallImportStatus.PROCESSING + db.commit() + db.refresh(call_import) + starting_from_mapped = True + + if use_diarised: + total_row_count = count_completed_source_rows(db, call_import.id) + else: + # Production runs score CSV text — rows need not wait for + # recording fetch to finish before they are evaluable. + total_row_count = count_source_rows_with_production_transcript( + db, call_import.id + ) + + requested_sources: List[str] = list(payload.transcript_sources) + + if ( + not use_diarised + and not starting_from_mapped + and count_all_source_rows(db, call_import.id) > 0 + and total_row_count == 0 + ): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "No rows have a production transcript. " + "Choose 'Diarize then evaluate' or import rows with " + "a transcript column." + ), + ) + + base_name = _normalize_name(payload.name) + + def _name_for_source(source: str) -> Optional[str]: + # Single-source runs preserve the user's chosen name verbatim. + del source + return base_name + + created_evaluations: List[CallImportEvaluation] = [] + + for source in requested_sources: + evaluation = CallImportEvaluation( + call_import_id=call_import.id, + organization_id=organization_id, + # Mirror the parent CallImport's workspace so listings can + # filter on workspace_id directly without joining. + workspace_id=call_import.workspace_id, + name=_name_for_source(source), + selected_metric_ids=[ + str(metric_id) for metric_id in leaf_metric_ids + ], + selected_metric_groups=selected_metric_groups or None, + status="pending", + total_rows=total_row_count, + completed_rows=0, + failed_rows=0, + llm_provider=llm_provider_norm, + llm_model=llm_model_norm, + llm_credential_id=payload.llm_credential_id, + llm_config=payload.llm_config, + metric_llm_overrides=metric_overrides_payload, + stt_provider=stt_provider_norm, + stt_model=stt_model_norm, + stt_credential_id=( + payload.stt_credential_id if auto_transcribe else None + ), + diarisation_llm_provider=diarisation_llm_provider_norm, + diarisation_llm_model=diarisation_llm_model_norm, + diarisation_llm_credential_id=( + payload.diarization_llm_credential_id if auto_transcribe else None + ), + diarisation_prompt=diarisation_prompt_norm, + transcribe_mode=transcribe_mode_norm, + transcript_source=source, + discover_new_metrics=bool( + getattr(payload, "discover_new_metrics", False) + ), + ) + stamp_evaluation_actor(evaluation, principal, creating=True) + db.add(evaluation) + db.flush() + created_evaluations.append(evaluation) + + db.commit() + for evaluation in created_evaluations: + db.refresh(evaluation) + + primary_evaluation = created_evaluations[0] + sibling_ids = [e.id for e in created_evaluations[1:]] + + if not total_row_count and not starting_from_mapped: + for evaluation in created_evaluations: + evaluation.status = "completed" + db.commit() + for evaluation in created_evaluations: + db.refresh(evaluation) + return _serialize_eval( + db, primary_evaluation, sibling_evaluation_ids=sibling_ids + ) + + if starting_from_mapped: + from app.workers.tasks.call_import_bulk_ops import ( + materialize_mapped_call_import_evaluation_task, + ) + + for evaluation in created_evaluations: + materialize_mapped_call_import_evaluation_task.delay( + str(call_import.id), + str(organization_id), + str(call_import.workspace_id), + str(evaluation.id), + transcribe_overwrite=payload.transcribe_overwrite, + ) + else: + from app.workers.tasks.call_import_bulk_ops import ( + materialize_call_import_evaluation_task, + ) + + for evaluation in created_evaluations: + materialize_call_import_evaluation_task.delay( + str(evaluation.id), + transcribe_overwrite=payload.transcribe_overwrite, + ) + + for evaluation in created_evaluations: + db.refresh(evaluation) + + return _serialize_eval( + db, primary_evaluation, sibling_evaluation_ids=sibling_ids + ) + + +@router.get( + "", + response_model=CallImportEvaluationListResponse, + operation_id="listCallImportEvaluations", +) +async def list_call_import_evaluations( + call_import_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationListResponse: + del api_key + _require_import(db, call_import_id, organization_id) + rows = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .order_by(desc(CallImportEvaluation.created_at)) + .all() + ) + email_map = emails_for_user_ids(db, user_ids_from_evaluations(rows)) + return CallImportEvaluationListResponse( + items=[_serialize_eval(db, row, user_emails=email_map) for row in rows], + total=len(rows), + ) + + +@router.get( + "/{eval_id}", + response_model=CallImportEvaluationResponse, + operation_id="getCallImportEvaluation", +) +async def get_call_import_evaluation( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationResponse: + del api_key + _require_import(db, call_import_id, organization_id) + row = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not row: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + return _serialize_eval(db, row) + + +@router.get( + "/{eval_id}/rows", + response_model=CallImportEvaluationRowListResponse, + operation_id="listCallImportEvaluationRows", +) +async def list_call_import_evaluation_rows( + call_import_id: UUID, + eval_id: UUID, + page: int = Query(1, ge=1), + page_size: int = Query(100, ge=1, le=500), + q: Optional[str] = Query( + None, + description=( + "Free-text search across conversation_id and transcript " + "(case-insensitive substring match)." + ), + ), + metric_id: Optional[UUID] = Query( + None, + description=( + "If set, only return rows whose ``metric_scores[metric_id].value`` " + "exactly matches ``metric_value`` (string-compared). " + "Use together with ``metric_value``." + ), + ), + metric_value: Optional[str] = Query( + None, + description="Value to match against metric_id (string compare).", + ), + status_filter: Optional[str] = Query( + None, + alias="status", + description="Restrict to rows with this evaluation row status.", + ), + flow_parent_id: Optional[UUID] = Query( + None, + description=( + "Parent (category) metric whose ``sequence`` array should be " + "checked against ``flow_node`` and ``flow_edge_target``. Used " + "to drill into the calls behind a flow-chart node or edge." + ), + ), + flow_node: Optional[str] = Query( + None, + description=( + "If set together with ``flow_parent_id``, only return rows " + "whose sequence under that parent contains this step. Accepts " + "either a child metric UUID (resolved to slug(name)), a " + "``disc:`` discovered-label id, or a raw slug." + ), + ), + flow_edge_target: Optional[str] = Query( + None, + description=( + "Optional companion to ``flow_node``: when set, restrict to " + "rows whose sequence contains the directed transition " + "``flow_node -> flow_edge_target`` (immediately adjacent). " + "Same id format as ``flow_node``." + ), + ), + discovered_parent_id: Optional[UUID] = Query( + None, + description=( + "Parent (category) metric that defines the discovery scope " + "for ``discovered_label_key`` / ``has_discovered``." + ), + ), + discovered_label_key: Optional[str] = Query( + None, + description=( + "If set together with ``discovered_parent_id``, only return " + "rows whose ``metric_scores[parent].discovered_labels`` " + "list contains an entry with this slug (after applying " + "evaluation-level merge aliases)." + ), + ), + has_discovered: Optional[bool] = Query( + None, + description=( + "If true together with ``discovered_parent_id``, only return " + "rows that have at least one LLM-discovered label for the " + "parent. Useful to triage which calls produced novel labels." + ), + ), + sort_by: Optional[str] = Query( + None, + description=( + "Column to sort by. Accepted values: ``row_index`` (default " + "when omitted), ``conversation_id``, ``status`` (the " + "evaluation-row status), or ``metric:`` to sort " + "by ``metric_scores[].value``. Metric sorts compare " + "the extracted JSON text — adequate for booleans, enum " + "labels, and 0-1 ratings; large integer values may sort " + "lexicographically (10 before 2)." + ), + ), + sort_dir: Optional[str] = Query( + "asc", + description="Sort direction: ``asc`` (default) or ``desc``.", + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationRowListResponse: + del api_key + _require_import(db, call_import_id, organization_id) + + eval_row = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not eval_row: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + query = ( + db.query(CallImportEvaluationRow, CallImportRow) + .join(CallImportRow, CallImportRow.id == CallImportEvaluationRow.call_import_row_id) + .filter(CallImportEvaluationRow.evaluation_id == eval_id) + ) + + # --- Filters ---------------------------------------------------------- + if q and q.strip(): + needle = f"%{q.strip()}%" + # Search across both transcript columns so a hit in either the + # production or the diarised version surfaces the row, + # independent of which source the evaluation actually scored. + query = query.filter( + or_( + CallImportRow.conversation_id.ilike(needle), + CallImportRow.transcript.ilike(needle), + CallImportRow.diarised_transcript.ilike(needle), + ) + ) + + if status_filter: + # The CallImportEvaluationRow.status column is a string in PG so a + # plain == filter works; we lowercase to match the stored values. + query = query.filter( + CallImportEvaluationRow.status == status_filter.strip().lower() + ) + + if metric_id is not None and metric_value is not None: + # ``metric_scores`` is a JSONB column shaped like + # ``{"": {"value": , "type": "boolean", ...}}``. We + # extract the nested ``value`` as text and compare to the user + # input as a string — that handles bool/int/enum without needing + # per-type casts. ``metric_value`` is matched case-insensitively + # so chart clicks on labels like "True" survive any casing drift + # between worker output and the chart label. + path_value = func.json_extract_path_text( + CallImportEvaluationRow.metric_scores, + str(metric_id), + "value", + ) + query = query.filter(func.lower(path_value) == metric_value.strip().lower()) + + # --- Flow chart drilldown filter ------------------------------------- + # Translates a clicked node (or edge) on the flow chart into a + # SQL filter against ``metric_scores[].sequence``. The + # frontend sends either a child UUID, a ``disc:`` discovered + # node id, or a raw slug — we normalize all three to the slug that + # actually appears in stored ``sequence`` arrays. + if flow_parent_id is not None and flow_node and flow_node.strip(): + parent_id_str_local = str(flow_parent_id) + alias_map_flow = _alias_map_for_parent(eval_row, flow_parent_id) + + def _flow_node_to_slug(raw: str) -> Optional[str]: + raw_clean = raw.strip() + if not raw_clean: + return None + if raw_clean == _FLOW_START_NODE_ID: + # The synthetic START node isn't a real sequence entry; + # filtering on it is meaningless so we skip silently. + return None + if raw_clean.startswith(_DISCOVERED_NODE_PREFIX): + return _resolve_alias( + alias_map_flow, + _slug_label(raw_clean[len(_DISCOVERED_NODE_PREFIX) :]), + ) + # Try to interpret as a child metric UUID first; fall back + # to treating it as a slug. + try: + child_uuid = UUID(raw_clean) + except (TypeError, ValueError): + return _resolve_alias(alias_map_flow, _slug_label(raw_clean)) + child = ( + db.query(Metric.name) + .filter( + Metric.id == child_uuid, + Metric.organization_id == organization_id, + ) + .first() + ) + if child and child[0]: + return _resolve_alias(alias_map_flow, _slug_label(child[0])) + return _resolve_alias(alias_map_flow, _slug_label(raw_clean)) + + from_slug = _flow_node_to_slug(flow_node) + target_slug: Optional[str] = None + if flow_edge_target and flow_edge_target.strip(): + target_slug = _flow_node_to_slug(flow_edge_target) + + if from_slug: + # The ``metric_scores`` column is declared as ``Column(JSON)`` + # in the model so on databases where the table was created + # from the model (rather than the migration) the physical + # type is ``json``, not ``jsonb``. The JSONB-only operators + # below (``jsonb_exists``, ``jsonb_array_elements_text``, + # ``@>``) require a JSONB input — we cast once up front so + # the same SQL works regardless of which path created the + # table. + scores_jsonb = ( + "(call_import_evaluation_rows.metric_scores)::jsonb" + ) + if target_slug: + # Edge filter: rows whose sequence under this parent + # contains ``from_slug`` immediately followed by + # ``target_slug``. Implemented as a correlated EXISTS + # over ``jsonb_array_elements_text`` with ORDINALITY, + # which is the portable way to express "next array + # index" against a JSONB array in Postgres. + edge_filter_sql = text( + f""" + EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text( + COALESCE( + {scores_jsonb} -> :p_id -> 'sequence', + '[]'::jsonb + ) + ) WITH ORDINALITY AS s1(elem, ord) + JOIN jsonb_array_elements_text( + COALESCE( + {scores_jsonb} -> :p_id -> 'sequence', + '[]'::jsonb + ) + ) WITH ORDINALITY AS s2(elem, ord) + ON s2.ord = s1.ord + 1 + WHERE s1.elem = :from_slug + AND s2.elem = :to_slug + ) + """ + ).bindparams( + p_id=parent_id_str_local, + from_slug=from_slug, + to_slug=target_slug, + ) + query = query.filter(edge_filter_sql) + else: + # Node filter: rows whose ``metric_scores -> parent -> + # 'sequence'`` array contains ``from_slug``. We use the + # function form ``jsonb_exists`` rather than the ``?`` + # operator to avoid psycopg2 mistaking the question + # mark for a parameter placeholder. + node_filter_sql = text( + f""" + jsonb_exists( + COALESCE( + {scores_jsonb} -> :p_id -> 'sequence', + '[]'::jsonb + ), + :slug + ) + """ + ).bindparams(p_id=parent_id_str_local, slug=from_slug) + query = query.filter(node_filter_sql) + + # --- Discovered label filters --------------------------------------- + # Surfaces "which calls produced THIS LLM-discovered label" and the + # broader "which calls produced ANY LLM-discovered label". Both + # operate on ``metric_scores[].discovered_labels`` (a list + # of dicts) plus the same ``sequence`` array — covering both legacy + # rows where the slug only made it into ``sequence`` and newer + # rows where it landed in both. + if discovered_parent_id is not None and ( + discovered_label_key or has_discovered + ): + d_parent_str = str(discovered_parent_id) + alias_map_disc = _alias_map_for_parent(eval_row, discovered_parent_id) + # See note above: cast once so the JSONB operators don't reject + # the column when it's typed as ``json`` in the database. + scores_jsonb = "(call_import_evaluation_rows.metric_scores)::jsonb" + if discovered_label_key and discovered_label_key.strip(): + target = _resolve_alias( + alias_map_disc, _slug_label(discovered_label_key) + ) + if target: + # Match rows whose discovered_labels list has an entry + # ``{"key": }`` OR whose sequence array still + # contains the slug. The latter covers older rows that + # were rewritten by a merge in the discovered_labels + # blob but whose sequence may have lagged. + contains_json = json.dumps( + {d_parent_str: {"discovered_labels": [{"key": target}]}} + ) + disc_filter_sql = text( + f""" + ( + {scores_jsonb} @> CAST(:contains AS JSONB) + OR + jsonb_exists( + COALESCE( + {scores_jsonb} -> :p_id -> 'sequence', + '[]'::jsonb + ), + :slug + ) + ) + """ + ).bindparams( + contains=contains_json, + p_id=d_parent_str, + slug=target, + ) + query = query.filter(disc_filter_sql) + elif has_discovered: + # No specific slug — just rows that surfaced any candidate + # under this parent. We coalesce missing paths to ``[]`` so + # ``jsonb_array_length`` always sees an array (it raises on + # non-array inputs, but our shape guarantees a list when + # the key is present). + has_disc_sql = text( + f""" + jsonb_array_length( + COALESCE( + {scores_jsonb} -> :p_id -> 'discovered_labels', + '[]'::jsonb + ) + ) > 0 + """ + ).bindparams(p_id=d_parent_str) + query = query.filter(has_disc_sql) + + # --- Sorting ---------------------------------------------------------- + # Column-click sorting from the UI. Falls back to ``row_index`` so + # paging stays stable when the user clears the sort. We always add a + # secondary ``row_index`` tiebreaker so duplicate sort keys (e.g. + # many rows with ``status = 'completed'``) keep a deterministic + # order across page boundaries — without this, pagination can + # double-show or skip rows when Postgres picks a different physical + # order on each query. + direction_desc = (sort_dir or "asc").strip().lower() == "desc" + + def _apply_direction(column_expr): + return column_expr.desc() if direction_desc else column_expr.asc() + + # Whether the caller's ``sort_by`` resolved to a known column. We + # use this flag to decide whether ``sort_dir`` is honoured on the + # fallback path: unrecognized columns (typos, stale UI state) fall + # back to the implicit ``row_index ASC`` default and intentionally + # ignore ``sort_dir`` so users don't get a surprise reverse order + # from a typo'd column name. + sort_recognized = False + sort_by_clean = (sort_by or "").strip() + primary_sort = None + metric_uuid: Optional[UUID] = None + if sort_by_clean == "row_index": + sort_recognized = True + # Falls through to the default ``order_by`` below with + # ``primary_sort`` still None — but ``sort_recognized=True`` + # tells the fallback branch to apply the requested direction. + elif sort_by_clean == "conversation_id": + sort_recognized = True + primary_sort = _apply_direction(CallImportRow.conversation_id) + elif sort_by_clean == "status": + sort_recognized = True + primary_sort = _apply_direction(CallImportEvaluationRow.status) + elif sort_by_clean.startswith("metric:"): + raw_metric_id = sort_by_clean.split(":", 1)[1].strip() + try: + metric_uuid = UUID(raw_metric_id) + except (TypeError, ValueError): + metric_uuid = None + if metric_uuid is not None: + sort_recognized = True + # ``metric_scores`` is JSON-typed but the helper functions + # for path extraction differ between Postgres (production) + # and SQLite (default test backend). Branch on the active + # dialect so we can use the right primitive: + # * Postgres → ``json_extract_path_text(col, key, "value")`` + # which returns the value as TEXT for both ``json`` and + # ``jsonb`` columns. + # * SQLite → ``json_extract(col, '$."".value')`` + # using JSONPath syntax. ``metric_uuid`` is already + # validated above (``UUID(raw_metric_id)``), so the + # interpolated path is safe from injection. + # NULL values (rows where the metric wasn't scored) sort + # to the END regardless of direction so un-scored rows + # don't crowd the top of an ascending sort. + dialect_name = ( + db.bind.dialect.name if db.bind is not None else "postgresql" + ) + if dialect_name == "sqlite": + json_path = f'$."{metric_uuid}".value' + path_value = func.json_extract( + CallImportEvaluationRow.metric_scores, + json_path, + ) + else: + path_value = func.json_extract_path_text( + CallImportEvaluationRow.metric_scores, + str(metric_uuid), + "value", + ) + primary_sort = ( + path_value.desc().nullslast() + if direction_desc + else path_value.asc().nullslast() + ) + + if primary_sort is not None: + query = query.order_by(primary_sort, CallImportRow.row_index.asc()) + elif sort_recognized: + # Explicit ``sort_by=row_index`` request — honour direction. + query = query.order_by(_apply_direction(CallImportRow.row_index)) + else: + # No sort requested OR unrecognized column — safe default of + # ``row_index ASC``. We deliberately ignore ``sort_dir`` here + # so a typo'd / stale ``sort_by`` doesn't quietly invert the + # default order. + query = query.order_by(CallImportRow.row_index.asc()) + from app.db_sharding.eval_rows import fetch_evaluation_row_pairs_page + from app.db_sharding.sessions import is_sharding_enabled + + def _pair_row_index( + pair: Tuple[CallImportEvaluationRow, CallImportRow], + ) -> int: + return int(pair[1].row_index or 0) + + def _directed_string(value: Optional[str], desc: bool) -> Tuple[int, ...]: + text = value or "" + if not desc: + return (0, *text.encode("utf-8")) + return (1, *(-byte for byte in text.encode("utf-8"))) + + if sort_by_clean == "conversation_id": + def _pair_sort_key( + pair: Tuple[CallImportEvaluationRow, CallImportRow], + ) -> Tuple[Any, ...]: + return ( + _directed_string(pair[1].conversation_id, direction_desc), + _pair_row_index(pair), + ) + elif sort_by_clean == "status": + def _pair_sort_key( + pair: Tuple[CallImportEvaluationRow, CallImportRow], + ) -> Tuple[Any, ...]: + return ( + _directed_string(pair[0].status, direction_desc), + _pair_row_index(pair), + ) + elif sort_by_clean.startswith("metric:") and metric_uuid is not None: + metric_id_str = str(metric_uuid) + + def _pair_sort_key( + pair: Tuple[CallImportEvaluationRow, CallImportRow], + ) -> Tuple[Any, ...]: + scores = pair[0].metric_scores or {} + entry = scores.get(metric_id_str, {}) + raw_value = entry.get("value") if isinstance(entry, dict) else None + null_rank = 1 if raw_value is None else 0 + return ( + null_rank, + _directed_string( + str(raw_value) if raw_value is not None else None, + direction_desc, + ), + _pair_row_index(pair), + ) + elif sort_recognized and sort_by_clean == "row_index": + def _pair_sort_key( + pair: Tuple[CallImportEvaluationRow, CallImportRow], + ) -> Tuple[int, ...]: + idx = _pair_row_index(pair) + return (-idx,) if direction_desc else (idx,) + else: + def _pair_sort_key( + pair: Tuple[CallImportEvaluationRow, CallImportRow], + ) -> Tuple[int, ...]: + return (_pair_row_index(pair),) + + if is_sharding_enabled(): + def _build_query(session: Session): + return query.with_session(session) + + total, rows = fetch_evaluation_row_pairs_page( + db, + _build_query, + page=page, + page_size=page_size, + sort_key=_pair_sort_key, + bounded_shard_fetch=( + not sort_recognized or sort_by_clean == "row_index" + ), + ) + else: + total = query.count() + rows = query.offset((page - 1) * page_size).limit(page_size).all() + + # Row detail shows the transcript for this run's chosen source. + items: List[CallImportEvaluationRowResponse] = [ + _to_evaluation_row_response(eval_row_obj, source_row, eval_row) + for eval_row_obj, source_row in rows + ] + + return CallImportEvaluationRowListResponse( + items=items, + total=total, + page=page, + page_size=page_size, + ) + + +@router.get( + "/{eval_id}/export", + operation_id="exportCallImportEvaluationCsv", +) +async def export_call_import_evaluation_csv( + call_import_id: UUID, + eval_id: UUID, + format: Literal["csv", "xlsx"] = Query( + "csv", + description=( + "Output format. ``csv`` returns a UTF-8 BOM CSV; ``xlsx`` " + "returns a native Excel workbook (single sheet)." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> StreamingResponse: + del api_key + call_import = _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + selected_metric_ids = _serialize_selected_metric_ids(evaluation.selected_metric_ids) + # Include parent metric ids referenced in selected_metric_groups so + # the export shows a parent "Chosen Label" column next to its + # children's true/false columns. + lookup_ids: List[UUID] = list(selected_metric_ids) + groups_raw = ( + evaluation.selected_metric_groups + if isinstance(evaluation.selected_metric_groups, dict) + else {} + ) + for parent_str in groups_raw.keys(): + try: + pid = UUID(parent_str) + if pid not in lookup_ids: + lookup_ids.append(pid) + except (TypeError, ValueError): + continue + metrics = _metrics_for_ids(db, organization_id, lookup_ids) + metric_names = {str(metric.id): metric.name for metric in metrics} + metrics_by_id = {str(metric.id): metric for metric in metrics} + + # Two export-time modes depending on how the batch was uploaded: + # + # * Schema-driven (new): ``call_imports.schema_id`` is set, + # ``parameter_mapping`` records which CSV header fed each + # parameter, and ``raw_columns`` on each row is keyed by + # parameter NAME. Export headers are the parameter names. + # * Legacy (pre-schema): ``column_mapping`` / ``extra_columns`` / + # ``custom_column_mapping`` drive the columns and + # ``raw_columns`` is keyed by the original CSV header. + # + # We bucket entries into ``standard_export_headers`` (raw_columns + # key == export header) and ``custom_export`` (export header + # differs from the raw_columns key) so the row-projection loop + # below stays mode-agnostic. + standard_export_headers: List[str] = [] + custom_export: List[tuple[str, str]] = [] # [(export_header, raw_columns_key)] + + if call_import.schema_id is not None: + # Use the live schema parameter list for column ordering. Falls + # back to whatever's in ``parameter_mapping`` if the schema was + # deleted (defensive - the FK is ON DELETE RESTRICT, but tests + # / future cascades may still hit this branch). + from app.models.database import CallImportSchema as _ImportSchema + + schema_obj = ( + db.query(_ImportSchema) + .filter(_ImportSchema.id == call_import.schema_id) + .first() + ) + if schema_obj is not None: + params_sorted = sorted( + schema_obj.parameters, key=lambda p: p.ordering or 0 + ) + for param in params_sorted: + if param.name and param.name not in standard_export_headers: + standard_export_headers.append(param.name) + else: + for param_name in (call_import.parameter_mapping or {}).keys(): + if param_name and param_name not in standard_export_headers: + standard_export_headers.append(param_name) + else: + mapping = call_import.column_mapping or {} + mapped_headers = [ + mapping.get("external_call_id"), + mapping.get("transcript"), + mapping.get("recording_url"), + ] + for header in [*mapped_headers, *(call_import.extra_columns or [])]: + if ( + isinstance(header, str) + and header + and header not in standard_export_headers + ): + standard_export_headers.append(header) + + custom_mapping = call_import.custom_column_mapping or {} + if isinstance(custom_mapping, dict): + for name, csv_header in custom_mapping.items(): + if not isinstance(name, str) or not isinstance(csv_header, str): + continue + if not name or not csv_header: + continue + if name in standard_export_headers: + continue # would clobber a real column + custom_export.append((name, csv_header)) + + if ( + call_import.source_format == "audio" + and "conversation_id" not in standard_export_headers + ): + standard_export_headers.insert(0, "conversation_id") + + # Build the metric columns: each parent (if any) gets a value column + # and (when capture_rationale=true) a " - LLM Rationale" + # column. The per-child boolean columns are intentionally suppressed + # — categorization metrics now collapse to exactly two columns in + # the export, mirroring the in-app table. + child_ids_in_groups: set[str] = set() + for parent_str, child_strs in groups_raw.items(): + for child_str in child_strs: + if isinstance(child_str, str): + child_ids_in_groups.add(child_str) + + metric_headers: List[str] = [] + rationale_headers: Dict[str, str] = {} # metric_id_str -> rationale column name + seen_metric_ids: set[str] = set() + + def _add_metric_column(metric: Metric) -> None: + mid_str = str(metric.id) + if mid_str in seen_metric_ids: + return + # Skip any child whose parent is part of this run — the parent + # column above already shows the chosen child name as its + # value. + if mid_str in child_ids_in_groups: + return + seen_metric_ids.add(mid_str) + header = metric_names[mid_str] + metric_headers.append(header) + if bool(getattr(metric, "capture_rationale", False)): + rationale_header = f"{header} - LLM Rationale" + metric_headers.append(rationale_header) + rationale_headers[mid_str] = rationale_header + + for parent_str in groups_raw.keys(): + parent = metrics_by_id.get(parent_str) + if parent: + _add_metric_column(parent) + # Children of an in-run parent are deliberately not emitted — + # the ``child_ids_in_groups`` guard inside ``_add_metric_column`` + # is what enforces this. We still iterate the keys above (not + # ``.items()``) so the parent-only emission is explicit. + # Append anything left over (standalone metrics not in any group, or + # legacy runs without ``selected_metric_groups``). + for metric in metrics: + if metric.selection_mode and not metric.parent_metric_id: + continue # already handled above + if str(metric.id) in seen_metric_ids: + continue + _add_metric_column(metric) + + # Three new fixed columns surface the two transcript fields and the + # evaluation's transcript_source as live values pulled from the + # ``CallImportRow`` (not from the frozen ``raw_columns`` snapshot). + # The user can now compare "what was in the CSV" vs "what the + # diarisation worker produced" without round-tripping through the + # UI, and downstream tools can verify which transcript the metrics + # were computed against. + PRODUCTION_TRANSCRIPT_HEADER = "Production Transcript" + DIARISED_TRANSCRIPT_HEADER = "Diarised Transcript" + EVAL_SOURCE_HEADER = "Evaluated Transcript Source" + + fieldnames = [ + *standard_export_headers, + *[h for h, _ in custom_export], + PRODUCTION_TRANSCRIPT_HEADER, + DIARISED_TRANSCRIPT_HEADER, + EVAL_SOURCE_HEADER, + *metric_headers, + ] + + from app.db_sharding.scatter_gather import load_evaluation_row_pairs + from app.db_sharding.sessions import is_sharding_enabled + + if is_sharding_enabled(): + rows = sorted( + load_evaluation_row_pairs(db, eval_id), + key=lambda pair: int(pair[1].row_index or 0), + ) + else: + rows = ( + db.query(CallImportEvaluationRow, CallImportRow) + .join( + CallImportRow, + CallImportRow.id == CallImportEvaluationRow.call_import_row_id, + ) + .filter(CallImportEvaluationRow.evaluation_id == eval_id) + .order_by(CallImportRow.row_index.asc()) + .all() + ) + + def _project_rows() -> Iterator[Dict[str, str]]: + for eval_row, source_row in rows: + row_out: Dict[str, str] = {} + raw = ( + source_row.raw_columns + if isinstance(source_row.raw_columns, dict) + else {} + ) + for header in standard_export_headers: + value = raw.get(header) + if value is None and header == "conversation_id": + value = source_row.conversation_id + row_out[header] = "" if value is None else str(value) + for export_header, csv_header in custom_export: + value = raw.get(csv_header) + row_out[export_header] = "" if value is None else str(value) + + # Live transcripts pulled from the row, NOT from raw_columns, + # so re-diarised values are always reflected in the export. + # Both transcript columns are flattened to a single line so the + # spreadsheet cell doesn't balloon vertically — the in-app + # ``TranscriptView`` still has the DB copy with line breaks + # intact for chat-bubble rendering. + row_out[PRODUCTION_TRANSCRIPT_HEADER] = _flatten_transcript( + source_row.transcript + ) + row_out[DIARISED_TRANSCRIPT_HEADER] = _flatten_transcript( + source_row.diarised_transcript + ) + row_out[EVAL_SOURCE_HEADER] = _evaluated_transcript_source_label( + evaluation, + source_row, + ) + + scores = ( + eval_row.metric_scores + if isinstance(eval_row.metric_scores, dict) + else {} + ) + for metric in metrics: + metric_score = ( + scores.get(str(metric.id)) + if isinstance(scores, dict) + else None + ) + value = ( + metric_score.get("value") + if isinstance(metric_score, dict) + else None + ) + # Parent metrics (selection_mode set) render the chosen + # child name for single_choice or the ";"-joined list of + # true child names for multi_label. + if ( + metric.selection_mode + and not metric.parent_metric_id + and isinstance(metric_score, dict) + ): + if metric.selection_mode == "multi_label": + selected = metric_score.get("selected_child_names") + if isinstance(selected, list): + value = ";".join(str(s) for s in selected) + else: + value = ( + metric_score.get("chosen_child_name") + or metric_score.get("value") + ) + row_out[metric.name] = "" if value is None else str(value) + rationale_header = rationale_headers.get(str(metric.id)) + if rationale_header is not None: + rationale = ( + metric_score.get("rationale") + if isinstance(metric_score, dict) + else None + ) + row_out[rationale_header] = ( + "" if rationale is None else str(rationale) + ) + yield row_out + + base_filename = f"call-import-{call_import_id}-evaluation-{eval_id}" + + if format == "xlsx": + # xlsx is unicode-native (Hindi/Devanagari, emoji, etc.) so the + # UTF-8-BOM dance isn't needed here. ``write_only`` mode keeps + # peak memory bounded for large evaluations because openpyxl + # only buffers the current row. + try: + from openpyxl import Workbook # type: ignore + from openpyxl.cell import WriteOnlyCell # type: ignore + from openpyxl.styles import Font # type: ignore + except ImportError as exc: # pragma: no cover - exercised by pyproject lock + raise HTTPException( + status_code=500, + detail=( + "Excel export requires the 'openpyxl' package which is " + "not installed." + ), + ) from exc + + workbook = Workbook(write_only=True) + worksheet = workbook.create_sheet(title="Evaluation") + + bold_font = Font(bold=True) + header_cells = [] + for header in fieldnames: + cell = WriteOnlyCell(worksheet, value=header) + cell.font = bold_font + header_cells.append(cell) + worksheet.append(header_cells) + + for row_dict in _project_rows(): + worksheet.append([row_dict.get(h, "") for h in fieldnames]) + + buffer = io.BytesIO() + workbook.save(buffer) + xlsx_bytes = buffer.getvalue() + filename = f"{base_filename}.xlsx" + return StreamingResponse( + iter([xlsx_bytes]), + media_type=( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ), + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + output = io.StringIO() + writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction="ignore") + writer.writeheader() + for row_dict in _project_rows(): + writer.writerow(row_dict) + + # Excel on Windows defaults to the system ANSI codepage (Windows-1252) + # when a CSV has no encoding marker, which turns UTF-8 Hindi/Devanagari + # / any non-ASCII text into mojibake (e.g. ``ठीक`` → ``ठीक``). + # A UTF-8 BOM tells Excel to switch to UTF-8 decoding and is silently + # skipped by every other UTF-8-aware reader (pandas, LibreOffice, + # Google Sheets, etc.), so the data round-trips correctly everywhere. + csv_text = output.getvalue() + # ``utf-8-sig`` adds the UTF-8 BOM so Excel on Windows decodes the file + # as UTF-8 instead of the system codepage. We also declare the same + # codec in the Content-Type header so well-behaved HTTP clients (incl. + # ``httpx`` / ``requests`` in our tests) strip the BOM during decode. + csv_bytes = csv_text.encode("utf-8-sig") + filename = f"{base_filename}.csv" + return StreamingResponse( + iter([csv_bytes]), + media_type="text/csv; charset=utf-8-sig", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + +def _report_filename_slug(value: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-") + return slug or "client" + + +def _report_branding_for_import_workspace( + db: Session, + organization_id: UUID, + workspace_id: UUID, + *, + internal_brand_image_id: Optional[str] = None, + external_brand_image_id: Optional[str] = None, +) -> tuple[dict[str, str] | list[str], Optional[str]]: + workspace = ( + db.query(Workspace) + .filter( + Workspace.id == workspace_id, + Workspace.organization_id == organization_id, + ) + .first() + ) + raw = workspace.report_branding if workspace and isinstance(workspace.report_branding, dict) else {} + images = raw.get("images") if isinstance(raw.get("images"), list) else [] + loaded_images: list[dict[str, str]] = [] + for item in images: + if not isinstance(item, dict) or not item.get("s3_key"): + continue + content_type = str(item.get("content_type") or "image/png") + try: + from app.services.storage.s3_service import s3_service + + image_bytes = s3_service.download_file_by_key(str(item["s3_key"])) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Unable to load report branding image for workspace {}: {}", + workspace_id, + exc, + ) + continue + encoded = base64.b64encode(image_bytes).decode("ascii") + role = str(item.get("role") or "generic") + if role not in {"internal", "external", "generic"}: + role = "generic" + loaded_images.append( + { + "id": str(item.get("id") or ""), + "role": role, + "data_uri": f"data:{content_type};base64,{encoded}", + } + ) + + def _pick(role: str, selected_id: Optional[str]) -> Optional[str]: + if selected_id: + for loaded in loaded_images: + if loaded["id"] == selected_id: + return loaded["data_uri"] + for loaded in loaded_images: + if loaded["role"] == role: + return loaded["data_uri"] + return None + + logo_data_uris: dict[str, str] = {} + internal_uri = _pick("internal", internal_brand_image_id) + external_uri = _pick("external", external_brand_image_id) + if internal_uri: + logo_data_uris["internal"] = internal_uri + if external_uri: + logo_data_uris["external"] = external_uri + if ( + not logo_data_uris + and not internal_brand_image_id + and not external_brand_image_id + ): + # Backward compatibility for workspaces that only had a generic logo + # library before the two-slot report header existed. + generic_uris = [ + loaded["data_uri"] + for loaded in loaded_images + if loaded.get("data_uri") + ] + if generic_uris: + heading = raw.get("heading") if isinstance(raw.get("heading"), str) else None + return generic_uris[:4], heading + heading = raw.get("heading") if isinstance(raw.get("heading"), str) else None + return logo_data_uris, heading + + +def _display_metrics_for_pdf_report( + db: Session, + organization_id: UUID, + evaluation: CallImportEvaluation, +) -> list[Metric]: + selected_metric_ids = _serialize_selected_metric_ids(evaluation.selected_metric_ids) + lookup_ids: List[UUID] = list(selected_metric_ids) + groups_raw = ( + evaluation.selected_metric_groups + if isinstance(evaluation.selected_metric_groups, dict) + else {} + ) + for parent_str in groups_raw.keys(): + try: + parent_id = UUID(parent_str) + except (TypeError, ValueError): + continue + if parent_id not in lookup_ids: + lookup_ids.append(parent_id) + + metrics = _metrics_for_ids(db, organization_id, lookup_ids) + child_ids_in_groups: set[str] = set() + for child_strs in groups_raw.values(): + if not isinstance(child_strs, list): + continue + child_ids_in_groups.update(str(child_id) for child_id in child_strs) + + metrics_by_id = {str(metric.id): metric for metric in metrics} + display: list[Metric] = [] + seen: set[str] = set() + + for parent_str in groups_raw.keys(): + parent = metrics_by_id.get(str(parent_str)) + if parent and str(parent.id) not in seen: + display.append(parent) + seen.add(str(parent.id)) + + for metric in metrics: + metric_id = str(metric.id) + if metric_id in seen or metric_id in child_ids_in_groups: + continue + if metric.selection_mode and not metric.parent_metric_id: + continue + display.append(metric) + seen.add(metric_id) + + return display + + +def _metrics_for_clustering( + db: Session, + evaluation: CallImportEvaluation, + eval_rows: List[CallImportEvaluationRow], +) -> List[Metric]: + """All enabled quality metrics scored in this run, normalized for clustering. + + Hierarchical children are collapsed to their parent metric so cluster + groups render at the category level (e.g. ``AI reveal``) instead of the + child label level (e.g. ``Yes`` / ``No``). + """ + aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) + aggregate_metric_ids: List[UUID] = [] + for agg in aggregates: + if (agg.metric_category or "quality") == "user_insight": + continue + try: + aggregate_metric_ids.append(UUID(agg.metric_id)) + except (TypeError, ValueError): + continue + if not aggregate_metric_ids: + return [] + + aggregate_metrics = _metrics_for_ids( + db, evaluation.organization_id, aggregate_metric_ids + ) + by_id = {metric.id: metric for metric in aggregate_metrics} + + normalized_ids: List[UUID] = [] + seen: set[UUID] = set() + for metric_id in aggregate_metric_ids: + metric = by_id.get(metric_id) + target_id = ( + metric.parent_metric_id + if metric is not None and metric.parent_metric_id + else metric_id + ) + if target_id in seen: + continue + seen.add(target_id) + normalized_ids.append(target_id) + + metrics = _metrics_for_ids(db, evaluation.organization_id, normalized_ids) + return [ + metric + for metric in metrics + if getattr(metric, "enabled", True) and not _metric_is_user_insight(metric) + ] + + +def _metric_is_user_insight(metric: Metric) -> bool: + if (getattr(metric, "metric_category", "quality") or "quality") == "user_insight": + return True + text_value = " ".join( + str(part or "").lower() + for part in (getattr(metric, "name", ""), getattr(metric, "description", "")) + ) + normalized = text_value.replace("-", " ").replace("_", " ") + phrases = ( + "call context", + "caller context", + "product identification", + "out of scope", + "identity match", + "user identity", + "caller identity", + "frustration trigger", + "video call offer", + "video call reception", + ) + return any(phrase in normalized for phrase in phrases) + + +def _evaluation_rows_for_period( + db: Session, + evaluation_id: UUID, +) -> list[tuple[CallImportEvaluationRow, CallImportRow]]: + return ( + db.query(CallImportEvaluationRow, CallImportRow) + .join(CallImportRow, CallImportRow.id == CallImportEvaluationRow.call_import_row_id) + .filter(CallImportEvaluationRow.evaluation_id == evaluation_id) + .order_by(CallImportRow.row_index.asc()) + .all() + ) + + +def _baseline_candidate_evaluations( + db: Session, + organization_id: UUID, + workspace_id: UUID, + current_evaluation: CallImportEvaluation, + current_period_start: Optional[date], + *, + limit: int = 20, +) -> list[dict[str, Any]]: + candidates = ( + db.query(CallImportEvaluation, CallImport) + .join(CallImport, CallImport.id == CallImportEvaluation.call_import_id) + .filter( + CallImportEvaluation.organization_id == organization_id, + CallImport.workspace_id == workspace_id, + CallImportEvaluation.id != current_evaluation.id, + CallImportEvaluation.status == "completed", + CallImportEvaluation.completed_rows > 0, + ) + .order_by(desc(CallImportEvaluation.created_at)) + .limit(limit * 3) + .all() + ) + items: list[dict[str, Any]] = [] + for candidate_eval, candidate_import in candidates: + rows = _evaluation_rows_for_period(db, candidate_eval.id) + period_start, period_end, period_label, period_display = _report_period_from_rows(rows) + if current_period_start and period_start and period_start >= current_period_start: + continue + dataset = ( + (candidate_import.dataset or "").strip() + or (candidate_import.original_filename or candidate_import.filename or "").strip() + or "Unknown dataset" + ) + evaluation_name = ( + (candidate_eval.name or "").strip() + or str(candidate_eval.id)[:8] + ) + items.append( + { + "evaluation_id": str(candidate_eval.id), + "name": evaluation_name, + "dataset": dataset, + "period_label": period_label, + "period_start": period_start, + "period_end": period_end, + "period_display": period_display, + "completed_rows": int(candidate_eval.completed_rows or 0), + "created_at": candidate_eval.created_at, + "is_default": False, + } + ) + if len(items) >= limit: + break + items.sort( + key=lambda item: ( + item["period_start"] or date.min, + item["created_at"] or datetime.min.replace(tzinfo=timezone.utc), + ), + reverse=True, + ) + if items: + items[0]["is_default"] = True + return items + + +def _resolve_baseline_evaluation( + db: Session, + organization_id: UUID, + workspace_id: UUID, + current_evaluation: CallImportEvaluation, + current_period_start: Optional[date], + baseline_evaluation_id: Optional[str], +) -> Optional[CallImportEvaluation]: + candidates = _baseline_candidate_evaluations( + db, + organization_id, + workspace_id, + current_evaluation, + current_period_start, + ) + allowed_ids = {item["evaluation_id"] for item in candidates} + if baseline_evaluation_id: + baseline_id = str(baseline_evaluation_id).strip() + if baseline_id not in allowed_ids: + raise HTTPException( + status_code=400, + detail="Selected baseline evaluation is not a valid prior run for this report.", + ) + return ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == UUID(baseline_id), + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not candidates: + return None + default_id = candidates[0]["evaluation_id"] + return ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == UUID(default_id), + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + + +def _benchmark_context_for_evaluation( + db: Session, + baseline_evaluation: Optional[CallImportEvaluation], +) -> Optional[dict[str, str]]: + if baseline_evaluation is None: + return None + baseline_import = ( + db.query(CallImport) + .filter(CallImport.id == baseline_evaluation.call_import_id) + .first() + ) + rows = _evaluation_rows_for_period(db, baseline_evaluation.id) + period_start, _period_end, period_label, _period_display = _report_period_from_rows(rows) + dataset = ( + (baseline_import.dataset or "").strip() + if baseline_import and baseline_import.dataset + else None + ) + filename = ( + (baseline_import.original_filename or baseline_import.filename or "").strip() + if baseline_import + else None + ) + evaluation_label = ( + (baseline_evaluation.name or "").strip() + if baseline_evaluation.name + else str(baseline_evaluation.id)[:8] + ) + period = period_label or ( + period_start.isoformat() if period_start else "previous report" + ) + return { + "dataset": dataset or filename or "Unknown dataset", + "evaluation": evaluation_label, + "evaluation_id": str(baseline_evaluation.id), + "period": period, + } + + +def _period_deltas_from_evaluation( + db: Session, + baseline_evaluation: CallImportEvaluation, + current_metric_aggregates: list[dict[str, Any]], + current_evaluation: CallImportEvaluation, + current_eval_rows: List[CallImportEvaluationRow], +) -> dict[str, dict[str, str]]: + baseline_rows = _evaluation_rows_for_period(db, baseline_evaluation.id) + baseline_eval_rows = [eval_row for eval_row, _source_row in baseline_rows] + baseline_aggregate_models = _compute_metric_aggregates( + db, + baseline_evaluation, + baseline_eval_rows, + ) + baseline_metric_aggregates = [ + _aggregate_to_dict(aggregate) for aggregate in baseline_aggregate_models + ] + _metrics, _aggs, policies, _source, _child_map = _clustering_context( + db, current_evaluation, current_eval_rows + ) + metric_by_id = {str(m.id): m for m in _metrics} + current_by_id = { + str(item.get("metric_id")): item for item in current_metric_aggregates + } + previous_by_id = { + str(item.get("metric_id")): item + for item in baseline_metric_aggregates + if isinstance(item, dict) + } + deltas: dict[str, dict[str, str]] = {} + for metric_id, current in current_by_id.items(): + metric = metric_by_id.get(metric_id) + policy = policies.get(metric_id) + previous_raw = previous_by_id.get(metric_id) + if metric is None or policy is None: + deltas[metric_id] = { + "label": "No previous-week baseline", + "detail": "No comparable prior report snapshot was found.", + } + continue + current_pct = failure_rate_percent_from_rows( + current_eval_rows, metric, policy + ) + previous_pct = failure_rate_percent_from_rows( + baseline_eval_rows, metric, policy + ) + if current_pct is None or previous_pct is None: + current_pct = current_pct or _aggregate_primary_percent(current, policy) + previous_pct = ( + previous_pct or _aggregate_primary_percent(previous_raw, policy) + if previous_raw + else None + ) + if current_pct is None or previous_pct is None: + deltas[metric_id] = { + "label": "No previous-week baseline", + "detail": "No comparable prior report snapshot was found.", + } + continue + delta = current_pct - previous_pct + sign = "+" if delta >= 0 else "" + deltas[metric_id] = { + "label": f"{sign}{delta:.1f} pp", + "detail": ( + f"Current report {current_pct:.1f}% vs previous report " + f"{previous_pct:.1f}%" + ), + } + return deltas + + +_DELTA_EXPLANATION_SYSTEM_PROMPT = ( + "You are a senior conversation-analytics reviewer. You will receive " + "week-over-week metric failure-rate deltas plus reconciled failure " + "cluster context per metric.\n\n" + "Return STRICT JSON only:\n" + "{\n" + ' "explanations": {"": "<1-2 sentence explanation of why the delta likely occurred>"}\n' + "}\n\n" + "Constraints:\n" + "- Only include metrics supplied in the prompt.\n" + "- Cluster labels are generated independently each run and are NOT stable " + "IDs. Never compare an unmatched current label to 0% baseline.\n" + "- Use matched_theme_shifts for label-aligned comparisons, " + "gap_label_shifts for structural shifts, and new_themes_current_period " + "for themes that emerged without a baseline match.\n" + "- If reconciliation is uncertain, explain using the numeric delta and " + "gap_label_shifts only.\n" + "- Keep each explanation to 1-2 short sentences (~220 chars).\n" + "- Vendor-safe, factual language; no markdown." +) + + +def _period_delta_explanation_cache_key( + baseline_evaluation_id: UUID, + *, + completed_rows: int, + baseline_completed_rows: int, +) -> str: + return ( + f"{baseline_evaluation_id}:{completed_rows}:" + f"{baseline_completed_rows}:reconciled-v2" + ) + + +def _normalize_cluster_label(label: str) -> str: + return re.sub(r"[^a-z0-9]+", " ", (label or "").lower()).strip() + + +_CLUSTER_LABEL_STOPWORDS = frozenset( + { + "a", + "an", + "the", + "and", + "or", + "during", + "while", + "with", + "for", + "from", + "into", + "general", + "user", + "bot", + "agent", + } +) + + +def _cluster_label_tokens(label: str) -> set[str]: + return { + token + for token in _normalize_cluster_label(label).split() + if token and token not in _CLUSTER_LABEL_STOPWORDS and len(token) > 2 + } + + +def _cluster_label_similarity(left: str, right: str) -> float: + tokens_left = _cluster_label_tokens(left) + tokens_right = _cluster_label_tokens(right) + if not tokens_left or not tokens_right: + return 0.0 + intersection = tokens_left & tokens_right + if not intersection: + return 0.0 + union = tokens_left | tokens_right + jaccard = len(intersection) / len(union) + smaller = tokens_left if len(tokens_left) <= len(tokens_right) else tokens_right + overlap_ratio = len(intersection) / len(smaller) + return max(jaccard, overlap_ratio * 0.85) + + +def _group_clusters_by_gap_label( + clusters: list[dict[str, Any]], +) -> dict[str, list[dict[str, Any]]]: + grouped: dict[str, list[dict[str, Any]]] = {} + for cluster in clusters: + gap_label = str(cluster.get("gap_label") or "UNKNOWN") + grouped.setdefault(gap_label, []).append(cluster) + return grouped + + +def _append_matched_cluster_pair( + matched: list[dict[str, Any]], + current: dict[str, Any], + baseline: dict[str, Any], + *, + match_confidence: float, + match_method: str, +) -> None: + matched.append( + { + "current_label": current.get("label"), + "baseline_label": baseline.get("label"), + "gap_label": current.get("gap_label") or baseline.get("gap_label"), + "current_share_pct": current.get("share_pct"), + "baseline_share_pct": baseline.get("share_pct"), + "share_delta_pp": round( + float(current.get("share_pct") or 0.0) + - float(baseline.get("share_pct") or 0.0), + 1, + ), + "match_confidence": round(match_confidence, 2), + "match_method": match_method, + } + ) + + +def _aggregate_share_by_gap_label( + clusters: list[dict[str, Any]], +) -> dict[str, float]: + totals: dict[str, float] = {} + for cluster in clusters: + gap_label = str(cluster.get("gap_label") or "UNKNOWN") + totals[gap_label] = totals.get(gap_label, 0.0) + float( + cluster.get("share_pct") or 0.0 + ) + return {gap: round(share, 1) for gap, share in totals.items()} + + +def _reconcile_cluster_periods( + current_clusters: list[dict[str, Any]], + baseline_clusters: list[dict[str, Any]], + *, + similarity_threshold: float = 0.35, +) -> dict[str, Any]: + """Align independently-generated cluster labels before delta explanation.""" + matched: list[dict[str, Any]] = [] + current_unmatched = list(current_clusters) + remaining_baseline = list(baseline_clusters) + + current_by_gap = _group_clusters_by_gap_label(current_unmatched) + baseline_by_gap = _group_clusters_by_gap_label(remaining_baseline) + for gap_label in list(current_by_gap): + current_group = current_by_gap.get(gap_label) or [] + baseline_group = baseline_by_gap.get(gap_label) or [] + if len(current_group) != 1 or len(baseline_group) != 1: + continue + current = current_group[0] + baseline = baseline_group[0] + _append_matched_cluster_pair( + matched, + current, + baseline, + match_confidence=0.75, + match_method="single_cluster_per_gap_label", + ) + current_unmatched.remove(current) + remaining_baseline.remove(baseline) + current_by_gap[gap_label] = [] + baseline_by_gap[gap_label] = [] + + for current in list(current_unmatched): + best_idx: Optional[int] = None + best_score = 0.0 + for idx, baseline in enumerate(remaining_baseline): + score = _cluster_label_similarity( + str(current.get("label") or ""), + str(baseline.get("label") or ""), + ) + if current.get("gap_label") == baseline.get("gap_label"): + score += 0.1 + if score > best_score: + best_score = score + best_idx = idx + + if best_idx is not None and best_score >= similarity_threshold: + baseline = remaining_baseline.pop(best_idx) + _append_matched_cluster_pair( + matched, + current, + baseline, + match_confidence=best_score, + match_method="label_similarity", + ) + + matched_current_labels = { + str(item.get("current_label") or "") for item in matched + } + matched_baseline_labels = { + str(item.get("baseline_label") or "") for item in matched + } + current_unmatched = [ + cluster + for cluster in current_clusters + if str(cluster.get("label") or "") not in matched_current_labels + ] + remaining_baseline = [ + cluster + for cluster in baseline_clusters + if str(cluster.get("label") or "") not in matched_baseline_labels + ] + + new_themes = [ + { + "label": cluster.get("label"), + "gap_label": cluster.get("gap_label"), + "share_pct": cluster.get("share_pct"), + "note": "New theme in current period (no close baseline match).", + } + for cluster in current_unmatched + ] + + retired_themes = [ + { + "label": baseline.get("label"), + "gap_label": baseline.get("gap_label"), + "share_pct": baseline.get("share_pct"), + "note": "Theme present in baseline only (retired or renamed).", + } + for baseline in remaining_baseline + ] + + current_gap = _aggregate_share_by_gap_label(current_clusters) + baseline_gap = _aggregate_share_by_gap_label(baseline_clusters) + gap_label_shifts: dict[str, dict[str, float]] = {} + for gap_label in set(current_gap) | set(baseline_gap): + current_share = current_gap.get(gap_label, 0.0) + baseline_share = baseline_gap.get(gap_label, 0.0) + if abs(current_share - baseline_share) >= 0.5: + gap_label_shifts[gap_label] = { + "current_share_pct": current_share, + "baseline_share_pct": baseline_share, + "share_delta_pp": round(current_share - baseline_share, 1), + } + + return { + "matched_theme_shifts": matched, + "new_themes_current_period": new_themes, + "retired_themes_baseline_period": retired_themes, + "gap_label_shifts": gap_label_shifts, + "reconciliation_note": ( + "Cluster labels are generated independently each run and may " + "rename the same failure mode. Do not treat unmatched current " + "labels as 0% in the baseline period." + ), + } + + +def _load_period_delta_explanations_cache( + evaluation: CallImportEvaluation, + cache_key: str, +) -> Optional[dict[str, str]]: + raw = getattr(evaluation, "period_delta_explanations", None) + if not isinstance(raw, dict): + return None + entry = raw.get(cache_key) + if not isinstance(entry, dict): + return None + explanations_raw = entry.get("explanations") + if not isinstance(explanations_raw, dict): + return None + return { + str(metric_id): str(why).strip() + for metric_id, why in explanations_raw.items() + if str(metric_id).strip() and isinstance(why, str) and why.strip() + } + + +def _save_period_delta_explanations_cache( + db: Session, + evaluation: CallImportEvaluation, + cache_key: str, + explanations: dict[str, str], +) -> None: + raw = evaluation.period_delta_explanations + if not isinstance(raw, dict): + raw = {} + updated = dict(raw) + updated[cache_key] = { + "explanations": explanations, + "generated_at": datetime.now(timezone.utc).isoformat(), + } + evaluation.period_delta_explanations = updated + flag_modified(evaluation, "period_delta_explanations") + db.commit() + + +def _cluster_summary_for_metric( + state: Optional[EvaluationMetricClustersState], + metric_id: str, +) -> list[dict[str, Any]]: + if state is None or state.status != "completed": + return [] + for group in state.groups: + if str(group.metric_id) != metric_id: + continue + return [ + { + "label": cluster.label, + "gap_label": cluster.gap_label, + "share_pct": round(cluster.share_pct, 1), + "count": cluster.count, + } + for cluster in group.clusters[:5] + ] + return [] + + +def _merge_delta_why( + raw_deltas: dict[str, dict[str, str]], + explanations: dict[str, str], +) -> dict[str, dict[str, str]]: + if not explanations: + return raw_deltas + merged: dict[str, dict[str, str]] = {} + for metric_id, delta in raw_deltas.items(): + updated = dict(delta) + why = explanations.get(metric_id) + if why: + updated["why"] = why + merged[metric_id] = updated + return merged + + +def _explain_period_deltas( + db: Session, + organization_id: UUID, + evaluation: CallImportEvaluation, + baseline_evaluation: CallImportEvaluation, + raw_deltas: dict[str, dict[str, str]], + *, + min_delta_pp: float = 0.5, +) -> dict[str, dict[str, str]]: + """Attach ``why`` explanations to period deltas using cached LLM output.""" + if not raw_deltas: + return raw_deltas + + cache_key = _period_delta_explanation_cache_key( + baseline_evaluation.id, + completed_rows=evaluation.completed_rows, + baseline_completed_rows=baseline_evaluation.completed_rows, + ) + cached = _load_period_delta_explanations_cache(evaluation, cache_key) + if cached is not None: + return _merge_delta_why(raw_deltas, cached) + + current_clusters = _metric_clusters_payload(evaluation) + baseline_clusters = _metric_clusters_payload(baseline_evaluation) + metrics_for_prompt: list[dict[str, Any]] = [] + for metric_id, delta in raw_deltas.items(): + label = delta.get("label") or "" + if "No previous-week baseline" in label: + continue + match = re.search(r"([+-]?\d+(?:\.\d+)?)\s*pp", label) + if match and abs(float(match.group(1))) < min_delta_pp: + continue + current_summary = _cluster_summary_for_metric(current_clusters, metric_id) + baseline_summary = _cluster_summary_for_metric(baseline_clusters, metric_id) + if not current_summary and not baseline_summary: + continue + cluster_reconciliation = _reconcile_cluster_periods( + current_summary, + baseline_summary, + ) + metrics_for_prompt.append( + { + "metric_id": metric_id, + "delta_label": label, + "delta_detail": delta.get("detail") or "", + "cluster_reconciliation": cluster_reconciliation, + } + ) + + if not metrics_for_prompt: + return raw_deltas + + provider_hint: Optional[str] = None + model_hint: Optional[str] = None + tldr_raw = evaluation.tldr_summary + if isinstance(tldr_raw, dict): + if isinstance(tldr_raw.get("provider"), str): + provider_hint = tldr_raw["provider"] + if isinstance(tldr_raw.get("model"), str): + model_hint = tldr_raw["model"] + + from app.services.ai.llm_resolver import get_llm_provider_and_model + from app.services.call_import_user_insights import _call_llm, _parse_json_object + + provider_enum, model_str = get_llm_provider_and_model( + organization_id, db, provider_hint, model_hint + ) + try: + text = _call_llm( + db, + organization_id, + provider_enum, + model_str, + [ + {"role": "system", "content": _DELTA_EXPLANATION_SYSTEM_PROMPT}, + { + "role": "user", + "content": json.dumps( + {"metrics": metrics_for_prompt}, + ensure_ascii=False, + default=str, + ), + }, + ], + temperature=0.3, + max_tokens=900, + ) + except Exception as exc: + logger.warning("[PeriodDeltaExplain] LLM call failed: {}", exc) + return raw_deltas + + parsed = _parse_json_object(text) + explanations_raw = parsed.get("explanations") + explanations: dict[str, str] = {} + if isinstance(explanations_raw, dict): + for metric_id, why in explanations_raw.items(): + if isinstance(why, str) and why.strip(): + explanations[str(metric_id)] = why.strip() + + if explanations: + _save_period_delta_explanations_cache( + db, evaluation, cache_key, explanations + ) + return _merge_delta_why(raw_deltas, explanations) + + +def _period_deltas_with_explanations( + db: Session, + organization_id: UUID, + evaluation: CallImportEvaluation, + baseline_evaluation: CallImportEvaluation, + raw_deltas: dict[str, dict[str, str]], +) -> dict[str, dict[str, str]]: + return _explain_period_deltas( + db, + organization_id, + evaluation, + baseline_evaluation, + raw_deltas, + ) + + +def _benchmark_context_for_snapshot( + db: Session, + previous_snapshot: Optional[CallImportEvaluationReportSnapshot], +) -> Optional[dict[str, str]]: + if previous_snapshot is None: + return None + previous_import = ( + db.query(CallImport) + .filter(CallImport.id == previous_snapshot.call_import_id) + .first() + ) + previous_eval = ( + db.query(CallImportEvaluation) + .filter(CallImportEvaluation.id == previous_snapshot.evaluation_id) + .first() + ) + dataset = ( + (previous_import.dataset or "").strip() + if previous_import and previous_import.dataset + else None + ) + filename = ( + (previous_import.original_filename or previous_import.filename or "").strip() + if previous_import + else None + ) + evaluation_label = ( + (previous_eval.name or "").strip() + if previous_eval and previous_eval.name + else str(previous_snapshot.evaluation_id)[:8] + ) + period = previous_snapshot.period_label or ( + previous_snapshot.period_start.isoformat() + if previous_snapshot.period_start + else "previous report" + ) + return { + "dataset": dataset or filename or "Unknown dataset", + "evaluation": evaluation_label, + "evaluation_id": str(previous_snapshot.evaluation_id), + "period": period, + } + + +def _clamp_prose_to_sentences( + text: str, + *, + max_sentences: int = 3, + max_chars: int = 300, +) -> str: + """Keep concise audit/TLDR prose within sentence and character limits.""" + cleaned = (text or "").strip() + if not cleaned: + return cleaned + cleaned = re.sub(r"\s*\n+\s*", " ", cleaned).strip() + sentences = [ + sentence.strip() + for sentence in re.split(r"(?<=[.!?])\s+", cleaned) + if sentence.strip() + ] + if sentences: + result = " ".join(sentences[:max_sentences]).strip() + else: + result = cleaned + if len(result) > max_chars: + trimmed = result[: max_chars - 3].rsplit(" ", 1)[0].rstrip(".,;:") + result = f"{trimmed}..." if trimmed else result[:max_chars] + return result + + +def _audit_summary_text_from_tldr( + summary: Optional[EvaluationTldrSummary], +) -> Optional[str]: + if summary is None: + return None + narrative = _clamp_prose_to_sentences(summary.narrative.strip()) + return narrative or None + + +def _metric_insights_from_tldr( + summary: Optional[EvaluationTldrSummary], +) -> dict[str, str]: + if summary is None: + return {} + return { + str(metric_id): insight.strip() + for metric_id, insight in summary.metric_insights.items() + if str(metric_id).strip() and insight.strip() + } + + +def _report_period_from_rows( + rows: list[tuple[CallImportEvaluationRow, CallImportRow]], +) -> tuple[Optional[date], Optional[date], Optional[str], str]: + dates = [ + source_row.recording_date + for eval_row, source_row in rows + if eval_row.status == "completed" and source_row.recording_date + ] + if not dates: + return None, None, None, "Not specified" + start = min(dates) + end = max(dates) + week_anchor = max(dates) + week_start = week_anchor - timedelta(days=week_anchor.weekday()) + week_end = week_start + timedelta(days=6) + iso_year, iso_week, _ = week_anchor.isocalendar() + label = f"{iso_year}-W{iso_week:02d}" + if week_start.year == week_end.year: + week_range = f"{week_start.strftime('%b %d')}–{week_end.strftime('%b %d, %Y')}" + else: + week_range = ( + f"{week_start.strftime('%b %d, %Y')}–{week_end.strftime('%b %d, %Y')}" + ) + display = f"W{iso_week:02d} · {week_range}" + return start, end, label, display + + +def _aggregate_to_dict(aggregate: CallImportMetricAggregate) -> dict[str, Any]: + if hasattr(aggregate, "model_dump"): + return aggregate.model_dump(mode="json") + return aggregate.dict() + + +def _aggregate_primary_percent( + raw: dict[str, Any], + policy: Optional[MetricFailurePolicy] = None, +) -> Optional[float]: + return aggregate_primary_percent(raw, policy) + + +def _child_names_by_parent( + db: Session, + organization_id: UUID, + parent_metric_ids: Sequence[UUID], +) -> Dict[str, List[str]]: + if not parent_metric_ids: + return {} + children = ( + db.query(Metric) + .filter( + Metric.organization_id == organization_id, + Metric.parent_metric_id.in_(list(parent_metric_ids)), + ) + .all() + ) + out: Dict[str, List[str]] = {} + for child in children: + pid = str(child.parent_metric_id) + out.setdefault(pid, []).append(child.name) + return out + + +def _clustering_context( + db: Session, + evaluation: CallImportEvaluation, + eval_rows: List[CallImportEvaluationRow], +) -> Tuple[ + List[Metric], + List[CallImportMetricAggregate], + Dict[str, MetricFailurePolicy], + Literal["inferred", "user"], + Dict[str, List[str]], +]: + metrics = _metrics_for_clustering(db, evaluation, eval_rows) + aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) + parent_ids = [ + m.id + for m in metrics + if getattr(m, "selection_mode", None) and not getattr(m, "parent_metric_id", None) + ] + child_names_by_parent = _child_names_by_parent( + db, evaluation.organization_id, parent_ids + ) + policies, source = effective_policies( + evaluation, + metrics, + aggregates, + child_names_by_parent=child_names_by_parent, + ) + return metrics, aggregates, policies, source, child_names_by_parent + + +def _period_deltas_from_aggregates( + previous_metric_aggregates: list[dict[str, Any]], + current_metric_aggregates: list[dict[str, Any]], + policies: Optional[Dict[str, MetricFailurePolicy]] = None, +) -> dict[str, dict[str, str]]: + current_by_id = {str(item.get("metric_id")): item for item in current_metric_aggregates} + previous_by_id = { + str(item.get("metric_id")): item + for item in previous_metric_aggregates + if isinstance(item, dict) + } + deltas: dict[str, dict[str, str]] = {} + for metric_id, current in current_by_id.items(): + previous_raw = previous_by_id.get(metric_id) + policy = (policies or {}).get(metric_id) + current_pct = _aggregate_primary_percent(current, policy) + previous_pct = ( + _aggregate_primary_percent(previous_raw, policy) + if previous_raw + else None + ) + if current_pct is None or previous_pct is None: + deltas[metric_id] = { + "label": "No previous-week baseline", + "detail": "No comparable prior report snapshot was found.", + } + continue + delta = current_pct - previous_pct + sign = "+" if delta >= 0 else "" + deltas[metric_id] = { + "label": f"{sign}{delta:.1f} pp", + "detail": f"Current report {current_pct:.1f}% vs previous report {previous_pct:.1f}%", + } + return deltas + + +def _period_deltas_from_snapshot( + previous: Optional[CallImportEvaluationReportSnapshot], + current_metric_aggregates: list[dict[str, Any]], +) -> dict[str, dict[str, str]]: + previous_items = ( + previous.metric_aggregates + if previous and isinstance(previous.metric_aggregates, list) + else [] + ) + return _period_deltas_from_aggregates(previous_items, current_metric_aggregates) + + +def _sample_evidence_for_metrics( + rows: list[tuple[CallImportEvaluationRow, CallImportRow]], + metric_ids: set[str], +) -> dict[str, list[dict[str, str]]]: + samples: dict[str, list[dict[str, str]]] = {metric_id: [] for metric_id in metric_ids} + for eval_row, source_row in rows: + scores = eval_row.metric_scores if isinstance(eval_row.metric_scores, dict) else {} + for metric_id in metric_ids: + if len(samples.get(metric_id, [])) >= 4: + continue + score = scores.get(metric_id) + if not isinstance(score, dict): + continue + rationale = score.get("rationale") + transcript = source_row.diarised_transcript or source_row.transcript or "" + quote = rationale if isinstance(rationale, str) and rationale.strip() else transcript[:350] + if quote: + samples.setdefault(metric_id, []).append( + { + "conversation_id": source_row.conversation_id, + "quote": str(quote).strip()[:500], + } + ) + return samples + + +def _fallback_report_narrative( + insight_aggregates: list[dict[str, Any]], + evidence_samples: dict[str, list[dict[str, str]]], +) -> dict[str, Any]: + observations: dict[str, str] = {} + evidence: dict[str, dict[str, str]] = {} + design_notes: list[str] = [] + for aggregate in insight_aggregates: + metric_id = str(aggregate.get("metric_id") or "") + name = str(aggregate.get("metric_name") or "Insight") + counts = aggregate.get("value_counts") if isinstance(aggregate.get("value_counts"), list) else [] + if counts: + top = counts[0] + total = int(aggregate.get("count") or 0) or sum( + int(item.get("count") or 0) for item in counts if isinstance(item, dict) + ) + pct = (int(top.get("count") or 0) / total) * 100 if total else 0 + observations[metric_id] = ( + f"{top.get('label')} is the dominant {name.lower()} category at {pct:.1f}% of classified calls." + ) + design_notes.append( + f"{name}: {top.get('label')} is the largest segment and should be reviewed for workflow or prompt improvements." + ) + sample = (evidence_samples.get(metric_id) or [{}])[0] + if sample: + evidence[metric_id] = sample + return { + "observations": observations, + "evidence": evidence, + "design_notes": design_notes[:7], + "audit_summary": None, + } + + +def _generate_report_narrative( + db: Session, + organization_id: UUID, + *, + metric_aggregates: list[dict[str, Any]], + insight_aggregates: list[dict[str, Any]], + period_delta_by_metric: dict[str, dict[str, str]], + evidence_samples: dict[str, list[dict[str, str]]], + report_config: dict[str, Any], +) -> dict[str, Any]: + if not insight_aggregates: + return {"observations": {}, "evidence": {}, "design_notes": [], "audit_summary": None} + try: + from app.services.ai.llm_resolver import get_llm_provider_and_model + from app.services.ai.llm_service import llm_service + + provider_enum, model_str = get_llm_provider_and_model(organization_id, db, None, None) + prompt = ( + "You are writing a vendor-safe external call quality audit report. " + "Return strict JSON with keys observations (object keyed by metric_id), " + "evidence (object keyed by metric_id with conversation_id and quote), " + "design_notes (array of concise numbered-note strings), and audit_summary (string). " + "Use only the supplied aggregates and evidence samples.\n\n" + + json.dumps( + { + "metric_aggregates": metric_aggregates[:30], + "insight_aggregates": insight_aggregates, + "period_deltas": period_delta_by_metric, + "evidence_samples": evidence_samples, + "report_config": report_config, + }, + default=str, + ) + ) + llm_result = llm_service.generate_response( + messages=[ + {"role": "system", "content": "Return JSON only. No markdown."}, + {"role": "user", "content": prompt}, + ], + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + temperature=0.2, + max_tokens=1200, + ) + parsed = json.loads(str(llm_result.content or "{}")) + if isinstance(parsed, dict): + fallback = _fallback_report_narrative(insight_aggregates, evidence_samples) + return { + "observations": parsed.get("observations") or fallback["observations"], + "evidence": parsed.get("evidence") or fallback["evidence"], + "design_notes": parsed.get("design_notes") or fallback["design_notes"], + "audit_summary": parsed.get("audit_summary") or fallback["audit_summary"], + } + except Exception as exc: # noqa: BLE001 + logger.warning("Report narrative LLM generation fell back to deterministic text: {}", exc) + return _fallback_report_narrative(insight_aggregates, evidence_samples) + + +@router.get( + "/{eval_id}/baseline-candidates", + response_model=CallImportEvaluationBaselineCandidatesResponse, + operation_id="listCallImportEvaluationBaselineCandidates", +) +async def list_call_import_evaluation_baseline_candidates( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationBaselineCandidatesResponse: + del api_key + call_import = _require_import(db, call_import_id, organization_id) + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + rows = _evaluation_rows_for_period(db, evaluation.id) + period_start, _period_end, _derived_period_label, _period_display = _report_period_from_rows( + rows + ) + candidates = _baseline_candidate_evaluations( + db, + organization_id, + call_import.workspace_id, + evaluation, + period_start, + ) + default_evaluation_id = next( + (item["evaluation_id"] for item in candidates if item.get("is_default")), + None, + ) + return CallImportEvaluationBaselineCandidatesResponse( + items=[CallImportEvaluationBaselineCandidate(**item) for item in candidates], + default_evaluation_id=default_evaluation_id, + ) + + +@router.post( + "/{eval_id}/pdf-report", + operation_id="generateCallImportEvaluationPdfReport", + dependencies=[Depends(require_call_import_capability(REPORTS_GENERATE))], +) +async def generate_call_import_evaluation_pdf_report( + call_import_id: UUID, + eval_id: UUID, + payload: CallImportEvaluationPdfReportRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> StreamingResponse: + del api_key + call_import = _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + is_internal = payload.report_type == "internal" + from app.db_sharding.scatter_gather import load_evaluation_row_pairs + from app.db_sharding.sessions import is_sharding_enabled + + if is_sharding_enabled(): + rows = sorted( + load_evaluation_row_pairs(db, eval_id), + key=lambda pair: int(pair[1].row_index or 0), + ) + else: + rows = ( + db.query(CallImportEvaluationRow, CallImportRow) + .join( + CallImportRow, + CallImportRow.id == CallImportEvaluationRow.call_import_row_id, + ) + .filter(CallImportEvaluationRow.evaluation_id == eval_id) + .order_by(CallImportRow.row_index.asc()) + .all() + ) + report_config = payload.report_config if isinstance(payload.report_config, dict) else {} + metrics = _display_metrics_for_pdf_report(db, organization_id, evaluation) + configured_quality_ids = { + str(item) + for item in report_config.get("quality_metric_ids", []) + if item + } + configured_insight_ids = { + str(item.get("metric_id") or item) + for item in report_config.get("insights", []) + if item + } + if configured_quality_ids or configured_insight_ids: + allowed_ids = configured_quality_ids | configured_insight_ids + metrics = [metric for metric in metrics if str(metric.id) in allowed_ids] + + eval_rows = [eval_row for eval_row, _source_row in rows] + aggregate_models = _compute_metric_aggregates(db, evaluation, eval_rows) + selected_report_metric_ids = {str(metric.id) for metric in metrics} + aggregate_dicts = [ + _aggregate_to_dict(aggregate) + for aggregate in aggregate_models + if aggregate.metric_id in selected_report_metric_ids + ] + insight_metric_ids = { + str(metric.id) + for metric in metrics + if _metric_is_user_insight(metric) + } + metric_aggregates = [ + item for item in aggregate_dicts if str(item.get("metric_id")) not in insight_metric_ids + ] + insight_aggregates = [ + item for item in aggregate_dicts if str(item.get("metric_id")) in insight_metric_ids + ] + period_start, period_end, derived_period_label, period_display = _report_period_from_rows(rows) + period_label = (payload.period_label or derived_period_label or "").strip() or None + include_period_delta = ( + payload.include_period_delta or payload.include_weekly_delta + ) + previous_snapshot = None + period_delta_by_metric: dict[str, dict[str, str]] = {} + baseline_evaluation: Optional[CallImportEvaluation] = None + if include_period_delta and period_start: + baseline_evaluation = _resolve_baseline_evaluation( + db, + organization_id, + call_import.workspace_id, + evaluation, + period_start, + payload.baseline_evaluation_id, + ) + if baseline_evaluation: + period_delta_by_metric = _period_deltas_from_evaluation( + db, + baseline_evaluation, + metric_aggregates, + evaluation, + [eval_row for eval_row, _ in rows], + ) + period_delta_by_metric = _period_deltas_with_explanations( + db, + organization_id, + evaluation, + baseline_evaluation, + period_delta_by_metric, + ) + benchmark_context = _benchmark_context_for_evaluation(db, baseline_evaluation) + evidence_samples = _sample_evidence_for_metrics(rows, insight_metric_ids) + cached_tldr_summary = _tldr_summary_payload(evaluation) + cached_user_insights = _user_insights_payload(evaluation) + cached_metric_clusters = _metric_clusters_payload(evaluation) + cached_prompt_improvements = _prompt_improvements_payload(evaluation) + generated_insights_for_pdf = _selected_generated_user_insights( + cached_user_insights, + report_config, + ) + metric_clusters_for_pdf = _selected_metric_clusters_for_pdf( + cached_metric_clusters, + report_config, + ) + prompt_improvements_for_pdf = _selected_prompt_improvements_for_pdf( + cached_prompt_improvements, + report_config, + ) + narrative = _generate_report_narrative( + db, + organization_id, + metric_aggregates=metric_aggregates, + insight_aggregates=insight_aggregates if is_internal else [], + period_delta_by_metric=period_delta_by_metric, + evidence_samples=evidence_samples if is_internal else {}, + report_config=report_config, + ) + + generated_at = datetime.now(timezone.utc) + branding_images, custom_heading = _report_branding_for_import_workspace( + db, + organization_id, + call_import.workspace_id, + internal_brand_image_id=payload.internal_brand_image_id, + external_brand_image_id=payload.external_brand_image_id, + ) + eval_row_list = [eval_row for eval_row, _ in rows] + pdf_aggregates = _compute_metric_aggregates(db, evaluation, eval_row_list) + pdf_parent_ids = [ + m.id + for m in metrics + if getattr(m, "selection_mode", None) + and not getattr(m, "parent_metric_id", None) + ] + pdf_child_map = _child_names_by_parent( + db, evaluation.organization_id, pdf_parent_ids + ) + failure_policies_for_pdf, _fp_source = effective_policies( + evaluation, + metrics, + pdf_aggregates, + child_names_by_parent=pdf_child_map, + ) + try: + pdf_started = datetime.now(timezone.utc) + pdf_bytes = await asyncio.to_thread( + call_import_evaluation_pdf_report_service.render_pdf, + vendor_name=payload.vendor_name, + call_import=call_import, + evaluation=evaluation, + metrics=metrics, + rows=rows, + failure_policies=failure_policies_for_pdf, + generated_at=generated_at, + internal=is_internal, + logo_data_uris=branding_images, + custom_heading=custom_heading, + include_weekly_delta=include_period_delta, + period_delta_by_metric=period_delta_by_metric, + use_case=payload.use_case, + period_display=period_display, + total_metric_count=db.query(Metric) + .filter(Metric.organization_id == organization_id, Metric.enabled.is_(True)) + .count(), + report_config=report_config, + narrative=narrative, + audit_summary=_audit_summary_text_from_tldr(cached_tldr_summary), + metric_insights=_metric_insights_from_tldr(cached_tldr_summary), + benchmark_context=benchmark_context, + generated_user_insights=generated_insights_for_pdf, + user_insights_overview=( + cached_user_insights.overview if cached_user_insights else None + ), + metric_clusters=metric_clusters_for_pdf, + metric_clusters_overview=( + cached_metric_clusters.overview if cached_metric_clusters else None + ), + prompt_improvements=prompt_improvements_for_pdf, + platform_base_url=payload.platform_base_url, + ) + logger.info( + "PDF report render finished in {:.1f}s for evaluation {}", + (datetime.now(timezone.utc) - pdf_started).total_seconds(), + eval_id, + ) + except Exception as exc: # noqa: BLE001 + logger.exception( + "Failed to generate PDF report for call import {} evaluation {}", + call_import_id, + eval_id, + ) + raise HTTPException( + status_code=500, + detail=f"Failed to generate PDF report: {exc}", + ) from exc + + snapshot = CallImportEvaluationReportSnapshot( + evaluation_id=evaluation.id, + call_import_id=call_import.id, + organization_id=organization_id, + workspace_id=call_import.workspace_id, + period_label=period_label, + period_start=period_start, + period_end=period_end, + report_config=report_config, + selected_metric_ids=[str(metric.id) for metric in metrics], + metric_aggregates=metric_aggregates, + insight_aggregates=insight_aggregates, + narrative=narrative, + total_calls=evaluation.total_rows, + selected_metric_count=len(metrics), + total_metric_count=db.query(Metric) + .filter(Metric.organization_id == organization_id, Metric.enabled.is_(True)) + .count(), + ) + db.add(snapshot) + db.commit() + + filename = ( + f"{_report_filename_slug(payload.vendor_name)}-" + f"{payload.report_type}-quality-metric-audit-{eval_id}.pdf" + ) + return StreamingResponse( + iter([pdf_bytes]), + media_type="application/pdf", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + +@router.patch( + "/{eval_id}", + response_model=CallImportEvaluationResponse, + operation_id="updateCallImportEvaluation", +) +async def update_call_import_evaluation( + call_import_id: UUID, + eval_id: UUID, + payload: CallImportEvaluationUpdate, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationResponse: + """Edit metadata on an existing evaluation run (currently just ``name``).""" + + del api_key + _require_import(db, call_import_id, organization_id) + + row = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not row: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + # Treat unset vs explicit ``None`` differently: unset = leave alone, + # explicit ``None`` or empty string = clear the name. + payload_data = payload.model_dump(exclude_unset=True) + if "name" in payload_data: + row.name = _normalize_name(payload_data["name"]) + + stamp_evaluation_actor(row, principal) + db.commit() + db.refresh(row) + return _serialize_eval(db, row) + + +def _revoke_pending_tasks(evaluation: CallImportEvaluation) -> None: + """Best-effort cancel of any in-flight Celery tasks for an evaluation.""" + + if not evaluation.celery_group_id and not any( + r.celery_task_id for r in evaluation.row_results + ): + return + try: + from app.workers.celery_app import celery_app + + pending_task_ids = [ + eval_row.celery_task_id + for eval_row in evaluation.row_results + if eval_row.celery_task_id + and eval_row.status in {"pending", "running"} + ] + if pending_task_ids: + celery_app.control.revoke(pending_task_ids, terminate=False) + except Exception: + # Best effort — DB delete remains the source of truth. + pass + + +# --------------------------------------------------------------------------- +# User-initiated cancel for in-flight evaluation rows +# --------------------------------------------------------------------------- +# +# Evaluation rows can sit in ``running`` for many minutes when the underlying +# LLM / audio metric call is slow or wedged (the worker carries an 8 min +# soft / 10 min hard time limit). Without a cancel affordance the operator's +# only recourse is to wait for Celery's time limit to fire — or to manually +# mutate the DB. These helpers + the two endpoints below give the UI a +# first-class "Abort" button mirroring the diarisation cancel pattern at +# ``app.api.v1.routes.call_imports`` (``_apply_diarisation_cancel`` etc.). +# +# Why ``terminate=True``: the legacy ``_revoke_pending_tasks`` above uses +# ``terminate=False`` because it's called from delete-flow paths where the +# task may simply not get to run (a worker pulls it off the queue and drops +# it). For a user-initiated cancel we want SIGTERM to interrupt the worker +# mid-LLM/audio call so the in-flight HTTP request actually aborts. +# ``terminate=True`` routes the signal to the executing process; we spell +# ``signal="SIGTERM"`` out for clarity even though it's the default. + +# Sentinel error message stamped on cancelled rows. Read by the eval worker's +# ``_was_cancelled_externally`` guard (see +# :mod:`app.workers.tasks.evaluate_call_import_row`) so a worker that's already +# past its slowest operation can't overwrite the cancelled state with its own +# terminal status. Touching either copy means touching both. +EVAL_CANCELLED_BY_USER_ERROR: str = "Evaluation cancelled by user" + + +def _cancellable_eval_states() -> Tuple[str, ...]: + """States that an evaluation row can be cancelled from. + + Kept as a tiny helper so adding a future ``"queued"`` / ``"retrying"`` + state only needs one edit. + """ + return ("pending", "running") + + +def _revoke_eval_task(eval_row: CallImportEvaluationRow) -> None: + """Best-effort revoke of a single eval row's Celery task. + + Always swallows control-plane exceptions — Celery's control bus is + inherently best-effort and a missed revoke is not catastrophic + because the DB row is already flipped to ``failed`` by the caller + before this runs (so the UI immediately reflects the cancel; if + the task happens to finish anyway, the worker's finaliser skips + over the row via :data:`EVAL_CANCELLED_BY_USER_ERROR`). + """ + task_id = (eval_row.celery_task_id or "").strip() + if not task_id: + return + try: + from app.workers.celery_app import celery_app + + celery_app.control.revoke( + task_id, terminate=True, signal="SIGTERM" + ) + logger.info( + "Revoked evaluation task {} for eval row {}", + task_id, + eval_row.id, + ) + except Exception as exc: # noqa: BLE001 — revoke is best-effort + logger.warning( + "Failed to revoke evaluation task {} for eval row {}: {}", + task_id, + eval_row.id, + exc, + ) + + +def _apply_evaluation_cancel( + eval_rows: List[CallImportEvaluationRow], +) -> Tuple[int, int]: + """Cancel every cancellable row in ``eval_rows``. + + Returns ``(cancelled, skipped)`` so the caller can build a typed + response without re-querying the DB. The caller is responsible for + ``db.commit()`` after this returns — we deliberately don't commit + here so a batch endpoint can flush all rows in one transaction. + """ + cancellable_states = _cancellable_eval_states() + cancelled = 0 + skipped = 0 + now = datetime.now(timezone.utc) + for eval_row in eval_rows: + if (eval_row.status or "").lower() not in cancellable_states: + skipped += 1 + continue + # Flip the row state BEFORE we revoke so the UI's next poll + # already shows the cancel, even if Celery's control plane is + # slow to ack. + eval_row.status = "failed" + eval_row.error_message = EVAL_CANCELLED_BY_USER_ERROR + eval_row.finished_at = now + _revoke_eval_task(eval_row) + # Drop the task id so a follow-up retry (or a stale poll) can't + # accidentally re-revoke or get confused. + eval_row.celery_task_id = None + cancelled += 1 + return cancelled, skipped + + +def _claim_evaluation_bulk_operation( + evaluation_id: UUID, + operation: str, +) -> None: + """Reserve the run for a single bulk worker pass; 409 if one is active.""" + from app.services.call_imports.evaluation_bulk_op import ( + get_evaluation_bulk_operation, + try_set_evaluation_bulk_operation, + ) + + if try_set_evaluation_bulk_operation(evaluation_id, operation): # type: ignore[arg-type] + return + existing = get_evaluation_bulk_operation(evaluation_id) or operation + raise HTTPException( + status_code=409, + detail=( + f"A bulk {existing.replace('_', ' ')} operation is already in " + "progress for this evaluation. Wait for it to finish before " + "starting another action." + ), + ) + + +def _require_no_evaluation_bulk_operation(evaluation_id: UUID) -> None: + from app.services.call_imports.evaluation_bulk_op import ( + get_evaluation_bulk_operation, + ) + + existing = get_evaluation_bulk_operation(evaluation_id) + if existing: + raise HTTPException( + status_code=409, + detail=( + f"A bulk {existing.replace('_', ' ')} operation is already in " + "progress for this evaluation. Wait for it to finish before " + "starting another action." + ), + ) + + +@router.post( + "/{eval_id}/cancel", + response_model=CallImportEvaluationBulkActionResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="cancelCallImportEvaluation", +) +async def cancel_call_import_evaluation( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationBulkActionResponse: + """Abort all in-flight (or queued) rows in a single evaluation run. + + Idempotent: calling on a run whose rows are already terminal returns + ``target_count=0`` with 202 so the UI can fire this from an + "Abort" button without having to pre-check the state. + + Heavy row resets and Celery revokes run in a background worker so + large batches do not block the API thread. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + from app.services.call_imports.bulk_ops import count_evaluation_cancel_targets + + target_count = count_evaluation_cancel_targets(db, eval_id, mode="abort") + if target_count == 0: + return CallImportEvaluationBulkActionResponse( + accepted=True, + target_count=0, + evaluation_id=eval_id, + ) + + _claim_evaluation_bulk_operation(eval_id, "abort") + evaluation.status = "cancelled" + stamp_evaluation_actor(evaluation, principal) + db.commit() + + from app.workers.tasks.call_import_bulk_ops import ( + cancel_call_import_evaluation_task, + ) + + cancel_call_import_evaluation_task.delay(str(eval_id), mode="abort") + return CallImportEvaluationBulkActionResponse( + accepted=True, + target_count=target_count, + evaluation_id=eval_id, + ) + + +@router.post( + "/{eval_id}/force-fail-pending", + response_model=CallImportEvaluationBulkActionResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="forceFailCallImportEvaluationPending", +) +async def force_fail_pending_call_import_evaluation_rows( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationBulkActionResponse: + """Force-fail only rows currently in ``pending`` for a single run. + + This is narrower than :func:`cancel_call_import_evaluation`: it leaves + ``running`` rows untouched so operators can clear permanently queued rows + without interrupting in-flight evaluations. + + Row updates run in a background worker so large batches do not block + the API thread. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + from app.services.call_imports.bulk_ops import count_evaluation_cancel_targets + + target_count = count_evaluation_cancel_targets( + db, eval_id, mode="force_fail_pending" + ) + if target_count == 0: + return CallImportEvaluationBulkActionResponse( + accepted=True, + target_count=0, + evaluation_id=eval_id, + ) + + _claim_evaluation_bulk_operation(eval_id, "force_fail_pending") + stamp_evaluation_actor(evaluation, principal) + db.commit() + + from app.workers.tasks.call_import_bulk_ops import ( + cancel_call_import_evaluation_task, + ) + + cancel_call_import_evaluation_task.delay( + str(eval_id), mode="force_fail_pending" + ) + return CallImportEvaluationBulkActionResponse( + accepted=True, + target_count=target_count, + evaluation_id=eval_id, + ) + + +@router.post( + "/{eval_id}/rows/{eval_row_id}/cancel", + response_model=CallImportEvaluationRowResponse, + status_code=status.HTTP_200_OK, + operation_id="cancelCallImportEvaluationRow", +) +async def cancel_call_import_evaluation_row( + call_import_id: UUID, + eval_id: UUID, + eval_row_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationRowResponse: + """Abort an in-flight (or queued) evaluation for a single row. + + Idempotent: calling on a row that's already terminal (``completed`` + / ``failed``) returns the row unchanged with a 200 so the UI can + wire this to a "Stop" button without having to pre-check the + state. Updates the parent run's rollup so its counters reflect + the cancel immediately. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + _require_no_evaluation_bulk_operation(eval_id) + + from app.db_sharding.eval_rows import evaluation_row_session + from app.db_sharding.sessions import is_sharding_enabled + + if is_sharding_enabled(): + try: + with evaluation_row_session(eval_row_id) as ( + row_db, + _catalog_db, + eval_row, + source_row, + _shard_id, + ): + if eval_row.evaluation_id != eval_id: + raise HTTPException( + status_code=404, + detail="Evaluation row not found in this run", + ) + _apply_evaluation_cancel([eval_row]) + row_db.commit() + _rollup_evaluation_status(evaluation, db) + stamp_evaluation_actor(evaluation, principal) + db.commit() + row_db.refresh(eval_row) + return _to_evaluation_row_response(eval_row, source_row, evaluation) + except LookupError as exc: + raise HTTPException( + status_code=404, detail="Evaluation row not found in this run" + ) from exc + + eval_row = ( + db.query(CallImportEvaluationRow) + .filter( + CallImportEvaluationRow.id == eval_row_id, + CallImportEvaluationRow.evaluation_id == eval_id, + ) + .first() + ) + if not eval_row: + raise HTTPException( + status_code=404, detail="Evaluation row not found in this run" + ) + + _apply_evaluation_cancel([eval_row]) + db.flush() + _rollup_evaluation_status(evaluation, db) + stamp_evaluation_actor(evaluation, principal) + db.commit() + db.refresh(eval_row) + + source_row = ( + db.query(CallImportRow) + .filter(CallImportRow.id == eval_row.call_import_row_id) + .first() + ) + + return _to_evaluation_row_response(eval_row, source_row, evaluation) + + +@router.delete( + "/{eval_id}", + status_code=status.HTTP_204_NO_CONTENT, + operation_id="deleteCallImportEvaluation", +) +async def delete_call_import_evaluation( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> Response: + del api_key + _require_import(db, call_import_id, organization_id) + + row = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not row: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + _revoke_pending_tasks(row) + + db.delete(row) + db.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post( + "/bulk-delete", + status_code=status.HTTP_200_OK, + operation_id="bulkDeleteCallImportEvaluations", +) +async def bulk_delete_call_import_evaluations( + call_import_id: UUID, + payload: CallImportEvaluationBulkDelete, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> Dict[str, int]: + """Delete multiple evaluation runs scoped to one call import. + + Mirrors :func:`delete_call_import_evaluation` but in bulk so the UI + can clear out a multi-select. Unknown ids (already deleted, or + belonging to a different org/import) are silently skipped — the + response just reports how many actually went away. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + if not payload.evaluation_ids: + return {"deleted": 0} + + rows = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id.in_(payload.evaluation_ids), + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .all() + ) + deleted = 0 + for row in rows: + _revoke_pending_tasks(row) + db.delete(row) + deleted += 1 + db.commit() + return {"deleted": deleted} + + +# --------------------------------------------------------------------------- +# Aggregation: turns per-row metric scores into histograms / value counts. +# +# Designed to be cheap enough to call on every page load: we read each +# evaluation row once, bucket numeric values into a fixed 10-bin +# histogram, and tally the top categorical values. Scaling concerns +# (millions of rows) are deferred — at that point we'd push this into a +# Postgres aggregate query, but for typical CSV imports (<10k rows) the +# Python pass is fast enough and dramatically simpler. +# --------------------------------------------------------------------------- + + +_HISTOGRAM_BUCKETS = 10 +_TOP_VALUE_COUNTS = 10 + + +def _coerce_numeric(value: Any) -> Optional[float]: + """Return ``value`` as ``float`` when it's numeric; ``None`` otherwise.""" + if isinstance(value, bool): + # Booleans are ints in Python; treat them as categorical so + # pass/fail metrics show up in value_counts instead of becoming + # a degenerate {0,1} histogram. + return None + if isinstance(value, (int, float)) and math.isfinite(value): + return float(value) + if isinstance(value, str): + try: + f = float(value) + if math.isfinite(f): + return f + except ValueError: + return None + return None + + +def _coerce_category(value: Any) -> Optional[str]: + """Render ``value`` as a label suitable for a value_counts bucket.""" + if value is None: + return None + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, str): + text = value.strip() + return text or None + # Lists / dicts: stringify so they still group sensibly without + # exploding the cardinality (worst case: everything is "[…]" once). + return str(value) + + +def _build_histogram( + values: List[float], +) -> List[CallImportMetricHistogramBucket]: + """Fixed-bin histogram over ``values``; returns [] for <2 values.""" + if len(values) < 2: + return [] + lo = min(values) + hi = max(values) + if lo == hi: + # All values identical — render a single bucket so the UI shows a + # spike rather than empty space. + return [ + CallImportMetricHistogramBucket(x0=lo, x1=hi, count=len(values)) + ] + width = (hi - lo) / _HISTOGRAM_BUCKETS + buckets: List[List[float]] = [[] for _ in range(_HISTOGRAM_BUCKETS)] + for v in values: + # Right-edge inclusive on the last bucket so ``hi`` doesn't fall + # off into a non-existent bucket index. + idx = int((v - lo) / width) + if idx >= _HISTOGRAM_BUCKETS: + idx = _HISTOGRAM_BUCKETS - 1 + buckets[idx].append(v) + return [ + CallImportMetricHistogramBucket( + x0=lo + i * width, + x1=lo + (i + 1) * width, + count=len(bucket), + ) + for i, bucket in enumerate(buckets) + ] + + +def _percentile(values: List[float], pct: float) -> Optional[float]: + """Linear-interpolated percentile compatible with NumPy default.""" + if not values: + return None + sorted_vals = sorted(values) + if len(sorted_vals) == 1: + return sorted_vals[0] + rank = (pct / 100.0) * (len(sorted_vals) - 1) + lo = int(math.floor(rank)) + hi = int(math.ceil(rank)) + if lo == hi: + return sorted_vals[lo] + frac = rank - lo + return sorted_vals[lo] + (sorted_vals[hi] - sorted_vals[lo]) * frac + + +def _compute_metric_aggregates( + db: Session, + evaluation: CallImportEvaluation, + eval_rows: List[CallImportEvaluationRow], +) -> List[CallImportMetricAggregate]: + """Collapse per-row ``metric_scores`` into one aggregate per metric. + + Selected metrics are read fresh from the DB so the response always + surfaces the current ``metric.name`` / ``metric_type`` even when a + metric was renamed after the run finished. + """ + + selected_ids = _serialize_selected_metric_ids(evaluation.selected_metric_ids) + # Include parent metrics from selected_metric_groups so they appear + # alongside their children in the aggregate response. Use ``getattr`` + # with a default so the helper still works for callers that pass + # lightweight objects (tests, in-memory shims) that don't carry the + # attribute at all. + groups_raw_candidate = getattr(evaluation, "selected_metric_groups", None) + groups_raw = ( + groups_raw_candidate if isinstance(groups_raw_candidate, dict) else {} + ) + for parent_str in groups_raw.keys(): + try: + pid = UUID(parent_str) + if pid not in selected_ids: + selected_ids.append(pid) + except (TypeError, ValueError): + continue + + metrics = _metrics_for_ids(db, evaluation.organization_id, selected_ids) + metric_meta: Dict[str, Metric] = {str(m.id): m for m in metrics} + + # Default to selected metrics, but also include any metric ids that + # surface in row scores even if missing from the metric registry — + # otherwise renaming/deleting a metric mid-run would silently drop + # results from the chart. + discovered_ids: List[str] = list(metric_meta.keys()) + for row in eval_rows: + scores = row.metric_scores if isinstance(row.metric_scores, dict) else {} + for metric_id_str in scores.keys(): + if metric_id_str not in metric_meta and metric_id_str not in discovered_ids: + discovered_ids.append(metric_id_str) + + results: List[CallImportMetricAggregate] = [] + + for metric_id_str in discovered_ids: + meta = metric_meta.get(metric_id_str) + numeric_values: List[float] = [] + category_counts: Dict[str, int] = {} + # For multi-label parents we still need to know how many rows + # were scored (each row votes for >=1 label) so the n-badge in + # the UI shows "n=50" instead of the misleading "n=208" sum. + multi_label_rows_scored = 0 + # Unordered pair tally for the co-occurrence heatmap. Keys are + # ``(label_a, label_b)`` with ``a < b`` so we never double-count + # the same unordered pair. Only populated for multi-label + # parents — every other metric leaves this empty. + multi_label_pair_counts: Dict[Tuple[str, str], int] = {} + skipped = 0 + errored = 0 + observed_metric_type: Optional[str] = None + observed_name: Optional[str] = None + + # ``meta`` is a real ``Metric`` row in production, but tests + # frequently pass a lightweight stub. Pull the two attributes + # we need via ``getattr`` so a stub that only sets ``id`` / + # ``name`` / ``metric_type`` doesn't blow up here. + is_multi_label_parent = bool( + meta + and getattr(meta, "selection_mode", None) == "multi_label" + and not getattr(meta, "parent_metric_id", None) + ) + + for row in eval_rows: + scores = ( + row.metric_scores + if isinstance(row.metric_scores, dict) + else {} + ) + entry = scores.get(metric_id_str) + if not isinstance(entry, dict): + continue + if entry.get("metric_name"): + observed_name = entry.get("metric_name") + if entry.get("type"): + observed_metric_type = entry.get("type") + if entry.get("skipped"): + skipped += 1 + continue + if entry.get("error"): + errored += 1 + continue + + # Multi-label parents store a comma-joined value that + # isn't useful as a single category; instead tally each + # selected child individually so the chart shows per-label + # counts that mirror the children's own boolean histograms. + if is_multi_label_parent: + selected = entry.get("selected_child_names") + if isinstance(selected, list) and selected: + multi_label_rows_scored += 1 + cleaned: List[str] = [] + for label in selected: + text_label = str(label).strip() or None + if text_label: + cleaned.append(text_label) + category_counts[text_label] = ( + category_counts.get(text_label, 0) + 1 + ) + # Emit one increment per unordered pair of distinct + # labels that fired together on this row. ``cleaned`` + # is deduplicated first because the LLM occasionally + # repeats a label inside ``selected_child_names``. + distinct = sorted(set(cleaned)) + for i in range(len(distinct)): + for j in range(i + 1, len(distinct)): + pair = (distinct[i], distinct[j]) + multi_label_pair_counts[pair] = ( + multi_label_pair_counts.get(pair, 0) + 1 + ) + continue + + value = entry.get("value") + numeric = _coerce_numeric(value) + if numeric is not None: + numeric_values.append(numeric) + continue + category = _coerce_category(value) + if category is not None: + category_counts[category] = category_counts.get(category, 0) + 1 + + # ``count`` is "rows scored". For numeric / single-choice + # metrics that's the same as ``len(numeric) + sum(categories)`` + # because each scored row contributes exactly one observation. + # Multi-label parents however contribute one observation per + # selected child, so summing ``category_counts`` over-counts — + # we tracked rows-scored separately above and use it here. + rows_scored = ( + multi_label_rows_scored + if is_multi_label_parent + else len(numeric_values) + sum(category_counts.values()) + ) + + # Build numeric stats first, then categorical (both can coexist). + agg = CallImportMetricAggregate( + metric_id=metric_id_str, + metric_name=( + (meta.name if meta else observed_name) or "Unknown metric" + ), + metric_type=( + meta.metric_type if meta else observed_metric_type + ), + metric_category=( + "user_insight" + if meta is not None and _metric_is_user_insight(meta) + else "quality" + ) + or "quality", + is_multi_label_parent=is_multi_label_parent, + count=rows_scored, + skipped_count=skipped, + error_count=errored, + ) + if numeric_values: + agg.mean = float(statistics.fmean(numeric_values)) + agg.median = float(statistics.median(numeric_values)) + agg.min = min(numeric_values) + agg.max = max(numeric_values) + agg.stddev = ( + float(statistics.pstdev(numeric_values)) + if len(numeric_values) > 1 + else 0.0 + ) + agg.p25 = _percentile(numeric_values, 25) + agg.p75 = _percentile(numeric_values, 75) + agg.p95 = _percentile(numeric_values, 95) + agg.histogram_buckets = _build_histogram(numeric_values) + if category_counts: + sorted_counts = sorted( + category_counts.items(), key=lambda kv: kv[1], reverse=True + ) + agg.value_counts = [ + CallImportMetricValueCount(label=label, count=count) + for label, count in sorted_counts[:_TOP_VALUE_COUNTS] + ] + # Restrict the heatmap to pairs of labels we actually + # rendered above so the frontend never has to match + # against truncated/missing rows. Sorted desc by pair + # count to keep the most informative cells in the + # response when ``_TOP_VALUE_COUNTS`` clipped the matrix. + if is_multi_label_parent and multi_label_pair_counts: + kept_labels = { + label for label, _ in sorted_counts[:_TOP_VALUE_COUNTS] + } + pair_items = [ + (a, b, count) + for (a, b), count in multi_label_pair_counts.items() + if a in kept_labels and b in kept_labels + ] + pair_items.sort(key=lambda t: t[2], reverse=True) + agg.co_occurrence = [ + CallImportMetricLabelPair(a=a, b=b, count=count) + for a, b, count in pair_items + ] + + results.append(agg) + + # Sort so each parent metric immediately precedes its children. + # The Visualizations grid renders metrics top-to-bottom in this + # order, so multi-label parents (the "summary" chart) sit above + # the per-child boolean histograms that drill into them. Metrics + # whose ``meta`` row was deleted mid-run (``meta is None``) sink + # to the bottom but keep their relative order. + enumerated = list(enumerate(results)) + + def _sort_key(item: Tuple[int, CallImportMetricAggregate]): + original_idx, agg = item + meta = metric_meta.get(agg.metric_id) + if meta is None: + return (1, "", 1, "", original_idx) + parent_id = getattr(meta, "parent_metric_id", None) + # Group key: a child shares its parent's UUID; a parent + # uses its own UUID. Within a group, depth=0 (parent) sorts + # before depth=1 (child); ties break alphabetically by name + # so children render in a stable order regardless of which + # row scored which label first. + if parent_id is None: + group_key = str(meta.id) + depth = 0 + else: + group_key = str(parent_id) + depth = 1 + return ( + 0, + group_key, + depth, + (getattr(meta, "name", "") or "").lower(), + original_idx, + ) + + enumerated.sort(key=_sort_key) + return [agg for _idx, agg in enumerated] + + +@router.get( + "/{eval_id}/aggregate", + response_model=CallImportEvaluationAggregateResponse, + operation_id="getCallImportEvaluationAggregate", +) +async def get_call_import_evaluation_aggregate( + call_import_id: UUID, + eval_id: UUID, + baseline_evaluation_id: Optional[UUID] = Query(None), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationAggregateResponse: + """Return per-metric distributions for the Visualizations tab. + + The shape is intentionally chart-friendly: histograms for numeric + metrics, top-N value counts for categorical/text metrics, plus + summary stats (mean/p50/p95) so the UI can render summary cards + without recomputing on the client. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + eval_rows = _load_eval_rows(db, eval_id) + + metrics = _compute_metric_aggregates(db, evaluation, eval_rows) + + period_deltas: dict[str, MetricPeriodDelta] = {} + resolved_baseline_id: Optional[UUID] = None + if baseline_evaluation_id is not None: + call_import = _require_import(db, call_import_id, organization_id) + from app.db_sharding.scatter_gather import load_evaluation_row_pairs + + rows = load_evaluation_row_pairs(db, eval_id) + period_start, _, _, _ = _report_period_from_rows(rows) + baseline_evaluation = _resolve_baseline_evaluation( + db, + organization_id, + call_import.workspace_id, + evaluation, + period_start, + str(baseline_evaluation_id), + ) + if baseline_evaluation: + resolved_baseline_id = baseline_evaluation.id + metric_aggregates_dicts = [ + _aggregate_to_dict(agg) for agg in metrics + ] + raw_deltas = _period_deltas_from_evaluation( + db, + baseline_evaluation, + metric_aggregates_dicts, + evaluation, + eval_rows, + ) + raw_deltas = _period_deltas_with_explanations( + db, + organization_id, + evaluation, + baseline_evaluation, + raw_deltas, + ) + period_deltas = { + metric_id: MetricPeriodDelta( + label=delta.get("label") or "", + detail=delta.get("detail") or "", + why=(delta.get("why") or "").strip() or None, + ) + for metric_id, delta in raw_deltas.items() + } + + _fp_stored, failure_policies_source = policies_from_evaluation_raw( + evaluation.metric_clusters + ) + return CallImportEvaluationAggregateResponse( + evaluation_id=eval_id, + total_rows=evaluation.total_rows, + completed_rows=evaluation.completed_rows, + failed_rows=evaluation.failed_rows, + metrics=metrics, + period_deltas=period_deltas, + baseline_evaluation_id=resolved_baseline_id, + failure_policies_source=failure_policies_source, + ) + + +# --------------------------------------------------------------------------- +# TLDR insights: LLM-generated narrative + bullet patterns rendered above +# the Visualizations charts. Cached on ``CallImportEvaluation.tldr_summary`` +# so the page never auto-burns LLM tokens; the user explicitly clicks +# "Generate summary" or "Regenerate" from the empty-state CTA. +# --------------------------------------------------------------------------- + + +_INSIGHTS_SYSTEM_PROMPT = ( + "You are a senior conversation-analytics reviewer. You will be " + "given aggregated metric statistics + a sample of rationales for " + "the rows of a single call-import evaluation. Identify the most " + "useful PATTERNS that hold ACROSS the calls -- not just per-metric " + "numbers. Look for combinations (e.g. `when X happens, Y also " + "tends to happen`), notable outliers, frequent failure modes, and " + "any signal that would change how a reviewer triages the run.\n\n" + "Return STRICT JSON only, with this shape and no extra keys:\n" + "{\n" + ' "narrative": "",\n' + ' "patterns": ["", "", ...],\n' + ' "metric_insights": {"": "<2-3 line business meaning>"}\n' + "}\n\n" + "Constraints:\n" + "- narrative is the ONLY text shown in the external audit summary and " + "Visualizations TLDR; keep it to at most 3 short sentences (~300 chars).\n" + "- patterns are optional supporting notes and are NOT rendered in the " + "audit summary; keep 0 to 3 bullets if supplied, each <= 120 characters.\n" + "- metric_insights must include one entry for each top-level metric id supplied.\n" + "- Each metric insight should explain what the metric means for the business and what the current distribution suggests, not restate the metric rubric.\n" + "- Avoid restating raw counts unless they reveal a pattern.\n" + "- Use neutral, factual language ('frustration appeared in...') " + "rather than judgemental ('the agents failed to...')." +) + + +def _tldr_summary_payload( + evaluation: CallImportEvaluation, +) -> Optional[EvaluationTldrSummary]: + """Return the cached TLDR (with ``is_stale`` set) or ``None``. + + ``CallImportEvaluation.tldr_summary`` is a ``JSON`` column so we + have to validate shape defensively -- a half-written or hand-edited + blob should not break the aggregate response. Returns ``None`` when + no cached summary exists. + """ + raw = evaluation.tldr_summary + if not isinstance(raw, dict): + return None + narrative = raw.get("narrative") + if not isinstance(narrative, str) or not narrative.strip(): + return None + patterns_raw = raw.get("patterns") + patterns = ( + [str(p) for p in patterns_raw if isinstance(p, str) and p.strip()] + if isinstance(patterns_raw, list) + else [] + ) + metric_insights_raw = raw.get("metric_insights") + metric_insights = ( + { + str(metric_id): str(insight).strip() + for metric_id, insight in metric_insights_raw.items() + if str(metric_id).strip() + and isinstance(insight, str) + and insight.strip() + } + if isinstance(metric_insights_raw, dict) + else {} + ) + generated_at_raw = raw.get("generated_at") + try: + generated_at = ( + datetime.fromisoformat(generated_at_raw) + if isinstance(generated_at_raw, str) + else evaluation.updated_at or datetime.now(timezone.utc) + ) + except ValueError: + generated_at = evaluation.updated_at or datetime.now(timezone.utc) + snapshot = raw.get("generated_at_completed_rows") + snapshot_int = int(snapshot) if isinstance(snapshot, (int, float)) else 0 + return EvaluationTldrSummary( + narrative=_clamp_prose_to_sentences(narrative.strip()), + patterns=patterns, + metric_insights=metric_insights, + generated_at=generated_at, + generated_at_completed_rows=snapshot_int, + provider=raw.get("provider") if isinstance(raw.get("provider"), str) else None, + model=raw.get("model") if isinstance(raw.get("model"), str) else None, + is_stale=evaluation.completed_rows > snapshot_int, + ) + + +def _sample_rationales_per_metric( + eval_rows: List[CallImportEvaluationRow], + *, + per_metric_cap: int = 3, + rationale_char_cap: int = 600, +) -> Dict[str, List[str]]: + """Collect up to ``per_metric_cap`` distinct rationales per metric. + + Distinctness is case- and whitespace-insensitive. We truncate each + rationale to ``rationale_char_cap`` so a few unusually verbose rows + can't dominate the prompt budget. Empty / non-string rationales are + skipped. + """ + out: Dict[str, List[str]] = {} + seen: Dict[str, set[str]] = {} + for row in eval_rows: + scores = row.metric_scores if isinstance(row.metric_scores, dict) else {} + for metric_id, entry in scores.items(): + if not isinstance(entry, dict): + continue + rationale = entry.get("rationale") + if not isinstance(rationale, str): + continue + text = rationale.strip() + if not text: + continue + bucket = out.setdefault(metric_id, []) + if len(bucket) >= per_metric_cap: + continue + key = " ".join(text.lower().split()) + seen_set = seen.setdefault(metric_id, set()) + if key in seen_set: + continue + seen_set.add(key) + bucket.append(text[:rationale_char_cap]) + return out + + +def _build_insights_messages( + evaluation: CallImportEvaluation, + aggregate: List[CallImportMetricAggregate], + rationale_samples: Dict[str, List[str]], + metric_meta: Dict[str, Metric], +) -> List[Dict[str, str]]: + """Render the user prompt fed to the LLM. + + The shape is plain markdown-ish text instead of JSON so the LLM can + skim it without us spending tokens on verbose schema delimiters. + Parent metrics surface their child metrics nested underneath so the + model sees the hierarchy and can talk about "X often co-occurred + with Y" rather than treating sub-labels as standalone metrics. + """ + name = evaluation.name or f"Run {str(evaluation.id)[:8]}" + lines: List[str] = [ + f"Evaluation: {name}", + ( + f"Rows: total={evaluation.total_rows} " + f"completed={evaluation.completed_rows} " + f"failed={evaluation.failed_rows}" + ), + "", + "## Per-metric aggregate", + ] + + # Group metrics by parent so the prompt mirrors the hierarchy. Any + # aggregate row whose ``metric_id`` is missing from ``metric_meta`` + # is rendered as a leaf at the top-level list (handles renamed / + # deleted parents). + children_by_parent: Dict[str, List[CallImportMetricAggregate]] = {} + top_level: List[CallImportMetricAggregate] = [] + for agg in aggregate: + meta = metric_meta.get(agg.metric_id) + parent_id = ( + str(meta.parent_metric_id) + if meta is not None and getattr(meta, "parent_metric_id", None) + else None + ) + if parent_id: + children_by_parent.setdefault(parent_id, []).append(agg) + else: + top_level.append(agg) + + def _format_metric_block(agg: CallImportMetricAggregate, indent: int) -> List[str]: + prefix = " " * indent + "- " + bits: List[str] = [f"{prefix}{agg.metric_name} [id={agg.metric_id}] (n={agg.count}"] + if agg.skipped_count: + bits.append(f", skipped={agg.skipped_count}") + if agg.error_count: + bits.append(f", errors={agg.error_count}") + bits.append(")") + meta = metric_meta.get(agg.metric_id) + description = (meta.description or "").strip() if meta else "" + if description: + bits.append(f" | definition={description[:500]}") + if agg.mean is not None: + mean_s = f"{agg.mean:.2f}" + stddev_s = f"{agg.stddev:.2f}" if agg.stddev is not None else "-" + bits.append(f" | mean={mean_s} stddev={stddev_s}") + if agg.min is not None and agg.max is not None: + bits.append(f" range=[{agg.min:.2f}, {agg.max:.2f}]") + if agg.value_counts: + total = sum(v.count for v in agg.value_counts) or 1 + top = agg.value_counts[:3] + shares = ", ".join( + f'"{v.label}"={v.count}/{total}' for v in top + ) + bits.append(f" | top={shares}") + result = ["".join(bits)] + rationales = rationale_samples.get(agg.metric_id, []) + for r in rationales: + result.append(" " * (indent + 1) + f"- rationale: {r}") + return result + + for agg in top_level: + lines.extend(_format_metric_block(agg, indent=0)) + meta = metric_meta.get(agg.metric_id) + children = children_by_parent.get(str(meta.id), []) if meta else [] + for child in children: + lines.extend(_format_metric_block(child, indent=1)) + + lines.append("") + top_level_ids = [agg.metric_id for agg in top_level] + if top_level_ids: + lines.append( + "metric_insights keys must exactly use these top-level metric ids: " + + ", ".join(top_level_ids) + ) + lines.append("") + lines.append( + "Write the JSON object as instructed. Do not include " + "preamble, code fences, or trailing commentary." + ) + + return [ + {"role": "system", "content": _INSIGHTS_SYSTEM_PROMPT}, + {"role": "user", "content": "\n".join(lines)}, + ] + + +def _parse_insights_response(text: str) -> EvaluationTldrSummary: + """Coerce the LLM response into ``narrative`` + ``patterns``. + + Matches the JSON-with-fallback pattern used by + ``app.api.v1.routes.metrics._parse_metric_generation_response``: try + ``json.loads`` first, then fall back to regex extraction of the + first ``{...}`` block. Raises ``HTTPException`` with a 502 when the + response can't be parsed at all. + """ + cleaned = (text or "").strip() + if not cleaned: + raise HTTPException( + status_code=502, detail="LLM returned an empty insights response" + ) + try: + parsed = json.loads(cleaned) + except json.JSONDecodeError: + import re + + match = re.search(r"\{.*\}", cleaned, re.DOTALL) + if not match: + raise HTTPException( + status_code=502, + detail="Could not parse LLM insights response as JSON", + ) + try: + parsed = json.loads(match.group(0)) + except json.JSONDecodeError as e: + raise HTTPException( + status_code=502, + detail=f"Could not parse LLM insights response: {e}", + ) + + if not isinstance(parsed, dict): + raise HTTPException( + status_code=502, detail="LLM insights JSON was not an object" + ) + + narrative = parsed.get("narrative") + if not isinstance(narrative, str) or not narrative.strip(): + raise HTTPException( + status_code=502, + detail="LLM insights JSON missing 'narrative' string", + ) + + patterns_raw = parsed.get("patterns") + if patterns_raw is None: + patterns: List[str] = [] + elif isinstance(patterns_raw, list): + patterns = [ + str(p).strip() + for p in patterns_raw + if isinstance(p, str) and p.strip() + ] + else: + raise HTTPException( + status_code=502, + detail="LLM insights JSON 'patterns' must be a list of strings", + ) + metric_insights_raw = parsed.get("metric_insights") + if metric_insights_raw is None: + metric_insights: Dict[str, str] = {} + elif isinstance(metric_insights_raw, dict): + metric_insights = { + str(metric_id): str(insight).strip() + for metric_id, insight in metric_insights_raw.items() + if str(metric_id).strip() + and isinstance(insight, str) + and insight.strip() + } + else: + raise HTTPException( + status_code=502, + detail="LLM insights JSON 'metric_insights' must be an object", + ) + + return EvaluationTldrSummary( + narrative=_clamp_prose_to_sentences(narrative.strip()), + patterns=patterns, + metric_insights=metric_insights, + generated_at=datetime.now(timezone.utc), + generated_at_completed_rows=0, # filled in by caller + is_stale=False, + ) + + +def _generate_and_persist_tldr_summary( + db: Session, + evaluation: CallImportEvaluation, + *, + organization_id: UUID, + provider: Optional[str] = None, + model: Optional[str] = None, +) -> EvaluationTldrSummary: + """LLM TLDR generation used by the imports-queue Celery worker.""" + eval_id = evaluation.id + from app.db_sharding.scatter_gather import load_evaluation_row_pairs + + pairs = load_evaluation_row_pairs(db, eval_id) + eval_rows = [eval_row for eval_row, _ in pairs] + aggregate = _compute_metric_aggregates(db, evaluation, eval_rows) + if not aggregate: + raise HTTPException( + status_code=400, + detail=( + "No metric data yet. Wait for at least one row to " + "finish scoring before generating a summary." + ), + ) + + metric_ids: List[UUID] = [] + for agg in aggregate: + try: + metric_ids.append(UUID(agg.metric_id)) + except (TypeError, ValueError): + continue + metrics = _metrics_for_ids(db, organization_id, metric_ids) + metric_meta: Dict[str, Metric] = {str(m.id): m for m in metrics} + + rationale_samples = _sample_rationales_per_metric(eval_rows) + messages = _build_insights_messages( + evaluation, aggregate, rationale_samples, metric_meta + ) + + from app.services.ai.llm_resolver import get_llm_provider_and_model + from app.services.ai.llm_service import llm_service + + provider_enum, model_str = get_llm_provider_and_model( + organization_id, db, provider, model + ) + + try: + llm_result = llm_service.generate_response( + messages=messages, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + temperature=0.4, + max_tokens=1400, + ) + except Exception as e: + logger.error(f"[CallImportInsights] LLM call failed: {e}") + raise HTTPException( + status_code=502, detail=f"LLM call failed: {e}" + ) from e + + summary = _parse_insights_response(llm_result.get("text", "")) + total = int(evaluation.total_rows or 0) + ui_completed = min(int(evaluation.completed_rows or 0), total) if total else int( + evaluation.completed_rows or 0 + ) + summary.generated_at_completed_rows = ui_completed + summary.provider = provider_enum.value + summary.model = model_str + summary.is_stale = False + + evaluation.tldr_summary = { + "narrative": summary.narrative, + "patterns": summary.patterns, + "metric_insights": summary.metric_insights, + "generated_at": summary.generated_at.isoformat(), + "generated_at_completed_rows": summary.generated_at_completed_rows, + "provider": summary.provider, + "model": summary.model, + } + flag_modified(evaluation, "tldr_summary") + db.commit() + db.refresh(evaluation) + return summary + + +@router.get( + "/{eval_id}/insights", + response_model=Optional[EvaluationTldrSummary], + operation_id="getCallImportEvaluationInsights", +) +async def get_call_import_evaluation_insights( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> Optional[EvaluationTldrSummary]: + """Return the cached TLDR (or ``null``) without contacting the LLM. + + Used by the Visualizations tab on first paint so the empty-state + CTA can show up before the user opts into generation. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + return _tldr_summary_payload(evaluation) + + +@router.post( + "/{eval_id}/insights", + response_model=EvaluationTldrSummary, + operation_id="generateCallImportEvaluationInsights", +) +async def generate_call_import_evaluation_insights( + call_import_id: UUID, + eval_id: UUID, + body: EvaluationInsightsRequest = Body(default_factory=EvaluationInsightsRequest), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> EvaluationTldrSummary: + """Generate (or return-cached) the LLM TLDR for an evaluation run. + + Behavior: + + * ``body.regenerate=False`` and a cached summary at the current + ``completed_rows`` watermark exists -> return it as-is. + * ``body.regenerate=False`` and a stale cached summary exists + (``generated_at_completed_rows < completed_rows``) -> return it + with ``is_stale=True``; the UI prompts the user to regenerate. + * Otherwise -> resolve provider+model (auto-detect when omitted), + call the LLM, persist the new summary, return it. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + if not body.regenerate: + cached = _tldr_summary_payload(evaluation) + if cached is not None: + return cached + + # Run the TLDR LLM on the imports worker (not the default worker or API). + from app.workers.tasks.generate_evaluation_tldr_insights import ( + generate_evaluation_tldr_insights_task, + ) + + try: + task_result = generate_evaluation_tldr_insights_task.apply_async( + kwargs={ + "evaluation_id": str(eval_id), + "call_import_id": str(call_import_id), + "organization_id": str(organization_id), + "provider": body.provider, + "model": body.model, + }, + ).get(timeout=25 * 60) + except Exception as exc: + logger.error( + "[CallImportInsights] TLDR task failed for evaluation {}: {}", + eval_id, + exc, + ) + raise HTTPException( + status_code=502, + detail=f"Summary generation failed: {exc}", + ) from exc + + if isinstance(task_result, dict) and task_result.get("error"): + status_code = int(task_result.get("status_code") or 502) + raise HTTPException( + status_code=status_code, + detail=str(task_result["error"]), + ) + + summary = EvaluationTldrSummary.model_validate(task_result) + db.refresh(evaluation) + + from app.services.ai.llm_resolver import get_llm_provider_and_model + + provider_enum, model_str = get_llm_provider_and_model( + organization_id, db, body.provider, body.model, body.credential_id + ) + + _enqueue_user_insights_job( + evaluation, + provider=summary.provider or provider_enum.value, + model=summary.model or model_str, + force=body.regenerate, + max_llm_calls=body.max_llm_calls, + db=db, + ) + + return summary + + +def _user_insights_payload( + evaluation: CallImportEvaluation, +) -> Optional[EvaluationUserInsightsState]: + raw = getattr(evaluation, "user_insights", None) + if raw is None: + return None + return user_insights_state_from_raw( + raw, + completed_rows=evaluation.completed_rows, + ) + + +def _selected_generated_user_insights( + state: Optional[EvaluationUserInsightsState], + report_config: dict[str, Any], +) -> list[dict[str, Any]]: + """Filter and order generated insights for PDF section 03.""" + if state is None or state.status != "completed" or not state.insights: + return [] + + selected_ids = report_config.get("user_insight_ids") + if isinstance(selected_ids, list) and selected_ids: + allowed = {str(item) for item in selected_ids if item} + items = [item for item in state.insights if item.id in allowed] + else: + items = list(state.insights) + + order_raw = report_config.get("order") + order_ids: list[str] = [] + if isinstance(order_raw, dict): + user_order = order_raw.get("user_insights") + if isinstance(user_order, list): + order_ids = [str(item) for item in user_order if item] + + if order_ids: + by_id = {item.id: item for item in items} + ordered = [by_id[iid] for iid in order_ids if iid in by_id] + seen = set(order_ids) + ordered.extend(item for item in items if item.id not in seen) + items = ordered + + return [item.model_dump(mode="json") for item in items] + + +def _enqueue_user_insights_job( + evaluation: CallImportEvaluation, + *, + provider: Optional[str] = None, + model: Optional[str] = None, + force: bool = False, + max_llm_calls: Optional[int] = None, + db: Optional[Session] = None, +) -> None: + """Enqueue background user-insights generation unless already running.""" + current = _user_insights_payload(evaluation) + if current is not None and current.status == "running" and not force: + return + + llm_budget = normalize_max_llm_calls(max_llm_calls) + + completed_count = ( + _count_completed_eval_rows(db, evaluation.id) + if db is not None + else evaluation.completed_rows + ) + total_calls = total_llm_calls_for_rows(completed_count, max_llm_calls=llm_budget) + evaluation.user_insights = { + "status": "running", + "insights": ( + (evaluation.user_insights or {}).get("insights", []) + if isinstance(evaluation.user_insights, dict) + else [] + ), + "generated_at": datetime.now(timezone.utc).isoformat(), + "generated_at_completed_rows": evaluation.completed_rows, + "progress": {"completed_llm_calls": 0, "total_llm_calls": total_calls}, + "provider": provider, + "model": model, + "max_llm_calls": llm_budget, + "llm_calls_used": 0, + "error_message": None, + } + if db is not None: + flag_modified(evaluation, "user_insights") + db.commit() + + from app.workers.tasks.generate_evaluation_user_insights import ( + generate_evaluation_user_insights_task, + ) + + generate_evaluation_user_insights_task.delay( + str(evaluation.id), + provider=provider, + model=model, + max_llm_calls=llm_budget, + ) + + +@router.get( + "/{eval_id}/user-insights", + response_model=Optional[EvaluationUserInsightsState], + operation_id="getCallImportEvaluationUserInsights", +) +async def get_call_import_evaluation_user_insights( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> Optional[EvaluationUserInsightsState]: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + return _user_insights_payload(evaluation) + + +@router.post( + "/{eval_id}/user-insights", + response_model=EvaluationUserInsightsState, + operation_id="generateCallImportEvaluationUserInsights", +) +async def generate_call_import_evaluation_user_insights( + call_import_id: UUID, + eval_id: UUID, + body: EvaluationUserInsightsRequest = Body( + default_factory=EvaluationUserInsightsRequest + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> EvaluationUserInsightsState: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + if not body.regenerate and not body.force: + cached = _user_insights_payload(evaluation) + if cached is not None and cached.status in {"running", "completed"}: + return cached + + eval_rows = _load_eval_rows(db, eval_id) + if not any(row.status == "completed" for row in eval_rows): + raise HTTPException( + status_code=400, + detail=( + "No completed rows yet. Wait for at least one row to " + "finish scoring before generating user insights." + ), + ) + + from app.services.ai.llm_resolver import get_llm_provider_and_model + + provider_enum, model_str = get_llm_provider_and_model( + organization_id, db, body.provider, body.model, body.credential_id + ) + + _enqueue_user_insights_job( + evaluation, + provider=provider_enum.value, + model=model_str, + force=body.force or body.regenerate, + max_llm_calls=body.max_llm_calls, + db=db, + ) + + db.refresh(evaluation) + return _user_insights_payload(evaluation) or EvaluationUserInsightsState( + status="running" + ) + + +def _metric_clusters_payload( + evaluation: CallImportEvaluation, +) -> Optional[EvaluationMetricClustersState]: + raw = getattr(evaluation, "metric_clusters", None) + if raw is None: + return None + return metric_clusters_state_from_raw( + raw, + completed_rows=evaluation.completed_rows, + ) + + +def _selected_metric_clusters_for_pdf( + state: Optional[EvaluationMetricClustersState], + report_config: dict[str, Any], +) -> dict[str, Any]: + if state is None or state.status != "completed": + return {} + sections = report_config.get("sections") + if isinstance(sections, dict) and sections.get("failure_diagnostics") is False: + return {} + payload: dict[str, Any] = { + "groups": [g.model_dump(mode="json") for g in state.groups], + "discovered_problems": [ + d.model_dump(mode="json") for d in state.discovered_problems + ], + } + if state.rca_summary is not None: + payload["rca_summary"] = state.rca_summary.model_dump(mode="json") + return payload + + +def _prompt_improvements_payload( + evaluation: CallImportEvaluation, +) -> Optional[EvaluationPromptImprovementsState]: + from app.services.call_import_prompt_improvements import ( + prompt_improvements_state_from_raw, + ) + + raw = getattr(evaluation, "prompt_improvements", None) + if raw is None: + return None + return prompt_improvements_state_from_raw( + raw, + completed_rows=evaluation.completed_rows, + ) + + +def _selected_prompt_improvements_for_pdf( + state: Optional[EvaluationPromptImprovementsState], + report_config: dict[str, Any], +) -> dict[str, Any]: + if state is None or state.status != "completed": + return {} + sections = report_config.get("sections") + if isinstance(sections, dict) and sections.get("prompt_improvements") is False: + return {} + return { + "imported_agent_id": state.imported_agent_id, + "imported_agent_name": state.imported_agent_name, + "overview": state.overview, + "suggestions": [s.model_dump(mode="json") for s in state.suggestions], + } + + +def _enqueue_prompt_improvements_job( + evaluation: CallImportEvaluation, + *, + imported_agent_id: UUID, + imported_agent_name: str, + provider: Optional[str] = None, + model: Optional[str] = None, + credential_id: Optional[UUID] = None, + force: bool = False, + db: Optional[Session] = None, +) -> None: + current = _prompt_improvements_payload(evaluation) + if current is not None and current.status == "running" and not force: + return + + evaluation.prompt_improvements = { + "status": "running", + "imported_agent_id": str(imported_agent_id), + "imported_agent_name": imported_agent_name, + "suggestions": [], + "generated_at": datetime.now(timezone.utc).isoformat(), + "generated_at_completed_rows": evaluation.completed_rows, + "provider": provider, + "model": model, + "error_message": None, + } + if db is not None: + flag_modified(evaluation, "prompt_improvements") + db.commit() + + from app.workers.tasks.generate_evaluation_prompt_improvements import ( + generate_evaluation_prompt_improvements_task, + ) + + async_result = generate_evaluation_prompt_improvements_task.apply_async( + kwargs={ + "evaluation_id": str(evaluation.id), + "imported_agent_id": str(imported_agent_id), + "provider": provider, + "model": model, + "credential_id": str(credential_id) if credential_id else None, + }, + queue="imports", + ) + if db is not None and isinstance(evaluation.prompt_improvements, dict): + evaluation.prompt_improvements["celery_task_id"] = async_result.id + flag_modified(evaluation, "prompt_improvements") + db.commit() + + +def _load_eval_rows(db: Session, evaluation_id: UUID) -> List[CallImportEvaluationRow]: + from app.db_sharding.eval_rows import load_evaluation_rows_for_run + + return load_evaluation_rows_for_run(db, evaluation_id) + + +def _count_completed_eval_rows(db: Session, evaluation_id: UUID) -> int: + from app.db_sharding.eval_rows import count_evaluation_rows_for_run + + return count_evaluation_rows_for_run( + db, evaluation_id, statuses=["completed"] + ) + + +def _completed_row_pairs_for_evaluation( + db: Session, + evaluation_id: UUID, +) -> List[Tuple[CallImportEvaluationRow, CallImportRow]]: + from app.db_sharding.scatter_gather import load_evaluation_row_pairs + + row_pairs = load_evaluation_row_pairs(db, evaluation_id) + return [ + (eval_row, source_row) + for eval_row, source_row in row_pairs + if eval_row.status == "completed" + ] + + +def _resolve_metric_cluster_row_selection( + db: Session, + evaluation: CallImportEvaluation, + eval_rows: List[CallImportEvaluationRow], + evaluation_row_ids: Optional[List[UUID]], + *, + row_limit: Optional[int] = None, + policies: Optional[Dict[str, MetricFailurePolicy]] = None, +) -> Tuple[List[Tuple[CallImportEvaluationRow, CallImportRow]], List[str]]: + """Return filtered completed row pairs and the selected row id strings.""" + completed_pairs = _completed_row_pairs_for_evaluation(db, evaluation.id) + metrics = _metrics_for_clustering(db, evaluation, eval_rows) + if policies is None: + aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) + parent_ids = [ + m.id + for m in metrics + if getattr(m, "selection_mode", None) + and not getattr(m, "parent_metric_id", None) + ] + child_names_by_parent = _child_names_by_parent( + db, evaluation.organization_id, parent_ids + ) + policies, _ = effective_policies( + evaluation, + metrics, + aggregates, + child_names_by_parent=child_names_by_parent, + ) + eligible = list_eligible_cluster_rows( + evaluation, completed_pairs, metrics, policies + ) + eligible_ordered_ids = [str(item["evaluation_row_id"]) for item in eligible] + eligible_id_set = set(eligible_ordered_ids) + + if evaluation_row_ids is None and row_limit is not None: + selected_ids = eligible_ordered_ids[:row_limit] + filtered = filter_completed_row_pairs( + completed_pairs, + [UUID(rid) for rid in selected_ids], + ) + return filtered, selected_ids + + if evaluation_row_ids is None: + selected_ids = eligible_ordered_ids + filtered = filter_completed_row_pairs( + completed_pairs, + [UUID(rid) for rid in selected_ids], + ) + return filtered, selected_ids + + requested = {str(rid) for rid in evaluation_row_ids} + completed_id_set = {str(eval_row.id) for eval_row, _ in completed_pairs} + unknown = sorted(requested - completed_id_set) + if unknown: + raise HTTPException( + status_code=400, + detail=( + "One or more evaluation_row_ids are missing or not completed: " + + ", ".join(unknown[:5]) + + ("…" if len(unknown) > 5 else "") + ), + ) + not_eligible = sorted(requested - eligible_id_set) + if not_eligible: + raise HTTPException( + status_code=400, + detail=( + "Each selected row must have at least one flagged quality metric. " + "Ineligible row(s): " + + ", ".join(not_eligible[:5]) + + ("…" if len(not_eligible) > 5 else "") + ), + ) + selected_ids = sorted(requested) + filtered = filter_completed_row_pairs(completed_pairs, evaluation_row_ids) + return filtered, selected_ids + + +def _enqueue_metric_clusters_job( + evaluation: CallImportEvaluation, + *, + provider: Optional[str] = None, + model: Optional[str] = None, + credential_id: Optional[UUID] = None, + force: bool = False, + max_llm_calls: Optional[int] = None, + evaluation_row_ids: Optional[List[UUID]] = None, + selected_evaluation_row_ids: Optional[List[str]] = None, + failure_policies: Optional[Dict[str, MetricFailurePolicy]] = None, + db: Optional[Session] = None, +) -> None: + current = _metric_clusters_payload(evaluation) + if current is not None and current.status == "running" and not force: + return + + llm_budget = normalize_max_llm_calls(max_llm_calls) + total_calls = 1 + row_ids_for_task: Optional[List[str]] = None + if db is not None: + eval_rows = _load_eval_rows(db, evaluation.id) + if selected_evaluation_row_ids is None: + _, selected_evaluation_row_ids = _resolve_metric_cluster_row_selection( + db, + evaluation, + eval_rows, + evaluation_row_ids, + ) + completed_pairs = filter_completed_row_pairs( + _completed_row_pairs_for_evaluation(db, evaluation.id), + [UUID(rid) for rid in selected_evaluation_row_ids], + ) + metrics = _metrics_for_clustering(db, evaluation, eval_rows) + policies_for_estimate = failure_policies + if policies_for_estimate is None: + aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) + parent_ids = [ + m.id + for m in metrics + if getattr(m, "selection_mode", None) + and not getattr(m, "parent_metric_id", None) + ] + child_names_by_parent = _child_names_by_parent( + db, evaluation.organization_id, parent_ids + ) + policies_for_estimate, _ = effective_policies( + evaluation, + metrics, + aggregates, + child_names_by_parent=child_names_by_parent, + ) + _, total_calls = estimate_metric_clusters_llm_calls( + evaluation, + metrics, + completed_pairs, + policies_for_estimate, + max_llm_calls=llm_budget, + ) + row_ids_for_task = list(selected_evaluation_row_ids) + + prior_raw = ( + evaluation.metric_clusters + if isinstance(evaluation.metric_clusters, dict) + else {} + ) + policy_blob: Dict[str, Any] = {} + if failure_policies: + policy_blob = failure_policies_to_db(failure_policies, source="user") + + evaluation.metric_clusters = { + "status": "running", + "groups": prior_raw.get("groups", []) if isinstance(prior_raw, dict) else [], + "discovered_problems": ( + prior_raw.get("discovered_problems", []) + if isinstance(prior_raw, dict) + else [] + ), + "generated_at": datetime.now(timezone.utc).isoformat(), + "generated_at_completed_rows": evaluation.completed_rows, + "progress": {"completed_llm_calls": 0, "total_llm_calls": total_calls}, + "provider": provider, + "model": model, + "max_llm_calls": llm_budget, + "llm_calls_used": 0, + "error_message": None, + "selected_evaluation_row_ids": selected_evaluation_row_ids or [], + **policy_blob, + } + if db is not None: + flag_modified(evaluation, "metric_clusters") + db.commit() + + from app.workers.tasks.generate_evaluation_metric_clusters import ( + generate_evaluation_metric_clusters_task, + ) + + async_result = generate_evaluation_metric_clusters_task.apply_async( + kwargs={ + "evaluation_id": str(evaluation.id), + "provider": provider, + "model": model, + "credential_id": str(credential_id) if credential_id else None, + "max_llm_calls": llm_budget, + "evaluation_row_ids": row_ids_for_task, + }, + queue="imports", + ) + if db is not None and isinstance(evaluation.metric_clusters, dict): + evaluation.metric_clusters["celery_task_id"] = async_result.id + flag_modified(evaluation, "metric_clusters") + db.commit() + + +def _revoke_metric_clusters_task(evaluation: CallImportEvaluation) -> None: + """Best-effort SIGTERM revoke of the in-flight clustering Celery task.""" + raw = evaluation.metric_clusters + if not isinstance(raw, dict): + return + task_id = str(raw.get("celery_task_id") or "").strip() + if not task_id: + return + try: + from app.workers.celery_app import celery_app + + celery_app.control.revoke(task_id, terminate=True, signal="SIGTERM") + logger.info( + "Revoked metric-clusters task {} for evaluation {}", + task_id, + evaluation.id, + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to revoke metric-clusters task {} for evaluation {}: {}", + task_id, + evaluation.id, + exc, + ) + + +def _apply_metric_clusters_cancel(evaluation: CallImportEvaluation) -> bool: + """Mark clustering as cancelled and revoke the worker task. + + Returns True if a running job was cancelled, False if already terminal. + """ + raw = evaluation.metric_clusters + if not isinstance(raw, dict): + return False + if (raw.get("status") or "").lower() != "running": + return False + + _revoke_metric_clusters_task(evaluation) + progress = raw.get("progress") if isinstance(raw.get("progress"), dict) else {} + evaluation.metric_clusters = { + **raw, + "status": "cancelled", + "error_message": METRIC_CLUSTERS_CANCELLED_BY_USER_ERROR, + "progress": progress, + "celery_task_id": None, + } + return True + + +@router.get( + "/{eval_id}/metric-clusters/failure-policies", + response_model=MetricFailurePoliciesResponse, + operation_id="getCallImportEvaluationMetricClusterFailurePolicies", +) +async def get_call_import_evaluation_metric_cluster_failure_policies( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> MetricFailurePoliciesResponse: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + eval_rows = _load_eval_rows(db, eval_id) + metrics, aggregates, policies, source, child_names_by_parent = _clustering_context( + db, evaluation, eval_rows + ) + previews = build_failure_policy_previews( + metrics, + aggregates, + child_names_by_parent=child_names_by_parent, + effective=policies, + ) + updated_at = None + raw_mc = evaluation.metric_clusters + if isinstance(raw_mc, dict) and raw_mc.get("failure_policies_updated_at"): + try: + updated_at = datetime.fromisoformat( + str(raw_mc["failure_policies_updated_at"]) + ) + except ValueError: + updated_at = None + return MetricFailurePoliciesResponse( + previews=previews, + policies=policies, + source=source, + updated_at=updated_at, + ) + + +@router.put( + "/{eval_id}/metric-clusters/failure-policies", + response_model=MetricFailurePoliciesResponse, + operation_id="saveCallImportEvaluationMetricClusterFailurePolicies", +) +async def save_call_import_evaluation_metric_cluster_failure_policies( + call_import_id: UUID, + eval_id: UUID, + body: MetricFailurePoliciesSaveRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> MetricFailurePoliciesResponse: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + eval_rows = _load_eval_rows(db, eval_id) + metrics, aggregates, _existing, _source, child_names_by_parent = _clustering_context( + db, evaluation, eval_rows + ) + try: + validate_failure_policies_for_metrics(body.policies, metrics) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + prior = ( + evaluation.metric_clusters + if isinstance(evaluation.metric_clusters, dict) + else {} + ) + evaluation.metric_clusters = merge_failure_policies_into_raw( + prior, + body.policies, + source="user", + ) + flag_modified(evaluation, "metric_clusters") + stamp_evaluation_actor(evaluation, principal) + db.commit() + db.refresh(evaluation) + + policies, source = policies_from_evaluation_raw(evaluation.metric_clusters) + if source != "user": + source = "user" + previews = build_failure_policy_previews( + metrics, + aggregates, + child_names_by_parent=child_names_by_parent, + effective=policies, + ) + updated_at = None + raw_mc = evaluation.metric_clusters + if isinstance(raw_mc, dict) and raw_mc.get("failure_policies_updated_at"): + try: + updated_at = datetime.fromisoformat( + str(raw_mc["failure_policies_updated_at"]) + ) + except ValueError: + updated_at = None + return MetricFailurePoliciesResponse( + previews=previews, + policies=policies, + source="user", + updated_at=updated_at, + ) + + +@router.get( + "/{eval_id}/metric-clusters/eligible-rows", + response_model=MetricClusterEligibleRowsResponse, + operation_id="listCallImportEvaluationMetricClusterEligibleRows", +) +async def list_call_import_evaluation_metric_cluster_eligible_rows( + call_import_id: UUID, + eval_id: UUID, + limit: Optional[int] = Query(default=None, ge=1), + count_only: bool = Query(default=False), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> MetricClusterEligibleRowsResponse: + """Completed rows that have at least one flagged quality metric.""" + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + eval_rows = _load_eval_rows(db, eval_id) + completed_pairs = _completed_row_pairs_for_evaluation(db, eval_id) + metrics, _aggregates, policies, _source, _child_map = _clustering_context( + db, evaluation, eval_rows + ) + all_eligible = list_eligible_cluster_rows( + evaluation, completed_pairs, metrics, policies + ) + total = len(all_eligible) + if count_only: + return MetricClusterEligibleRowsResponse(items=[], total=total) + raw_items = all_eligible if limit is None else all_eligible[:limit] + items = [MetricClusterEligibleRow.model_validate(item) for item in raw_items] + return MetricClusterEligibleRowsResponse(items=items, total=total) + + +@router.get( + "/{eval_id}/metric-clusters", + response_model=Optional[EvaluationMetricClustersState], + operation_id="getCallImportEvaluationMetricClusters", +) +async def get_call_import_evaluation_metric_clusters( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> Optional[EvaluationMetricClustersState]: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + return _metric_clusters_payload(evaluation) + + +@router.post( + "/{eval_id}/metric-clusters", + response_model=EvaluationMetricClustersState, + operation_id="generateCallImportEvaluationMetricClusters", +) +async def generate_call_import_evaluation_metric_clusters( + call_import_id: UUID, + eval_id: UUID, + body: EvaluationMetricClustersRequest = Body( + default_factory=EvaluationMetricClustersRequest + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> EvaluationMetricClustersState: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + if not body.regenerate and not body.force: + cached = _metric_clusters_payload(evaluation) + if cached is not None and cached.status in {"running", "completed"}: + return cached + + eval_rows = _load_eval_rows(db, eval_id) + if not any(row.status == "completed" for row in eval_rows): + raise HTTPException( + status_code=400, + detail=( + "No completed rows yet. Wait for at least one row to " + "finish scoring before generating metric clusters." + ), + ) + + if body.evaluation_row_ids and body.row_limit is not None: + raise HTTPException( + status_code=400, + detail="Specify either evaluation_row_ids or row_limit, not both.", + ) + + if body.evaluation_row_ids: + completed_pairs = _completed_row_pairs_for_evaluation(db, evaluation.id) + completed_id_set = {str(eval_row.id) for eval_row, _ in completed_pairs} + requested = {str(rid) for rid in body.evaluation_row_ids} + unknown = sorted(requested - completed_id_set) + if unknown: + raise HTTPException( + status_code=400, + detail=( + "One or more evaluation_row_ids are missing or not completed: " + + ", ".join(unknown[:5]) + + ("…" if len(unknown) > 5 else "") + ), + ) + + from app.services.ai.llm_resolver import get_llm_provider_and_model + + provider_enum, model_str = get_llm_provider_and_model( + organization_id, db, body.provider, body.model, body.credential_id + ) + + metrics, aggregates, _inferred, _source, child_names_by_parent = _clustering_context( + db, evaluation, eval_rows + ) + merged_policies = merge_clustering_policies( + body.failure_policies, + evaluation, + metrics, + aggregates, + child_names_by_parent=child_names_by_parent, + ) + try: + validate_failure_policies_for_metrics( + body.failure_policies or merged_policies, metrics + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + if not has_clusterable_metrics(metrics, merged_policies, eval_rows): + raise HTTPException( + status_code=400, + detail=( + "No calls match any failure policy. Select failure values on " + "metrics that have matching rows, or leave metrics with no " + "failures unchecked — they are skipped automatically." + ), + ) + + filtered_pairs, selected_row_ids = _resolve_metric_cluster_row_selection( + db, + evaluation, + eval_rows, + body.evaluation_row_ids, + row_limit=body.row_limit, + policies=merged_policies, + ) + if not selected_row_ids: + raise HTTPException( + status_code=400, + detail=( + "No eligible rows to cluster. Select completed calls that match " + "at least one configured failure policy." + ), + ) + if not filtered_pairs: + raise HTTPException( + status_code=400, + detail="No completed rows match the selected evaluation_row_ids.", + ) + + _enqueue_metric_clusters_job( + evaluation, + provider=provider_enum.value, + model=model_str, + credential_id=body.credential_id, + force=body.force or body.regenerate, + max_llm_calls=body.max_llm_calls, + evaluation_row_ids=body.evaluation_row_ids, + selected_evaluation_row_ids=selected_row_ids, + failure_policies=merged_policies, + db=db, + ) + + db.refresh(evaluation) + return _metric_clusters_payload(evaluation) or EvaluationMetricClustersState( + status="running" + ) + + +@router.post( + "/{eval_id}/metric-clusters/cancel", + response_model=EvaluationMetricClustersState, + operation_id="cancelCallImportEvaluationMetricClusters", +) +async def cancel_call_import_evaluation_metric_clusters( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> EvaluationMetricClustersState: + """Abort in-flight failure-diagnostics clustering. + + Idempotent: if clustering is not ``running``, returns the current state + unchanged. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + _apply_metric_clusters_cancel(evaluation) + flag_modified(evaluation, "metric_clusters") + stamp_evaluation_actor(evaluation, principal) + db.commit() + db.refresh(evaluation) + + return _metric_clusters_payload(evaluation) or EvaluationMetricClustersState( + status="idle" + ) + + +@router.get( + "/{eval_id}/prompt-improvements", + response_model=Optional[EvaluationPromptImprovementsState], + operation_id="getCallImportEvaluationPromptImprovements", +) +async def get_call_import_evaluation_prompt_improvements( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> Optional[EvaluationPromptImprovementsState]: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + return _prompt_improvements_payload(evaluation) + + +@router.post( + "/{eval_id}/prompt-improvements", + response_model=EvaluationPromptImprovementsState, + operation_id="generateCallImportEvaluationPromptImprovements", +) +async def generate_call_import_evaluation_prompt_improvements( + call_import_id: UUID, + eval_id: UUID, + body: EvaluationPromptImprovementsRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> EvaluationPromptImprovementsState: + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + clusters = _metric_clusters_payload(evaluation) + if clusters is None or clusters.status != "completed": + raise HTTPException( + status_code=400, + detail=( + "Metric clusters must be completed before generating prompt " + "improvements. Run failure diagnostics first." + ), + ) + + from app.services.call_import_prompt_improvements import is_imported_agent + from app.services.ai.llm_resolver import get_llm_provider_and_model + + imported_agent = ( + db.query(PromptPartial) + .filter( + PromptPartial.id == body.imported_agent_id, + PromptPartial.organization_id == organization_id, + PromptPartial.workspace_id == workspace_id, + ) + .first() + ) + if imported_agent is None or not is_imported_agent(imported_agent): + raise HTTPException( + status_code=404, + detail="Imported agent not found in the active workspace", + ) + + if not body.regenerate and not body.force: + cached = _prompt_improvements_payload(evaluation) + if ( + cached is not None + and cached.status in {"running", "completed"} + and cached.imported_agent_id == str(body.imported_agent_id) + ): + return cached + + provider_enum, model_str = get_llm_provider_and_model( + organization_id, db, body.provider, body.model, body.credential_id + ) + + _enqueue_prompt_improvements_job( + evaluation, + imported_agent_id=body.imported_agent_id, + imported_agent_name=imported_agent.name, + provider=provider_enum.value, + model=model_str, + credential_id=body.credential_id, + force=body.force or body.regenerate, + db=db, + ) + + db.refresh(evaluation) + return _prompt_improvements_payload(evaluation) or EvaluationPromptImprovementsState( + status="running", + imported_agent_id=str(body.imported_agent_id), + imported_agent_name=imported_agent.name, + ) + + +# --------------------------------------------------------------------------- +# Flow chart: turns per-row LLM-inferred ``sequence`` arrays into a +# directed graph of (label -> label) transitions across the whole run. +# Powers the aggregate Sankey-style React Flow chart on the evaluation +# overview; per-call flow charts are built client-side from the same +# ``sequence`` field on a single row's metric_scores entry. +# --------------------------------------------------------------------------- + + +_FLOW_TERMINAL_THRESHOLD = 0.2 # Mark as terminal when >=20% of sequences end here. +_FLOW_START_NODE_ID = "__START__" +_DISCOVERED_NODE_PREFIX = "disc:" + + +def _slug_label(value: Any) -> str: + """Lowercase + whitespace-collapse + underscore-join. + + Used everywhere we need a stable key for a metric/label name — + matching the same convention the worker uses when emitting + ``sequence`` entries and discovered keys. + """ + if value is None: + return "" + return "_".join(str(value).strip().lower().split()) + + +def _resolve_alias(alias_map: Dict[str, str], key: str) -> str: + """Walk the alias map until we hit a slug that doesn't redirect. + + The merge endpoint stores ``from_slug -> to_slug`` pairs. The delete + endpoint stores ``from_slug -> ""`` (empty string sentinel) to mark + a slug as tombstoned. Chains can accumulate when the user merges + A→B and later merges B→C; this helper collapses them so callers + always land on the final canonical slug. + + Returns: + * the canonical slug if it still resolves to a real label, + * an empty string if the slug has been tombstoned (callers MUST + treat an empty result as "drop this entry entirely"), + * the input ``key`` if it isn't aliased. + + Cycles are guarded by a hard step limit since the alias map is + user-driven. + """ + if not key: + return "" + if not alias_map: + return key + current = key + seen: set[str] = set() + for _ in range(16): + if current in seen: + return current + seen.add(current) + if current not in alias_map: + return current + nxt = alias_map[current] + if nxt == current: + return current + if nxt == "": + # Deletion sentinel — the user has explicitly retired this + # slug. Propagate the empty string up so callers drop it. + return "" + current = nxt + return current + + +# Reserved JSON key under which the worker stores top-level metric +# discoveries on each row's ``metric_scores`` dict. Mirrors the constant +# in ``app/workers/tasks/helpers/llm_evaluation.py`` — kept local here to +# avoid a worker import cycle from the routes module. +DISCOVERED_METRICS_KEY = "__discovered_metrics__" + +# Allowed values for an LLM-suggested top-level metric type. Kept in +# sync with ``DiscoveredMetricSuggestedType`` in +# ``app/models/schemas.py``. +_DISCOVERED_METRIC_TYPES = ("boolean", "rating", "category") + + +def normalize_scores_with_aliases( + metric_scores: Dict[str, Any], + evaluation: CallImportEvaluation, + db: Session, + organization_id: UUID, +) -> Dict[str, Any]: + """Rewrite per-row ``metric_scores`` to honor merges + promotions. + + Called by the worker right after ``evaluate_with_llm`` returns so + every row that finishes AFTER a user has merged or promoted a + discovered label persists data already reflecting that decision. + Without this hook, a worker holding a stale prompt could re-emit a + ``from_key`` slug long after the user merged it away. + + For every parent entry (``selection_mode != null`` and a + ``discovered_labels`` / ``sequence`` field) we: + + * resolve discovered slugs through the evaluation's + ``discovered_label_aliases`` map (transitively), + * drop any discovered_labels entry whose canonical slug now + matches a real promoted child of the parent (merging them out + of the panel for free), and + * collapse adjacent duplicate sequence entries that result. + + Returns ``metric_scores`` (mutated in place) for chaining. + """ + if not isinstance(metric_scores, dict): + return metric_scores + + aliases_top = ( + evaluation.discovered_label_aliases + if isinstance(evaluation.discovered_label_aliases, dict) + else {} + ) + + # Identify the parent entries inside metric_scores. They're the + # dicts that carry a ``selection_mode`` key (set by the LLM + # hierarchy parser) and either a ``sequence`` or a + # ``discovered_labels`` list. + for key, entry in list(metric_scores.items()): + if not isinstance(entry, dict): + continue + if entry.get("type") != "category" and not entry.get("selection_mode"): + continue + try: + parent_uuid = UUID(str(key)) + except (TypeError, ValueError): + continue + + alias_map = {} + sub = aliases_top.get(str(parent_uuid)) + if isinstance(sub, dict): + alias_map = { + str(k): str(v) + for k, v in sub.items() + if isinstance(k, str) and isinstance(v, str) + } + promoted = _promoted_child_slugs(db, parent_uuid, organization_id) + + # Rewrite discovered_labels: alias-resolve keys, drop duplicates + # post-resolution, and drop entries that have been promoted. + discovered = entry.get("discovered_labels") + if isinstance(discovered, list): + kept_disc: List[Dict[str, Any]] = [] + seen: set[str] = set() + for d in discovered: + if not isinstance(d, dict): + continue + slug = _slug_label(d.get("key") or d.get("name")) + slug = _resolve_alias(alias_map, slug) + if not slug or slug in promoted or slug in seen: + continue + seen.add(slug) + new_entry = dict(d) + new_entry["key"] = slug + kept_disc.append(new_entry) + entry["discovered_labels"] = kept_disc + + # Rewrite sequence: alias-resolve every entry; collapse adjacent + # duplicates that result. We DON'T drop slugs that match + # promoted children — the promoted child slug is still a valid + # sequence entry; the flow chart will resolve it to the real + # child node. + seq = entry.get("sequence") + if isinstance(seq, list): + new_seq: List[str] = [] + last: Optional[str] = None + for item in seq: + if not isinstance(item, str): + continue + slug = _resolve_alias(alias_map, _slug_label(item)) + if not slug or slug == last: + continue + new_seq.append(slug) + last = slug + entry["sequence"] = new_seq + + # Top-level metric discoveries live alongside the parent entries + # under the reserved ``DISCOVERED_METRICS_KEY`` slot. Apply the + # flat evaluation-level alias/tombstone map + suppress slugs that + # already correspond to a real top-level Metric so workers that + # finish AFTER the user has merged / deleted / promoted can't + # resurrect a retired candidate. + discovered_metrics_payload = metric_scores.get(DISCOVERED_METRICS_KEY) + if isinstance(discovered_metrics_payload, list): + flat_alias_map = ( + evaluation.discovered_metric_aliases + if isinstance(evaluation.discovered_metric_aliases, dict) + else {} + ) + promoted_metric_slugs = _promoted_top_level_metric_slugs( + db, organization_id + ) + kept_metrics: List[Dict[str, Any]] = [] + seen_metrics: set[str] = set() + for d in discovered_metrics_payload: + if not isinstance(d, dict): + continue + slug = _slug_label(d.get("key") or d.get("name")) + slug = _resolve_alias(flat_alias_map, slug) + if ( + not slug + or slug in promoted_metric_slugs + or slug in seen_metrics + ): + continue + seen_metrics.add(slug) + new_entry = dict(d) + new_entry["key"] = slug + kept_metrics.append(new_entry) + if kept_metrics: + metric_scores[DISCOVERED_METRICS_KEY] = kept_metrics + else: + # No survivors — drop the empty array so empty-discovery rows + # keep their pre-feature payload shape. + metric_scores.pop(DISCOVERED_METRICS_KEY, None) + + return metric_scores + + +def _alias_map_for_parent( + evaluation: CallImportEvaluation, parent_metric_id: UUID +) -> Dict[str, str]: + """Pull ``{from_slug: to_slug}`` for one parent out of the eval's blob. + + Stored shape on the evaluation row is + ``{parent_id_str: {from_slug: to_slug, ...}}``. Returns an empty + dict for parents that have never had a merge applied. + """ + raw = getattr(evaluation, "discovered_label_aliases", None) + if not isinstance(raw, dict): + return {} + submap = raw.get(str(parent_metric_id)) + if not isinstance(submap, dict): + return {} + return { + str(k): str(v) + for k, v in submap.items() + if isinstance(k, str) and isinstance(v, str) + } + + +def _promoted_child_slugs( + db: Session, parent_metric_id: UUID, organization_id: UUID +) -> set[str]: + """Slugs of every real child currently sitting under the parent. + + The Discovered Labels panel hides any candidate whose slug already + matches a real child — that covers both freshly-promoted candidates + and legacy children the LLM happened to re-discover. We pull from + the live ``metrics`` table rather than the eval's + ``selected_metric_groups`` snapshot so newly-promoted children take + effect immediately, even on evaluations that ran before the + promotion. + """ + children = ( + db.query(Metric.name) + .filter( + Metric.parent_metric_id == parent_metric_id, + Metric.organization_id == organization_id, + ) + .all() + ) + out: set[str] = set() + for (name,) in children: + slug = _slug_label(name) + if slug: + out.add(slug) + return out + + +def _promoted_top_level_metric_slugs( + db: Session, organization_id: UUID +) -> set[str]: + """Slugs of every top-level (non-child) Metric in the organization. + + Used to suppress discovered-metric candidates whose slug already + matches a real standalone metric. We intentionally include both + standalone metrics AND parent category metrics — a top-level + discovery that collides with either name is a duplicate by + definition. + """ + rows = ( + db.query(Metric.name) + .filter( + Metric.organization_id == organization_id, + Metric.parent_metric_id.is_(None), + ) + .all() + ) + out: set[str] = set() + for (name,) in rows: + slug = _slug_label(name) + if slug: + out.add(slug) + return out + + +def _get_running_discovered_labels( + db: Session, + eval_id: UUID, + parent_metric_id: UUID, + organization_id: Optional[UUID] = None, + alias_map: Optional[Dict[str, str]] = None, +) -> List[Dict[str, Any]]: + """Slug-deduped view of every discovered label seen in this eval so far. + + Walks each ``call_import_evaluation_rows`` row's + ``metric_scores[parent_id]["discovered_labels"]`` and folds entries + that share the same slug. Returns a list ordered by descending + count and stable on label key, shaped like:: + + [{"key": "customer_on_hold", "name": "Customer put on hold", + "description": "...", "sample_rationale": "...", "count": 12}] + + Powers two callers: + * The worker prompt builder ("REUSE the existing key if it fits") + — invoked just before each row's LLM call to feed the model the + running list of previously-discovered labels in this evaluation. + * The ``/discovered-labels`` API surface used by the frontend + Discovered Labels panel to render candidates with counts + + sample rationales. + + Non-completed rows are skipped: an in-flight row's discoveries are + not yet reliable (the row could fail and never produce final + metric_scores). We accept the tradeoff that rows running + concurrently won't see each other's labels — slug-collision dedup + catches identical re-inventions, and near-paraphrases surface in + the UI panel where the user can manually merge. + """ + + parent_id_str = str(parent_metric_id) + from app.db_sharding.eval_rows import load_evaluation_rows_for_run + + eval_rows = load_evaluation_rows_for_run(db, eval_id) + rows = [ + (row.metric_scores,) + for row in eval_rows + if row.status == CallImportRowStatus.COMPLETED.value + ] + + # Suppress slugs that have either: + # * been promoted to a real child of the parent (so the panel doesn't + # keep nagging the user about a candidate they've already + # accepted), or + # * been merged INTO another slug (the "from" side of a merge) — + # those occurrences fold into the canonical target instead. + promoted_slugs: set[str] = set() + if organization_id is not None: + promoted_slugs = _promoted_child_slugs( + db, parent_metric_id, organization_id + ) + aliases = alias_map or {} + + by_key: Dict[str, Dict[str, Any]] = {} + for (scores,) in rows: + if not isinstance(scores, dict): + continue + parent_entry = scores.get(parent_id_str) + if not isinstance(parent_entry, dict): + continue + discovered = parent_entry.get("discovered_labels") + if not isinstance(discovered, list): + continue + for entry in discovered: + if not isinstance(entry, dict): + continue + raw_key = entry.get("key") or entry.get("name") + key = _slug_label(raw_key) + if not key: + continue + # Apply user merges + deletions first, THEN drop anything + # that ended up on a real child slug. Order matters: a + # candidate that was merged into a slug which has since + # been promoted should disappear, not show up at the + # canonical slug. An empty resolved key means the slug was + # tombstoned via the delete endpoint. + key = _resolve_alias(aliases, key) + if not key or key in promoted_slugs: + continue + name = (entry.get("name") or "").strip() or key.replace("_", " ") + description = (entry.get("description") or "").strip() or None + sample = (entry.get("rationale") or "").strip() or None + + existing = by_key.get(key) + if existing is None: + # Track up to N=3 distinct rationales per candidate so + # the Promote-to-child flow can pre-fill the new + # sub-metric's rubric with concrete LLM examples + # without the user copy-pasting from the row table. + # ``sample_rationale`` is preserved for back-compat + # with older clients; ``examples`` is the new field. + examples = [sample] if sample else [] + by_key[key] = { + "key": key, + "name": name, + "description": description, + "sample_rationale": sample, + "examples": examples, + "count": 1, + } + continue + + existing["count"] += 1 + if not existing["description"] and description: + existing["description"] = description + if not existing["sample_rationale"] and sample: + existing["sample_rationale"] = sample + # Append distinct rationales (case-insensitive trim) up + # to a small cap. Headroom is intentionally one above + # what the UI surfaces (2) so we have a backup when the + # first rationale is unhelpful. + if sample: + ex_list: List[str] = existing.setdefault("examples", []) + if len(ex_list) < 3 and not any( + s.strip().lower() == sample.strip().lower() for s in ex_list + ): + ex_list.append(sample) + + return sorted( + by_key.values(), + key=lambda item: (-item["count"], item["key"]), + ) + + +def _get_running_discovered_metrics( + db: Session, + eval_id: UUID, + organization_id: Optional[UUID] = None, + alias_map: Optional[Dict[str, str]] = None, +) -> List[Dict[str, Any]]: + """Slug-deduped view of every discovered top-level metric in this eval. + + Mirrors :func:`_get_running_discovered_labels` but is keyed at the + evaluation level (no ``parent_metric_id``). Walks each completed + row's ``metric_scores[DISCOVERED_METRICS_KEY]`` list, folds entries + that share the same slug (post-alias resolution), and suppresses + slugs that already correspond to a real top-level :class:`Metric` + in the organization. + + Each returned entry is shaped:: + + {"key": "customer_satisfaction", + "name": "Customer Satisfaction", + "description": "...", + "suggested_type": "boolean" | "rating" | "category", + "sample_rationale": "...", + "examples": ["..."], + "count": 12} + """ + + from app.db_sharding.eval_rows import load_evaluation_rows_for_run + + eval_rows = load_evaluation_rows_for_run(db, eval_id) + rows = [ + (row.metric_scores,) + for row in eval_rows + if row.status == CallImportRowStatus.COMPLETED.value + ] + + promoted_slugs: set[str] = set() + if organization_id is not None: + promoted_slugs = _promoted_top_level_metric_slugs( + db, organization_id + ) + aliases = alias_map or {} + + by_key: Dict[str, Dict[str, Any]] = {} + for (scores,) in rows: + if not isinstance(scores, dict): + continue + discovered = scores.get(DISCOVERED_METRICS_KEY) + if not isinstance(discovered, list): + continue + for entry in discovered: + if not isinstance(entry, dict): + continue + raw_key = entry.get("key") or entry.get("name") + key = _slug_label(raw_key) + if not key: + continue + # Apply user merges + deletions first, THEN drop anything + # that ended up on an already-existing top-level metric + # slug. Empty resolved key = tombstoned. + key = _resolve_alias(aliases, key) + if not key or key in promoted_slugs: + continue + name = (entry.get("name") or "").strip() or key.replace( + "_", " " + ) + description = (entry.get("description") or "").strip() or None + sample = (entry.get("rationale") or "").strip() or None + raw_type = str(entry.get("suggested_type") or "").strip().lower() + if raw_type not in _DISCOVERED_METRIC_TYPES: + raw_type = "boolean" + + existing = by_key.get(key) + if existing is None: + examples = [sample] if sample else [] + by_key[key] = { + "key": key, + "name": name, + "description": description, + "suggested_type": raw_type, + "sample_rationale": sample, + "examples": examples, + "count": 1, + } + continue + + existing["count"] += 1 + if not existing["description"] and description: + existing["description"] = description + if not existing["sample_rationale"] and sample: + existing["sample_rationale"] = sample + # Keep the most-frequently-suggested type. We don't track + # per-type frequency yet; defer to the first non-default + # type encountered when the existing entry has the default. + if existing.get("suggested_type") == "boolean" and raw_type != "boolean": + existing["suggested_type"] = raw_type + if sample: + ex_list: List[str] = existing.setdefault("examples", []) + if len(ex_list) < 3 and not any( + s.strip().lower() == sample.strip().lower() for s in ex_list + ): + ex_list.append(sample) + + return sorted( + by_key.values(), + key=lambda item: (-item["count"], item["key"]), + ) + + +def _build_flow_graph( + eval_rows: List[CallImportEvaluationRow], + parent_metric: Metric, + children: List[Metric], + alias_map: Optional[Dict[str, str]] = None, + extra_children: Optional[List[Metric]] = None, +) -> MetricFlowResponse: + """Walk per-row ``sequence`` arrays and produce aggregate nodes/edges. + + A synthetic ``START`` node is prepended to every sequence so the + diagram has a single origin. Children that never appear in any + sequence are still emitted as nodes (count=0) so the UI can render + them in the legend. + + ``alias_map`` lets callers fold merged-out discovered slugs into + their canonical target before building the graph; ``extra_children`` + are children of the parent that aren't in the legend list (e.g. + children promoted *after* the evaluation was created and therefore + missing from ``selected_metric_groups``) but should still resolve in + sequences so the slug doesn't get redrawn as a discovered candidate. + """ + parent_id_str = str(parent_metric.id) + aliases = alias_map or {} + # Build a fast lookup keyed by both the lower_snake child key (what the + # LLM emits in ``sequence``) and the child UUID (what some clients may + # store) so legacy / drifted payloads still resolve. + child_lookup: Dict[str, Metric] = {} + for child in children: + slug = _slug_label(child.name) + child_lookup[slug] = child + child_lookup[str(child.id)] = child + # ``extra_children`` are resolved-only — they shouldn't add legend + # nodes (those come from the explicit ``children`` argument), but + # they need to be in ``child_lookup`` so a sequence step that + # matches a freshly-promoted child resolves to the real child UUID + # instead of falling through to ``discovered_lookup`` and rendering + # as a "discovered" node. + if extra_children: + for child in extra_children: + slug = _slug_label(child.name) + if slug and slug not in child_lookup: + child_lookup[slug] = child + cid = str(child.id) + child_lookup.setdefault(cid, child) + + # Discovered labels: walk every row's discovered_labels first so we + # know which discovered slugs are valid before resolving sequences. + # Discovered nodes get a ``disc:`` prefixed id so they can't collide + # with real child UUIDs in the node/edge graph. We apply + # ``alias_map`` first so merged-out source slugs fold into their + # canonical target — preserving the user's "merge" intent on still- + # in-flight rows whose JSON wasn't rewritten by the merge endpoint. + discovered_lookup: Dict[str, Dict[str, Any]] = {} + for row in eval_rows: + scores = ( + row.metric_scores if isinstance(row.metric_scores, dict) else {} + ) + parent_entry = scores.get(parent_id_str) + if not isinstance(parent_entry, dict): + continue + raw_discovered = parent_entry.get("discovered_labels") + if not isinstance(raw_discovered, list): + continue + for entry in raw_discovered: + if not isinstance(entry, dict): + continue + slug = _slug_label(entry.get("key") or entry.get("name")) + slug = _resolve_alias(aliases, slug) + if not slug or slug in child_lookup: + continue + name = (entry.get("name") or "").strip() or slug.replace("_", " ") + existing = discovered_lookup.get(slug) + if existing is None: + discovered_lookup[slug] = { + "id": f"{_DISCOVERED_NODE_PREFIX}{slug}", + "name": name, + } + + node_counts: Dict[str, int] = {} + edge_counts: Dict[tuple[str, str], int] = {} + terminal_counts: Dict[str, int] = {} + + total_rows = len(eval_rows) + rows_with_sequence = 0 + + for row in eval_rows: + scores = ( + row.metric_scores if isinstance(row.metric_scores, dict) else {} + ) + parent_entry = scores.get(parent_id_str) + if not isinstance(parent_entry, dict): + continue + raw_sequence = parent_entry.get("sequence") + if not isinstance(raw_sequence, list): + continue + + resolved_ids: List[str] = [] + last_resolved: Optional[str] = None + for item in raw_sequence: + if not isinstance(item, str): + continue + normalized = _resolve_alias(aliases, _slug_label(item)) + child = child_lookup.get(normalized) or child_lookup.get(item) + if child is not None: + cid = str(child.id) + # Adjacent dedupe AFTER alias resolution so two + # different raw slugs that fold to the same target + # don't draw a self-edge through the chart. + if cid == last_resolved: + continue + resolved_ids.append(cid) + last_resolved = cid + continue + disc = discovered_lookup.get(normalized) + if disc is not None: + if disc["id"] == last_resolved: + continue + resolved_ids.append(disc["id"]) + last_resolved = disc["id"] + + if not resolved_ids: + continue + + rows_with_sequence += 1 + for nid in resolved_ids: + node_counts[nid] = node_counts.get(nid, 0) + 1 + + edge_counts[(_FLOW_START_NODE_ID, resolved_ids[0])] = ( + edge_counts.get((_FLOW_START_NODE_ID, resolved_ids[0]), 0) + 1 + ) + for src, tgt in zip(resolved_ids, resolved_ids[1:]): + if src == tgt: + continue + edge_counts[(src, tgt)] = edge_counts.get((src, tgt), 0) + 1 + + terminal_id = resolved_ids[-1] + terminal_counts[terminal_id] = terminal_counts.get(terminal_id, 0) + 1 + + nodes: List[MetricFlowNode] = [] + # Always include a START node so the UI has a stable entry point. + nodes.append( + MetricFlowNode( + id=_FLOW_START_NODE_ID, + label="Start", + count=rows_with_sequence, + is_terminal=False, + ) + ) + + def _emit_child_node(child: Metric) -> None: + cid = str(child.id) + count = node_counts.get(cid, 0) + terminal_count = terminal_counts.get(cid, 0) + is_terminal = False + if rows_with_sequence > 0: + is_terminal = ( + terminal_count / rows_with_sequence + ) >= _FLOW_TERMINAL_THRESHOLD + nodes.append( + MetricFlowNode( + id=cid, + label=child.name, + count=count, + is_terminal=is_terminal, + ) + ) + + emitted_child_ids: set[str] = set() + for child in children: + cid = str(child.id) + if cid in emitted_child_ids: + continue + emitted_child_ids.add(cid) + _emit_child_node(child) + # Extra children (promoted after the eval was created) only get + # legend nodes if they actually appear in the data — otherwise we'd + # pollute the diagram with every standalone promotion the user has + # ever made under this parent. + if extra_children: + for child in extra_children: + cid = str(child.id) + if cid in emitted_child_ids: + continue + if node_counts.get(cid, 0) == 0: + continue + emitted_child_ids.add(cid) + _emit_child_node(child) + # Append discovered nodes after the real children so legend ordering + # keeps user-defined labels first. + for slug, info in discovered_lookup.items(): + nid = info["id"] + count = node_counts.get(nid, 0) + terminal_count = terminal_counts.get(nid, 0) + is_terminal = False + if rows_with_sequence > 0: + is_terminal = ( + terminal_count / rows_with_sequence + ) >= _FLOW_TERMINAL_THRESHOLD + nodes.append( + MetricFlowNode( + id=nid, + label=info["name"], + count=count, + is_terminal=is_terminal, + is_discovered=True, + ) + ) + + edges: List[MetricFlowEdge] = [ + MetricFlowEdge(source=src, target=tgt, count=count) + for (src, tgt), count in sorted( + edge_counts.items(), key=lambda kv: kv[1], reverse=True + ) + ] + + return MetricFlowResponse( + parent_metric_id=parent_id_str, + parent_metric_name=parent_metric.name, + selection_mode=parent_metric.selection_mode, + nodes=nodes, + edges=edges, + total_rows=total_rows, + rows_with_sequence=rows_with_sequence, + ) + + +@router.get( + "/{eval_id}/flow", + response_model=MetricFlowResponse, + operation_id="getCallImportEvaluationFlow", +) +async def get_call_import_evaluation_flow( + call_import_id: UUID, + eval_id: UUID, + parent_metric_id: UUID = Query( + ..., + description=( + "Parent (category) metric whose children's sequences should be " + "aggregated into a flow graph." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> MetricFlowResponse: + """Aggregate the LLM-inferred per-row sequences into one flow graph. + + Returns ``nodes`` (one per child of the parent metric, plus a + synthetic ``START`` node) and ``edges`` (counts of consecutive + label transitions across every row that produced a sequence). The + frontend feeds this directly into a React Flow / xyflow canvas; + edge thickness should scale with ``count / total_rows`` and + ``is_terminal`` nodes should be styled as outcomes. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + parent = ( + db.query(Metric) + .filter( + Metric.id == parent_metric_id, + Metric.organization_id == organization_id, + ) + .first() + ) + if not parent: + raise HTTPException( + status_code=404, + detail="Parent metric not found in this organization.", + ) + if not parent.selection_mode: + raise HTTPException( + status_code=400, + detail=( + "Flow charts are only meaningful for parent metrics " + "(selection_mode set). This metric is standalone." + ), + ) + + # Children are taken from selected_metric_groups when present so the + # flow chart reflects exactly the subset that ran in this + # evaluation; otherwise fall back to every enabled child of the + # parent. + groups_raw = ( + evaluation.selected_metric_groups + if isinstance(evaluation.selected_metric_groups, dict) + else {} + ) + parent_id_str = str(parent.id) + children: List[Metric] = [] + if parent_id_str in groups_raw and isinstance( + groups_raw[parent_id_str], list + ): + child_ids: List[UUID] = [] + for c in groups_raw[parent_id_str]: + try: + child_ids.append(UUID(str(c))) + except (TypeError, ValueError): + continue + if child_ids: + children = ( + db.query(Metric) + .filter( + Metric.organization_id == organization_id, + Metric.id.in_(child_ids), + ) + .order_by(Metric.created_at.asc()) + .all() + ) + if not children: + children = ( + db.query(Metric) + .filter( + Metric.organization_id == organization_id, + Metric.parent_metric_id == parent.id, + ) + .order_by(Metric.created_at.asc()) + .all() + ) + + # Children promoted AFTER this evaluation was created aren't in + # ``selected_metric_groups`` but their slugs still appear in already- + # scored rows' sequences. Pass them as ``extra_children`` so those + # sequence entries resolve against the real (now promoted) child + # instead of being redrawn as discovered candidates. + extra_children: List[Metric] = [] + if children: + existing_ids = {child.id for child in children} + all_children = ( + db.query(Metric) + .filter( + Metric.organization_id == organization_id, + Metric.parent_metric_id == parent.id, + ) + .all() + ) + extra_children = [c for c in all_children if c.id not in existing_ids] + + eval_rows = _load_eval_rows(db, eval_id) + + alias_map = _alias_map_for_parent(evaluation, parent.id) + return _build_flow_graph( + eval_rows, + parent, + children, + alias_map=alias_map, + extra_children=extra_children, + ) + + +@router.get( + "/{eval_id}/discovered-labels", + response_model=DiscoveredLabelsResponse, + operation_id="getCallImportEvaluationDiscoveredLabels", +) +async def get_call_import_evaluation_discovered_labels( + call_import_id: UUID, + eval_id: UUID, + parent_metric_id: UUID = Query( + ..., + description=( + "Parent (category) metric whose LLM-discovered candidate " + "sub-labels should be aggregated across rows." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> DiscoveredLabelsResponse: + """Aggregate candidate sub-labels the LLM discovered during this eval. + + Only meaningful for parents with ``allow_discovery=true``; for other + parents we just return an empty ``items`` list rather than 400-ing + so the frontend can call the endpoint unconditionally for every + parent on the Flow tab without branching. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + parent = ( + db.query(Metric) + .filter( + Metric.id == parent_metric_id, + Metric.organization_id == organization_id, + ) + .first() + ) + if not parent: + raise HTTPException( + status_code=404, + detail="Parent metric not found in this organization.", + ) + + alias_map = _alias_map_for_parent(evaluation, parent_metric_id) + items_raw = _get_running_discovered_labels( + db, + eval_id, + parent_metric_id, + organization_id=organization_id, + alias_map=alias_map, + ) + items = [DiscoveredLabelItem(**item) for item in items_raw] + return DiscoveredLabelsResponse( + parent_metric_id=str(parent.id), items=items + ) + + +@router.post( + "/{eval_id}/discovered-labels/merge", + response_model=DiscoveredLabelsResponse, + operation_id="mergeCallImportEvaluationDiscoveredLabels", +) +async def merge_call_import_evaluation_discovered_labels( + call_import_id: UUID, + eval_id: UUID, + body: DiscoveredLabelMergeRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> DiscoveredLabelsResponse: + """Rewrite every row's ``discovered_labels`` entry from from_key -> to_key. + + Idempotent — re-merging the same pair is a no-op. Discovered slugs + inside per-row ``sequence`` arrays are also rewritten so the flow + chart stays consistent with the panel. When a row already has + ``to_key`` and we're merging ``from_key`` into it, we drop the + ``from_key`` entry instead of producing two entries with the same + slug. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + parent = ( + db.query(Metric) + .filter( + Metric.id == body.parent_metric_id, + Metric.organization_id == organization_id, + ) + .first() + ) + if not parent: + raise HTTPException( + status_code=404, + detail="Parent metric not found in this organization.", + ) + + from_key = _slug_label(body.from_key) + to_key = _slug_label(body.to_key) + if not from_key or not to_key: + raise HTTPException( + status_code=400, + detail="from_key and to_key must be non-empty slugs.", + ) + if from_key == to_key: + # No-op; just return the current aggregate so the client can + # refresh its view. + alias_map_existing = _alias_map_for_parent(evaluation, parent.id) + items_raw = _get_running_discovered_labels( + db, + eval_id, + body.parent_metric_id, + organization_id=organization_id, + alias_map=alias_map_existing, + ) + return DiscoveredLabelsResponse( + parent_metric_id=str(parent.id), + items=[DiscoveredLabelItem(**item) for item in items_raw], + ) + + parent_id_str = str(parent.id) + from app.db_sharding.eval_rows import foreach_evaluation_row_mutating + + def _merge_discovered_label_row(row: CallImportEvaluationRow) -> bool: + scores = ( + row.metric_scores + if isinstance(row.metric_scores, dict) + else None + ) + if not scores: + return False + parent_entry = scores.get(parent_id_str) + if not isinstance(parent_entry, dict): + return False + + mutated = False + discovered = parent_entry.get("discovered_labels") + if isinstance(discovered, list): + kept: List[Dict[str, Any]] = [] + existing_to = next( + ( + e + for e in discovered + if isinstance(e, dict) + and _slug_label(e.get("key") or e.get("name")) == to_key + ), + None, + ) + for entry in discovered: + if not isinstance(entry, dict): + kept.append(entry) + continue + key = _slug_label(entry.get("key") or entry.get("name")) + if key == from_key: + if existing_to is not None: + mutated = True + continue + new_entry = dict(entry) + new_entry["key"] = to_key + kept.append(new_entry) + mutated = True + else: + kept.append(entry) + if mutated: + parent_entry["discovered_labels"] = kept + + seq = parent_entry.get("sequence") + if isinstance(seq, list): + new_seq: List[str] = [] + seq_changed = False + last_added: Optional[str] = None + for item in seq: + if isinstance(item, str) and _slug_label(item) == from_key: + seq_changed = True + if last_added == to_key: + continue + new_seq.append(to_key) + last_added = to_key + else: + new_seq.append(item) + last_added = ( + _slug_label(item) if isinstance(item, str) else None + ) + if seq_changed: + parent_entry["sequence"] = new_seq + mutated = True + + if mutated: + row.metric_scores = dict(scores) + return mutated + + foreach_evaluation_row_mutating(db, eval_id, _merge_discovered_label_row) + + # Persist the merge at the evaluation level too. This is what makes + # the merge survive future scoring: rows that finish AFTER this + # call (e.g. retries, in-flight workers) will go through the + # alias map in the API surface even if the per-row JSON they + # write still mentions ``from_key``. We chain through any existing + # alias so merging A→B and then B→C resolves A→C in the panel. + raw_aliases = ( + evaluation.discovered_label_aliases + if isinstance(evaluation.discovered_label_aliases, dict) + else {} + ) + aliases_top = dict(raw_aliases) + parent_aliases = dict(aliases_top.get(parent_id_str) or {}) + # Resolve transitively: if to_key itself was previously merged into + # something else, point from_key at the canonical end-of-chain. + canonical_to = _resolve_alias(parent_aliases, to_key) + parent_aliases[from_key] = canonical_to + # Re-target any earlier aliases that pointed AT from_key — without + # this, A→B and then B→C would leave A still pointing to B (now a + # broken pointer because B is gone). Rewriting them keeps the + # alias map self-consistent. + for k, v in list(parent_aliases.items()): + if v == from_key: + parent_aliases[k] = canonical_to + aliases_top[parent_id_str] = parent_aliases + evaluation.discovered_label_aliases = aliases_top + + stamp_evaluation_actor(evaluation, principal) + db.commit() + + alias_map_after = _alias_map_for_parent(evaluation, parent.id) + items_raw = _get_running_discovered_labels( + db, + eval_id, + body.parent_metric_id, + organization_id=organization_id, + alias_map=alias_map_after, + ) + return DiscoveredLabelsResponse( + parent_metric_id=str(parent.id), + items=[DiscoveredLabelItem(**item) for item in items_raw], + ) + + +@router.post( + "/{eval_id}/discovered-labels/delete", + response_model=DiscoveredLabelsResponse, + operation_id="deleteCallImportEvaluationDiscoveredLabel", +) +async def delete_call_import_evaluation_discovered_label( + call_import_id: UUID, + eval_id: UUID, + body: DiscoveredLabelDeleteRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> DiscoveredLabelsResponse: + """Tombstone a single LLM-discovered candidate for this evaluation. + + Symmetric with the merge endpoint, but instead of redirecting the + slug at another candidate we mark it as deleted. After this call: + + * the slug is stripped from every row's + ``metric_scores[parent].discovered_labels`` list, and from + every row's ``sequence`` array (so the flow chart no longer + draws a node for it); + * the slug is recorded in + ``evaluation.discovered_label_aliases[parent][slug] = ""`` + so any worker that finishes a row AFTER this call (e.g. a row + still in flight when the user clicked Delete) silently drops + the slug instead of resurrecting it. + + Idempotent: deleting an already-deleted slug is a no-op. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + parent = ( + db.query(Metric) + .filter( + Metric.id == body.parent_metric_id, + Metric.organization_id == organization_id, + ) + .first() + ) + if not parent: + raise HTTPException( + status_code=404, + detail="Parent metric not found in this organization.", + ) + + target_key = _slug_label(body.key) + if not target_key: + raise HTTPException( + status_code=400, + detail="key must be a non-empty slug.", + ) + + parent_id_str = str(parent.id) + from app.db_sharding.eval_rows import foreach_evaluation_row_mutating + + def _delete_discovered_label_row(row: CallImportEvaluationRow) -> bool: + scores = ( + row.metric_scores + if isinstance(row.metric_scores, dict) + else None + ) + if not scores: + return False + parent_entry = scores.get(parent_id_str) + if not isinstance(parent_entry, dict): + return False + + mutated = False + discovered = parent_entry.get("discovered_labels") + if isinstance(discovered, list): + kept = [ + e + for e in discovered + if not ( + isinstance(e, dict) + and _slug_label(e.get("key") or e.get("name")) + == target_key + ) + ] + if len(kept) != len(discovered): + parent_entry["discovered_labels"] = kept + mutated = True + + seq = parent_entry.get("sequence") + if isinstance(seq, list): + new_seq: List[str] = [] + seq_changed = False + last_added: Optional[str] = None + for item in seq: + if isinstance(item, str) and _slug_label(item) == target_key: + seq_changed = True + continue + if isinstance(item, str): + norm = _slug_label(item) + if norm == last_added: + seq_changed = True + continue + last_added = norm + new_seq.append(item) + if seq_changed: + parent_entry["sequence"] = new_seq + mutated = True + + if mutated: + row.metric_scores = dict(scores) + return mutated + + foreach_evaluation_row_mutating(db, eval_id, _delete_discovered_label_row) + + # 3. Persist the tombstone on the evaluation so workers that finish + # later don't re-surface the deleted slug. We also retarget any + # existing aliases whose ``to_key`` was the deleted slug — without + # this, a previous merge that pointed at this slug would leave a + # dangling pointer. + raw_aliases = ( + evaluation.discovered_label_aliases + if isinstance(evaluation.discovered_label_aliases, dict) + else {} + ) + aliases_top = dict(raw_aliases) + parent_aliases = dict(aliases_top.get(parent_id_str) or {}) + parent_aliases[target_key] = "" # deletion sentinel + for k, v in list(parent_aliases.items()): + if v == target_key: + parent_aliases[k] = "" + aliases_top[parent_id_str] = parent_aliases + evaluation.discovered_label_aliases = aliases_top + + stamp_evaluation_actor(evaluation, principal) + db.commit() + + alias_map_after = _alias_map_for_parent(evaluation, parent.id) + items_raw = _get_running_discovered_labels( + db, + eval_id, + body.parent_metric_id, + organization_id=organization_id, + alias_map=alias_map_after, + ) + return DiscoveredLabelsResponse( + parent_metric_id=str(parent.id), + items=[DiscoveredLabelItem(**item) for item in items_raw], + ) + + +# --------------------------------------------------------------------------- +# Discovered TOP-LEVEL METRICS (per-evaluation discovery) +# +# These endpoints are the parallel of the discovered-labels trio above but +# scoped to the evaluation as a whole instead of to a parent metric. They +# all live under ``/{eval_id}/discovered-metrics`` and operate on the +# reserved ``DISCOVERED_METRICS_KEY`` slot of each per-row +# ``metric_scores`` plus the flat ``CallImportEvaluation.discovered_metric_aliases`` +# map (no parent-id nesting). +# --------------------------------------------------------------------------- + + +def _flat_metric_aliases( + evaluation: CallImportEvaluation, +) -> Dict[str, str]: + """Pull the flat ``{from_slug: to_slug}`` map for an evaluation.""" + raw = getattr(evaluation, "discovered_metric_aliases", None) + if not isinstance(raw, dict): + return {} + return { + str(k): str(v) + for k, v in raw.items() + if isinstance(k, str) and isinstance(v, str) + } + + +@router.get( + "/{eval_id}/discovered-metrics", + response_model=DiscoveredMetricsResponse, + operation_id="getCallImportEvaluationDiscoveredMetrics", +) +async def get_call_import_evaluation_discovered_metrics( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> DiscoveredMetricsResponse: + """Aggregate top-level metric candidates the LLM discovered during this eval. + + Returns an empty ``items`` list when the evaluation did not opt + into top-level metric discovery; this keeps the frontend able to + call the endpoint unconditionally without branching on the + evaluation's ``discover_new_metrics`` flag. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + if not bool(getattr(evaluation, "discover_new_metrics", False)): + return DiscoveredMetricsResponse(evaluation_id=evaluation.id, items=[]) + + items_raw = _get_running_discovered_metrics( + db, + eval_id, + organization_id=organization_id, + alias_map=_flat_metric_aliases(evaluation), + ) + return DiscoveredMetricsResponse( + evaluation_id=evaluation.id, + items=[DiscoveredMetricItem(**item) for item in items_raw], + ) + + +@router.post( + "/{eval_id}/discovered-metrics/merge", + response_model=DiscoveredMetricsResponse, + operation_id="mergeCallImportEvaluationDiscoveredMetrics", +) +async def merge_call_import_evaluation_discovered_metrics( + call_import_id: UUID, + eval_id: UUID, + body: DiscoveredMetricMergeRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> DiscoveredMetricsResponse: + """Rewrite every row's ``__discovered_metrics__`` entry from→to. + + Mirrors the discovered-labels merge endpoint but operates on the + flat top-level metric list. Idempotent — re-merging is a no-op. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + from_key = _slug_label(body.from_key) + to_key = _slug_label(body.to_key) + if not from_key or not to_key: + raise HTTPException( + status_code=400, + detail="from_key and to_key must be non-empty slugs.", + ) + if from_key == to_key: + items_raw = _get_running_discovered_metrics( + db, + eval_id, + organization_id=organization_id, + alias_map=_flat_metric_aliases(evaluation), + ) + return DiscoveredMetricsResponse( + evaluation_id=evaluation.id, + items=[DiscoveredMetricItem(**item) for item in items_raw], + ) + + from app.db_sharding.eval_rows import foreach_evaluation_row_mutating + + def _merge_discovered_metric_row(row: CallImportEvaluationRow) -> bool: + scores = ( + row.metric_scores + if isinstance(row.metric_scores, dict) + else None + ) + if not scores: + return False + discovered = scores.get(DISCOVERED_METRICS_KEY) + if not isinstance(discovered, list): + return False + + kept: List[Dict[str, Any]] = [] + mutated = False + existing_to = next( + ( + e + for e in discovered + if isinstance(e, dict) + and _slug_label(e.get("key") or e.get("name")) == to_key + ), + None, + ) + for entry in discovered: + if not isinstance(entry, dict): + kept.append(entry) + continue + key = _slug_label(entry.get("key") or entry.get("name")) + if key == from_key: + if existing_to is not None: + mutated = True + continue + new_entry = dict(entry) + new_entry["key"] = to_key + kept.append(new_entry) + mutated = True + else: + kept.append(entry) + if mutated: + scores[DISCOVERED_METRICS_KEY] = kept + row.metric_scores = dict(scores) + return mutated + + foreach_evaluation_row_mutating(db, eval_id, _merge_discovered_metric_row) + + raw_aliases = ( + evaluation.discovered_metric_aliases + if isinstance(evaluation.discovered_metric_aliases, dict) + else {} + ) + aliases = dict(raw_aliases) + canonical_to = _resolve_alias(aliases, to_key) + aliases[from_key] = canonical_to + for k, v in list(aliases.items()): + if v == from_key: + aliases[k] = canonical_to + evaluation.discovered_metric_aliases = aliases + + stamp_evaluation_actor(evaluation, principal) + db.commit() + + items_raw = _get_running_discovered_metrics( + db, + eval_id, + organization_id=organization_id, + alias_map=_flat_metric_aliases(evaluation), + ) + return DiscoveredMetricsResponse( + evaluation_id=evaluation.id, + items=[DiscoveredMetricItem(**item) for item in items_raw], + ) + + +@router.post( + "/{eval_id}/discovered-metrics/delete", + response_model=DiscoveredMetricsResponse, + operation_id="deleteCallImportEvaluationDiscoveredMetric", +) +async def delete_call_import_evaluation_discovered_metric( + call_import_id: UUID, + eval_id: UUID, + body: DiscoveredMetricDeleteRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> DiscoveredMetricsResponse: + """Tombstone a single LLM-discovered top-level metric candidate.""" + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + target_key = _slug_label(body.key) + if not target_key: + raise HTTPException( + status_code=400, + detail="key must be a non-empty slug.", + ) + + from app.db_sharding.eval_rows import foreach_evaluation_row_mutating + + def _delete_discovered_metric_row(row: CallImportEvaluationRow) -> bool: + scores = ( + row.metric_scores + if isinstance(row.metric_scores, dict) + else None + ) + if not scores: + return False + discovered = scores.get(DISCOVERED_METRICS_KEY) + if not isinstance(discovered, list): + return False + kept = [ + e + for e in discovered + if not ( + isinstance(e, dict) + and _slug_label(e.get("key") or e.get("name")) + == target_key + ) + ] + if len(kept) == len(discovered): + return False + if kept: + scores[DISCOVERED_METRICS_KEY] = kept + else: + scores.pop(DISCOVERED_METRICS_KEY, None) + row.metric_scores = dict(scores) + return True + + foreach_evaluation_row_mutating(db, eval_id, _delete_discovered_metric_row) + + raw_aliases = ( + evaluation.discovered_metric_aliases + if isinstance(evaluation.discovered_metric_aliases, dict) + else {} + ) + aliases = dict(raw_aliases) + aliases[target_key] = "" # tombstone + for k, v in list(aliases.items()): + if v == target_key: + aliases[k] = "" + evaluation.discovered_metric_aliases = aliases + + stamp_evaluation_actor(evaluation, principal) + db.commit() + + items_raw = _get_running_discovered_metrics( + db, + eval_id, + organization_id=organization_id, + alias_map=_flat_metric_aliases(evaluation), + ) + return DiscoveredMetricsResponse( + evaluation_id=evaluation.id, + items=[DiscoveredMetricItem(**item) for item in items_raw], + ) + + +@router.delete( + "/{eval_id}/rows/{eval_row_id}", + status_code=status.HTTP_204_NO_CONTENT, + operation_id="deleteCallImportEvaluationRow", +) +async def delete_call_import_evaluation_row( + call_import_id: UUID, + eval_id: UUID, + eval_row_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> Response: + """Delete a single per-row scoring entry within an evaluation run. + + Useful when the user wants to drop a noisy row before re-exporting + the CSV — e.g. a row whose audio was corrupt and skewed the + aggregate. Counters on the parent are recomputed so the rolled-up + status stays accurate. + """ + + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + from app.db_sharding.sessions import is_sharding_enabled + + if is_sharding_enabled(): + from app.db_sharding.eval_rows import delete_evaluation_row_on_shards + + if not delete_evaluation_row_on_shards(eval_row_id, eval_id): + raise HTTPException( + status_code=404, detail="Evaluation row not found in this run" + ) + _rollup_evaluation_status(evaluation, db) + db.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + eval_row = ( + db.query(CallImportEvaluationRow) + .filter( + CallImportEvaluationRow.id == eval_row_id, + CallImportEvaluationRow.evaluation_id == eval_id, + ) + .first() + ) + if not eval_row: + raise HTTPException( + status_code=404, detail="Evaluation row not found in this run" + ) + + # If the row was still in flight, best-effort revoke the worker task + # so it doesn't try to write into a deleted DB row mid-execution. + if eval_row.celery_task_id and eval_row.status in {"pending", "running"}: + try: + from app.workers.celery_app import celery_app + + celery_app.control.revoke(eval_row.celery_task_id, terminate=False) + except Exception: + pass + + db.delete(eval_row) + db.flush() + _rollup_evaluation_status(evaluation, db) + stamp_evaluation_actor(evaluation, principal) + db.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +# --------------------------------------------------------------------------- +# Retry endpoints +# --------------------------------------------------------------------------- +# +# The create endpoint enqueues every row of a fresh run; these endpoints +# let the user re-enqueue a *subset* of rows in an existing run — most +# commonly the ones that failed. We keep the worker contract identical +# (``evaluate_call_import_row_task(eval_row_id)``), so the retry path +# only has to reset row state and re-fan-out. When a row is missing its +# diarised transcript and the run was configured for diarised +# transcripts, we chain through ``transcribe_call_import_row_task`` the +# same way the create endpoint does — that's what makes "retry" feel +# like "just fix it" instead of "fail again immediately". + + +def _prepare_source_row_for_retry( + source_row: CallImportRow, + *, + transcribe_overwrite: bool, +) -> None: + """Clear stale diarisation markers so retry dispatch can re-run the pipeline.""" + source_row.celery_task_id = None + + # Re-fetch recordings when a prior import failed or stalled without S3 audio. + # Mirrors retry_failed_call_import_rows so eval retry can re-enqueue imports. + if ( + source_row.status + in (CallImportRowStatus.FAILED, CallImportRowStatus.PROCESSING) + and not (source_row.recording_s3_key or "").strip() + ): + source_row.status = CallImportRowStatus.PENDING + source_row.error_message = None + + if transcribe_overwrite and (source_row.diarised_transcript or "").strip(): + source_row.diarised_transcript = None + + has_dia = bool((source_row.diarised_transcript or "").strip()) + dia_status = (source_row.diarised_transcript_status or "").strip().lower() + + if has_dia and not transcribe_overwrite: + source_row.diarised_transcript_status = "completed" + source_row.diarised_transcript_error = None + return + + if dia_status in {"failed", "pending", "running", "idle"}: + source_row.diarised_transcript_status = "idle" + source_row.diarised_transcript_error = None + + +def _reset_eval_row_for_retry( + eval_row: CallImportEvaluationRow, + *, + metric_ids: Optional[List[UUID]] = None, + skip_revoke: bool = False, +) -> None: + """Wipe per-row state so the worker can re-run it cleanly. + + Mirrors the initial state used by ``create_call_import_evaluation`` + when it first inserts a row, with the addition of revoking any + lingering Celery task id. + + When ``metric_ids`` is provided, this is a **metric-subset retry**: + only the scores for those metrics are removed from + ``metric_scores`` (other metrics' previously-computed values are + preserved so the worker's partial-merge write keeps them intact). + Otherwise the entire ``metric_scores`` dict is reset, matching the + legacy behaviour. + """ + if ( + not skip_revoke + and eval_row.celery_task_id + and eval_row.status in {"pending", "running"} + ): + try: + from app.workers.celery_app import celery_app + + celery_app.control.revoke(eval_row.celery_task_id, terminate=False) + except Exception: # noqa: BLE001 — revoke is best-effort + pass + eval_row.status = "pending" + eval_row.error_message = None + if metric_ids: + # Strip ONLY the targeted metric keys. Both string and UUID + # forms can appear in ``metric_scores`` depending on which + # code path wrote the dict, so we normalise to lower-case + # strings for the comparison. + existing = ( + eval_row.metric_scores if isinstance(eval_row.metric_scores, dict) else {} + ) + target_keys = {str(mid).lower() for mid in metric_ids} + eval_row.metric_scores = { + key: value + for key, value in existing.items() + if str(key).lower() not in target_keys + } + else: + eval_row.metric_scores = {} + eval_row.started_at = None + eval_row.finished_at = None + eval_row.celery_task_id = None + + +def _enqueue_eval_rows_with_optional_transcribe( + db: Session, + evaluation: CallImportEvaluation, + eval_rows_with_source: List[ + Tuple[CallImportEvaluationRow, CallImportRow] + ], + *, + transcribe_overwrite: bool = False, + restricted_metric_ids: Optional[List[UUID]] = None, +) -> Tuple[int, int]: + """Schedule throttled evaluation dispatch for pending eval rows. + + Returns ``(evaluate_only_count, transcribe_then_evaluate_count)`` for + logging/UI compatibility. Actual Celery fan-out is handled by + :func:`dispatch_evaluation_rows_task` under Redis fair-share limits. + """ + from app.workers.concurrency.eval_dispatch import _needs_transcribe_for_eval + from app.workers.concurrency.fair_dispatch import ( + schedule_fair_dispatch, + store_evaluation_transcribe_overwrite, + store_row_restricted_metrics, + ) + + eval_only_count = 0 + transcribe_count = 0 + if eval_rows_with_source: + for eval_row, source_row in eval_rows_with_source: + if _needs_transcribe_for_eval( + evaluation, + source_row, + transcribe_overwrite=transcribe_overwrite, + ): + transcribe_count += 1 + else: + eval_only_count += 1 + + restricted_metric_ids_str: Optional[List[str]] = ( + [str(mid) for mid in restricted_metric_ids] + if restricted_metric_ids + else None + ) + if restricted_metric_ids_str: + for eval_row, _ in eval_rows_with_source: + store_row_restricted_metrics(eval_row.id, restricted_metric_ids_str) + else: + restricted_metric_ids_str = ( + [str(mid) for mid in restricted_metric_ids] if restricted_metric_ids else None + ) + store_evaluation_transcribe_overwrite( + evaluation.id, + overwrite=transcribe_overwrite, + ) + schedule_fair_dispatch(max_workspace_turns=999) + return eval_only_count, transcribe_count + + +def _apply_telephony_retry_overrides( + db: Session, + *, + call_import: CallImport, + organization_id: UUID, + payload: CallImportEvaluationRetryRequest, +) -> None: + """Pin or clear telephony credentials on the batch for this retry pass.""" + fields_set = payload.model_fields_set + if ( + "provider" not in fields_set + and "telephony_integration_id" not in fields_set + ): + return + + from app.api.v1.routes.call_imports import _resolve_telephony_integration + + if payload.telephony_integration_id is not None: + integration = _resolve_telephony_integration( + db, + organization_id, + payload.telephony_integration_id, + payload.provider or "", + ) + call_import.provider = integration.provider + call_import.telephony_integration_id = integration.id + else: + call_import.provider = None + call_import.telephony_integration_id = None + db.flush() + + +def _apply_retry_overrides( + db: Session, + evaluation: CallImportEvaluation, + organization_id: UUID, + payload: CallImportEvaluationRetryRequest, +) -> None: + """Validate + persist the LLM/STT override fields on the run. + + Mirrors the validation in ``create_call_import_evaluation`` but + only touches the fields the caller actually sent — leaving any + field ``None`` preserves the run's existing value. Raises + ``HTTPException(400)`` on bad input so the route handler can let + FastAPI turn it into a clean 400 response. + """ + # --- LLM provider + model (must be sent together) --- + if payload.llm_provider is not None or payload.llm_model is not None: + if not (payload.llm_provider and payload.llm_model): + raise HTTPException( + status_code=400, + detail=( + "Both llm_provider and llm_model are required when " + "overriding the run LLM on retry." + ), + ) + try: + evaluation.llm_provider = ModelProvider( + payload.llm_provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Unknown LLM provider '{payload.llm_provider}'. " + "Valid keys are documented in ModelProvider." + ), + ) + new_model = payload.llm_model.strip() or None + if not new_model: + raise HTTPException( + status_code=400, detail="llm_model cannot be empty." + ) + evaluation.llm_model = new_model + + # --- LLM credential pin --- + if payload.llm_credential_id is not None: + cred = ( + db.query(AIProvider) + .filter( + AIProvider.id == payload.llm_credential_id, + AIProvider.organization_id == organization_id, + ) + .first() + ) + if not cred: + raise HTTPException( + status_code=400, + detail=( + "The provided llm_credential_id does not exist in " + "this organization." + ), + ) + evaluation.llm_credential_id = payload.llm_credential_id + + if payload.llm_config is not None: + evaluation.llm_config = payload.llm_config + + # --- Per-metric LLM overrides --- + # We accept the same dict shape as the create endpoint but + # constrain keys to leaf metrics that are actually in this run. + # Passing an empty dict explicitly clears existing overrides. + if payload.metric_llm_overrides is not None: + valid_leaf_ids = { + str(mid) for mid in (evaluation.selected_metric_ids or []) + } + overrides_payload: Dict[str, Dict[str, Any]] = {} + for metric_id, override in payload.metric_llm_overrides.items(): + if metric_id not in valid_leaf_ids: + raise HTTPException( + status_code=400, + detail=( + "metric_llm_overrides references metric " + f"{metric_id} which is not a leaf metric in " + "this run." + ), + ) + override_dict: Dict[str, Any] = {} + if override.provider is not None: + if not override.model: + raise HTTPException( + status_code=400, + detail=( + f"Override for metric {metric_id} has a " + "provider but no model." + ), + ) + try: + override_dict["provider"] = ModelProvider( + override.provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=( + f"Override for metric {metric_id} uses " + f"unknown provider '{override.provider}'." + ), + ) + override_dict["model"] = override.model.strip() + elif override.model: + raise HTTPException( + status_code=400, + detail=( + f"Override for metric {metric_id} has a model " + "but no provider." + ), + ) + if override.credential_id is not None: + override_dict["credential_id"] = str(override.credential_id) + if override.llm_config is not None: + override_dict["llm_config"] = override.llm_config + if override_dict: + overrides_payload[metric_id] = override_dict + evaluation.metric_llm_overrides = overrides_payload or None + + # --- STT provider + model (must be sent together) --- + if payload.stt_provider is not None or payload.stt_model is not None: + if not (payload.stt_provider and payload.stt_model): + raise HTTPException( + status_code=400, + detail=( + "Both stt_provider and stt_model are required " + "when overriding the run STT on retry." + ), + ) + try: + evaluation.stt_provider = ModelProvider( + payload.stt_provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=f"Unknown STT provider '{payload.stt_provider}'.", + ) + new_stt_model = payload.stt_model.strip() or None + if not new_stt_model: + raise HTTPException( + status_code=400, detail="stt_model cannot be empty." + ) + evaluation.stt_model = new_stt_model + + # --- STT credential pin --- + if payload.stt_credential_id is not None: + evaluation.stt_credential_id = payload.stt_credential_id + + # --- LLM diariser provider + model (must be sent together) --- + if ( + payload.diarization_llm_provider is not None + or payload.diarization_llm_model is not None + ): + if not ( + payload.diarization_llm_provider + and payload.diarization_llm_model + ): + raise HTTPException( + status_code=400, + detail=( + "Both diarization_llm_provider and " + "diarization_llm_model are required when overriding " + "the run diariser on retry." + ), + ) + try: + evaluation.diarisation_llm_provider = ModelProvider( + payload.diarization_llm_provider.lower() + ).value + except ValueError: + raise HTTPException( + status_code=400, + detail=( + "Unknown diarisation LLM provider " + f"'{payload.diarization_llm_provider}'." + ), + ) + new_diariser_model = ( + payload.diarization_llm_model.strip() or None + ) + if not new_diariser_model: + raise HTTPException( + status_code=400, + detail="diarization_llm_model cannot be empty.", + ) + evaluation.diarisation_llm_model = new_diariser_model + + if payload.diarization_llm_credential_id is not None: + evaluation.diarisation_llm_credential_id = ( + payload.diarization_llm_credential_id + ) + + # ``diarization_prompt`` semantics: None = leave untouched; + # empty string = clear (fall back to the canonical default at + # worker time); anything else = persist verbatim. + if payload.diarization_prompt is not None: + cleaned = payload.diarization_prompt.strip() + evaluation.diarisation_prompt = cleaned or None + + if payload.transcribe_mode is not None: + mode = payload.transcribe_mode.strip().lower() + if mode not in {"stt_llm", "llm_only"}: + raise HTTPException( + status_code=400, + detail=( + f"Unknown transcribe_mode '{payload.transcribe_mode}'. " + "Valid values are 'stt_llm' and 'llm_only'." + ), + ) + evaluation.transcribe_mode = mode + + +def _gather_retry_targets( + db: Session, + evaluation: CallImportEvaluation, + requested_ids: Optional[List[UUID]], + *, + include_completed: bool = False, +) -> Tuple[ + List[Tuple[CallImportEvaluationRow, CallImportRow]], + List[CallImportEvaluationRetrySkippedItem], +]: + """Resolve which rows to retry + reasons for any we refuse. + + When ``requested_ids`` is None we retry every row whose status is + ``failed`` (or every row when ``include_completed`` is also set — + used by the metric-subset retry path which legitimately wants to + recompute a metric on already-successful rows). When the caller + passes ids explicitly we still filter out rows that are currently + in flight; ``include_completed`` controls whether previously- + successful rows are eligible. + """ + from app.db_sharding.sessions import is_sharding_enabled + + if is_sharding_enabled(): + from app.db_sharding.eval_rows import gather_retry_targets_sharded + + return gather_retry_targets_sharded( + db, + evaluation, + requested_ids, + include_completed=include_completed, + ) + + eval_rows_query = db.query(CallImportEvaluationRow).filter( + CallImportEvaluationRow.evaluation_id == evaluation.id + ) + + targets: List[Tuple[CallImportEvaluationRow, CallImportRow]] = [] + skipped: List[CallImportEvaluationRetrySkippedItem] = [] + + if requested_ids is None: + if include_completed: + # "Retry everything" path used by the metric-subset re-run + # UI. Still skip in-flight rows below so we don't trample + # work the worker is actively doing. + candidate_rows = eval_rows_query.filter( + CallImportEvaluationRow.status.in_(["failed", "completed"]) + ).all() + else: + candidate_rows = eval_rows_query.filter( + CallImportEvaluationRow.status == "failed" + ).all() + else: + requested_set = set(requested_ids) + candidate_rows = eval_rows_query.filter( + CallImportEvaluationRow.id.in_(requested_set) + ).all() + found_ids = {row.id for row in candidate_rows} + for missing in requested_set - found_ids: + skipped.append( + CallImportEvaluationRetrySkippedItem( + eval_row_id=missing, + reason="unknown", + ) + ) + + if not candidate_rows: + return targets, skipped + + source_row_ids = [row.call_import_row_id for row in candidate_rows] + source_rows = ( + db.query(CallImportRow) + .filter(CallImportRow.id.in_(source_row_ids)) + .all() + ) + source_by_id = {row.id: row for row in source_rows} + + for eval_row in candidate_rows: + if eval_row.status in {"pending", "running"}: + skipped.append( + CallImportEvaluationRetrySkippedItem( + eval_row_id=eval_row.id, + reason="in_progress", + ) + ) + continue + if eval_row.status == "completed" and not include_completed: + skipped.append( + CallImportEvaluationRetrySkippedItem( + eval_row_id=eval_row.id, + reason="completed", + ) + ) + continue + source_row = source_by_id.get(eval_row.call_import_row_id) + if source_row is None: + skipped.append( + CallImportEvaluationRetrySkippedItem( + eval_row_id=eval_row.id, + reason="source_row_missing", + ) + ) + continue + targets.append((eval_row, source_row)) + + return targets, skipped + + +@router.post( + "/{eval_id}/retry", + response_model=CallImportEvaluationRetryResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="retryCallImportEvaluation", +) +async def retry_call_import_evaluation( + call_import_id: UUID, + eval_id: UUID, + payload: Optional[CallImportEvaluationRetryRequest] = Body(default=None), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationRetryResponse: + """Re-enqueue failed rows in an evaluation run. + + Default behavior (no body) is "retry every row that failed". Pass + ``eval_row_ids`` to scope the retry to a specific subset (e.g. the + single row a user clicked in the UI). Rows that are still + in-flight or already completed are returned in ``skipped`` rather + than re-enqueued, so this endpoint is always safe to call. + + When ``metric_ids`` is set in the payload, this is a **metric- + subset retry**: only the listed metrics are recomputed (and merged + into the row's existing ``metric_scores`` — other metrics' values + are preserved). The route auto-flips ``include_completed=True`` in + that case so previously-successful rows are eligible for re- + scoring; without it the call would no-op because every row would + be skipped as ``completed``. + + The worker contract is the same as the create endpoint: + ``evaluate_call_import_row_task(eval_row_id, [restricted_metric_ids])``. + When the run is configured for diarised transcripts and the row's + diarised transcript is missing, we chain through + ``transcribe_call_import_row_task`` first — matching the + auto-transcribe behavior of POST ``/evaluations``. + """ + del api_key + call_import = _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + requested_ids = payload.eval_row_ids if payload else None + # Metric-subset retry: validate that every metric is something this + # run actually scored. Empty list is rejected too — callers that + # want a full re-run should omit the field entirely. + # + # ``selected_metric_ids`` holds the LEAVES only (children for + # hierarchical / category metrics, standalone metrics otherwise) — + # see ``leaf_metric_ids`` in :func:`create_call_import_evaluation`. + # Parent IDs for hierarchical metrics live separately in + # ``selected_metric_groups`` (``{parent_id: [child_ids]}``) so the + # UI can reconstruct the tree without round-tripping through the + # metric table. + # + # The Re-run-metrics modal surfaces PARENTS for hierarchical + # metrics (it suppresses individual children via + # ``childrenInGroups`` in ``CallImportEvaluationDetail.tsx``), so a + # naive ``metric_ids ⊆ selected_metric_ids`` check rejects every + # parent-ID request with a misleading "unknown ids" 400. We accept + # both shapes here and then EXPAND any parent IDs into + # ``{parent_id, *child_ids}`` so the downstream helpers see the + # full set of keys that need clearing + the full set of leaves + # that need re-scoring. + metric_ids: Optional[List[UUID]] = ( + payload.metric_ids if payload else None + ) + if metric_ids is not None: + if not metric_ids: + raise HTTPException( + status_code=400, + detail=( + "metric_ids must be a non-empty list. Omit the " + "field to re-run all metrics." + ), + ) + + leaf_set: Set[str] = { + str(item).lower() + for item in (evaluation.selected_metric_ids or []) + } + # ``selected_metric_groups`` is a dict ``{parent_id_str: + # [child_id_str, ...]}`` (see line ~487 in + # ``create_call_import_evaluation``). We tolerate stale data + # (string / UUID / non-dict) without crashing the retry path — + # if it's malformed we just treat it as "no parents" and fall + # back to the leaf-only check. + groups_raw = ( + evaluation.selected_metric_groups + if isinstance(evaluation.selected_metric_groups, dict) + else {} + ) + parent_to_children_str: Dict[str, List[str]] = {} + for parent_key, children_raw in groups_raw.items(): + if not isinstance(children_raw, (list, tuple)): + continue + children_norm = [ + str(c).lower() for c in children_raw if c is not None + ] + parent_to_children_str[str(parent_key).lower()] = children_norm + parent_set = set(parent_to_children_str.keys()) + + unknown = [ + mid for mid in metric_ids + if str(mid).lower() not in leaf_set + and str(mid).lower() not in parent_set + ] + if unknown: + raise HTTPException( + status_code=400, + detail=( + "metric_ids must be a subset of this evaluation's " + f"selected metrics; unknown ids: {[str(u) for u in unknown]}." + ), + ) + + # Expand parent IDs into ``{parent, *children}`` so: + # * ``_reset_eval_row_for_retry`` strips BOTH the parent + # entry (with ``chosen_child_id`` / rationale) AND every + # per-child boolean entry that the LLM evaluator wrote + # under each child's ID (see + # ``app/workers/tasks/helpers/llm_evaluation.py`` lines + # 1584 and 1649). + # * ``_enqueue_eval_rows_with_optional_transcribe`` → + # ``evaluate_call_import_row_task`` filters the work-list + # off ``selected_metric_ids`` (leaves), so we MUST hand it + # the child IDs for the parent to actually get re-scored. + # Leaves pass through unchanged. + expanded: List[UUID] = [] + seen: Set[str] = set() + for mid in metric_ids: + mid_norm = str(mid).lower() + children_str = parent_to_children_str.get(mid_norm) + if children_str is not None: + # Parent: include the parent ID itself (so the parent + # entry in ``metric_scores`` is also cleared) and all + # of its children. + candidates = [mid_norm, *children_str] + else: + candidates = [mid_norm] + for candidate in candidates: + if candidate in seen: + continue + try: + expanded.append(UUID(candidate)) + except (TypeError, ValueError): + # Defensive: skip junk values rather than 500. + continue + seen.add(candidate) + metric_ids = expanded + + # ``include_completed`` is auto-enabled when the caller asked for a + # metric subset (otherwise the metric-subset retry would always + # no-op on a green run, which is the whole reason this feature + # exists). The explicit payload flag wins for full-row retries. + include_completed = bool( + (payload.include_completed if payload else False) + or (metric_ids is not None) + ) + + transcribe_overwrite = bool( + payload.transcribe_overwrite if payload else False + ) + + skipped: List[CallImportEvaluationRetrySkippedItem] = [] + if requested_ids is None: + from app.db_sharding.eval_rows import count_evaluation_rows_for_run + from app.db_sharding.sessions import is_sharding_enabled + + if is_sharding_enabled(): + statuses = ( + ["failed", "completed"] if include_completed else ["failed"] + ) + target_count = count_evaluation_rows_for_run( + db, eval_id, statuses=statuses + ) + else: + from sqlalchemy import func + + count_query = db.query(func.count(CallImportEvaluationRow.id)).filter( + CallImportEvaluationRow.evaluation_id == eval_id + ) + if include_completed: + count_query = count_query.filter( + CallImportEvaluationRow.status.in_(["failed", "completed"]) + ) + else: + count_query = count_query.filter( + CallImportEvaluationRow.status == "failed" + ) + target_count = int(count_query.scalar() or 0) + if target_count == 0: + return CallImportEvaluationRetryResponse( + requeued=0, + transcribe_requeued=0, + skipped=skipped, + ) + else: + targets, skipped = _gather_retry_targets( + db, + evaluation, + requested_ids, + include_completed=include_completed, + ) + if not targets: + return CallImportEvaluationRetryResponse( + requeued=0, + transcribe_requeued=0, + skipped=skipped, + ) + target_count = len(targets) + + # Apply LLM / STT overrides BEFORE enqueueing so the persisted run + # config is correct by the time the worker reads it. + if payload is not None: + _apply_retry_overrides(db, evaluation, organization_id, payload) + _apply_telephony_retry_overrides( + db, + call_import=call_import, + organization_id=organization_id, + payload=payload, + ) + + evaluation.error_message = None + evaluation.finished_at = None + evaluation.status = "running" + if not evaluation.started_at: + from datetime import datetime, timezone + + evaluation.started_at = datetime.now(timezone.utc) + + _claim_evaluation_bulk_operation(eval_id, "retry") + stamp_evaluation_actor(evaluation, principal) + db.commit() + + from app.workers.tasks.call_import_bulk_ops import ( + retry_call_import_evaluation_task, + ) + + retry_call_import_evaluation_task.delay( + str(eval_id), + { + "eval_row_ids": [str(rid) for rid in requested_ids] + if requested_ids + else None, + "metric_ids": [str(mid) for mid in metric_ids] if metric_ids else None, + "include_completed": include_completed, + "transcribe_overwrite": transcribe_overwrite, + }, + ) + + return CallImportEvaluationRetryResponse( + requeued=target_count, + transcribe_requeued=0, + skipped=skipped, + ) + + +@router.post( + "/{eval_id}/rows/{eval_row_id}/retry", + response_model=CallImportEvaluationRowResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="retryCallImportEvaluationRow", +) +async def retry_call_import_evaluation_row( + call_import_id: UUID, + eval_id: UUID, + eval_row_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportEvaluationRowResponse: + """Re-enqueue a single failed evaluation row. + + Convenience wrapper around ``retry_call_import_evaluation`` for the + "Retry this row" affordance in the row table. Returns the + refreshed row so the UI can update its badge immediately, without + waiting for the next polling tick. + """ + del api_key + _require_import(db, call_import_id, organization_id) + + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException( + status_code=404, detail="Call import evaluation not found" + ) + + _require_no_evaluation_bulk_operation(eval_id) + + from app.db_sharding.eval_rows import ( + evaluation_row_session, + find_evaluation_row_in_run, + ) + from app.db_sharding.sessions import is_sharding_enabled + + eval_row, _source_stub = find_evaluation_row_in_run(db, eval_id, eval_row_id) + if eval_row is None: + raise HTTPException( + status_code=404, detail="Evaluation row not found in this run" + ) + + if eval_row.status in {"pending", "running"}: + raise HTTPException( + status_code=409, + detail=( + "This row is still in progress — wait for it to finish " + "before retrying." + ), + ) + + targets, _ = _gather_retry_targets(db, evaluation, [eval_row.id]) + if not targets: + raise HTTPException( + status_code=409, + detail=( + "This row cannot be retried in its current state " + f"(status={eval_row.status})." + ), + ) + + if is_sharding_enabled(): + with evaluation_row_session(eval_row_id) as ( + row_db, + _catalog_db, + eval_row, + source_row, + _shard_id, + ): + _prepare_source_row_for_retry(source_row, transcribe_overwrite=False) + _reset_eval_row_for_retry(eval_row) + row_db.commit() + targets = [(eval_row, source_row)] + else: + for er, source_row in targets: + _prepare_source_row_for_retry(source_row, transcribe_overwrite=False) + _reset_eval_row_for_retry(er) + + evaluation.error_message = None + evaluation.finished_at = None + evaluation.status = "running" + if not evaluation.started_at: + from datetime import datetime, timezone + + evaluation.started_at = datetime.now(timezone.utc) + db.flush() + _rollup_evaluation_status(evaluation, db) + stamp_evaluation_actor(evaluation, principal) + db.commit() + + try: + _enqueue_eval_rows_with_optional_transcribe(db, evaluation, targets) + except Exception as exc: # noqa: BLE001 + logger.exception( + "Failed to re-enqueue retry for evaluation row {}", eval_row_id + ) + if is_sharding_enabled(): + with evaluation_row_session(eval_row_id) as ( + row_db, + _catalog_db, + eval_row, + _source_row, + _shard_id, + ): + eval_row.status = "failed" + eval_row.error_message = f"Failed to re-enqueue retry: {exc}" + row_db.commit() + else: + eval_row.status = "failed" + eval_row.error_message = f"Failed to re-enqueue retry: {exc}" + _rollup_evaluation_status(evaluation, db) + db.commit() + raise HTTPException( + status_code=500, + detail=f"Failed to re-enqueue retry: {exc}", + ) + + if is_sharding_enabled(): + with evaluation_row_session(eval_row_id) as ( + _row_db, + _catalog_db, + eval_row, + source_row, + _shard_id, + ): + return _to_evaluation_row_response(eval_row, source_row, evaluation) + + db.refresh(eval_row) + source_row = targets[0][1] + return _to_evaluation_row_response(eval_row, source_row, evaluation) + + +from app.core.auth.capabilities import EVALS_RUN, EVALS_VIEW, REPORTS_GENERATE +from app.core.auth.workspace_route_capabilities import apply_workspace_route_capabilities + +apply_workspace_route_capabilities( + router, + view_capability=EVALS_VIEW, + manage_capability=EVALS_RUN, + run_capability=EVALS_RUN, + report_capability=REPORTS_GENERATE, +) diff --git a/app/api/v1/routes/call_imports.py b/app/api/v1/routes/call_imports.py index e7461504..3b7bab55 100644 --- a/app/api/v1/routes/call_imports.py +++ b/app/api/v1/routes/call_imports.py @@ -1,3947 +1,4016 @@ -"""CSV-driven call import routes. - -Users upload a CSV plus a per-batch column mapping (CSV header -> system -field). The backend persists a CallImport batch + one CallImportRow per -line, then fans the rows out to the Celery ``imports`` queue where each -row is downloaded using the telephony credential pinned on the batch. -Exotel credentialed imports require a ``recording_url`` on every row; -direct-URL imports (no credential) also require a mapped recording URL. -""" - -from __future__ import annotations - -import csv -import io -import json -import re -from dataclasses import dataclass, field -from datetime import date, datetime, time, timedelta -from typing import Any, Dict, Iterable, List, Optional, Tuple -from uuid import UUID, uuid4 - -from fastapi import APIRouter, Body, BackgroundTasks, Depends, File, Form, HTTPException, Query, Response, UploadFile, status -from loguru import logger -from sqlalchemy import desc, func, or_ -from sqlalchemy.orm import Session - -from app.config import settings -from app.core.auth.rbac import require_admin -from app.database import get_db -from app.db_sharding.sessions import is_sharding_enabled -from app.dependencies import ( - get_api_key, - get_organization_id, - get_workspace_id, - require_enterprise_feature, -) -from app.services.billing.flexprice_service import record_call_import_batch_created -from app.services.call_imports.dispatch_diagnostics import ( - build_call_import_dispatch_diagnostics, -) -from app.models.database import ( - CallImport, - CallImportRow, - CallImportSchema, - CallImportSchemaParameter, - CallImportTag, - TelephonyIntegration, -) -from app.models.enums import ( - CallImportParameterType, - CallImportRowStatus, - CallImportStatus, -) -from app.models.schemas import ( - CallImportCancelDiarisationRequest, - CallImportCancelDiarisationResponse, - CallImportDetailResponse, - CallImportDeleteResponse, - CallImportDiarisationPromptDefaultResponse, - CallImportDispatchDiagnosticsResponse, - CallImportInsightsMetric, - CallImportInsightsResponse, - CallImportInsightsRunPoint, - CallImportListResponse, - CallImportMappingUpdate, - CallImportMetricAggregate, - CallImportPreviewResponse, - CallImportPreviewSheet, - CallImportRetryFailedRowsRequest, - CallImportRetryFailedRowsResponse, - CallImportResponse, - CallImportRowIdsResponse, - CallImportRowBulkDelete, - CallImportRowBulkDeleteResponse, - CallImportRowResponse, - CallImportStartRequest, - CallImportTranscribeRequest, - CallImportTranscribeResponse, - CallImportUpdate, - CallImportUploadResponse, -) - - -router = APIRouter( - prefix="/call-imports", - tags=["Call Imports"], - dependencies=[Depends(require_enterprise_feature("call_imports"))], -) - - -@dataclass(frozen=True) -class CallImportParseSkip: - """One source row excluded during CSV/Excel parse (identity / recording URL).""" - - source_row: int - reason: str - message: str - - -@dataclass -class CallImportParseResult: - rows: List[Dict[str, Any]] = field(default_factory=list) - skipped: List[CallImportParseSkip] = field(default_factory=list) - - -def parse_skips_to_json(skips: List[CallImportParseSkip]) -> List[Dict[str, Any]]: - """Persistable JSON shape for ``CallImport.source_row_skips``.""" - return [ - { - "source_row": item.source_row, - "reason": item.reason, - "message": item.message, - } - for item in skips - ] - - -def _normalize_dataset(raw: Optional[str]) -> Optional[str]: - """Trim and treat empty strings as 'no dataset' (NULL).""" - if raw is None: - return None - cleaned = raw.strip() - return cleaned or None - - -def _serialize_call_import(db: Session, call_import: CallImport) -> CallImportResponse: - """Catalog parent fields; counters come from SQL rollup (not Redis merge).""" - from app.services.call_imports.bulk_ops import rollup_call_import_batch_status - from app.services.call_imports.progress_counters import ( - clear_import_progress_redis, - read_import_progress, - ) - - redis_completed, redis_failed = read_import_progress(call_import.id) - if ( - redis_completed - or redis_failed - or int(call_import.completed_rows or 0) > int(call_import.total_rows or 0) - or int(call_import.failed_rows or 0) > int(call_import.total_rows or 0) - ): - rollup_call_import_batch_status(db, call_import) - db.flush() - - clear_import_progress_redis(call_import.id) - db.refresh(call_import) - total = int(call_import.total_rows or 0) - completed = min(int(call_import.completed_rows or 0), total) if total else int( - call_import.completed_rows or 0 - ) - failed = min(int(call_import.failed_rows or 0), total) if total else int( - call_import.failed_rows or 0 - ) - base = CallImportResponse.model_validate(call_import) - return base.model_copy(update={"completed_rows": completed, "failed_rows": failed}) - - -def _resolve_tags( - db: Session, organization_id: UUID, tag_ids: Optional[List[UUID]] -) -> List[CallImportTag]: - """Look up tag rows by id, scoped to the organization. - - Raises HTTPException(400) if any id is unknown for the org. - """ - if not tag_ids: - return [] - rows = ( - db.query(CallImportTag) - .filter( - CallImportTag.organization_id == organization_id, - CallImportTag.id.in_(tag_ids), - ) - .all() - ) - found_ids = {row.id for row in rows} - missing = [str(tag_id) for tag_id in tag_ids if tag_id not in found_ids] - if missing: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Unknown call_import_tag id(s): {missing}", - ) - return rows - - -MAX_UPLOAD_BYTES = 15 * 1024 * 1024 # 15 MB upload cap (CSV or Excel) - -# File extensions accepted by the upload + preview endpoints. Keep in -# lockstep with the frontend ``accept`` attribute on the file picker. -CSV_EXTENSIONS = (".csv",) -XLSX_EXTENSIONS = (".xlsx", ".xlsm") -ALLOWED_EXTENSIONS = CSV_EXTENSIONS + XLSX_EXTENSIONS - -AUDIO_CONTENT_TYPES = { - "wav": "audio/wav", - "mp3": "audio/mpeg", - "flac": "audio/flac", - "m4a": "audio/mp4", -} - - -def _file_format(filename: Optional[str]) -> Optional[str]: - """Classify ``filename`` as ``'csv'`` / ``'xlsx'`` or ``None`` if unsupported.""" - if not filename: - return None - name = filename.lower() - if name.endswith(CSV_EXTENSIONS): - return "csv" - if name.endswith(XLSX_EXTENSIONS): - return "xlsx" - return None - - -def _audio_extension(filename: Optional[str]) -> Optional[str]: - """Return the validated lower-case extension for a manual recording.""" - if not filename or "." not in filename: - return None - ext = filename.rsplit(".", 1)[-1].lower().strip() - allowed = {fmt.lower().lstrip(".") for fmt in settings.ALLOWED_AUDIO_FORMATS} - return ext if ext in allowed else None - - -def _audio_content_type(ext: str, upload_content_type: Optional[str]) -> str: - """Prefer the browser-supplied audio content type, with a safe fallback.""" - supplied = (upload_content_type or "").strip() - if supplied and supplied != "application/octet-stream": - return supplied - return AUDIO_CONTENT_TYPES.get(ext.lower(), "application/octet-stream") - - -def _audio_s3_key( - organization_id: UUID, call_import_id: UUID, row_id: UUID, ext: str -) -> str: - """Build the canonical S3 key for a manually uploaded recording.""" - from app.services.storage.s3_service import s3_service - - return ( - f"{s3_service.prefix}organizations/{organization_id}/call_imports/" - f"{call_import_id}/{row_id}.{ext}" - ) - - -def _filename_stem(filename: Optional[str]) -> str: - """Extract a cross-platform filename stem from an UploadFile name.""" - raw = (filename or "").strip() - basename = re.split(r"[\\/]", raw)[-1] if raw else "" - if "." in basename: - basename = basename.rsplit(".", 1)[0] - return basename.strip() - - -def _sanitize_conversation_id(raw: str) -> str: - """Turn a filename stem into a stable conversation_id.""" - cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", raw.strip()) - cleaned = re.sub(r"_+", "_", cleaned).strip("._-") - return (cleaned or "recording")[:255] - - -def _dedupe_conversation_id( - base: str, counts: Dict[str, int] -) -> str: - """Make conversation ids unique within one manual upload batch.""" - count = counts.get(base, 0) + 1 - counts[base] = count - if count == 1: - return base - suffix = f"-{count}" - return f"{base[: 255 - len(suffix)]}{suffix}" - - -def _normalize_header(name: str) -> str: - return (name or "").strip().lower() - - -def _header_lookup(fieldnames: List[str]) -> Dict[str, str]: - """Map normalized header -> original header for case-insensitive lookup.""" - return {_normalize_header(h): h for h in fieldnames or []} - - -def _resolve_mapped_header( - mapping_value: Optional[str], header_lookup: Dict[str, str] -) -> Optional[str]: - """Translate a user-supplied CSV header into the actual column key. - - The frontend sends headers exactly as they appear in the source file, - but we still normalize on the server so trailing whitespace / casing - doesn't break matching. Returns the canonical fieldname or ``None`` - if not present in the file. - """ - if not mapping_value: - return None - return header_lookup.get(_normalize_header(mapping_value)) - - -def _xlsx_cell_to_str(value: Any) -> str: - """Coerce an openpyxl cell value to the string the rest of the - pipeline expects. - - openpyxl returns native Python types (int, float, datetime, bool, - None). The CSV path always works with strings, so we mirror that: - integers stringify cleanly (no ``.0`` suffix on whole-number floats), - datetimes use ISO-8601, booleans use SQL-style ``TRUE`` / ``FALSE``. - """ - if value is None: - return "" - if isinstance(value, bool): - return "TRUE" if value else "FALSE" - if isinstance(value, int): - return str(value) - if isinstance(value, float): - if value.is_integer(): - return str(int(value)) - return str(value) - if isinstance(value, datetime): - return value.isoformat() - if isinstance(value, date): - return value.isoformat() - if isinstance(value, time): - return value.isoformat() - if isinstance(value, timedelta): - return str(value) - return str(value) - - -def _parse_recording_date_cell(cell: str) -> date: - """Parse day-first dates with one/two digit day-month parts.""" - match = re.fullmatch(r"\s*(\d{1,2})[/-](\d{1,2})[/-](\d{4})\s*", cell) - if match: - day, month, year = (int(part) for part in match.groups()) - return date(year, month, day) - - # Native Excel date cells arrive from ``_xlsx_cell_to_str`` as ISO - # datetimes (e.g. ``2026-01-04T00:00:00``). Accept that resolved date, - # while keeping plain ISO dates rejected for hand-entered text/CSV cells. - if "T" in cell: - return datetime.fromisoformat(cell.replace("Z", "+00:00")).date() - - raise ValueError("expected D/M/YYYY or D-M-YYYY") - - -def _coerce_parameter_value( - raw: str, - param_type: CallImportParameterType, - *, - row_idx: int, - param_name: str, -) -> Any: - """Validate + coerce a single CSV cell against its declared type. - - Returns the typed Python value to surface in ``raw_columns``. Empty - strings are returned as ``None`` regardless of the parameter type so - optional cells stay null end-to-end. Coercion failures raise a - 400 with a row-anchored message. - """ - cell = (raw or "").strip() - if not cell: - return None - - if param_type == CallImportParameterType.CONVERSATION_ID: - return cell - if param_type == CallImportParameterType.RECORDING_URL: - # Recording URLs are exercised by the worker (which downloads - # them); we only do a light "starts with http" check here so a - # paste-error surfaces immediately at upload time. - lower = cell.lower() - if not (lower.startswith("http://") or lower.startswith("https://")): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {row_idx + 1}: value for '{param_name}' is not a " - "valid recording URL (must start with http:// or https://)." - ), - ) - return cell - if param_type == CallImportParameterType.RECORDING_DATE: - try: - parsed_date = _parse_recording_date_cell(cell) - except ValueError: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {row_idx + 1}: value for '{param_name}' is not a " - f"valid recording date ({cell!r}); expected day-first " - "D/M/YYYY or D-M-YYYY." - ), - ) - return parsed_date.strftime("%d/%m/%Y") - if param_type == CallImportParameterType.TRANSCRIPT: - return cell - if param_type == CallImportParameterType.TEXT: - return cell - if param_type == CallImportParameterType.NUMBER: - try: - value = float(cell) - except ValueError: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {row_idx + 1}: value for '{param_name}' is not a " - f"valid number ({cell!r})." - ), - ) - if value.is_integer(): - return int(value) - return value - if param_type == CallImportParameterType.BOOLEAN: - truthy = {"true", "yes", "y", "1", "t"} - falsy = {"false", "no", "n", "0", "f"} - norm = cell.lower() - if norm in truthy: - return True - if norm in falsy: - return False - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {row_idx + 1}: value for '{param_name}' is not a " - f"valid boolean ({cell!r})." - ), - ) - if param_type == CallImportParameterType.DATETIME: - try: - parsed = datetime.fromisoformat(cell.replace("Z", "+00:00")) - except ValueError: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {row_idx + 1}: value for '{param_name}' is not a " - f"valid ISO-8601 date/time ({cell!r})." - ), - ) - return parsed.isoformat() - if param_type == CallImportParameterType.URL: - lower = cell.lower() - if not (lower.startswith("http://") or lower.startswith("https://")): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {row_idx + 1}: value for '{param_name}' is not a " - "valid URL (must start with http:// or https://)." - ), - ) - return cell - # Unknown types: store as text and let the next migration catch up. - return cell - - -def _recording_url_cell_is_valid_http(raw: str) -> bool: - cell = (raw or "").strip() - if not cell: - return False - lower = cell.lower() - return lower.startswith("http://") or lower.startswith("https://") - - -def _parameter_is_required(param: CallImportSchemaParameter) -> bool: - """Return whether a schema parameter must be mapped on every upload.""" - if param.is_required: - return True - try: - param_type = CallImportParameterType(param.type) - except ValueError: - return False - return param_type in ( - CallImportParameterType.CONVERSATION_ID, - CallImportParameterType.RECORDING_URL, - ) - - -def _apply_schema_mapping( - fieldnames: List[str], - rows_iter: Iterable[Dict[str, str]], - parameters: List[CallImportSchemaParameter], - parameter_mapping: Dict[str, str], - skipped_columns: List[str], - *, - source_label: str = "CSV", - validate_only: bool = False, -) -> CallImportParseResult: - """Schema-driven row projection: parameter -> CSV header -> typed value. - - Validates that every required schema parameter is mapped to a CSV - header that actually exists in the file, and that every CSV header - is either mapped to a parameter or explicitly listed in - ``skipped_columns``. Returns one dict per non-empty data row with: - - * ``conversation_id`` (str, mandatory) - * ``recording_date`` (Optional[str], DD/MM/YYYY date) - * ``recording_url`` (Optional[str]) - * ``transcript`` (Optional[str]) - * ``parameter_values`` (Dict[str, Any]) of typed values keyed by - parameter name (drives ``raw_columns`` so the export can - reproduce the source). - - ``validate_only=True`` runs the header / mapping / skipped-column - checks (every check that doesn't need to read row data) and then - returns an empty list — used by the MAP stage to validate a - mapping payload against the cached sheet snapshot without - re-fetching the source bytes from S3. - """ - header_lookup = _header_lookup(list(fieldnames)) - - # 1. Look up the conversation_id parameter so we can address it - # directly while building each row. - conv_param = next( - (p for p in parameters if p.type == CallImportParameterType.CONVERSATION_ID), - None, - ) - if conv_param is None: - # The schema invariant should have caught this on create/update, - # but a defensive 400 here keeps us safe against hand-rolled - # API callers that bypassed validation. - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Selected schema is missing the mandatory conversation_id parameter.", - ) - # 2. Resolve every mapped parameter to a canonical fieldname. - # Required parameters MUST resolve; optional ones may resolve to - # None if the user left them blank (no mapping). - canonical_by_param: Dict[str, Optional[str]] = {} - recording_date_param_name: Optional[str] = None - rec_url_param_name: Optional[str] = None - transcript_param_name: Optional[str] = None - for param in parameters: - mapped_header = parameter_mapping.get(param.name) - canonical = ( - _resolve_mapped_header(mapped_header, header_lookup) - if mapped_header - else None - ) - if _parameter_is_required(param) and canonical is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"{source_label} does not contain the column " - f"'{mapped_header or ''}' mapped to required parameter " - f"'{param.name}'." - ), - ) - canonical_by_param[param.name] = canonical - if param.type == CallImportParameterType.RECORDING_DATE: - recording_date_param_name = param.name - elif param.type == CallImportParameterType.RECORDING_URL: - rec_url_param_name = param.name - elif param.type == CallImportParameterType.TRANSCRIPT: - transcript_param_name = param.name - - # 3. Every CSV column must either be mapped to a parameter or - # explicitly skipped. Catches "I forgot to skip the email - # column" gracefully instead of dropping data silently. - mapped_canonicals = {c for c in canonical_by_param.values() if c} - skipped_canonicals = { - _resolve_mapped_header(h, header_lookup) - for h in skipped_columns - } - skipped_canonicals.discard(None) - unhandled = [ - h - for h in fieldnames - if h not in mapped_canonicals and h not in skipped_canonicals - ] - if unhandled: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"{source_label} columns must either be mapped to a schema " - f"parameter or explicitly skipped. Unhandled: {unhandled}." - ), - ) - - conv_canonical = canonical_by_param[conv_param.name] - rec_canonical = ( - canonical_by_param.get(rec_url_param_name) - if rec_url_param_name - else None - ) - recording_date_canonical = ( - canonical_by_param.get(recording_date_param_name) - if recording_date_param_name - else None - ) - transcript_canonical = ( - canonical_by_param.get(transcript_param_name) - if transcript_param_name - else None - ) - - if validate_only: - # MAP-stage validation: every header check above has already - # run; the row loop only matters at IMPORT time. Skip it (and - # the "no data rows" guard at the bottom of the function) so - # the caller gets a clean pass when the mapping is shaped right. - return CallImportParseResult() - - parsed: List[Dict[str, Any]] = [] - skipped: List[CallImportParseSkip] = [] - for idx, row in enumerate(rows_iter): - # Drop fully-blank lines - matches the legacy parser behavior so - # trailing-newline edge cases don't fail an otherwise-good upload. - non_blank = any( - (row.get(c) or "").strip() - for c in mapped_canonicals - if c - ) - if not non_blank: - continue - - source_row = idx + 1 - conv_value = (row.get(conv_canonical) or "").strip() if conv_canonical else "" - if not conv_value: - skipped.append( - CallImportParseSkip( - source_row=source_row, - reason="missing_conversation_id", - message=( - f"Row {source_row} is missing the '{conv_param.name}' " - "(conversation_id) value." - ), - ) - ) - continue - - if rec_canonical and rec_url_param_name: - rec_param = next( - (p for p in parameters if p.name == rec_url_param_name), - None, - ) - if rec_param is not None and _parameter_is_required(rec_param): - rec_raw = (row.get(rec_canonical) or "").strip() - if not rec_raw: - skipped.append( - CallImportParseSkip( - source_row=source_row, - reason="missing_recording_url", - message=( - f"Row {source_row} is missing the required " - f"'{rec_url_param_name}' value." - ), - ) - ) - continue - if not _recording_url_cell_is_valid_http(rec_raw): - skipped.append( - CallImportParseSkip( - source_row=source_row, - reason="invalid_recording_url", - message=( - f"Row {source_row}: value for " - f"'{rec_url_param_name}' is not a valid recording " - "URL (must start with http:// or https://)." - ), - ) - ) - continue - - # Materialize every mapped parameter into the per-row snapshot, - # running per-type coercion so a bad cell aborts the upload - # rather than silently storing garbage. - parameter_values: Dict[str, Any] = {} - row_skipped = False - for param in parameters: - canonical = canonical_by_param[param.name] - if canonical is None: - continue - try: - param_type = CallImportParameterType(param.type) - except ValueError: - param_type = CallImportParameterType.TEXT - coerced = _coerce_parameter_value( - row.get(canonical) or "", - param_type, - row_idx=idx, - param_name=param.name, - ) - if _parameter_is_required(param) and coerced is None: - if param_type == CallImportParameterType.RECORDING_URL: - skipped.append( - CallImportParseSkip( - source_row=source_row, - reason="missing_recording_url", - message=( - f"Row {source_row} is missing the required " - f"'{param.name}' value." - ), - ) - ) - row_skipped = True - break - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Row {source_row} is missing the required " - f"'{param.name}' value." - ), - ) - parameter_values[param.name] = coerced - if row_skipped: - continue - - rec_value = ( - (row.get(rec_canonical) or "").strip() if rec_canonical else "" - ) - transcript_value = ( - (row.get(transcript_canonical) or "").strip() - if transcript_canonical - else "" - ) - recording_date_value = ( - parameter_values.get(recording_date_param_name) - if recording_date_param_name - else None - ) - - parsed.append( - { - "conversation_id": conv_value, - "recording_date": recording_date_value, - "recording_url": rec_value or None, - "transcript": transcript_value or None, - "parameter_values": parameter_values, - } - ) - - if not parsed and not skipped: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"{source_label} did not contain any data rows.", - ) - - return CallImportParseResult(rows=parsed, skipped=skipped) - - -def _raise_if_no_importable_rows( - result: CallImportParseResult, *, source_label: str = "CSV" -) -> None: - """Sync upload / API callers fail fast when every data row was skipped.""" - if result.rows: - return - if result.skipped: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"No importable rows. {len(result.skipped)} row(s) skipped due " - "to missing or invalid conversation ID or recording URL." - ), - ) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"{source_label} did not contain any data rows.", - ) - - -def _parse_csv( - file_bytes: bytes, - parameters: List[CallImportSchemaParameter], - parameter_mapping: Dict[str, str], - skipped_columns: List[str], -) -> CallImportParseResult: - """Parse a CSV file using the resolved schema parameters.""" - if not file_bytes: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Uploaded CSV is empty.", - ) - - try: - text_stream = io.StringIO(file_bytes.decode("utf-8-sig")) - except UnicodeDecodeError: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="CSV must be UTF-8 encoded.", - ) - - reader = csv.DictReader(text_stream) - if not reader.fieldnames: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="CSV is missing a header row.", - ) - - return _apply_schema_mapping( - list(reader.fieldnames), - reader, - parameters, - parameter_mapping, - skipped_columns, - source_label="CSV", - ) - - -def _open_xlsx_workbook(file_bytes: bytes): - """Open an xlsx/xlsm workbook from in-memory bytes (read-only stream). - - Imports openpyxl lazily so the module loads even in environments that - haven't installed the optional dep yet (e.g. lightweight tooling - images). Surfaces a clean 400 if openpyxl is missing or the file is - not a valid Office Open XML workbook. - """ - if not file_bytes: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Uploaded Excel file is empty.", - ) - try: - from openpyxl import load_workbook # type: ignore - from openpyxl.utils.exceptions import InvalidFileException # type: ignore - except ImportError as exc: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=( - "Excel uploads require the 'openpyxl' package which is " - "not installed in this environment." - ), - ) from exc - - try: - return load_workbook( - io.BytesIO(file_bytes), - read_only=True, - data_only=True, - ) - except InvalidFileException as exc: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"File is not a valid .xlsx workbook: {exc}", - ) from exc - except Exception as exc: # zipfile.BadZipFile etc. - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Could not open Excel workbook: {exc}", - ) from exc - - -def _xlsx_sheet_headers_and_rows( - worksheet, -) -> Tuple[List[str], List[Dict[str, str]]]: - """Read row 1 as headers and the rest as dicts of stringified cells. - - Empty trailing header cells are dropped. Duplicate headers preserve - the first occurrence (matches ``csv.DictReader`` behavior, which - silently drops duplicates). - """ - iterator = worksheet.iter_rows(values_only=True) - try: - header_row = next(iterator) - except StopIteration: - return [], [] - - headers: List[str] = [] - seen: set[str] = set() - for cell in header_row: - name = _xlsx_cell_to_str(cell).strip() - if not name: - # Stop at the first blank header — treats trailing empty - # columns as not part of the table (matches typical Excel - # workbook conventions). - break - norm = name.lower() - if norm in seen: - continue - seen.add(norm) - headers.append(name) - - rows: List[Dict[str, str]] = [] - for row in iterator: - if row is None: - continue - # Pad / truncate to the header length so dict construction is - # stable even when a row has fewer / extra cells than the header. - cells = list(row[: len(headers)]) - if len(cells) < len(headers): - cells.extend([None] * (len(headers) - len(cells))) - if not any(_xlsx_cell_to_str(c).strip() for c in cells): - # Skip fully-blank rows (openpyxl read_only routinely yields - # trailing empties when the worksheet's used range exceeds - # the actual data). - continue - rows.append( - { - header: _xlsx_cell_to_str(value) - for header, value in zip(headers, cells) - } - ) - - return headers, rows - - -def _parse_xlsx( - file_bytes: bytes, - sheet_name: Optional[str], - parameters: List[CallImportSchemaParameter], - parameter_mapping: Dict[str, str], - skipped_columns: List[str], -) -> CallImportParseResult: - """Parse a single worksheet from an xlsx/xlsm workbook. - - ``sheet_name`` must match one of the workbook's sheets (case - insensitive whitespace-trimmed match). Returns the same shape as - :func:`_parse_csv` so the upload handler can persist either format - through the same code path. - """ - if not sheet_name or not sheet_name.strip(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="sheet_name is required when uploading an Excel workbook.", - ) - - workbook = _open_xlsx_workbook(file_bytes) - try: - sheet_names = list(workbook.sheetnames) - target_norm = sheet_name.strip().lower() - match = next( - (s for s in sheet_names if s.strip().lower() == target_norm), - None, - ) - if match is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Sheet '{sheet_name}' not found in workbook. " - f"Available sheets: {sheet_names}" - ), - ) - worksheet = workbook[match] - headers, rows = _xlsx_sheet_headers_and_rows(worksheet) - finally: - workbook.close() - - if not headers: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Sheet '{sheet_name}' is missing a header row.", - ) - - return _apply_schema_mapping( - headers, - rows, - parameters, - parameter_mapping, - skipped_columns, - source_label=f"Sheet '{sheet_name}'", - ) - - -def _csv_preview_sheets( - file_bytes: bytes, filename: Optional[str] -) -> List[CallImportPreviewSheet]: - """Build the synthetic single-sheet preview entry for a CSV upload.""" - if not file_bytes: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Uploaded CSV is empty.", - ) - try: - text_stream = io.StringIO(file_bytes.decode("utf-8-sig")) - except UnicodeDecodeError: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="CSV must be UTF-8 encoded.", - ) - reader = csv.DictReader(text_stream) - if not reader.fieldnames: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="CSV is missing a header row.", - ) - headers = list(reader.fieldnames) - row_count = 0 - for row in reader: - # Match the parse-time skip: ignore fully blank rows so the - # count the user sees lines up with what /upload will ingest. - if any((v or "").strip() for v in row.values()): - row_count += 1 - - sheet_label = (filename or "sheet1").rsplit("/", 1)[-1] or "sheet1" - return [ - CallImportPreviewSheet( - name=sheet_label, - headers=headers, - row_count=row_count, - ) - ] - - -def _xlsx_preview_sheets(file_bytes: bytes) -> List[CallImportPreviewSheet]: - """List every worksheet in the workbook with its headers and row count.""" - workbook = _open_xlsx_workbook(file_bytes) - sheets: List[CallImportPreviewSheet] = [] - try: - for name in workbook.sheetnames: - worksheet = workbook[name] - headers, rows = _xlsx_sheet_headers_and_rows(worksheet) - sheets.append( - CallImportPreviewSheet( - name=name, - headers=headers, - row_count=len(rows), - ) - ) - finally: - workbook.close() - return sheets - - -def _parse_json_form_field(name: str, raw: Optional[str], default): - """Decode a JSON-encoded form field with a friendly 400 on bad JSON.""" - if raw is None or raw == "": - return default - try: - return json.loads(raw) - except json.JSONDecodeError as exc: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"{name} must be valid JSON: {exc}", - ) - - -# --------------------------------------------------------------------------- -# Shared helpers used by the staged endpoints (UPLOAD / MAP / IMPORT) and the -# legacy one-shot ``POST /upload`` shim. Extracted here so each stage and the -# back-compat path operate on the exact same validation + persistence code. -# --------------------------------------------------------------------------- - - -def _source_content_type(fmt: str) -> str: - """Return the canonical ``Content-Type`` for a parsed file format.""" - if fmt == "csv": - return "text/csv" - if fmt == "xlsx": - return ( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ) - return "application/octet-stream" - - -def _source_s3_key( - organization_id: UUID, call_import_id: UUID, fmt: str -) -> str: - """Build the canonical S3 key for an upload's source file. - - Mirrors the per-row recording key convention used by - ``process_call_import_row`` so a single prefix sweep on delete still - cleans up both the source artefact and every fetched recording. - """ - from app.services.storage.s3_service import s3_service - - ext = "xlsx" if fmt == "xlsx" else "csv" - return ( - f"{s3_service.prefix}organizations/{organization_id}/call_imports/" - f"{call_import_id}/source.{ext}" - ) - - -def _build_available_sheets( - file_bytes: bytes, fmt: str, filename: Optional[str] -) -> List[CallImportPreviewSheet]: - """Snapshot of sheets + headers cached on the batch at UPLOAD time.""" - if fmt == "csv": - return _csv_preview_sheets(file_bytes, filename) - return _xlsx_preview_sheets(file_bytes) - - -def _resolve_schema( - db: Session, - organization_id: UUID, - workspace_id: UUID, - schema_id: UUID, -) -> CallImportSchema: - """Fetch + validate a schema row in the active workspace. - - Eager-loads ``parameters`` so callers can iterate without re-querying. - """ - from sqlalchemy.orm import selectinload as _selectinload - - schema = ( - db.query(CallImportSchema) - .options(_selectinload(CallImportSchema.parameters)) - .filter( - CallImportSchema.id == schema_id, - CallImportSchema.organization_id == organization_id, - CallImportSchema.workspace_id == workspace_id, - ) - .first() - ) - if not schema: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Call import schema not found in the active workspace.", - ) - if not list(schema.parameters): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Selected schema has no parameters defined.", - ) - return schema - - -def _validate_direct_url_import_ready( - parameters: List[CallImportSchemaParameter], - parameter_mapping: Dict[str, Any], -) -> None: - """Ensure direct-URL import has a mapped recording_url column.""" - rec_url_param = next( - ( - p - for p in parameters - if p.type == CallImportParameterType.RECORDING_URL.value - ), - None, - ) - if rec_url_param is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Direct URL import requires a schema parameter of type " - "'recording_url'." - ), - ) - mapped_header = (parameter_mapping or {}).get(rec_url_param.name) - if not (mapped_header or "").strip(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Direct URL import requires the 'recording_url' parameter to " - "be mapped to a source column." - ), - ) - - -def _validate_exotel_import_ready( - parameters: List[CallImportSchemaParameter], - parameter_mapping: Dict[str, Any], -) -> None: - """Ensure Exotel credentialed import has a mapped recording_url column.""" - rec_url_param = next( - ( - p - for p in parameters - if p.type == CallImportParameterType.RECORDING_URL.value - ), - None, - ) - if rec_url_param is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Exotel import requires a schema parameter of type " - "'recording_url'." - ), - ) - mapped_header = (parameter_mapping or {}).get(rec_url_param.name) - if not (mapped_header or "").strip(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Exotel import requires the 'recording_url' parameter to " - "be mapped to a source column." - ), - ) - - -def _resolve_telephony_integration( - db: Session, - organization_id: UUID, - telephony_integration_id: UUID, - provider: str, -) -> TelephonyIntegration: - """Fetch + validate a telephony credential against the requested provider.""" - integration = ( - db.query(TelephonyIntegration) - .filter( - TelephonyIntegration.id == telephony_integration_id, - TelephonyIntegration.organization_id == organization_id, - ) - .first() - ) - if not integration: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Telephony credential not found for this organization.", - ) - if (integration.provider or "").lower() != provider.lower(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Selected credential is for provider '{integration.provider}', " - f"but request specified '{provider}'." - ), - ) - if not integration.is_active: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Selected telephony credential is inactive.", - ) - return integration - - -def _clean_parameter_mapping( - mapping_payload: Any, - parameters: List[CallImportSchemaParameter], - schema_name: str, -) -> Dict[str, str]: - """Trim values and drop empties; reject unknown parameter names. - - Accepts an already-decoded value (dict-shaped) so the same helper - works for the JSON-form upload path and the JSON-body PATCH path. - """ - if not isinstance(mapping_payload, dict) or not all( - isinstance(k, str) and (v is None or isinstance(v, str)) - for k, v in mapping_payload.items() - ): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "parameter_mapping must be an object of " - "{parameter_name: csv_header}." - ), - ) - - valid_param_names = {p.name for p in parameters} - cleaned: Dict[str, str] = {} - for raw_name, raw_header in mapping_payload.items(): - if raw_name not in valid_param_names: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"parameter_mapping references unknown parameter " - f"'{raw_name}' on schema '{schema_name}'." - ), - ) - header = (raw_header or "").strip() - if header: - cleaned[raw_name] = header - return cleaned - - -def _clean_skipped_columns(skipped_payload: Any) -> List[str]: - """Dedupe (case-insensitively) and drop blanks; preserve original casing.""" - if not isinstance(skipped_payload, list) or not all( - isinstance(item, str) for item in skipped_payload - ): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="skipped_columns must be a list of header strings.", - ) - cleaned: List[str] = [] - seen: set[str] = set() - for item in skipped_payload: - norm = _normalize_header(item) - if not norm or norm in seen: - continue - seen.add(norm) - cleaned.append(item) - return cleaned - - -def _parse_source_file( - file_bytes: bytes, - fmt: str, - sheet_name: Optional[str], - parameters: List[CallImportSchemaParameter], - cleaned_mapping: Dict[str, str], - cleaned_skipped: List[str], -) -> CallImportParseResult: - """Run the format-appropriate parser against a buffer of file bytes.""" - if fmt == "csv": - return _parse_csv(file_bytes, parameters, cleaned_mapping, cleaned_skipped) - return _parse_xlsx( - file_bytes, sheet_name, parameters, cleaned_mapping, cleaned_skipped - ) - - -def _materialize_rows( - db: Session, - call_import: CallImport, - parsed_rows: List[Dict[str, Any]], - organization_id: UUID, -) -> List[CallImportRow]: - """Insert one ``CallImportRow`` per parsed row, returning the new models.""" - row_models: List[CallImportRow] = [] - for idx, row in enumerate(parsed_rows): - # Stamp ``transcript_source='csv'`` when the upload actually - # provided a transcript so the UI badge ("From CSV") works from - # day one. Blank cells stay NULL so the row reads as "no - # production transcript yet". - csv_transcript = row["transcript"] - row_model = CallImportRow( - call_import_id=call_import.id, - organization_id=organization_id, - workspace_id=call_import.workspace_id, - row_index=idx, - conversation_id=row["conversation_id"], - recording_date=( - _parse_recording_date_cell(row["recording_date"]) - if row.get("recording_date") - else None - ), - recording_url=row["recording_url"], - transcript=csv_transcript, - transcript_source=( - "csv" if csv_transcript and csv_transcript.strip() else None - ), - raw_columns=row["parameter_values"] or None, - status=CallImportRowStatus.PENDING, - ) - db.add(row_model) - row_models.append(row_model) - return row_models - - -def _enqueue_row_tasks( - db: Session, - call_import: CallImport, - row_models: List[CallImportRow], -) -> None: - """Schedule fair round-robin dispatch for pending import rows.""" - del db, call_import, row_models - from app.workers.concurrency.fair_import_dispatch import ( - schedule_fair_import_dispatch, - ) - - schedule_fair_import_dispatch(max_workspace_turns=999) - - -def _ensure_blob_storage_enabled() -> None: - """Hard-fail UPLOAD if cloud blob storage isn't configured (no local fallback).""" - from app.services.storage.s3_service import s3_service - - if not s3_service.is_enabled(): - err = ( - s3_service.get_status_message() - or "Cloud blob storage is not enabled or not configured" - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail=( - "Call uploads require cloud blob storage so the file can be " - f"persisted between stages: {err}" - ), - ) - - -def _validate_sheet_choice( - fmt: str, - sheet_name: Optional[str], - available_sheets: Optional[List[Dict[str, Any]]], -) -> Optional[str]: - """Normalize / validate ``sheet_name`` against the persisted snapshot. - - Returns the canonical sheet name (matching the workbook's casing) - so downstream parsing addresses the right worksheet. - """ - if fmt == "csv": - if sheet_name and sheet_name.strip(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="sheet_name is not applicable to CSV uploads.", - ) - return None - - cleaned = (sheet_name or "").strip() or None - if cleaned is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="sheet_name is required when the source is an Excel workbook.", - ) - - if not available_sheets: - # Nothing to validate against (e.g. legacy batch without snapshot); - # let downstream parsing error out instead of silently importing. - return cleaned - - target = cleaned.strip().lower() - for entry in available_sheets: - name = entry.get("name") if isinstance(entry, dict) else None - if isinstance(name, str) and name.strip().lower() == target: - return name - sheet_names = [ - entry.get("name") for entry in available_sheets if isinstance(entry, dict) - ] - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - f"Sheet '{cleaned}' not found in the staged file. " - f"Available sheets: {sheet_names}" - ), - ) - - -def _tag_response_payload(tags: Optional[List[CallImportTag]]) -> List[Dict[str, Any]]: - """Shape a CallImport's tag relationship for the upload response.""" - return [ - { - "id": tag.id, - "name": tag.name, - "color": tag.color, - "created_at": tag.created_at, - "updated_at": tag.updated_at, - } - for tag in (tags or []) - ] - - -@router.post( - "/preview", - response_model=CallImportPreviewResponse, - operation_id="previewCallImportFile", -) -async def preview_call_import_file( - file: UploadFile = File(...), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportPreviewResponse: - """Inspect an uploaded CSV / Excel file and return its sheets + headers. - - Drives the column-mapping UI without forcing the frontend to parse - CSV / xlsx itself — keeps client and server in lockstep on quoted - fields, encodings, and Excel cell coercion. CSVs return a single - synthetic sheet named after the filename; Excel workbooks return one - entry per worksheet (in workbook order). - """ - del api_key, organization_id, workspace_id, db # auth only - - fmt = _file_format(file.filename) - if fmt is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Unsupported file format. Allowed extensions: " - f"{', '.join(ALLOWED_EXTENSIONS)}." - ), - ) - - file_bytes = await file.read() - if len(file_bytes) > MAX_UPLOAD_BYTES: - raise HTTPException( - status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, - detail=f"File exceeds {MAX_UPLOAD_BYTES} bytes", - ) - - if fmt == "csv": - sheets = _csv_preview_sheets(file_bytes, file.filename) - else: - sheets = _xlsx_preview_sheets(file_bytes) - - return CallImportPreviewResponse(format=fmt, sheets=sheets) - - -@router.post( - "", - response_model=CallImportResponse, - status_code=status.HTTP_201_CREATED, - operation_id="createCallImport", -) -async def create_call_import( - file: UploadFile = File( - ..., - description="CSV / Excel file to stage. Persisted to S3 between stages.", - ), - dataset: str = Form( - ..., - description=( - "Required free-text dataset label. Collected up-front so the " - "batch is filterable from the moment it lands." - ), - ), - tag_ids: Optional[List[UUID]] = Form( - None, - description="Optional list of CallImportTag ids to attach to the new batch.", - ), - schema_id: Optional[UUID] = Form( - None, - description=( - "Optional schema pre-pick. The user can still change it during " - "the MAP stage; provided here only so the detail page can pre-" - "select the schema dropdown." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportResponse: - """UPLOAD stage of the staged call-import flow. - - Persists the source file to S3 and creates a ``CallImport`` row with - ``status='uploaded'``. No mapping, no provider, no rows yet — the - user moves through MAP and IMPORT as separate idempotent steps. - - Dataset is collected here (rather than at IMPORT) so the batch is - filterable from the moment it appears in the list view. - """ - del api_key - - fmt = _file_format(file.filename) - if fmt is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Unsupported file format. Allowed extensions: " - f"{', '.join(ALLOWED_EXTENSIONS)}." - ), - ) - - normalized_dataset = _normalize_dataset(dataset) - if not normalized_dataset: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="dataset is required and must be a non-empty string.", - ) - - file_bytes = await file.read() - if len(file_bytes) > MAX_UPLOAD_BYTES: - raise HTTPException( - status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, - detail=f"File exceeds {MAX_UPLOAD_BYTES} bytes", - ) - - # Parse-now so we (a) reject garbage uploads up-front instead of - # later in the MAP step, and (b) capture the sheets snapshot the - # MAP UI needs without having to re-fetch the file from S3. - sheets = _build_available_sheets(file_bytes, fmt, file.filename) - - # Optional schema pre-pick: validated only if supplied (the user is - # allowed to set it for the first time during MAP). - if schema_id is not None: - _resolve_schema(db, organization_id, workspace_id, schema_id) - - tag_rows = _resolve_tags(db, organization_id, tag_ids) - - _ensure_blob_storage_enabled() - - # Pre-generate the id so we can compute a deterministic S3 key - # before the row is persisted, keeping ``source_s3_key`` consistent - # with the prefix sweep used at delete-time. - import uuid as _uuid - - call_import_id = _uuid.uuid4() - s3_key = _source_s3_key(organization_id, call_import_id, fmt) - content_type = _source_content_type(fmt) - - from app.services.storage.s3_service import s3_service, StorageError - - try: - s3_service.upload_file_by_key(file_bytes, s3_key, content_type=content_type) - except StorageError as exc: - logger.exception( - "Failed to upload source file to S3 for new call import {}", - call_import_id, - ) - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail=f"Failed to persist upload to S3: {exc}", - ) - - call_import = CallImport( - id=call_import_id, - organization_id=organization_id, - workspace_id=workspace_id, - # Provider + credential aren't known until the IMPORT stage; leave - # them NULL so the staged-vs-legacy distinction is visible at a - # glance from the DB. - provider=None, - telephony_integration_id=None, - original_filename=file.filename, - sheet_name=None, - dataset=normalized_dataset, - schema_id=schema_id, - parameter_mapping={}, - skipped_columns=[], - column_mapping={}, - extra_columns=[], - custom_column_mapping={}, - source_s3_key=s3_key, - source_format=fmt, - source_size_bytes=len(file_bytes), - source_content_type=content_type, - available_sheets=[sheet.model_dump() for sheet in sheets], - total_rows=0, - completed_rows=0, - failed_rows=0, - status=CallImportStatus.UPLOADED, - ) - if tag_rows: - call_import.tags = tag_rows - - db.add(call_import) - try: - db.commit() - except Exception: - db.rollback() - # Best-effort cleanup of the uploaded S3 object so a failed - # commit doesn't leak storage. - try: - s3_service.delete_file_by_key(s3_key) - except Exception as cleanup_exc: # noqa: BLE001 - logger.warning( - "Failed to clean up orphaned S3 object {} after DB rollback: {}", - s3_key, - cleanup_exc, - ) - raise - - db.refresh(call_import) - return _serialize_call_import(db, call_import) - - -@router.patch( - "/{call_import_id}/mapping", - response_model=CallImportResponse, - operation_id="updateCallImportMapping", -) -async def update_call_import_mapping( - call_import_id: UUID, - payload: CallImportMappingUpdate, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportResponse: - """MAP stage of the staged call-import flow. - - Validates ``parameter_mapping`` + ``skipped_columns`` against the - sheet headers captured at UPLOAD time and persists them on the - batch. Idempotent: callers may submit this multiple times while - the batch is in ``uploaded`` or ``mapped`` state. - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - CallImport.workspace_id == workspace_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - if call_import.status not in ( - CallImportStatus.UPLOADED, - CallImportStatus.MAPPED, - ): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - f"Cannot edit mapping on a batch in status " - f"'{call_import.status.value}'. Mapping can only be edited " - "before the IMPORT stage." - ), - ) - - if not call_import.source_s3_key or not call_import.source_format: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "This batch was not uploaded through the staged flow and " - "cannot have its mapping edited." - ), - ) - - schema = _resolve_schema( - db, organization_id, workspace_id, payload.schema_id - ) - parameters = list(schema.parameters) - - canonical_sheet = _validate_sheet_choice( - call_import.source_format, - payload.sheet_name, - call_import.available_sheets, - ) - - # Pull the headers for the selected sheet straight out of the - # snapshot so we don't have to re-download the file from S3 just to - # validate the mapping. - headers: List[str] = [] - if call_import.available_sheets: - if canonical_sheet is None: - # CSV: single synthetic sheet. - entry = call_import.available_sheets[0] - headers = list(entry.get("headers") or []) - else: - for entry in call_import.available_sheets: - if not isinstance(entry, dict): - continue - name = entry.get("name") - if isinstance(name, str) and name == canonical_sheet: - headers = list(entry.get("headers") or []) - break - - cleaned_mapping = _clean_parameter_mapping( - payload.parameter_mapping, parameters, schema.name - ) - cleaned_skipped = _clean_skipped_columns(payload.skipped_columns) - - # Run the same per-column validation as the parse path so the user - # gets an immediate 400 if a required parameter is left unmapped or - # a header is neither mapped nor skipped — without needing to read - # the file. ``validate_only`` skips the row loop (and the empty-rows - # guard) since the row data lives in S3, not in this request. - if headers: - _apply_schema_mapping( - headers, - iter(()), - parameters, - cleaned_mapping, - cleaned_skipped, - source_label=( - f"Sheet '{canonical_sheet}'" - if canonical_sheet is not None - else "CSV" - ), - validate_only=True, - ) - - call_import.schema_id = schema.id - call_import.parameter_mapping = dict(cleaned_mapping) - call_import.skipped_columns = list(cleaned_skipped) - call_import.sheet_name = canonical_sheet - call_import.status = CallImportStatus.MAPPED - db.commit() - db.refresh(call_import) - return _serialize_call_import(db, call_import) - - -@router.post( - "/{call_import_id}/import", - response_model=CallImportUploadResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="startCallImport", -) -async def start_call_import( - call_import_id: UUID, - payload: CallImportStartRequest, - background_tasks: BackgroundTasks, - legacy: bool = Query( - False, - description=( - "Deprecated escape hatch for import-only processing. " - "New batches should use Run Evaluation instead." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportUploadResponse: - """Deprecated IMPORT stage — use Run Evaluation for new batches. - - Recording fetch is part of the unified evaluation pipeline. This - endpoint remains available only with ``?legacy=true`` for backward - compatibility. - """ - del api_key, background_tasks - - if not legacy: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "Standalone import is deprecated. Use Run Evaluation — " - "recording fetch is part of the evaluation pipeline. " - "Append ?legacy=true to use the import-only path." - ), - ) - - from sqlalchemy.orm import selectinload as _selectinload - - call_import = ( - db.query(CallImport) - .options(_selectinload(CallImport.tags)) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - CallImport.workspace_id == workspace_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - if call_import.status != CallImportStatus.MAPPED: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - f"Cannot start import for a batch in status " - f"'{call_import.status.value}'. Map the columns first." - ), - ) - - if not call_import.source_s3_key or not call_import.source_format: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "This batch has no staged source file and cannot be imported " - "through the staged flow." - ), - ) - - if not call_import.schema_id: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Cannot start import without a mapped schema.", - ) - - schema = _resolve_schema( - db, organization_id, workspace_id, call_import.schema_id - ) - parameters = list(schema.parameters) - - if payload.telephony_integration_id is not None: - integration = _resolve_telephony_integration( - db, - organization_id, - payload.telephony_integration_id, - payload.provider or "", - ) - if (integration.provider or "").lower() == "exotel": - _validate_exotel_import_ready( - parameters, dict(call_import.parameter_mapping or {}) - ) - else: - _validate_direct_url_import_ready( - parameters, dict(call_import.parameter_mapping or {}) - ) - integration = None - - _ensure_blob_storage_enabled() - - if integration is not None: - call_import.provider = integration.provider - call_import.telephony_integration_id = integration.id - else: - call_import.provider = None - call_import.telephony_integration_id = None - - call_import.total_rows = 0 - call_import.completed_rows = 0 - call_import.failed_rows = 0 - call_import.error_message = None - call_import.status = CallImportStatus.PROCESSING - db.commit() - db.refresh(call_import) - - from app.workers.tasks.call_import_bulk_ops import ( - materialize_call_import_rows_task, - ) - - materialize_call_import_rows_task.delay( - str(call_import_id), - str(organization_id), - str(workspace_id), - schedule_import_dispatch=True, - ) - - return CallImportUploadResponse( - id=call_import.id, - total_rows=0, - status=call_import.status, - dataset=call_import.dataset, - tags=_tag_response_payload(call_import.tags), - message=( - "Import accepted. Rows are being materialized in the background; " - "recordings will be fetched asynchronously." - ), - ) - - -@router.post( - "/upload", - response_model=CallImportUploadResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="uploadCallImportCsv", - deprecated=True, -) -async def upload_call_import_csv( - background_tasks: BackgroundTasks, - file: UploadFile = File(...), - provider: Optional[str] = Form( - None, - description=( - "Telephony provider key (e.g. 'exotel', 'plivo'). Must match the " - "selected telephony_integration_id's provider. Omit together " - "with telephony_integration_id for direct-URL import." - ), - ), - telephony_integration_id: Optional[UUID] = Form( - None, - description=( - "Specific TelephonyIntegration credential row to use when " - "downloading recordings for this batch. Omit together with " - "provider for direct-URL import." - ), - ), - schema_id: UUID = Form( - ..., - description=( - "Reusable Input Parameter schema this upload is mapped against. " - "Must belong to the active workspace." - ), - ), - parameter_mapping: str = Form( - ..., - description=( - "JSON-encoded ``{schema_parameter_name: source_header}`` map " - "covering every required schema parameter. Optional parameters " - "may be omitted or set to an empty string." - ), - ), - skipped_columns: Optional[str] = Form( - None, - description=( - "JSON-encoded list of source header strings the uploader has " - "explicitly skipped. Every header in the file must either be " - "mapped or appear here; otherwise the upload is rejected so a " - "forgotten column never silently drops." - ), - ), - dataset: Optional[str] = Form( - None, - description=( - "Optional free-text dataset label for high-level segregation. " - "Empty strings are stored as NULL." - ), - ), - tag_ids: Optional[List[UUID]] = Form( - None, - description="Optional list of CallImportTag ids to attach to the new batch.", - ), - sheet_name: Optional[str] = Form( - None, - description=( - "Worksheet to import when the file is an Excel workbook " - "(.xlsx / .xlsm). REQUIRED for Excel uploads. Ignored for CSV " - "uploads (rejected with 400 if non-empty so typos surface " - "instead of silently importing the wrong source)." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportUploadResponse: - """Legacy one-shot upload kept for backward compatibility. - - DEPRECATED: prefer the staged flow - (``POST /`` → ``PATCH /{id}/mapping`` → ``POST /{id}/import``) so - each step is idempotent and resumable. This endpoint runs all three - stages inline in a single transaction so existing scripts / - integrations keep working unchanged. - """ - del api_key - - fmt = _file_format(file.filename) - if fmt is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "Unsupported file format. Allowed extensions: " - f"{', '.join(ALLOWED_EXTENSIONS)}." - ), - ) - - sheet_name_clean = (sheet_name or "").strip() or None - if fmt == "csv" and sheet_name_clean is not None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="sheet_name is not applicable to CSV uploads.", - ) - if fmt == "xlsx" and sheet_name_clean is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="sheet_name is required when uploading an Excel workbook.", - ) - - file_bytes = await file.read() - if len(file_bytes) > MAX_UPLOAD_BYTES: - raise HTTPException( - status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, - detail=f"File exceeds {MAX_UPLOAD_BYTES} bytes", - ) - - schema = _resolve_schema(db, organization_id, workspace_id, schema_id) - parameters = list(schema.parameters) - - mapping_payload = _parse_json_form_field( - "parameter_mapping", parameter_mapping, {} - ) - cleaned_mapping = _clean_parameter_mapping( - mapping_payload, parameters, schema.name - ) - - skipped_payload = _parse_json_form_field("skipped_columns", skipped_columns, []) - cleaned_skipped = _clean_skipped_columns(skipped_payload) - - has_provider = bool((provider or "").strip()) - has_integration = telephony_integration_id is not None - if has_provider != has_integration: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=( - "provider and telephony_integration_id must both be provided " - "or both omitted for direct-URL import." - ), - ) - - if telephony_integration_id is not None: - integration = _resolve_telephony_integration( - db, organization_id, telephony_integration_id, provider or "" - ) - if (integration.provider or "").lower() == "exotel": - _validate_exotel_import_ready(parameters, cleaned_mapping) - else: - _validate_direct_url_import_ready(parameters, cleaned_mapping) - integration = None - - parsed_rows = _parse_source_file( - file_bytes, fmt, sheet_name_clean, parameters, cleaned_mapping, cleaned_skipped - ) - _raise_if_no_importable_rows(parsed_rows, source_label=fmt) - - tag_rows = _resolve_tags(db, organization_id, tag_ids) - - call_import = CallImport( - organization_id=organization_id, - workspace_id=workspace_id, - provider=integration.provider if integration is not None else None, - telephony_integration_id=integration.id if integration is not None else None, - original_filename=file.filename, - sheet_name=sheet_name_clean, - dataset=_normalize_dataset(dataset), - schema_id=schema.id, - parameter_mapping=dict(cleaned_mapping), - skipped_columns=list(cleaned_skipped), - # Legacy columns are left empty on new uploads; the detail page - # falls back to ``parameter_mapping`` when ``schema_id`` is set. - column_mapping={}, - extra_columns=[], - custom_column_mapping={}, - total_rows=len(parsed_rows.rows), - completed_rows=0, - failed_rows=0, - status=CallImportStatus.PENDING, - source_row_skips=parse_skips_to_json(parsed_rows.skipped), - ) - if tag_rows: - call_import.tags = tag_rows - db.add(call_import) - db.flush() # populate call_import.id - if integration is None: - # The model's historical Python default is "exotel"; direct-URL - # imports intentionally have no telephony provider. - call_import.provider = None - - row_models = _materialize_rows( - db, call_import, parsed_rows.rows, organization_id - ) - - call_import.status = CallImportStatus.PROCESSING - db.commit() - db.refresh(call_import) - - background_tasks.add_task( - record_call_import_batch_created, - organization_id, - call_import.id, - workspace_id=workspace_id, - total_rows=call_import.total_rows, - source="csv", - provider=call_import.provider, - ) - - _enqueue_row_tasks(db, call_import, row_models) - - return CallImportUploadResponse( - id=call_import.id, - total_rows=call_import.total_rows, - status=call_import.status, - dataset=call_import.dataset, - tags=_tag_response_payload(call_import.tags), - message=( - f"Accepted {call_import.total_rows} rows for import. " - "Recordings will be fetched asynchronously." - ), - ) - - -@router.post( - "/audio-upload", - response_model=CallImportUploadResponse, - status_code=status.HTTP_201_CREATED, - operation_id="uploadCallImportAudio", -) -async def upload_call_import_audio( - background_tasks: BackgroundTasks, - files: List[UploadFile] = File( - ..., - description="One or more manual call recording audio files.", - ), - dataset: str = Form( - ..., - description="Required free-text dataset label for the manual upload batch.", - ), - tag_ids: Optional[List[UUID]] = Form( - None, - description="Optional list of CallImportTag ids to attach to the new batch.", - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportUploadResponse: - """Persist manually uploaded recordings as completed CallImport rows. - - The rows skip the provider-download worker entirely because the audio - bytes are already in hand. From this point onward they behave exactly - like completed CSV-import rows: playback reads ``recording_s3_key`` and - the existing diarisation/evaluation endpoints can operate on them. - """ - - normalized_dataset = _normalize_dataset(dataset) - if not normalized_dataset: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="dataset is required and must be a non-empty string.", - ) - if not files: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="At least one audio file is required.", - ) - - _ensure_blob_storage_enabled() - tag_rows = _resolve_tags(db, organization_id, tag_ids) - - max_bytes = int(settings.MAX_FILE_SIZE_MB) * 1024 * 1024 - prepared: List[Dict[str, Any]] = [] - conversation_counts: Dict[str, int] = {} - - for idx, upload in enumerate(files): - filename = upload.filename or f"recording-{idx + 1}" - ext = _audio_extension(filename) - if not ext: - allowed = ", ".join(settings.ALLOWED_AUDIO_FORMATS) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Unsupported audio file '{filename}'. Allowed formats: {allowed}.", - ) - - contents = await upload.read() - if not contents: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Audio file '{filename}' is empty.", - ) - if len(contents) > max_bytes: - raise HTTPException( - status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, - detail=( - f"Audio file '{filename}' exceeds " - f"{settings.MAX_FILE_SIZE_MB} MB." - ), - ) - - base_conversation_id = _sanitize_conversation_id(_filename_stem(filename)) - conversation_id = _dedupe_conversation_id( - base_conversation_id, - conversation_counts, - ) - prepared.append( - { - "filename": filename, - "extension": ext, - "content_type": _audio_content_type(ext, upload.content_type), - "contents": contents, - "conversation_id": conversation_id, - } - ) - - original_filename = ( - prepared[0]["filename"] - if len(prepared) == 1 - else f"{len(prepared)} manual recordings" - ) - total_size = sum(len(item["contents"]) for item in prepared) - uploaded_keys: List[str] = [] - - from app.services.storage.s3_service import s3_service - - call_import = CallImport( - organization_id=organization_id, - workspace_id=workspace_id, - provider=None, - telephony_integration_id=None, - original_filename=original_filename, - source_format="audio", - source_size_bytes=total_size, - source_content_type="audio/*", - dataset=normalized_dataset, - total_rows=len(prepared), - completed_rows=len(prepared), - failed_rows=0, - status=CallImportStatus.COMPLETED, - ) - if tag_rows: - call_import.tags = tag_rows - - try: - db.add(call_import) - db.flush() - # The model's historical Python default is "exotel"; manual uploads - # intentionally have no telephony provider. - call_import.provider = None - - row_mappings: List[Dict[str, Any]] = [] - for idx, item in enumerate(prepared): - row_id = uuid4() - key = _audio_s3_key( - organization_id, - call_import.id, - row_id, - item["extension"], - ) - s3_service.upload_file_by_key( - item["contents"], - key, - content_type=item["content_type"], - ) - uploaded_keys.append(key) - - row_mappings.append( - { - "id": row_id, - "call_import_id": call_import.id, - "organization_id": organization_id, - "workspace_id": workspace_id, - "row_index": idx, - "conversation_id": item["conversation_id"], - "recording_url": None, - "transcript": None, - "transcript_source": None, - "raw_columns": {"conversation_id": item["conversation_id"]}, - "status": CallImportRowStatus.COMPLETED, - "recording_s3_key": key, - "recording_content_type": item["content_type"], - "recording_size_bytes": len(item["contents"]), - } - ) - - if is_sharding_enabled(): - from app.db_sharding.row_ops import ( - bulk_insert_mappings_on_shards, - register_shard_slices, - ) - - bulk_insert_mappings_on_shards(db, call_import.id, row_mappings) - register_shard_slices(db, call_import.id, len(row_mappings)) - else: - for mapping in row_mappings: - db.add(CallImportRow(**mapping)) - - db.commit() - except Exception as exc: - db.rollback() - if uploaded_keys and s3_service.is_enabled(): - try: - s3_service.delete_keys(uploaded_keys) - except Exception: - logger.exception( - "Failed to clean up manual audio upload keys after error" - ) - logger.exception("Failed to persist manual call recording upload") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to upload manual recordings: {exc}", - ) from exc - - db.refresh(call_import) - background_tasks.add_task( - record_call_import_batch_created, - organization_id, - call_import.id, - workspace_id=workspace_id, - total_rows=call_import.total_rows, - source="audio", - provider=None, - ) - return CallImportUploadResponse( - id=call_import.id, - total_rows=call_import.total_rows, - status=call_import.status, - dataset=call_import.dataset, - tags=_tag_response_payload(call_import.tags), - message=( - f"Uploaded {call_import.total_rows} manual recording" - f"{'' if call_import.total_rows == 1 else 's'}." - ), - ) - - -@router.get( - "", - response_model=CallImportListResponse, - operation_id="listCallImports", -) -async def list_call_imports( - page: int = Query(1, ge=1), - page_size: int = Query(20, ge=1, le=100), - status_filter: Optional[CallImportStatus] = Query(None, alias="status"), - dataset: Optional[str] = Query( - None, - description=( - "Filter by exact dataset string (case-insensitive). Pass the " - "literal value '__none__' to filter to imports with no dataset." - ), - ), - tag_id: Optional[List[UUID]] = Query( - None, - description="Filter to imports tagged with ALL of the given tag ids.", - ), - source_format: Optional[str] = Query( - None, - description=( - "Filter by source format. Use 'audio' for manual recordings or " - "'__non_audio__' for CSV/Excel/legacy imports." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportListResponse: - """List call-import batches for the active workspace, newest first. - - Scoped to (organization_id, workspace_id) so users only see imports - for the workspace they're currently in. Supports a high-level - ``dataset`` filter (powers the segregation dropdown at the top of - the imports page) plus an AND-style multi-tag filter via repeated - ``tag_id`` parameters. - """ - - query = ( - db.query(CallImport) - .filter( - CallImport.organization_id == organization_id, - CallImport.workspace_id == workspace_id, - ) - ) - if status_filter is not None: - query = query.filter(CallImport.status == status_filter) - - source_filter = (source_format or "").strip().lower() - if source_filter == "__non_audio__": - query = query.filter( - or_(CallImport.source_format.is_(None), CallImport.source_format != "audio") - ) - elif source_filter: - query = query.filter(func.lower(CallImport.source_format) == source_filter) - - if dataset is not None: - if dataset == "__none__": - query = query.filter(CallImport.dataset.is_(None)) - elif dataset.strip(): - query = query.filter( - func.lower(CallImport.dataset) == dataset.strip().lower() - ) - - if tag_id: - from app.models.database import CallImportTagAssignment - - for single_tag_id in tag_id: - sub = ( - db.query(CallImportTagAssignment.call_import_id) - .filter(CallImportTagAssignment.tag_id == single_tag_id) - .subquery() - ) - query = query.filter(CallImport.id.in_(sub)) - - total = query.count() - items = ( - query.order_by(desc(CallImport.created_at)) - .offset((page - 1) * page_size) - .limit(page_size) - .all() - ) - - return CallImportListResponse( - items=[_serialize_call_import(db, item) for item in items], - total=total, - page=page, - page_size=page_size, - ) - - -@router.get( - "/dispatch-diagnostics", - response_model=CallImportDispatchDiagnosticsResponse, - operation_id="getCallImportDispatchDiagnostics", - dependencies=[Depends(require_admin)], -) -async def get_call_import_dispatch_diagnostics( - workspace_id: Optional[UUID] = Query( - None, - description=( - "Optional workspace filter. When omitted, returns every workspace " - "in the organization with active eval dispatch state." - ), - ), - include_idle_workspaces: bool = Query( - False, - description=( - "When true, include org workspaces with zero pending rows and " - "zero in-flight slots." - ), - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportDispatchDiagnosticsResponse: - """Live eval slot usage and fair-dispatch state for operators. - - Org admins use this to diagnose cross-workspace starvation (e.g. one - workspace's 10k run blocking another's pending eval rows) by inspecting - Redis in-flight counters, pending dispatch rows, and scheduler cursors. - """ - del api_key - payload = build_call_import_dispatch_diagnostics( - db, - organization_id, - workspace_id=workspace_id, - include_idle_workspaces=include_idle_workspaces, - ) - return CallImportDispatchDiagnosticsResponse.model_validate(payload) - - -@router.get( - "/datasets", - response_model=List[str], - operation_id="listCallImportDatasets", -) -async def list_call_import_datasets( - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> List[str]: - """Return the distinct, non-null dataset labels in use for the active - workspace. - - Scoped per-workspace so each workspace's Dataset dropdown only shows - its own segregation labels. - """ - rows = ( - db.query(CallImport.dataset) - .filter( - CallImport.organization_id == organization_id, - CallImport.workspace_id == workspace_id, - CallImport.dataset.isnot(None), - CallImport.dataset != "", - ) - .distinct() - .order_by(CallImport.dataset.asc()) - .all() - ) - return [row[0] for row in rows if row[0]] - - -@router.get( - "/diarisation-prompt-default", - response_model=CallImportDiarisationPromptDefaultResponse, - operation_id="getCallImportDiarisationPromptDefault", -) -async def get_call_import_diarisation_prompt_default( - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), -) -> CallImportDiarisationPromptDefaultResponse: - """Return the canonical LLM diariser prompt. - - The Transcribe / Run Evaluation modals call this on open so they - can pre-fill the prompt textarea. Returning the constant from the - backend (rather than hard-coding it in the frontend) keeps the - fallback used by the worker and the placeholder shown in the UI - in lock-step — operators always see the *actual* default they'd - get if they leave the field blank. - - Registered before ``GET /{call_import_id}`` so the static path is - not mistaken for a UUID import id (which would 422). - """ - del api_key, organization_id - from app.workers.tasks.helpers.llm_diarisation import ( - DEFAULT_DIARIZATION_PROMPT, - ) - - return CallImportDiarisationPromptDefaultResponse( - prompt=DEFAULT_DIARIZATION_PROMPT - ) - - -@router.patch( - "/{call_import_id}", - response_model=CallImportResponse, - operation_id="updateCallImport", -) -async def update_call_import( - call_import_id: UUID, - payload: CallImportUpdate, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - workspace_id: UUID = Depends(get_workspace_id), - db: Session = Depends(get_db), -) -> CallImportResponse: - """Edit dataset / tag assignments (and schema, pre-import) on a batch. - - ``dataset = ""`` clears the label; ``tag_ids = []`` removes all tag - assignments. Fields omitted from the body are left untouched. - - ``schema_id`` is only honoured while the batch is in - ``uploaded`` / ``mapped`` state — once rows have been materialised - the schema is locked. Changing the schema resets any persisted - mapping (the user must re-MAP) and rewinds status to ``uploaded``. - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - body = payload.model_dump(exclude_unset=True) - if "dataset" in body: - call_import.dataset = _normalize_dataset(body["dataset"]) - - if "tag_ids" in body: - tag_ids = body["tag_ids"] or [] - call_import.tags = _resolve_tags(db, organization_id, tag_ids) - - if "schema_id" in body and body["schema_id"] is not None: - if call_import.status not in ( - CallImportStatus.UPLOADED, - CallImportStatus.MAPPED, - ): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - f"Cannot reassign schema on a batch in status " - f"'{call_import.status.value}'." - ), - ) - new_schema = _resolve_schema( - db, organization_id, workspace_id, body["schema_id"] - ) - if call_import.schema_id != new_schema.id: - # Switching schemas invalidates the persisted mapping — - # parameter names won't line up with the new schema, so - # reset to UPLOADED and force a fresh MAP. - call_import.schema_id = new_schema.id - call_import.parameter_mapping = {} - call_import.skipped_columns = [] - call_import.sheet_name = None - call_import.status = CallImportStatus.UPLOADED - - db.commit() - db.refresh(call_import) - return _serialize_call_import(db, call_import) - - -@router.get( - "/{call_import_id}", - response_model=CallImportDetailResponse, - operation_id="getCallImportDetail", -) -async def get_call_import_detail( - call_import_id: UUID, - row_limit: int = Query(500, ge=0, le=5000), - row_offset: int = Query(0, ge=0), - q: Optional[str] = Query( - None, - description=( - "Optional case-insensitive substring filter on " - "``conversation_id``. When set, ``filtered_total_rows`` in " - "the response reflects the post-filter row count so the UI " - "can paginate against the filtered slice." - ), - ), - diarised_status: Optional[str] = Query( - None, - description=( - "Optional filter on ``CallImportRow.diarised_transcript_status``. " - "Accepts one of ``pending``, ``running``, ``completed``, " - "``failed``. When set, ``filtered_total_rows`` reflects the " - "post-filter row count (combined with the ``q`` filter when " - "both are supplied) so the UI can paginate against the same " - "slice it's displaying." - ), - pattern="^(pending|running|completed|failed)$", - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportDetailResponse: - """Fetch a single import batch with a slice of its rows. - - ``row_limit=0`` is intentionally allowed so callers that only need the - batch metadata (e.g. the evaluation-detail page rendering the parent's - column mapping) can skip the rows payload entirely. - """ - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - if call_import.status == CallImportStatus.PROCESSING: - from app.services.call_imports.bulk_ops import rollup_call_import_batch_status - - prior_status = call_import.status - rollup_call_import_batch_status(db, call_import) - if call_import.status != prior_status: - db.commit() - db.refresh(call_import) - - search_term = (q or "").strip() - diarised_status_filter = (diarised_status or "").strip() or None - filtered_total_rows: Optional[int] = None - has_row_filters = bool(search_term or diarised_status_filter) - - if has_row_filters: - if is_sharding_enabled(): - from app.db_sharding.scatter_gather import count_call_import_rows_filtered - - filtered_total_rows = count_call_import_rows_filtered( - db, - call_import.id, - search_term=search_term, - diarised_status_filter=diarised_status_filter, - ) - else: - rows_query = db.query(CallImportRow).filter( - CallImportRow.call_import_id == call_import.id - ) - if search_term: - rows_query = rows_query.filter( - CallImportRow.conversation_id.ilike(f"%{search_term}%") - ) - if diarised_status_filter: - rows_query = rows_query.filter( - CallImportRow.diarised_transcript_status == diarised_status_filter - ) - filtered_total_rows = rows_query.count() - - if row_limit == 0: - rows: List[CallImportRow] = [] - elif is_sharding_enabled(): - from app.db_sharding.scatter_gather import ( - fetch_call_import_rows_filtered_page, - fetch_call_import_rows_page, - ) - - if has_row_filters: - rows = fetch_call_import_rows_filtered_page( - db, - call_import.id, - search_term=search_term, - diarised_status_filter=diarised_status_filter, - offset=row_offset, - limit=row_limit, - ) - else: - rows = fetch_call_import_rows_page( - db, - call_import.id, - offset=row_offset, - limit=row_limit, - ) - else: - rows_query = db.query(CallImportRow).filter( - CallImportRow.call_import_id == call_import.id - ) - if search_term: - rows_query = rows_query.filter( - CallImportRow.conversation_id.ilike(f"%{search_term}%") - ) - if diarised_status_filter: - rows_query = rows_query.filter( - CallImportRow.diarised_transcript_status == diarised_status_filter - ) - rows = ( - rows_query.order_by(CallImportRow.row_index) - .offset(row_offset) - .limit(row_limit) - .all() - ) - - # Batch-wide diarisation status aggregate. One ``GROUP BY`` query - # across the whole batch — much cheaper than paging through every - # row to recount on the client and lets the UI render a - # transcribe/diarise progress bar without a separate roundtrip. - if is_sharding_enabled(): - from app.db_sharding.scatter_gather import aggregate_diarised_transcript_counts - - diarised_status_counts = aggregate_diarised_transcript_counts( - db, call_import.id - ) - else: - diarised_status_counts: Dict[str, int] = {} - for status_value, count in ( - db.query(CallImportRow.diarised_transcript_status, func.count()) - .filter(CallImportRow.call_import_id == call_import.id) - .group_by(CallImportRow.diarised_transcript_status) - .all() - ): - if isinstance(status_value, str): - diarised_status_counts[status_value] = int(count or 0) - - detail = CallImportDetailResponse.model_validate( - _serialize_call_import(db, call_import).model_dump() - ) - detail.rows = [CallImportRowResponse.model_validate(r) for r in rows] - detail.filtered_total_rows = filtered_total_rows - detail.diarised_pending_rows = diarised_status_counts.get("pending", 0) - detail.diarised_running_rows = diarised_status_counts.get("running", 0) - detail.diarised_completed_rows = diarised_status_counts.get("completed", 0) - detail.diarised_failed_rows = diarised_status_counts.get("failed", 0) - return detail - - -@router.get( - "/{call_import_id}/row-ids", - response_model=CallImportRowIdsResponse, - operation_id="listCallImportRowIds", -) -async def list_call_import_row_ids( - call_import_id: UUID, - q: Optional[str] = Query( - None, - description=( - "Optional case-insensitive substring filter on " - "``conversation_id``. Same semantics as the detail endpoint." - ), - ), - diarised_status: Optional[str] = Query( - None, - description=( - "Optional filter on ``CallImportRow.diarised_transcript_status``. " - "Accepts ``pending`` / ``running`` / ``completed`` / ``failed``." - ), - pattern="^(pending|running|completed|failed)$", - ), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportRowIdsResponse: - """Return every matching ``CallImportRow.id`` for cross-page bulk select. - - Lightweight companion to ``GET /{call_import_id}`` — the detail - endpoint caps ``row_limit`` at 5000 and ships the entire row body - on each page, so harvesting ids that way is wasteful when the - user just wants to bulk-delete or bulk-transcribe everything that - matches the current filters. This endpoint applies the same ``q`` - and ``diarised_status`` filters and returns only the ids. - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - search_term = (q or "").strip() - status_filter = (diarised_status or "").strip() or None - - if is_sharding_enabled(): - from app.db_sharding.scatter_gather import list_call_import_row_ids_filtered - - ids = list_call_import_row_ids_filtered( - db, - call_import.id, - search_term=search_term, - diarised_status_filter=status_filter, - ) - return CallImportRowIdsResponse(ids=ids, total=len(ids)) - - rows_query = db.query(CallImportRow.id).filter( - CallImportRow.call_import_id == call_import.id - ) - if search_term: - rows_query = rows_query.filter( - CallImportRow.conversation_id.ilike(f"%{search_term}%") - ) - if status_filter: - rows_query = rows_query.filter( - CallImportRow.diarised_transcript_status == status_filter - ) - - ids = [ - row_id - for (row_id,) in rows_query.order_by(CallImportRow.row_index).all() - ] - return CallImportRowIdsResponse(ids=ids, total=len(ids)) - - -def _revoke_pending_tasks(rows: List[CallImportRow]) -> None: - """Best-effort revoke of in-flight Celery tasks for the given rows. - - Failures are logged and swallowed — Celery's control plane is async and - best-effort by design, and we always do an idempotent S3 cleanup - afterwards so a missed revoke can't leak storage. - """ - task_ids = [ - r.celery_task_id - for r in rows - if r.celery_task_id - and r.status in (CallImportRowStatus.PENDING, CallImportRowStatus.PROCESSING) - ] - if not task_ids: - return - - try: - from app.workers.celery_app import celery_app - - celery_app.control.revoke(task_ids, terminate=False) - logger.info("Revoked {} pending call-import tasks", len(task_ids)) - except Exception as exc: # noqa: BLE001 - logger.warning("Failed to revoke pending call-import tasks: {}", exc) - - -def _delete_s3_objects( - organization_id: UUID, - call_import_id: UUID, - rows: List[CallImportRow], -) -> tuple[int, int]: - """Delete every recording associated with ``rows`` plus a prefix sweep. - - The prefix sweep also cleans up the staged source file written at - UPLOAD time (``…/call_imports/{id}/source.{csv,xlsx}``) — both the - per-row recording keys and the source artefact share the same - organization-scoped prefix, so a single sweep covers them all. - - Returns ``(deleted_count, error_count)``. Never raises — callers proceed - with the DB delete regardless; orphans, if any, can be cleaned up by - re-running the same delete (it's idempotent). - """ - from app.services.storage.s3_service import s3_service - - if not s3_service.is_enabled(): - return 0, 0 - - keys = [r.recording_s3_key for r in rows if r.recording_s3_key] - deleted = 0 - errors = 0 - - if keys: - try: - d, errs = s3_service.delete_keys(keys) - deleted += d - errors += len(errs) - if errs: - logger.warning( - "S3 bulk-delete reported {} errors for call_import {}", - len(errs), - call_import_id, - ) - except Exception as exc: # noqa: BLE001 - logger.exception( - "Bulk S3 delete failed for call_import {}: {}", call_import_id, exc - ) - errors += len(keys) - - # Belt-and-braces sweep: catch anything that landed under the import's - # prefix but never made it into a row's recording_s3_key (narrow - # window where the S3 upload succeeded but the DB commit didn't). - sweep_prefix = ( - f"{s3_service.prefix}organizations/{organization_id}/" - f"call_imports/{call_import_id}/" - ) - try: - d, errs = s3_service.delete_keys_by_prefix(sweep_prefix) - deleted += d - errors += len(errs) - except Exception as exc: # noqa: BLE001 - logger.exception( - "S3 prefix sweep failed for {}: {}", sweep_prefix, exc - ) - - return deleted, errors - - -@router.delete( - "/{call_import_id}", - response_model=CallImportDeleteResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="deleteCallImport", -) -async def delete_call_import( - call_import_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportDeleteResponse: - """Delete a call-import batch asynchronously. - - Flips the batch to ``deleting`` and enqueues background teardown so - large imports (thousands of rows + S3 objects) do not block the API. - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - return CallImportDeleteResponse( - id=call_import_id, - status="completed", - ) - - if call_import.status == CallImportStatus.DELETING: - return CallImportDeleteResponse( - id=call_import.id, - status="accepted", - ) - - call_import.status = CallImportStatus.DELETING - call_import.error_message = None - db.commit() - - from app.workers.tasks.call_import_bulk_ops import delete_call_import_task - - delete_call_import_task.delay( - str(call_import_id), - str(organization_id), - ) - - return CallImportDeleteResponse( - id=call_import.id, - status="accepted", - ) - - -def _locate_call_import_row_or_404( - catalog_db: Session, - *, - call_import_id: UUID, - row_id: UUID, - organization_id: UUID, -) -> Tuple[Session, CallImportRow, Optional[Session]]: - """Find a call import row on the correct DB session for mutation. - - When sharding is enabled rows live on shard databases; ``get_db`` only - opens the catalog. Returns ``(row_db, row, extra_catalog_to_close)`` - where ``extra_catalog_to_close`` is the catalog session opened by - :func:`locate_call_import_row` (distinct from the route's catalog - session) and must be closed via :func:`close_row_sessions`. - """ - from app.db_sharding.row_ops import close_row_sessions, locate_call_import_row - - try: - row_db, located_catalog, row, _shard_id = locate_call_import_row(row_id) - except LookupError: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import row not found", - ) from None - if ( - row.call_import_id != call_import_id - or row.organization_id != organization_id - ): - close_row_sessions( - row_db, - located_catalog if located_catalog is not row_db else None, - ) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import row not found", - ) - extra_catalog = located_catalog if located_catalog is not row_db else None - return row_db, row, extra_catalog - - -@router.delete( - "/{call_import_id}/rows/{row_id}", - status_code=status.HTTP_204_NO_CONTENT, - operation_id="deleteCallImportRow", -) -async def delete_call_import_row( - call_import_id: UUID, - row_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> Response: - """Delete a single CallImportRow and its S3 recording. - - The parent ``CallImport`` is left in place. After deletion we recompute - its ``total_rows`` / ``completed_rows`` / ``failed_rows`` / ``status`` - so the UI's progress bar stays consistent with reality. - """ - from app.services.storage.s3_service import s3_service - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - from app.db_sharding.row_ops import close_row_sessions - - row_db, row, extra_catalog = _locate_call_import_row_or_404( - db, - call_import_id=call_import.id, - row_id=row_id, - organization_id=organization_id, - ) - try: - _revoke_pending_tasks([row]) - - if row.recording_s3_key and s3_service.is_enabled(): - try: - s3_service.delete_file_by_key(row.recording_s3_key) - except Exception as exc: # noqa: BLE001 — best-effort, DB is source of truth - logger.warning( - "Failed to delete S3 object {} for row {}: {}", - row.recording_s3_key, - row.id, - exc, - ) - - row_db.delete(row) - row_db.commit() - - _recompute_call_import_counters(db, call_import) - db.commit() - finally: - close_row_sessions(row_db, extra_catalog) - - logger.info( - "Deleted call_import_row {} (call_import={}, org={})", - row_id, - call_import.id, - organization_id, - ) - - return Response(status_code=status.HTTP_204_NO_CONTENT) - - -def _recompute_call_import_counters( - db: Session, call_import: CallImport -) -> None: - """Resync ``total/completed/failed_rows`` + status on the parent batch. - - Called after row-level mutations (single delete, bulk delete) so the - UI's progress bar stays consistent with the actual row set. The - rules mirror :func:`delete_call_import_row` so behavior doesn't - diverge between the per-row and bulk paths. - """ - - from app.services.call_imports.bulk_ops import rollup_call_import_batch_status - - rollup_call_import_batch_status(db, call_import) - - -@router.post( - "/{call_import_id}/retry-failed", - response_model=CallImportRetryFailedRowsResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="retryFailedCallImportRows", -) -async def retry_failed_call_import_rows( - call_import_id: UUID, - payload: Optional[CallImportRetryFailedRowsRequest] = Body(None), - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportRetryFailedRowsResponse: - """Re-enqueue every failed import row in this batch. - - Useful when transient provider issues are resolved and the operator wants - a one-click "try failed downloads again" pass without re-uploading the CSV. - - Pass ``provider`` + ``telephony_integration_id`` (or both omitted for - direct-URL retry) to change how recordings are fetched on this pass. - When the body is omitted entirely, the batch keeps its existing pinned - credentials. - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - if payload is not None: - if payload.telephony_integration_id is not None: - integration = _resolve_telephony_integration( - db, - organization_id, - payload.telephony_integration_id, - payload.provider or "", - ) - call_import.provider = integration.provider - call_import.telephony_integration_id = integration.id - if (integration.provider or "").lower() == "exotel": - schema = _resolve_schema( - db, - organization_id, - call_import.workspace_id, - call_import.schema_id, - ) - _validate_exotel_import_ready( - list(schema.parameters), - dict(call_import.parameter_mapping or {}), - ) - else: - call_import.provider = None - call_import.telephony_integration_id = None - db.flush() - - failed_rows = ( - db.query(CallImportRow) - .filter( - CallImportRow.call_import_id == call_import.id, - CallImportRow.status == CallImportRowStatus.FAILED, - ) - .order_by(CallImportRow.row_index.asc()) - .all() - ) - if not failed_rows: - return CallImportRetryFailedRowsResponse( - requeued=0, - enqueue_failed=0, - skipped=0, - ) - - from app.workers.concurrency.fair_import_dispatch import ( - schedule_fair_import_dispatch, - ) - - # Reset rows to pending BEFORE enqueue so the UI reflects "retry in - # progress" immediately even if the worker queue is backlogged. - for row in failed_rows: - row.status = CallImportRowStatus.PENDING - row.error_message = None - row.celery_task_id = None - - db.flush() - _recompute_call_import_counters(db, call_import) - db.commit() - - try: - schedule_fair_import_dispatch(max_workspace_turns=999) - requeued = len(failed_rows) - enqueue_failed = 0 - skipped = 0 - except Exception as exc: # noqa: BLE001 - logger.exception( - "Failed to schedule fair import dispatch for import {}", - call_import.id, - ) - requeued = 0 - enqueue_failed = len(failed_rows) - skipped = 0 - for row in failed_rows: - db.refresh(row) - if row.status != CallImportRowStatus.PENDING: - skipped += 1 - enqueue_failed -= 1 - continue - row.status = CallImportRowStatus.FAILED - row.error_message = f"Failed to enqueue retry: {exc}" - db.flush() - _recompute_call_import_counters(db, call_import) - db.commit() - - return CallImportRetryFailedRowsResponse( - requeued=requeued, - enqueue_failed=enqueue_failed, - skipped=skipped, - ) - - -@router.post( - "/{call_import_id}/rows/bulk-delete", - response_model=CallImportRowBulkDeleteResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="bulkDeleteCallImportRows", -) -async def bulk_delete_call_import_rows( - call_import_id: UUID, - payload: CallImportRowBulkDelete, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportRowBulkDeleteResponse: - """Delete multiple ``CallImportRow`` rows in one request. - - Unknown / cross-tenant row ids are silently skipped — the response - reports how many actually went away so a UI that holds onto stale - ids (e.g. after another tab already deleted a row) doesn't 404 - the entire bulk action. - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - if not payload.row_ids: - return CallImportRowBulkDeleteResponse(deleted=0, status="completed") - - from app.workers.tasks.call_import_bulk_ops import bulk_delete_call_import_rows_task - - row_id_strs = [str(rid) for rid in payload.row_ids] - - bulk_delete_call_import_rows_task.delay( - str(call_import_id), - str(organization_id), - row_id_strs, - ) - - return CallImportRowBulkDeleteResponse(deleted=0, status="accepted") - - -# --------------------------------------------------------------------------- -# Diarization / transcription endpoints -# --------------------------------------------------------------------------- - - -def _select_rows_for_transcription( - db: Session, - call_import: CallImport, - payload: CallImportTranscribeRequest, - requested_row_ids: Optional[List[UUID]] = None, -) -> tuple[List[CallImportRow], Dict[str, int]]: - """Pick which rows to enqueue for diarisation (delegates to bulk_ops).""" - from app.services.call_imports.bulk_ops import select_rows_for_transcription - - try: - return select_rows_for_transcription( - db, call_import, payload, requested_row_ids=requested_row_ids - ) - except ValueError as exc: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=str(exc), - ) from exc - - -@router.post( - "/{call_import_id}/transcribe", - response_model=CallImportTranscribeResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="transcribeCallImport", -) -async def transcribe_call_import( - call_import_id: UUID, - payload: CallImportTranscribeRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportTranscribeResponse: - """Fan out diarization tasks for many rows in a single call. - - Returns a summary with how many rows were queued and how many were - skipped (broken down by reason) so the UI can show a meaningful - toast even when nothing actually got enqueued (e.g. "All 12 rows - already have transcripts"). - """ - - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - from app.workers.tasks.call_import_bulk_ops import bulk_diarize_call_import_task - - bulk_diarize_call_import_task.delay( - str(call_import_id), - str(organization_id), - payload.model_dump(mode="json"), - [str(rid) for rid in payload.row_ids] if payload.row_ids else None, - ) - - return CallImportTranscribeResponse( - queued=0, - skipped_rows=0, - skipped_reason_counts={}, - accepted=True, - ) - - -@router.post( - "/{call_import_id}/rows/{row_id}/transcribe", - response_model=CallImportTranscribeResponse, - status_code=status.HTTP_202_ACCEPTED, - operation_id="transcribeCallImportRow", -) -async def transcribe_call_import_row( - call_import_id: UUID, - row_id: UUID, - payload: CallImportTranscribeRequest, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportTranscribeResponse: - """Diarize / transcribe a single row. - - Thin wrapper over the batch endpoint that hard-codes a single - ``row_ids`` filter. Skip counts still surface so the UI can render - "Skipped — transcript present" diagnostics consistently. - """ - - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - from app.services.call_imports.bulk_ops import execute_bulk_diarization - - try: - result = execute_bulk_diarization( - db, - call_import, - payload, - requested_row_ids=[row_id], - ) - except ValueError as exc: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=str(exc), - ) from exc - - return CallImportTranscribeResponse( - queued=result.queued, - skipped_rows=result.skipped_rows, - skipped_reason_counts=result.skipped_reason_counts, - ) - - -# --------------------------------------------------------------------------- -# Cancel-in-flight diarisation -# --------------------------------------------------------------------------- -# -# Long-running multimodal LLM diarisation calls (especially LLM-only mode on -# slow audio) can sit in ``pending`` / ``running`` for tens of minutes when an -# upstream provider stalls. Without an abort affordance the operator's only -# recourse is to wait for Celery's ``time_limit`` to fire — which can be -# several minutes — or to manually mutate the DB. These helpers + the two -# endpoints below give the UI a first-class "Stop diarisation" button. -# -# Why ``terminate=True``: the legacy ``_revoke_pending_tasks`` helper uses -# ``terminate=False`` because it's called from delete-flow paths where the -# task may simply not get to run (a worker pulls it off the queue and drops -# it). For a user-initiated cancel we want SIGTERM to interrupt the worker -# mid-LLM call so the audio HTTP request actually aborts. ``terminate=True`` -# routes SIGTERM to the executing process; ``signal="SIGTERM"`` is the -# default but we spell it out so the intent is obvious to reviewers. - -# Sentinel error message stamped on cancelled rows. Read by the transcribe -# worker's finaliser (see ``app/workers/tasks/transcribe_call_import_row.py``) -# to detect a row that was cancelled mid-flight and AVOID overwriting it -# with whatever partial result the worker had managed to compute before the -# SIGTERM landed. -CANCELLED_BY_USER_ERROR: str = "Diarisation cancelled by user" - - -def _cancellable_diarisation_states() -> Tuple[str, ...]: - """States that a diarisation row can be cancelled from. - - Kept as a tiny helper so adding a future ``"queued"`` / ``"retrying"`` - state only needs one edit. - """ - return ("pending", "running") - - -def _revoke_diarisation_task(row: CallImportRow) -> None: - """Best-effort revoke of a single row's diarisation Celery task. - - Always swallows control-plane exceptions — Celery's control bus is - inherently best-effort and a missed revoke is not catastrophic - because the DB row is already flipped to ``failed`` by the caller - before this runs (so the UI immediately reflects the cancel; if - the task happens to finish anyway, the worker's finaliser skips - over the row via :data:`CANCELLED_BY_USER_ERROR`). - """ - task_id = (row.celery_task_id or "").strip() - if not task_id: - return - try: - from app.workers.celery_app import celery_app - - celery_app.control.revoke( - task_id, terminate=True, signal="SIGTERM" - ) - logger.info( - "Revoked diarisation task {} for call-import row {}", - task_id, - row.id, - ) - except Exception as exc: # noqa: BLE001 — revoke is best-effort - logger.warning( - "Failed to revoke diarisation task {} for row {}: {}", - task_id, - row.id, - exc, - ) - - -def _apply_diarisation_cancel(rows: List[CallImportRow]) -> Tuple[int, int]: - """Cancel diarisation on every cancellable row in ``rows``. - - Returns ``(cancelled, skipped)`` so the caller can build a typed - response without re-querying the DB. The caller is responsible for - ``db.commit()`` after this returns — we deliberately don't commit - here so a batch endpoint can flush all rows in one transaction. - """ - cancellable_states = _cancellable_diarisation_states() - cancelled = 0 - skipped = 0 - for row in rows: - if (row.diarised_transcript_status or "").lower() not in cancellable_states: - skipped += 1 - continue - # Flip the row state BEFORE we revoke so the UI's next poll - # already shows the cancel, even if Celery's control plane is - # slow to ack. - row.diarised_transcript_status = "failed" - row.diarised_transcript_error = CANCELLED_BY_USER_ERROR - _revoke_diarisation_task(row) - # Drop the task id so a follow-up retry (or a stale poll) can't - # accidentally re-revoke or get confused. - row.celery_task_id = None - cancelled += 1 - return cancelled, skipped - - -@router.post( - "/{call_import_id}/rows/{row_id}/cancel-diarisation", - response_model=CallImportRowResponse, - status_code=status.HTTP_200_OK, - operation_id="cancelCallImportRowDiarisation", -) -async def cancel_call_import_row_diarisation( - call_import_id: UUID, - row_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportRowResponse: - """Abort an in-flight (or queued) diarisation for a single row. - - Idempotent: calling on a row that's already terminal (``completed`` - / ``failed`` / ``idle``) returns the row unchanged with a 200, so - the UI can fire this from a "Stop" button without having to - pre-check the state. - - Race notes: - - * The row's ``diarised_transcript_status`` is flipped to ``failed`` - with :data:`CANCELLED_BY_USER_ERROR` BEFORE the Celery revoke, - so the polling UI sees the cancel immediately. - * If the worker happens to finish between our DB flip and the - SIGTERM landing, its finaliser will detect the cancelled - sentinel on the row and skip its own status / score writes - (see :mod:`app.workers.tasks.transcribe_call_import_row`). - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - from app.db_sharding.row_ops import close_row_sessions - - row_db, row, extra_catalog = _locate_call_import_row_or_404( - db, - call_import_id=call_import_id, - row_id=row_id, - organization_id=organization_id, - ) - try: - _apply_diarisation_cancel([row]) - row_db.commit() - row_db.refresh(row) - return CallImportRowResponse.model_validate(row) - finally: - close_row_sessions(row_db, extra_catalog) - - -@router.post( - "/{call_import_id}/cancel-diarisation", - response_model=CallImportCancelDiarisationResponse, - status_code=status.HTTP_200_OK, - operation_id="cancelCallImportDiarisation", -) -async def cancel_call_import_diarisation( - call_import_id: UUID, - payload: Optional[CallImportCancelDiarisationRequest] = None, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportCancelDiarisationResponse: - """Abort in-flight diarisation for many rows in a single call. - - Default body (no ``row_ids``) cancels every row in this import - whose ``diarised_transcript_status`` is ``pending`` or - ``running`` — the "stop everything" button. Pass ``row_ids`` to - scope the cancel to the rows the operator has selected. - - Returns ``(cancelled, skipped)`` so the UI can render a tight - toast ("Cancelled 3 rows · 1 skipped (already completed)"). - """ - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - base_query = db.query(CallImportRow).filter( - CallImportRow.call_import_id == call_import_id - ) - - requested_ids = ( - payload.row_ids if payload and payload.row_ids is not None else None - ) - if requested_ids is not None: - if not requested_ids: - # Empty list is "no rows requested" — treat as a no-op - # 200 rather than 400 so the UI can pass through an empty - # selection without a special-case. - return CallImportCancelDiarisationResponse(cancelled=0, skipped=0) - rows = base_query.filter(CallImportRow.id.in_(requested_ids)).all() - found_ids = {r.id for r in rows} - # Treat requested-but-not-found ids as ``skipped`` so the UI's - # numbers reconcile (a stale selection that includes deleted - # rows shouldn't 404 the whole call). - missing = [rid for rid in requested_ids if rid not in found_ids] - skipped_missing = len(missing) - else: - # Implicit "cancel every cancellable row in this import" path. - rows = base_query.filter( - CallImportRow.diarised_transcript_status.in_( - list(_cancellable_diarisation_states()) - ) - ).all() - skipped_missing = 0 - - cancelled, skipped = _apply_diarisation_cancel(rows) - db.commit() - return CallImportCancelDiarisationResponse( - cancelled=cancelled, - skipped=skipped + skipped_missing, - ) - - -def _render_diarised_segments_text( - segments: Optional[List[Dict[str, Any]]], - *, - swap: bool = False, -) -> str: - """Render ``CallImportRow.diarised_segments`` as ``: `` lines. - - Mirrors the worker's ``_render_turns_as_text`` (kept duplicated so - the route doesn't need to import a Celery task module just to - rebuild the rendered transcript). Only ``agent`` and ``user`` are - swapped — multi-party calls keep their ``speaker_N`` labels through - a swap so we don't silently collapse a third speaker into the user - side. - """ - if not segments: - return "" - out: List[str] = [] - for turn in segments: - if not isinstance(turn, dict): - continue - speaker = (turn.get("speaker") or "").strip() - text = (turn.get("text") or "").strip() - if not speaker or not text: - continue - if swap: - if speaker == "agent": - speaker = "user" - elif speaker == "user": - speaker = "agent" - out.append(f"{speaker}: {text}") - return "\n".join(out) - - -@router.post( - "/{call_import_id}/rows/{row_id}/diarised-speaker-swap", - response_model=CallImportRowResponse, - operation_id="toggleCallImportRowSpeakerSwap", -) -async def toggle_call_import_row_speaker_swap( - call_import_id: UUID, - row_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportRowResponse: - """Flip the user <-> agent mapping on a diarised row. - - The worker's "first speaker is the agent" heuristic is right most of - the time but does fail on inbound recordings where the customer - greets first, on recordings where the agent stays silent for the - intro, etc. Rather than rerun the (expensive) STT + pyannote - pipeline for those cases, we let reviewers flip the mapping in - place: the structured ``diarised_segments`` are the source of truth - and we re-render the plain-text ``diarised_transcript`` from them - with the swap applied. The next CSV export will then show the - corrected labels. - - Returns the updated row so the frontend can refresh without an - extra round-trip. - """ - - del api_key - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - from app.db_sharding.row_ops import close_row_sessions - - row_db, row, extra_catalog = _locate_call_import_row_or_404( - db, - call_import_id=call_import_id, - row_id=row_id, - organization_id=organization_id, - ) - try: - segments = ( - row.diarised_segments if isinstance(row.diarised_segments, list) else None - ) - if not segments: - # Without structured turns the swap toggle would have nothing to - # re-render — surface a clear error rather than silently - # flipping a flag the UI never read. - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=( - "This row has no structured diarised segments to swap. " - "Re-run diarisation to generate per-speaker turns first." - ), - ) - - new_swap = not bool(row.diarised_speaker_swap) - row.diarised_speaker_swap = new_swap - row.diarised_transcript = ( - _render_diarised_segments_text(segments, swap=new_swap) or None - ) - row_db.commit() - row_db.refresh(row) - return CallImportRowResponse.model_validate(row) - finally: - close_row_sessions(row_db, extra_catalog) - - -# --------------------------------------------------------------------------- -# Cross-run insights for the import detail page -# --------------------------------------------------------------------------- - - -@router.get( - "/{call_import_id}/insights", - response_model=CallImportInsightsResponse, - operation_id="getCallImportInsights", -) -async def get_call_import_insights( - call_import_id: UUID, - api_key: str = Depends(get_api_key), - organization_id: UUID = Depends(get_organization_id), - db: Session = Depends(get_db), -) -> CallImportInsightsResponse: - """Aggregate signals across every evaluation run on this import. - - Powers the Insights tab on the call-import detail page: returns - per-metric "latest run" summaries plus a trend series of mean values - across runs so the UI can render a small line chart per metric. Also - bundles transcript coverage stats since those are the cheapest - pre-eval health-check (e.g. "30 of 50 rows still missing - transcripts"). - """ - - del api_key - - from app.models.database import ( - CallImportEvaluation, - CallImportEvaluationRow, - Metric, - ) - - call_import = ( - db.query(CallImport) - .filter( - CallImport.id == call_import_id, - CallImport.organization_id == organization_id, - ) - .first() - ) - if not call_import: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Call import not found", - ) - - rows = ( - db.query(CallImportRow) - .filter(CallImportRow.call_import_id == call_import_id) - .all() - ) - # A row "has a transcript" if EITHER the production (CSV) or the - # diarised (worker) column is populated — the insights tile reports - # the union so users see total coverage regardless of which source - # produced the value. - rows_with_transcript = sum( - 1 - for r in rows - if (r.transcript or "").strip() - or (r.diarised_transcript or "").strip() - ) - rows_without_transcript = len(rows) - rows_with_transcript - source_counts: Dict[str, int] = {} - for r in rows: - has_production = bool((r.transcript or "").strip()) - has_diarised = bool((r.diarised_transcript or "").strip()) - if has_production: - key = r.transcript_source or "csv" - source_counts[key] = source_counts.get(key, 0) + 1 - if has_diarised: - source_counts["diarised"] = source_counts.get("diarised", 0) + 1 - - evaluations = ( - db.query(CallImportEvaluation) - .filter( - CallImportEvaluation.call_import_id == call_import_id, - CallImportEvaluation.organization_id == organization_id, - ) - .order_by(CallImportEvaluation.created_at.asc()) - .all() - ) - - # Defer heavy lifting to the aggregation helper so this endpoint and - # the per-run aggregate endpoint share the exact same metric - # bucketing math (no chance of "trend" disagreeing with "latest" on - # the same data set). - from app.api.v1.routes.call_import_evaluations import ( - _compute_metric_aggregates, - ) - - metric_history: Dict[str, List[CallImportInsightsRunPoint]] = {} - metric_meta: Dict[str, Metric] = {} - metric_latest: Dict[str, CallImportMetricAggregate] = {} - - for evaluation in evaluations: - eval_rows = ( - db.query(CallImportEvaluationRow) - .filter(CallImportEvaluationRow.evaluation_id == evaluation.id) - .all() - ) - aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) - for agg in aggregates: - if agg.metric_id not in metric_meta: - # ``agg.metric_id`` is normally a UUID string, but the - # aggregator also emits ids that surface in row scores - # without a matching ``Metric`` row (e.g. a metric the - # user deleted mid-run, or LLM-discovered slugs). Those - # are not valid UUIDs, so coerce defensively and skip - # the metric registry lookup when the cast fails — the - # ``meta is None`` branch below already handles the - # display via the values stored on ``agg`` itself. - try: - metric_uuid = UUID(agg.metric_id) - except (ValueError, AttributeError, TypeError): - metric_uuid = None - if metric_uuid is not None: - metric_obj = ( - db.query(Metric) - .filter( - Metric.id == metric_uuid, - Metric.organization_id == organization_id, - ) - .first() - ) - if metric_obj is not None: - metric_meta[agg.metric_id] = metric_obj - history = metric_history.setdefault(agg.metric_id, []) - history.append( - CallImportInsightsRunPoint( - evaluation_id=evaluation.id, - name=evaluation.name, - created_at=evaluation.created_at, - mean=agg.mean, - completed_rows=agg.count, - ) - ) - metric_latest[agg.metric_id] = agg - - metrics_payload: List[CallImportInsightsMetric] = [] - for metric_id, latest in metric_latest.items(): - meta = metric_meta.get(metric_id) - metrics_payload.append( - CallImportInsightsMetric( - metric_id=metric_id, - metric_name=(meta.name if meta else latest.metric_name), - metric_type=(meta.metric_type if meta else latest.metric_type), - latest=latest, - trend=metric_history.get(metric_id, []), - ) - ) - - return CallImportInsightsResponse( - call_import_id=call_import_id, - total_rows=len(rows), - rows_with_transcript=rows_with_transcript, - rows_without_transcript=rows_without_transcript, - transcript_source_counts=source_counts, - evaluation_count=len(evaluations), - metrics=metrics_payload, - ) - - -from app.core.auth.capabilities import CALLS_DELETE, CALLS_IMPORT, CALLS_VIEW -from app.core.auth.workspace_route_capabilities import apply_workspace_route_capabilities - -apply_workspace_route_capabilities( - router, - view_capability=CALLS_VIEW, - manage_capability=CALLS_IMPORT, - delete_capability=CALLS_DELETE, -) +"""CSV-driven call import routes. + +Users upload a CSV plus a per-batch column mapping (CSV header -> system +field). The backend persists a CallImport batch + one CallImportRow per +line, then fans the rows out to the Celery ``imports`` queue where each +row is downloaded using the telephony credential pinned on the batch. +Exotel credentialed imports require a ``recording_url`` on every row; +direct-URL imports (no credential) also require a mapped recording URL. +""" + +from __future__ import annotations + +import csv +import io +import json +import re +from dataclasses import dataclass, field +from datetime import date, datetime, time, timedelta +from typing import Any, Dict, Iterable, List, Optional, Tuple +from uuid import UUID, uuid4 + +from fastapi import APIRouter, Body, BackgroundTasks, Depends, File, Form, HTTPException, Query, Response, UploadFile, status +from loguru import logger +from sqlalchemy import desc, func, or_ +from sqlalchemy.orm import Session + +from app.config import settings +from app.core.auth import Principal, get_principal +from app.core.auth.rbac import require_admin +from app.database import get_db +from app.db_sharding.sessions import is_sharding_enabled +from app.dependencies import ( + get_api_key, + get_organization_id, + get_workspace_id, + require_enterprise_feature, +) +from app.services.billing.flexprice_service import record_call_import_batch_created +from app.services.call_imports.audit import ( + actor_emails_for_call_import, + emails_for_user_ids, + stamp_call_import_actor, + user_ids_from_call_imports, +) +from app.services.call_imports.dispatch_diagnostics import ( + build_call_import_dispatch_diagnostics, +) +from app.models.database import ( + CallImport, + CallImportRow, + CallImportSchema, + CallImportSchemaParameter, + CallImportTag, + TelephonyIntegration, +) +from app.models.enums import ( + CallImportParameterType, + CallImportRowStatus, + CallImportStatus, +) +from app.models.schemas import ( + CallImportCancelDiarisationRequest, + CallImportCancelDiarisationResponse, + CallImportDetailResponse, + CallImportDeleteResponse, + CallImportDiarisationPromptDefaultResponse, + CallImportDispatchDiagnosticsResponse, + CallImportInsightsMetric, + CallImportInsightsResponse, + CallImportInsightsRunPoint, + CallImportListResponse, + CallImportMappingUpdate, + CallImportMetricAggregate, + CallImportPreviewResponse, + CallImportPreviewSheet, + CallImportRetryFailedRowsRequest, + CallImportRetryFailedRowsResponse, + CallImportResponse, + CallImportRowIdsResponse, + CallImportRowBulkDelete, + CallImportRowBulkDeleteResponse, + CallImportRowResponse, + CallImportStartRequest, + CallImportTranscribeRequest, + CallImportTranscribeResponse, + CallImportUpdate, + CallImportUploadResponse, +) + + +router = APIRouter( + prefix="/call-imports", + tags=["Call Imports"], + dependencies=[Depends(require_enterprise_feature("call_imports"))], +) + + +@dataclass(frozen=True) +class CallImportParseSkip: + """One source row excluded during CSV/Excel parse (identity / recording URL).""" + + source_row: int + reason: str + message: str + + +@dataclass +class CallImportParseResult: + rows: List[Dict[str, Any]] = field(default_factory=list) + skipped: List[CallImportParseSkip] = field(default_factory=list) + + +def parse_skips_to_json(skips: List[CallImportParseSkip]) -> List[Dict[str, Any]]: + """Persistable JSON shape for ``CallImport.source_row_skips``.""" + return [ + { + "source_row": item.source_row, + "reason": item.reason, + "message": item.message, + } + for item in skips + ] + + +def _normalize_dataset(raw: Optional[str]) -> Optional[str]: + """Trim and treat empty strings as 'no dataset' (NULL).""" + if raw is None: + return None + cleaned = raw.strip() + return cleaned or None + + +def _serialize_call_import( + db: Session, + call_import: CallImport, + *, + user_emails: Optional[Dict[UUID, str]] = None, +) -> CallImportResponse: + """Catalog parent fields; counters come from SQL rollup (not Redis merge).""" + from app.services.call_imports.bulk_ops import rollup_call_import_batch_status + from app.services.call_imports.progress_counters import ( + clear_import_progress_redis, + read_import_progress, + ) + + redis_completed, redis_failed = read_import_progress(call_import.id) + if ( + redis_completed + or redis_failed + or int(call_import.completed_rows or 0) > int(call_import.total_rows or 0) + or int(call_import.failed_rows or 0) > int(call_import.total_rows or 0) + ): + rollup_call_import_batch_status(db, call_import) + db.flush() + + clear_import_progress_redis(call_import.id) + db.refresh(call_import) + total = int(call_import.total_rows or 0) + completed = min(int(call_import.completed_rows or 0), total) if total else int( + call_import.completed_rows or 0 + ) + failed = min(int(call_import.failed_rows or 0), total) if total else int( + call_import.failed_rows or 0 + ) + if user_emails is None: + user_emails = emails_for_user_ids( + db, user_ids_from_call_imports([call_import]) + ) + created_email, updated_email = actor_emails_for_call_import( + call_import, user_emails + ) + base = CallImportResponse.model_validate(call_import) + return base.model_copy( + update={ + "completed_rows": completed, + "failed_rows": failed, + "created_by_email": created_email, + "last_updated_by_email": updated_email, + } + ) + + +def _resolve_tags( + db: Session, organization_id: UUID, tag_ids: Optional[List[UUID]] +) -> List[CallImportTag]: + """Look up tag rows by id, scoped to the organization. + + Raises HTTPException(400) if any id is unknown for the org. + """ + if not tag_ids: + return [] + rows = ( + db.query(CallImportTag) + .filter( + CallImportTag.organization_id == organization_id, + CallImportTag.id.in_(tag_ids), + ) + .all() + ) + found_ids = {row.id for row in rows} + missing = [str(tag_id) for tag_id in tag_ids if tag_id not in found_ids] + if missing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unknown call_import_tag id(s): {missing}", + ) + return rows + + +MAX_UPLOAD_BYTES = 15 * 1024 * 1024 # 15 MB upload cap (CSV or Excel) + +# File extensions accepted by the upload + preview endpoints. Keep in +# lockstep with the frontend ``accept`` attribute on the file picker. +CSV_EXTENSIONS = (".csv",) +XLSX_EXTENSIONS = (".xlsx", ".xlsm") +ALLOWED_EXTENSIONS = CSV_EXTENSIONS + XLSX_EXTENSIONS + +AUDIO_CONTENT_TYPES = { + "wav": "audio/wav", + "mp3": "audio/mpeg", + "flac": "audio/flac", + "m4a": "audio/mp4", +} + + +def _file_format(filename: Optional[str]) -> Optional[str]: + """Classify ``filename`` as ``'csv'`` / ``'xlsx'`` or ``None`` if unsupported.""" + if not filename: + return None + name = filename.lower() + if name.endswith(CSV_EXTENSIONS): + return "csv" + if name.endswith(XLSX_EXTENSIONS): + return "xlsx" + return None + + +def _audio_extension(filename: Optional[str]) -> Optional[str]: + """Return the validated lower-case extension for a manual recording.""" + if not filename or "." not in filename: + return None + ext = filename.rsplit(".", 1)[-1].lower().strip() + allowed = {fmt.lower().lstrip(".") for fmt in settings.ALLOWED_AUDIO_FORMATS} + return ext if ext in allowed else None + + +def _audio_content_type(ext: str, upload_content_type: Optional[str]) -> str: + """Prefer the browser-supplied audio content type, with a safe fallback.""" + supplied = (upload_content_type or "").strip() + if supplied and supplied != "application/octet-stream": + return supplied + return AUDIO_CONTENT_TYPES.get(ext.lower(), "application/octet-stream") + + +def _audio_s3_key( + organization_id: UUID, call_import_id: UUID, row_id: UUID, ext: str +) -> str: + """Build the canonical S3 key for a manually uploaded recording.""" + from app.services.storage.s3_service import s3_service + + return ( + f"{s3_service.prefix}organizations/{organization_id}/call_imports/" + f"{call_import_id}/{row_id}.{ext}" + ) + + +def _filename_stem(filename: Optional[str]) -> str: + """Extract a cross-platform filename stem from an UploadFile name.""" + raw = (filename or "").strip() + basename = re.split(r"[\\/]", raw)[-1] if raw else "" + if "." in basename: + basename = basename.rsplit(".", 1)[0] + return basename.strip() + + +def _sanitize_conversation_id(raw: str) -> str: + """Turn a filename stem into a stable conversation_id.""" + cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", raw.strip()) + cleaned = re.sub(r"_+", "_", cleaned).strip("._-") + return (cleaned or "recording")[:255] + + +def _dedupe_conversation_id( + base: str, counts: Dict[str, int] +) -> str: + """Make conversation ids unique within one manual upload batch.""" + count = counts.get(base, 0) + 1 + counts[base] = count + if count == 1: + return base + suffix = f"-{count}" + return f"{base[: 255 - len(suffix)]}{suffix}" + + +def _normalize_header(name: str) -> str: + return (name or "").strip().lower() + + +def _header_lookup(fieldnames: List[str]) -> Dict[str, str]: + """Map normalized header -> original header for case-insensitive lookup.""" + return {_normalize_header(h): h for h in fieldnames or []} + + +def _resolve_mapped_header( + mapping_value: Optional[str], header_lookup: Dict[str, str] +) -> Optional[str]: + """Translate a user-supplied CSV header into the actual column key. + + The frontend sends headers exactly as they appear in the source file, + but we still normalize on the server so trailing whitespace / casing + doesn't break matching. Returns the canonical fieldname or ``None`` + if not present in the file. + """ + if not mapping_value: + return None + return header_lookup.get(_normalize_header(mapping_value)) + + +def _xlsx_cell_to_str(value: Any) -> str: + """Coerce an openpyxl cell value to the string the rest of the + pipeline expects. + + openpyxl returns native Python types (int, float, datetime, bool, + None). The CSV path always works with strings, so we mirror that: + integers stringify cleanly (no ``.0`` suffix on whole-number floats), + datetimes use ISO-8601, booleans use SQL-style ``TRUE`` / ``FALSE``. + """ + if value is None: + return "" + if isinstance(value, bool): + return "TRUE" if value else "FALSE" + if isinstance(value, int): + return str(value) + if isinstance(value, float): + if value.is_integer(): + return str(int(value)) + return str(value) + if isinstance(value, datetime): + return value.isoformat() + if isinstance(value, date): + return value.isoformat() + if isinstance(value, time): + return value.isoformat() + if isinstance(value, timedelta): + return str(value) + return str(value) + + +def _parse_recording_date_cell(cell: str) -> date: + """Parse day-first dates with one/two digit day-month parts.""" + match = re.fullmatch(r"\s*(\d{1,2})[/-](\d{1,2})[/-](\d{4})\s*", cell) + if match: + day, month, year = (int(part) for part in match.groups()) + return date(year, month, day) + + # Native Excel date cells arrive from ``_xlsx_cell_to_str`` as ISO + # datetimes (e.g. ``2026-01-04T00:00:00``). Accept that resolved date, + # while keeping plain ISO dates rejected for hand-entered text/CSV cells. + if "T" in cell: + return datetime.fromisoformat(cell.replace("Z", "+00:00")).date() + + raise ValueError("expected D/M/YYYY or D-M-YYYY") + + +def _coerce_parameter_value( + raw: str, + param_type: CallImportParameterType, + *, + row_idx: int, + param_name: str, +) -> Any: + """Validate + coerce a single CSV cell against its declared type. + + Returns the typed Python value to surface in ``raw_columns``. Empty + strings are returned as ``None`` regardless of the parameter type so + optional cells stay null end-to-end. Coercion failures raise a + 400 with a row-anchored message. + """ + cell = (raw or "").strip() + if not cell: + return None + + if param_type == CallImportParameterType.CONVERSATION_ID: + return cell + if param_type == CallImportParameterType.RECORDING_URL: + # Recording URLs are exercised by the worker (which downloads + # them); we only do a light "starts with http" check here so a + # paste-error surfaces immediately at upload time. + lower = cell.lower() + if not (lower.startswith("http://") or lower.startswith("https://")): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {row_idx + 1}: value for '{param_name}' is not a " + "valid recording URL (must start with http:// or https://)." + ), + ) + return cell + if param_type == CallImportParameterType.RECORDING_DATE: + try: + parsed_date = _parse_recording_date_cell(cell) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {row_idx + 1}: value for '{param_name}' is not a " + f"valid recording date ({cell!r}); expected day-first " + "D/M/YYYY or D-M-YYYY." + ), + ) + return parsed_date.strftime("%d/%m/%Y") + if param_type == CallImportParameterType.TRANSCRIPT: + return cell + if param_type == CallImportParameterType.TEXT: + return cell + if param_type == CallImportParameterType.NUMBER: + try: + value = float(cell) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {row_idx + 1}: value for '{param_name}' is not a " + f"valid number ({cell!r})." + ), + ) + if value.is_integer(): + return int(value) + return value + if param_type == CallImportParameterType.BOOLEAN: + truthy = {"true", "yes", "y", "1", "t"} + falsy = {"false", "no", "n", "0", "f"} + norm = cell.lower() + if norm in truthy: + return True + if norm in falsy: + return False + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {row_idx + 1}: value for '{param_name}' is not a " + f"valid boolean ({cell!r})." + ), + ) + if param_type == CallImportParameterType.DATETIME: + try: + parsed = datetime.fromisoformat(cell.replace("Z", "+00:00")) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {row_idx + 1}: value for '{param_name}' is not a " + f"valid ISO-8601 date/time ({cell!r})." + ), + ) + return parsed.isoformat() + if param_type == CallImportParameterType.URL: + lower = cell.lower() + if not (lower.startswith("http://") or lower.startswith("https://")): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {row_idx + 1}: value for '{param_name}' is not a " + "valid URL (must start with http:// or https://)." + ), + ) + return cell + # Unknown types: store as text and let the next migration catch up. + return cell + + +def _recording_url_cell_is_valid_http(raw: str) -> bool: + cell = (raw or "").strip() + if not cell: + return False + lower = cell.lower() + return lower.startswith("http://") or lower.startswith("https://") + + +def _parameter_is_required(param: CallImportSchemaParameter) -> bool: + """Return whether a schema parameter must be mapped on every upload.""" + if param.is_required: + return True + try: + param_type = CallImportParameterType(param.type) + except ValueError: + return False + return param_type in ( + CallImportParameterType.CONVERSATION_ID, + CallImportParameterType.RECORDING_URL, + ) + + +def _apply_schema_mapping( + fieldnames: List[str], + rows_iter: Iterable[Dict[str, str]], + parameters: List[CallImportSchemaParameter], + parameter_mapping: Dict[str, str], + skipped_columns: List[str], + *, + source_label: str = "CSV", + validate_only: bool = False, +) -> CallImportParseResult: + """Schema-driven row projection: parameter -> CSV header -> typed value. + + Validates that every required schema parameter is mapped to a CSV + header that actually exists in the file, and that every CSV header + is either mapped to a parameter or explicitly listed in + ``skipped_columns``. Returns one dict per non-empty data row with: + + * ``conversation_id`` (str, mandatory) + * ``recording_date`` (Optional[str], DD/MM/YYYY date) + * ``recording_url`` (Optional[str]) + * ``transcript`` (Optional[str]) + * ``parameter_values`` (Dict[str, Any]) of typed values keyed by + parameter name (drives ``raw_columns`` so the export can + reproduce the source). + + ``validate_only=True`` runs the header / mapping / skipped-column + checks (every check that doesn't need to read row data) and then + returns an empty list — used by the MAP stage to validate a + mapping payload against the cached sheet snapshot without + re-fetching the source bytes from S3. + """ + header_lookup = _header_lookup(list(fieldnames)) + + # 1. Look up the conversation_id parameter so we can address it + # directly while building each row. + conv_param = next( + (p for p in parameters if p.type == CallImportParameterType.CONVERSATION_ID), + None, + ) + if conv_param is None: + # The schema invariant should have caught this on create/update, + # but a defensive 400 here keeps us safe against hand-rolled + # API callers that bypassed validation. + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Selected schema is missing the mandatory conversation_id parameter.", + ) + # 2. Resolve every mapped parameter to a canonical fieldname. + # Required parameters MUST resolve; optional ones may resolve to + # None if the user left them blank (no mapping). + canonical_by_param: Dict[str, Optional[str]] = {} + recording_date_param_name: Optional[str] = None + rec_url_param_name: Optional[str] = None + transcript_param_name: Optional[str] = None + for param in parameters: + mapped_header = parameter_mapping.get(param.name) + canonical = ( + _resolve_mapped_header(mapped_header, header_lookup) + if mapped_header + else None + ) + if _parameter_is_required(param) and canonical is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"{source_label} does not contain the column " + f"'{mapped_header or ''}' mapped to required parameter " + f"'{param.name}'." + ), + ) + canonical_by_param[param.name] = canonical + if param.type == CallImportParameterType.RECORDING_DATE: + recording_date_param_name = param.name + elif param.type == CallImportParameterType.RECORDING_URL: + rec_url_param_name = param.name + elif param.type == CallImportParameterType.TRANSCRIPT: + transcript_param_name = param.name + + # 3. Every CSV column must either be mapped to a parameter or + # explicitly skipped. Catches "I forgot to skip the email + # column" gracefully instead of dropping data silently. + mapped_canonicals = {c for c in canonical_by_param.values() if c} + skipped_canonicals = { + _resolve_mapped_header(h, header_lookup) + for h in skipped_columns + } + skipped_canonicals.discard(None) + unhandled = [ + h + for h in fieldnames + if h not in mapped_canonicals and h not in skipped_canonicals + ] + if unhandled: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"{source_label} columns must either be mapped to a schema " + f"parameter or explicitly skipped. Unhandled: {unhandled}." + ), + ) + + conv_canonical = canonical_by_param[conv_param.name] + rec_canonical = ( + canonical_by_param.get(rec_url_param_name) + if rec_url_param_name + else None + ) + recording_date_canonical = ( + canonical_by_param.get(recording_date_param_name) + if recording_date_param_name + else None + ) + transcript_canonical = ( + canonical_by_param.get(transcript_param_name) + if transcript_param_name + else None + ) + + if validate_only: + # MAP-stage validation: every header check above has already + # run; the row loop only matters at IMPORT time. Skip it (and + # the "no data rows" guard at the bottom of the function) so + # the caller gets a clean pass when the mapping is shaped right. + return CallImportParseResult() + + parsed: List[Dict[str, Any]] = [] + skipped: List[CallImportParseSkip] = [] + for idx, row in enumerate(rows_iter): + # Drop fully-blank lines - matches the legacy parser behavior so + # trailing-newline edge cases don't fail an otherwise-good upload. + non_blank = any( + (row.get(c) or "").strip() + for c in mapped_canonicals + if c + ) + if not non_blank: + continue + + source_row = idx + 1 + conv_value = (row.get(conv_canonical) or "").strip() if conv_canonical else "" + if not conv_value: + skipped.append( + CallImportParseSkip( + source_row=source_row, + reason="missing_conversation_id", + message=( + f"Row {source_row} is missing the '{conv_param.name}' " + "(conversation_id) value." + ), + ) + ) + continue + + if rec_canonical and rec_url_param_name: + rec_param = next( + (p for p in parameters if p.name == rec_url_param_name), + None, + ) + if rec_param is not None and _parameter_is_required(rec_param): + rec_raw = (row.get(rec_canonical) or "").strip() + if not rec_raw: + skipped.append( + CallImportParseSkip( + source_row=source_row, + reason="missing_recording_url", + message=( + f"Row {source_row} is missing the required " + f"'{rec_url_param_name}' value." + ), + ) + ) + continue + if not _recording_url_cell_is_valid_http(rec_raw): + skipped.append( + CallImportParseSkip( + source_row=source_row, + reason="invalid_recording_url", + message=( + f"Row {source_row}: value for " + f"'{rec_url_param_name}' is not a valid recording " + "URL (must start with http:// or https://)." + ), + ) + ) + continue + + # Materialize every mapped parameter into the per-row snapshot, + # running per-type coercion so a bad cell aborts the upload + # rather than silently storing garbage. + parameter_values: Dict[str, Any] = {} + row_skipped = False + for param in parameters: + canonical = canonical_by_param[param.name] + if canonical is None: + continue + try: + param_type = CallImportParameterType(param.type) + except ValueError: + param_type = CallImportParameterType.TEXT + coerced = _coerce_parameter_value( + row.get(canonical) or "", + param_type, + row_idx=idx, + param_name=param.name, + ) + if _parameter_is_required(param) and coerced is None: + if param_type == CallImportParameterType.RECORDING_URL: + skipped.append( + CallImportParseSkip( + source_row=source_row, + reason="missing_recording_url", + message=( + f"Row {source_row} is missing the required " + f"'{param.name}' value." + ), + ) + ) + row_skipped = True + break + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Row {source_row} is missing the required " + f"'{param.name}' value." + ), + ) + parameter_values[param.name] = coerced + if row_skipped: + continue + + rec_value = ( + (row.get(rec_canonical) or "").strip() if rec_canonical else "" + ) + transcript_value = ( + (row.get(transcript_canonical) or "").strip() + if transcript_canonical + else "" + ) + recording_date_value = ( + parameter_values.get(recording_date_param_name) + if recording_date_param_name + else None + ) + + parsed.append( + { + "conversation_id": conv_value, + "recording_date": recording_date_value, + "recording_url": rec_value or None, + "transcript": transcript_value or None, + "parameter_values": parameter_values, + } + ) + + if not parsed and not skipped: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"{source_label} did not contain any data rows.", + ) + + return CallImportParseResult(rows=parsed, skipped=skipped) + + +def _raise_if_no_importable_rows( + result: CallImportParseResult, *, source_label: str = "CSV" +) -> None: + """Sync upload / API callers fail fast when every data row was skipped.""" + if result.rows: + return + if result.skipped: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"No importable rows. {len(result.skipped)} row(s) skipped due " + "to missing or invalid conversation ID or recording URL." + ), + ) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"{source_label} did not contain any data rows.", + ) + + +def _parse_csv( + file_bytes: bytes, + parameters: List[CallImportSchemaParameter], + parameter_mapping: Dict[str, str], + skipped_columns: List[str], +) -> CallImportParseResult: + """Parse a CSV file using the resolved schema parameters.""" + if not file_bytes: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Uploaded CSV is empty.", + ) + + try: + text_stream = io.StringIO(file_bytes.decode("utf-8-sig")) + except UnicodeDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="CSV must be UTF-8 encoded.", + ) + + reader = csv.DictReader(text_stream) + if not reader.fieldnames: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="CSV is missing a header row.", + ) + + return _apply_schema_mapping( + list(reader.fieldnames), + reader, + parameters, + parameter_mapping, + skipped_columns, + source_label="CSV", + ) + + +def _open_xlsx_workbook(file_bytes: bytes): + """Open an xlsx/xlsm workbook from in-memory bytes (read-only stream). + + Imports openpyxl lazily so the module loads even in environments that + haven't installed the optional dep yet (e.g. lightweight tooling + images). Surfaces a clean 400 if openpyxl is missing or the file is + not a valid Office Open XML workbook. + """ + if not file_bytes: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Uploaded Excel file is empty.", + ) + try: + from openpyxl import load_workbook # type: ignore + from openpyxl.utils.exceptions import InvalidFileException # type: ignore + except ImportError as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=( + "Excel uploads require the 'openpyxl' package which is " + "not installed in this environment." + ), + ) from exc + + try: + return load_workbook( + io.BytesIO(file_bytes), + read_only=True, + data_only=True, + ) + except InvalidFileException as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File is not a valid .xlsx workbook: {exc}", + ) from exc + except Exception as exc: # zipfile.BadZipFile etc. + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Could not open Excel workbook: {exc}", + ) from exc + + +def _xlsx_sheet_headers_and_rows( + worksheet, +) -> Tuple[List[str], List[Dict[str, str]]]: + """Read row 1 as headers and the rest as dicts of stringified cells. + + Empty trailing header cells are dropped. Duplicate headers preserve + the first occurrence (matches ``csv.DictReader`` behavior, which + silently drops duplicates). + """ + iterator = worksheet.iter_rows(values_only=True) + try: + header_row = next(iterator) + except StopIteration: + return [], [] + + headers: List[str] = [] + seen: set[str] = set() + for cell in header_row: + name = _xlsx_cell_to_str(cell).strip() + if not name: + # Stop at the first blank header — treats trailing empty + # columns as not part of the table (matches typical Excel + # workbook conventions). + break + norm = name.lower() + if norm in seen: + continue + seen.add(norm) + headers.append(name) + + rows: List[Dict[str, str]] = [] + for row in iterator: + if row is None: + continue + # Pad / truncate to the header length so dict construction is + # stable even when a row has fewer / extra cells than the header. + cells = list(row[: len(headers)]) + if len(cells) < len(headers): + cells.extend([None] * (len(headers) - len(cells))) + if not any(_xlsx_cell_to_str(c).strip() for c in cells): + # Skip fully-blank rows (openpyxl read_only routinely yields + # trailing empties when the worksheet's used range exceeds + # the actual data). + continue + rows.append( + { + header: _xlsx_cell_to_str(value) + for header, value in zip(headers, cells) + } + ) + + return headers, rows + + +def _parse_xlsx( + file_bytes: bytes, + sheet_name: Optional[str], + parameters: List[CallImportSchemaParameter], + parameter_mapping: Dict[str, str], + skipped_columns: List[str], +) -> CallImportParseResult: + """Parse a single worksheet from an xlsx/xlsm workbook. + + ``sheet_name`` must match one of the workbook's sheets (case + insensitive whitespace-trimmed match). Returns the same shape as + :func:`_parse_csv` so the upload handler can persist either format + through the same code path. + """ + if not sheet_name or not sheet_name.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="sheet_name is required when uploading an Excel workbook.", + ) + + workbook = _open_xlsx_workbook(file_bytes) + try: + sheet_names = list(workbook.sheetnames) + target_norm = sheet_name.strip().lower() + match = next( + (s for s in sheet_names if s.strip().lower() == target_norm), + None, + ) + if match is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Sheet '{sheet_name}' not found in workbook. " + f"Available sheets: {sheet_names}" + ), + ) + worksheet = workbook[match] + headers, rows = _xlsx_sheet_headers_and_rows(worksheet) + finally: + workbook.close() + + if not headers: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Sheet '{sheet_name}' is missing a header row.", + ) + + return _apply_schema_mapping( + headers, + rows, + parameters, + parameter_mapping, + skipped_columns, + source_label=f"Sheet '{sheet_name}'", + ) + + +def _csv_preview_sheets( + file_bytes: bytes, filename: Optional[str] +) -> List[CallImportPreviewSheet]: + """Build the synthetic single-sheet preview entry for a CSV upload.""" + if not file_bytes: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Uploaded CSV is empty.", + ) + try: + text_stream = io.StringIO(file_bytes.decode("utf-8-sig")) + except UnicodeDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="CSV must be UTF-8 encoded.", + ) + reader = csv.DictReader(text_stream) + if not reader.fieldnames: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="CSV is missing a header row.", + ) + headers = list(reader.fieldnames) + row_count = 0 + for row in reader: + # Match the parse-time skip: ignore fully blank rows so the + # count the user sees lines up with what /upload will ingest. + if any((v or "").strip() for v in row.values()): + row_count += 1 + + sheet_label = (filename or "sheet1").rsplit("/", 1)[-1] or "sheet1" + return [ + CallImportPreviewSheet( + name=sheet_label, + headers=headers, + row_count=row_count, + ) + ] + + +def _xlsx_preview_sheets(file_bytes: bytes) -> List[CallImportPreviewSheet]: + """List every worksheet in the workbook with its headers and row count.""" + workbook = _open_xlsx_workbook(file_bytes) + sheets: List[CallImportPreviewSheet] = [] + try: + for name in workbook.sheetnames: + worksheet = workbook[name] + headers, rows = _xlsx_sheet_headers_and_rows(worksheet) + sheets.append( + CallImportPreviewSheet( + name=name, + headers=headers, + row_count=len(rows), + ) + ) + finally: + workbook.close() + return sheets + + +def _parse_json_form_field(name: str, raw: Optional[str], default): + """Decode a JSON-encoded form field with a friendly 400 on bad JSON.""" + if raw is None or raw == "": + return default + try: + return json.loads(raw) + except json.JSONDecodeError as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"{name} must be valid JSON: {exc}", + ) + + +# --------------------------------------------------------------------------- +# Shared helpers used by the staged endpoints (UPLOAD / MAP / IMPORT) and the +# legacy one-shot ``POST /upload`` shim. Extracted here so each stage and the +# back-compat path operate on the exact same validation + persistence code. +# --------------------------------------------------------------------------- + + +def _source_content_type(fmt: str) -> str: + """Return the canonical ``Content-Type`` for a parsed file format.""" + if fmt == "csv": + return "text/csv" + if fmt == "xlsx": + return ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + return "application/octet-stream" + + +def _source_s3_key( + organization_id: UUID, call_import_id: UUID, fmt: str +) -> str: + """Build the canonical S3 key for an upload's source file. + + Mirrors the per-row recording key convention used by + ``process_call_import_row`` so a single prefix sweep on delete still + cleans up both the source artefact and every fetched recording. + """ + from app.services.storage.s3_service import s3_service + + ext = "xlsx" if fmt == "xlsx" else "csv" + return ( + f"{s3_service.prefix}organizations/{organization_id}/call_imports/" + f"{call_import_id}/source.{ext}" + ) + + +def _build_available_sheets( + file_bytes: bytes, fmt: str, filename: Optional[str] +) -> List[CallImportPreviewSheet]: + """Snapshot of sheets + headers cached on the batch at UPLOAD time.""" + if fmt == "csv": + return _csv_preview_sheets(file_bytes, filename) + return _xlsx_preview_sheets(file_bytes) + + +def _resolve_schema( + db: Session, + organization_id: UUID, + workspace_id: UUID, + schema_id: UUID, +) -> CallImportSchema: + """Fetch + validate a schema row in the active workspace. + + Eager-loads ``parameters`` so callers can iterate without re-querying. + """ + from sqlalchemy.orm import selectinload as _selectinload + + schema = ( + db.query(CallImportSchema) + .options(_selectinload(CallImportSchema.parameters)) + .filter( + CallImportSchema.id == schema_id, + CallImportSchema.organization_id == organization_id, + CallImportSchema.workspace_id == workspace_id, + ) + .first() + ) + if not schema: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Call import schema not found in the active workspace.", + ) + if not list(schema.parameters): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Selected schema has no parameters defined.", + ) + return schema + + +def _validate_direct_url_import_ready( + parameters: List[CallImportSchemaParameter], + parameter_mapping: Dict[str, Any], +) -> None: + """Ensure direct-URL import has a mapped recording_url column.""" + rec_url_param = next( + ( + p + for p in parameters + if p.type == CallImportParameterType.RECORDING_URL.value + ), + None, + ) + if rec_url_param is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Direct URL import requires a schema parameter of type " + "'recording_url'." + ), + ) + mapped_header = (parameter_mapping or {}).get(rec_url_param.name) + if not (mapped_header or "").strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Direct URL import requires the 'recording_url' parameter to " + "be mapped to a source column." + ), + ) + + +def _validate_exotel_import_ready( + parameters: List[CallImportSchemaParameter], + parameter_mapping: Dict[str, Any], +) -> None: + """Ensure Exotel credentialed import has a mapped recording_url column.""" + rec_url_param = next( + ( + p + for p in parameters + if p.type == CallImportParameterType.RECORDING_URL.value + ), + None, + ) + if rec_url_param is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Exotel import requires a schema parameter of type " + "'recording_url'." + ), + ) + mapped_header = (parameter_mapping or {}).get(rec_url_param.name) + if not (mapped_header or "").strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Exotel import requires the 'recording_url' parameter to " + "be mapped to a source column." + ), + ) + + +def _resolve_telephony_integration( + db: Session, + organization_id: UUID, + telephony_integration_id: UUID, + provider: str, +) -> TelephonyIntegration: + """Fetch + validate a telephony credential against the requested provider.""" + integration = ( + db.query(TelephonyIntegration) + .filter( + TelephonyIntegration.id == telephony_integration_id, + TelephonyIntegration.organization_id == organization_id, + ) + .first() + ) + if not integration: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Telephony credential not found for this organization.", + ) + if (integration.provider or "").lower() != provider.lower(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Selected credential is for provider '{integration.provider}', " + f"but request specified '{provider}'." + ), + ) + if not integration.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Selected telephony credential is inactive.", + ) + return integration + + +def _clean_parameter_mapping( + mapping_payload: Any, + parameters: List[CallImportSchemaParameter], + schema_name: str, +) -> Dict[str, str]: + """Trim values and drop empties; reject unknown parameter names. + + Accepts an already-decoded value (dict-shaped) so the same helper + works for the JSON-form upload path and the JSON-body PATCH path. + """ + if not isinstance(mapping_payload, dict) or not all( + isinstance(k, str) and (v is None or isinstance(v, str)) + for k, v in mapping_payload.items() + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "parameter_mapping must be an object of " + "{parameter_name: csv_header}." + ), + ) + + valid_param_names = {p.name for p in parameters} + cleaned: Dict[str, str] = {} + for raw_name, raw_header in mapping_payload.items(): + if raw_name not in valid_param_names: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"parameter_mapping references unknown parameter " + f"'{raw_name}' on schema '{schema_name}'." + ), + ) + header = (raw_header or "").strip() + if header: + cleaned[raw_name] = header + return cleaned + + +def _clean_skipped_columns(skipped_payload: Any) -> List[str]: + """Dedupe (case-insensitively) and drop blanks; preserve original casing.""" + if not isinstance(skipped_payload, list) or not all( + isinstance(item, str) for item in skipped_payload + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="skipped_columns must be a list of header strings.", + ) + cleaned: List[str] = [] + seen: set[str] = set() + for item in skipped_payload: + norm = _normalize_header(item) + if not norm or norm in seen: + continue + seen.add(norm) + cleaned.append(item) + return cleaned + + +def _parse_source_file( + file_bytes: bytes, + fmt: str, + sheet_name: Optional[str], + parameters: List[CallImportSchemaParameter], + cleaned_mapping: Dict[str, str], + cleaned_skipped: List[str], +) -> CallImportParseResult: + """Run the format-appropriate parser against a buffer of file bytes.""" + if fmt == "csv": + return _parse_csv(file_bytes, parameters, cleaned_mapping, cleaned_skipped) + return _parse_xlsx( + file_bytes, sheet_name, parameters, cleaned_mapping, cleaned_skipped + ) + + +def _materialize_rows( + db: Session, + call_import: CallImport, + parsed_rows: List[Dict[str, Any]], + organization_id: UUID, +) -> List[CallImportRow]: + """Insert one ``CallImportRow`` per parsed row, returning the new models.""" + row_models: List[CallImportRow] = [] + for idx, row in enumerate(parsed_rows): + # Stamp ``transcript_source='csv'`` when the upload actually + # provided a transcript so the UI badge ("From CSV") works from + # day one. Blank cells stay NULL so the row reads as "no + # production transcript yet". + csv_transcript = row["transcript"] + row_model = CallImportRow( + call_import_id=call_import.id, + organization_id=organization_id, + workspace_id=call_import.workspace_id, + row_index=idx, + conversation_id=row["conversation_id"], + recording_date=( + _parse_recording_date_cell(row["recording_date"]) + if row.get("recording_date") + else None + ), + recording_url=row["recording_url"], + transcript=csv_transcript, + transcript_source=( + "csv" if csv_transcript and csv_transcript.strip() else None + ), + raw_columns=row["parameter_values"] or None, + status=CallImportRowStatus.PENDING, + ) + db.add(row_model) + row_models.append(row_model) + return row_models + + +def _enqueue_row_tasks( + db: Session, + call_import: CallImport, + row_models: List[CallImportRow], +) -> None: + """Schedule fair round-robin dispatch for pending import rows.""" + del db, call_import, row_models + from app.workers.concurrency.fair_import_dispatch import ( + schedule_fair_import_dispatch, + ) + + schedule_fair_import_dispatch(max_workspace_turns=999) + + +def _ensure_blob_storage_enabled() -> None: + """Hard-fail UPLOAD if cloud blob storage isn't configured (no local fallback).""" + from app.services.storage.s3_service import s3_service + + if not s3_service.is_enabled(): + err = ( + s3_service.get_status_message() + or "Cloud blob storage is not enabled or not configured" + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=( + "Call uploads require cloud blob storage so the file can be " + f"persisted between stages: {err}" + ), + ) + + +def _validate_sheet_choice( + fmt: str, + sheet_name: Optional[str], + available_sheets: Optional[List[Dict[str, Any]]], +) -> Optional[str]: + """Normalize / validate ``sheet_name`` against the persisted snapshot. + + Returns the canonical sheet name (matching the workbook's casing) + so downstream parsing addresses the right worksheet. + """ + if fmt == "csv": + if sheet_name and sheet_name.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="sheet_name is not applicable to CSV uploads.", + ) + return None + + cleaned = (sheet_name or "").strip() or None + if cleaned is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="sheet_name is required when the source is an Excel workbook.", + ) + + if not available_sheets: + # Nothing to validate against (e.g. legacy batch without snapshot); + # let downstream parsing error out instead of silently importing. + return cleaned + + target = cleaned.strip().lower() + for entry in available_sheets: + name = entry.get("name") if isinstance(entry, dict) else None + if isinstance(name, str) and name.strip().lower() == target: + return name + sheet_names = [ + entry.get("name") for entry in available_sheets if isinstance(entry, dict) + ] + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + f"Sheet '{cleaned}' not found in the staged file. " + f"Available sheets: {sheet_names}" + ), + ) + + +def _tag_response_payload(tags: Optional[List[CallImportTag]]) -> List[Dict[str, Any]]: + """Shape a CallImport's tag relationship for the upload response.""" + return [ + { + "id": tag.id, + "name": tag.name, + "color": tag.color, + "created_at": tag.created_at, + "updated_at": tag.updated_at, + } + for tag in (tags or []) + ] + + +@router.post( + "/preview", + response_model=CallImportPreviewResponse, + operation_id="previewCallImportFile", +) +async def preview_call_import_file( + file: UploadFile = File(...), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + db: Session = Depends(get_db), +) -> CallImportPreviewResponse: + """Inspect an uploaded CSV / Excel file and return its sheets + headers. + + Drives the column-mapping UI without forcing the frontend to parse + CSV / xlsx itself — keeps client and server in lockstep on quoted + fields, encodings, and Excel cell coercion. CSVs return a single + synthetic sheet named after the filename; Excel workbooks return one + entry per worksheet (in workbook order). + """ + del api_key, organization_id, workspace_id, db # auth only + + fmt = _file_format(file.filename) + if fmt is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Unsupported file format. Allowed extensions: " + f"{', '.join(ALLOWED_EXTENSIONS)}." + ), + ) + + file_bytes = await file.read() + if len(file_bytes) > MAX_UPLOAD_BYTES: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"File exceeds {MAX_UPLOAD_BYTES} bytes", + ) + + if fmt == "csv": + sheets = _csv_preview_sheets(file_bytes, file.filename) + else: + sheets = _xlsx_preview_sheets(file_bytes) + + return CallImportPreviewResponse(format=fmt, sheets=sheets) + + +@router.post( + "", + response_model=CallImportResponse, + status_code=status.HTTP_201_CREATED, + operation_id="createCallImport", +) +async def create_call_import( + file: UploadFile = File( + ..., + description="CSV / Excel file to stage. Persisted to S3 between stages.", + ), + dataset: str = Form( + ..., + description=( + "Required free-text dataset label. Collected up-front so the " + "batch is filterable from the moment it lands." + ), + ), + tag_ids: Optional[List[UUID]] = Form( + None, + description="Optional list of CallImportTag ids to attach to the new batch.", + ), + schema_id: Optional[UUID] = Form( + None, + description=( + "Optional schema pre-pick. The user can still change it during " + "the MAP stage; provided here only so the detail page can pre-" + "select the schema dropdown." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportResponse: + """UPLOAD stage of the staged call-import flow. + + Persists the source file to S3 and creates a ``CallImport`` row with + ``status='uploaded'``. No mapping, no provider, no rows yet — the + user moves through MAP and IMPORT as separate idempotent steps. + + Dataset is collected here (rather than at IMPORT) so the batch is + filterable from the moment it appears in the list view. + """ + del api_key + + fmt = _file_format(file.filename) + if fmt is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Unsupported file format. Allowed extensions: " + f"{', '.join(ALLOWED_EXTENSIONS)}." + ), + ) + + normalized_dataset = _normalize_dataset(dataset) + if not normalized_dataset: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="dataset is required and must be a non-empty string.", + ) + + file_bytes = await file.read() + if len(file_bytes) > MAX_UPLOAD_BYTES: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"File exceeds {MAX_UPLOAD_BYTES} bytes", + ) + + # Parse-now so we (a) reject garbage uploads up-front instead of + # later in the MAP step, and (b) capture the sheets snapshot the + # MAP UI needs without having to re-fetch the file from S3. + sheets = _build_available_sheets(file_bytes, fmt, file.filename) + + # Optional schema pre-pick: validated only if supplied (the user is + # allowed to set it for the first time during MAP). + if schema_id is not None: + _resolve_schema(db, organization_id, workspace_id, schema_id) + + tag_rows = _resolve_tags(db, organization_id, tag_ids) + + _ensure_blob_storage_enabled() + + # Pre-generate the id so we can compute a deterministic S3 key + # before the row is persisted, keeping ``source_s3_key`` consistent + # with the prefix sweep used at delete-time. + import uuid as _uuid + + call_import_id = _uuid.uuid4() + s3_key = _source_s3_key(organization_id, call_import_id, fmt) + content_type = _source_content_type(fmt) + + from app.services.storage.s3_service import s3_service, StorageError + + try: + s3_service.upload_file_by_key(file_bytes, s3_key, content_type=content_type) + except StorageError as exc: + logger.exception( + "Failed to upload source file to S3 for new call import {}", + call_import_id, + ) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Failed to persist upload to S3: {exc}", + ) + + call_import = CallImport( + id=call_import_id, + organization_id=organization_id, + workspace_id=workspace_id, + # Provider + credential aren't known until the IMPORT stage; leave + # them NULL so the staged-vs-legacy distinction is visible at a + # glance from the DB. + provider=None, + telephony_integration_id=None, + original_filename=file.filename, + sheet_name=None, + dataset=normalized_dataset, + schema_id=schema_id, + parameter_mapping={}, + skipped_columns=[], + column_mapping={}, + extra_columns=[], + custom_column_mapping={}, + source_s3_key=s3_key, + source_format=fmt, + source_size_bytes=len(file_bytes), + source_content_type=content_type, + available_sheets=[sheet.model_dump() for sheet in sheets], + total_rows=0, + completed_rows=0, + failed_rows=0, + status=CallImportStatus.UPLOADED, + ) + if tag_rows: + call_import.tags = tag_rows + + stamp_call_import_actor(call_import, principal, creating=True) + db.add(call_import) + try: + db.commit() + except Exception: + db.rollback() + # Best-effort cleanup of the uploaded S3 object so a failed + # commit doesn't leak storage. + try: + s3_service.delete_file_by_key(s3_key) + except Exception as cleanup_exc: # noqa: BLE001 + logger.warning( + "Failed to clean up orphaned S3 object {} after DB rollback: {}", + s3_key, + cleanup_exc, + ) + raise + + db.refresh(call_import) + return _serialize_call_import(db, call_import) + + +@router.patch( + "/{call_import_id}/mapping", + response_model=CallImportResponse, + operation_id="updateCallImportMapping", +) +async def update_call_import_mapping( + call_import_id: UUID, + payload: CallImportMappingUpdate, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportResponse: + """MAP stage of the staged call-import flow. + + Validates ``parameter_mapping`` + ``skipped_columns`` against the + sheet headers captured at UPLOAD time and persists them on the + batch. Idempotent: callers may submit this multiple times while + the batch is in ``uploaded`` or ``mapped`` state. + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + CallImport.workspace_id == workspace_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + if call_import.status not in ( + CallImportStatus.UPLOADED, + CallImportStatus.MAPPED, + ): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + f"Cannot edit mapping on a batch in status " + f"'{call_import.status.value}'. Mapping can only be edited " + "before the IMPORT stage." + ), + ) + + if not call_import.source_s3_key or not call_import.source_format: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "This batch was not uploaded through the staged flow and " + "cannot have its mapping edited." + ), + ) + + schema = _resolve_schema( + db, organization_id, workspace_id, payload.schema_id + ) + parameters = list(schema.parameters) + + canonical_sheet = _validate_sheet_choice( + call_import.source_format, + payload.sheet_name, + call_import.available_sheets, + ) + + # Pull the headers for the selected sheet straight out of the + # snapshot so we don't have to re-download the file from S3 just to + # validate the mapping. + headers: List[str] = [] + if call_import.available_sheets: + if canonical_sheet is None: + # CSV: single synthetic sheet. + entry = call_import.available_sheets[0] + headers = list(entry.get("headers") or []) + else: + for entry in call_import.available_sheets: + if not isinstance(entry, dict): + continue + name = entry.get("name") + if isinstance(name, str) and name == canonical_sheet: + headers = list(entry.get("headers") or []) + break + + cleaned_mapping = _clean_parameter_mapping( + payload.parameter_mapping, parameters, schema.name + ) + cleaned_skipped = _clean_skipped_columns(payload.skipped_columns) + + # Run the same per-column validation as the parse path so the user + # gets an immediate 400 if a required parameter is left unmapped or + # a header is neither mapped nor skipped — without needing to read + # the file. ``validate_only`` skips the row loop (and the empty-rows + # guard) since the row data lives in S3, not in this request. + if headers: + _apply_schema_mapping( + headers, + iter(()), + parameters, + cleaned_mapping, + cleaned_skipped, + source_label=( + f"Sheet '{canonical_sheet}'" + if canonical_sheet is not None + else "CSV" + ), + validate_only=True, + ) + + call_import.schema_id = schema.id + call_import.parameter_mapping = dict(cleaned_mapping) + call_import.skipped_columns = list(cleaned_skipped) + call_import.sheet_name = canonical_sheet + call_import.status = CallImportStatus.MAPPED + stamp_call_import_actor(call_import, principal) + db.commit() + db.refresh(call_import) + return _serialize_call_import(db, call_import) + + +@router.post( + "/{call_import_id}/import", + response_model=CallImportUploadResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="startCallImport", +) +async def start_call_import( + call_import_id: UUID, + payload: CallImportStartRequest, + background_tasks: BackgroundTasks, + legacy: bool = Query( + False, + description=( + "Deprecated escape hatch for import-only processing. " + "New batches should use Run Evaluation instead." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportUploadResponse: + """Deprecated IMPORT stage — use Run Evaluation for new batches. + + Recording fetch is part of the unified evaluation pipeline. This + endpoint remains available only with ``?legacy=true`` for backward + compatibility. + """ + del api_key, background_tasks + + if not legacy: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "Standalone import is deprecated. Use Run Evaluation — " + "recording fetch is part of the evaluation pipeline. " + "Append ?legacy=true to use the import-only path." + ), + ) + + from sqlalchemy.orm import selectinload as _selectinload + + call_import = ( + db.query(CallImport) + .options(_selectinload(CallImport.tags)) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + CallImport.workspace_id == workspace_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + if call_import.status != CallImportStatus.MAPPED: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + f"Cannot start import for a batch in status " + f"'{call_import.status.value}'. Map the columns first." + ), + ) + + if not call_import.source_s3_key or not call_import.source_format: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "This batch has no staged source file and cannot be imported " + "through the staged flow." + ), + ) + + if not call_import.schema_id: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Cannot start import without a mapped schema.", + ) + + schema = _resolve_schema( + db, organization_id, workspace_id, call_import.schema_id + ) + parameters = list(schema.parameters) + + if payload.telephony_integration_id is not None: + integration = _resolve_telephony_integration( + db, + organization_id, + payload.telephony_integration_id, + payload.provider or "", + ) + if (integration.provider or "").lower() == "exotel": + _validate_exotel_import_ready( + parameters, dict(call_import.parameter_mapping or {}) + ) + else: + _validate_direct_url_import_ready( + parameters, dict(call_import.parameter_mapping or {}) + ) + integration = None + + _ensure_blob_storage_enabled() + + if integration is not None: + call_import.provider = integration.provider + call_import.telephony_integration_id = integration.id + else: + call_import.provider = None + call_import.telephony_integration_id = None + + call_import.total_rows = 0 + call_import.completed_rows = 0 + call_import.failed_rows = 0 + call_import.error_message = None + call_import.status = CallImportStatus.PROCESSING + stamp_call_import_actor(call_import, principal) + db.commit() + db.refresh(call_import) + + from app.workers.tasks.call_import_bulk_ops import ( + materialize_call_import_rows_task, + ) + + materialize_call_import_rows_task.delay( + str(call_import_id), + str(organization_id), + str(workspace_id), + schedule_import_dispatch=True, + ) + + return CallImportUploadResponse( + id=call_import.id, + total_rows=0, + status=call_import.status, + dataset=call_import.dataset, + tags=_tag_response_payload(call_import.tags), + message=( + "Import accepted. Rows are being materialized in the background; " + "recordings will be fetched asynchronously." + ), + ) + + +@router.post( + "/upload", + response_model=CallImportUploadResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="uploadCallImportCsv", + deprecated=True, +) +async def upload_call_import_csv( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + provider: Optional[str] = Form( + None, + description=( + "Telephony provider key (e.g. 'exotel', 'plivo'). Must match the " + "selected telephony_integration_id's provider. Omit together " + "with telephony_integration_id for direct-URL import." + ), + ), + telephony_integration_id: Optional[UUID] = Form( + None, + description=( + "Specific TelephonyIntegration credential row to use when " + "downloading recordings for this batch. Omit together with " + "provider for direct-URL import." + ), + ), + schema_id: UUID = Form( + ..., + description=( + "Reusable Input Parameter schema this upload is mapped against. " + "Must belong to the active workspace." + ), + ), + parameter_mapping: str = Form( + ..., + description=( + "JSON-encoded ``{schema_parameter_name: source_header}`` map " + "covering every required schema parameter. Optional parameters " + "may be omitted or set to an empty string." + ), + ), + skipped_columns: Optional[str] = Form( + None, + description=( + "JSON-encoded list of source header strings the uploader has " + "explicitly skipped. Every header in the file must either be " + "mapped or appear here; otherwise the upload is rejected so a " + "forgotten column never silently drops." + ), + ), + dataset: Optional[str] = Form( + None, + description=( + "Optional free-text dataset label for high-level segregation. " + "Empty strings are stored as NULL." + ), + ), + tag_ids: Optional[List[UUID]] = Form( + None, + description="Optional list of CallImportTag ids to attach to the new batch.", + ), + sheet_name: Optional[str] = Form( + None, + description=( + "Worksheet to import when the file is an Excel workbook " + "(.xlsx / .xlsm). REQUIRED for Excel uploads. Ignored for CSV " + "uploads (rejected with 400 if non-empty so typos surface " + "instead of silently importing the wrong source)." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportUploadResponse: + """Legacy one-shot upload kept for backward compatibility. + + DEPRECATED: prefer the staged flow + (``POST /`` → ``PATCH /{id}/mapping`` → ``POST /{id}/import``) so + each step is idempotent and resumable. This endpoint runs all three + stages inline in a single transaction so existing scripts / + integrations keep working unchanged. + """ + del api_key + + fmt = _file_format(file.filename) + if fmt is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "Unsupported file format. Allowed extensions: " + f"{', '.join(ALLOWED_EXTENSIONS)}." + ), + ) + + sheet_name_clean = (sheet_name or "").strip() or None + if fmt == "csv" and sheet_name_clean is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="sheet_name is not applicable to CSV uploads.", + ) + if fmt == "xlsx" and sheet_name_clean is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="sheet_name is required when uploading an Excel workbook.", + ) + + file_bytes = await file.read() + if len(file_bytes) > MAX_UPLOAD_BYTES: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"File exceeds {MAX_UPLOAD_BYTES} bytes", + ) + + schema = _resolve_schema(db, organization_id, workspace_id, schema_id) + parameters = list(schema.parameters) + + mapping_payload = _parse_json_form_field( + "parameter_mapping", parameter_mapping, {} + ) + cleaned_mapping = _clean_parameter_mapping( + mapping_payload, parameters, schema.name + ) + + skipped_payload = _parse_json_form_field("skipped_columns", skipped_columns, []) + cleaned_skipped = _clean_skipped_columns(skipped_payload) + + has_provider = bool((provider or "").strip()) + has_integration = telephony_integration_id is not None + if has_provider != has_integration: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=( + "provider and telephony_integration_id must both be provided " + "or both omitted for direct-URL import." + ), + ) + + if telephony_integration_id is not None: + integration = _resolve_telephony_integration( + db, organization_id, telephony_integration_id, provider or "" + ) + if (integration.provider or "").lower() == "exotel": + _validate_exotel_import_ready(parameters, cleaned_mapping) + else: + _validate_direct_url_import_ready(parameters, cleaned_mapping) + integration = None + + parsed_rows = _parse_source_file( + file_bytes, fmt, sheet_name_clean, parameters, cleaned_mapping, cleaned_skipped + ) + _raise_if_no_importable_rows(parsed_rows, source_label=fmt) + + tag_rows = _resolve_tags(db, organization_id, tag_ids) + + call_import = CallImport( + organization_id=organization_id, + workspace_id=workspace_id, + provider=integration.provider if integration is not None else None, + telephony_integration_id=integration.id if integration is not None else None, + original_filename=file.filename, + sheet_name=sheet_name_clean, + dataset=_normalize_dataset(dataset), + schema_id=schema.id, + parameter_mapping=dict(cleaned_mapping), + skipped_columns=list(cleaned_skipped), + # Legacy columns are left empty on new uploads; the detail page + # falls back to ``parameter_mapping`` when ``schema_id`` is set. + column_mapping={}, + extra_columns=[], + custom_column_mapping={}, + total_rows=len(parsed_rows.rows), + completed_rows=0, + failed_rows=0, + status=CallImportStatus.PENDING, + source_row_skips=parse_skips_to_json(parsed_rows.skipped), + ) + if tag_rows: + call_import.tags = tag_rows + stamp_call_import_actor(call_import, principal, creating=True) + db.add(call_import) + db.flush() # populate call_import.id + if integration is None: + # The model's historical Python default is "exotel"; direct-URL + # imports intentionally have no telephony provider. + call_import.provider = None + + row_models = _materialize_rows( + db, call_import, parsed_rows.rows, organization_id + ) + + call_import.status = CallImportStatus.PROCESSING + stamp_call_import_actor(call_import, principal) + db.commit() + db.refresh(call_import) + + background_tasks.add_task( + record_call_import_batch_created, + organization_id, + call_import.id, + workspace_id=workspace_id, + total_rows=call_import.total_rows, + source="csv", + provider=call_import.provider, + ) + + _enqueue_row_tasks(db, call_import, row_models) + + return CallImportUploadResponse( + id=call_import.id, + total_rows=call_import.total_rows, + status=call_import.status, + dataset=call_import.dataset, + tags=_tag_response_payload(call_import.tags), + message=( + f"Accepted {call_import.total_rows} rows for import. " + "Recordings will be fetched asynchronously." + ), + ) + + +@router.post( + "/audio-upload", + response_model=CallImportUploadResponse, + status_code=status.HTTP_201_CREATED, + operation_id="uploadCallImportAudio", +) +async def upload_call_import_audio( + background_tasks: BackgroundTasks, + files: List[UploadFile] = File( + ..., + description="One or more manual call recording audio files.", + ), + dataset: str = Form( + ..., + description="Required free-text dataset label for the manual upload batch.", + ), + tag_ids: Optional[List[UUID]] = Form( + None, + description="Optional list of CallImportTag ids to attach to the new batch.", + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportUploadResponse: + """Persist manually uploaded recordings as completed CallImport rows. + + The rows skip the provider-download worker entirely because the audio + bytes are already in hand. From this point onward they behave exactly + like completed CSV-import rows: playback reads ``recording_s3_key`` and + the existing diarisation/evaluation endpoints can operate on them. + """ + + normalized_dataset = _normalize_dataset(dataset) + if not normalized_dataset: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="dataset is required and must be a non-empty string.", + ) + if not files: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="At least one audio file is required.", + ) + + _ensure_blob_storage_enabled() + tag_rows = _resolve_tags(db, organization_id, tag_ids) + + max_bytes = int(settings.MAX_FILE_SIZE_MB) * 1024 * 1024 + prepared: List[Dict[str, Any]] = [] + conversation_counts: Dict[str, int] = {} + + for idx, upload in enumerate(files): + filename = upload.filename or f"recording-{idx + 1}" + ext = _audio_extension(filename) + if not ext: + allowed = ", ".join(settings.ALLOWED_AUDIO_FORMATS) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Unsupported audio file '{filename}'. Allowed formats: {allowed}.", + ) + + contents = await upload.read() + if not contents: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Audio file '{filename}' is empty.", + ) + if len(contents) > max_bytes: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=( + f"Audio file '{filename}' exceeds " + f"{settings.MAX_FILE_SIZE_MB} MB." + ), + ) + + base_conversation_id = _sanitize_conversation_id(_filename_stem(filename)) + conversation_id = _dedupe_conversation_id( + base_conversation_id, + conversation_counts, + ) + prepared.append( + { + "filename": filename, + "extension": ext, + "content_type": _audio_content_type(ext, upload.content_type), + "contents": contents, + "conversation_id": conversation_id, + } + ) + + original_filename = ( + prepared[0]["filename"] + if len(prepared) == 1 + else f"{len(prepared)} manual recordings" + ) + total_size = sum(len(item["contents"]) for item in prepared) + uploaded_keys: List[str] = [] + + from app.services.storage.s3_service import s3_service + + call_import = CallImport( + organization_id=organization_id, + workspace_id=workspace_id, + provider=None, + telephony_integration_id=None, + original_filename=original_filename, + source_format="audio", + source_size_bytes=total_size, + source_content_type="audio/*", + dataset=normalized_dataset, + total_rows=len(prepared), + completed_rows=len(prepared), + failed_rows=0, + status=CallImportStatus.COMPLETED, + ) + if tag_rows: + call_import.tags = tag_rows + + stamp_call_import_actor(call_import, principal, creating=True) + try: + db.add(call_import) + db.flush() + # The model's historical Python default is "exotel"; manual uploads + # intentionally have no telephony provider. + call_import.provider = None + + row_mappings: List[Dict[str, Any]] = [] + for idx, item in enumerate(prepared): + row_id = uuid4() + key = _audio_s3_key( + organization_id, + call_import.id, + row_id, + item["extension"], + ) + s3_service.upload_file_by_key( + item["contents"], + key, + content_type=item["content_type"], + ) + uploaded_keys.append(key) + + row_mappings.append( + { + "id": row_id, + "call_import_id": call_import.id, + "organization_id": organization_id, + "workspace_id": workspace_id, + "row_index": idx, + "conversation_id": item["conversation_id"], + "recording_url": None, + "transcript": None, + "transcript_source": None, + "raw_columns": {"conversation_id": item["conversation_id"]}, + "status": CallImportRowStatus.COMPLETED, + "recording_s3_key": key, + "recording_content_type": item["content_type"], + "recording_size_bytes": len(item["contents"]), + } + ) + + if is_sharding_enabled(): + from app.db_sharding.row_ops import ( + bulk_insert_mappings_on_shards, + register_shard_slices, + ) + + bulk_insert_mappings_on_shards(db, call_import.id, row_mappings) + register_shard_slices(db, call_import.id, len(row_mappings)) + else: + for mapping in row_mappings: + db.add(CallImportRow(**mapping)) + + db.commit() + except Exception as exc: + db.rollback() + if uploaded_keys and s3_service.is_enabled(): + try: + s3_service.delete_keys(uploaded_keys) + except Exception: + logger.exception( + "Failed to clean up manual audio upload keys after error" + ) + logger.exception("Failed to persist manual call recording upload") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to upload manual recordings: {exc}", + ) from exc + + db.refresh(call_import) + background_tasks.add_task( + record_call_import_batch_created, + organization_id, + call_import.id, + workspace_id=workspace_id, + total_rows=call_import.total_rows, + source="audio", + provider=None, + ) + return CallImportUploadResponse( + id=call_import.id, + total_rows=call_import.total_rows, + status=call_import.status, + dataset=call_import.dataset, + tags=_tag_response_payload(call_import.tags), + message=( + f"Uploaded {call_import.total_rows} manual recording" + f"{'' if call_import.total_rows == 1 else 's'}." + ), + ) + + +@router.get( + "", + response_model=CallImportListResponse, + operation_id="listCallImports", +) +async def list_call_imports( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + status_filter: Optional[CallImportStatus] = Query(None, alias="status"), + dataset: Optional[str] = Query( + None, + description=( + "Filter by exact dataset string (case-insensitive). Pass the " + "literal value '__none__' to filter to imports with no dataset." + ), + ), + tag_id: Optional[List[UUID]] = Query( + None, + description="Filter to imports tagged with ALL of the given tag ids.", + ), + source_format: Optional[str] = Query( + None, + description=( + "Filter by source format. Use 'audio' for manual recordings or " + "'__non_audio__' for CSV/Excel/legacy imports." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + db: Session = Depends(get_db), +) -> CallImportListResponse: + """List call-import batches for the active workspace, newest first. + + Scoped to (organization_id, workspace_id) so users only see imports + for the workspace they're currently in. Supports a high-level + ``dataset`` filter (powers the segregation dropdown at the top of + the imports page) plus an AND-style multi-tag filter via repeated + ``tag_id`` parameters. + """ + + query = ( + db.query(CallImport) + .filter( + CallImport.organization_id == organization_id, + CallImport.workspace_id == workspace_id, + ) + ) + if status_filter is not None: + query = query.filter(CallImport.status == status_filter) + + source_filter = (source_format or "").strip().lower() + if source_filter == "__non_audio__": + query = query.filter( + or_(CallImport.source_format.is_(None), CallImport.source_format != "audio") + ) + elif source_filter: + query = query.filter(func.lower(CallImport.source_format) == source_filter) + + if dataset is not None: + if dataset == "__none__": + query = query.filter(CallImport.dataset.is_(None)) + elif dataset.strip(): + query = query.filter( + func.lower(CallImport.dataset) == dataset.strip().lower() + ) + + if tag_id: + from app.models.database import CallImportTagAssignment + + for single_tag_id in tag_id: + sub = ( + db.query(CallImportTagAssignment.call_import_id) + .filter(CallImportTagAssignment.tag_id == single_tag_id) + .subquery() + ) + query = query.filter(CallImport.id.in_(sub)) + + total = query.count() + items = ( + query.order_by(desc(CallImport.created_at)) + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) + + email_map = emails_for_user_ids(db, user_ids_from_call_imports(items)) + return CallImportListResponse( + items=[ + _serialize_call_import(db, item, user_emails=email_map) + for item in items + ], + total=total, + page=page, + page_size=page_size, + ) + + +@router.get( + "/dispatch-diagnostics", + response_model=CallImportDispatchDiagnosticsResponse, + operation_id="getCallImportDispatchDiagnostics", + dependencies=[Depends(require_admin)], +) +async def get_call_import_dispatch_diagnostics( + workspace_id: Optional[UUID] = Query( + None, + description=( + "Optional workspace filter. When omitted, returns every workspace " + "in the organization with active eval dispatch state." + ), + ), + include_idle_workspaces: bool = Query( + False, + description=( + "When true, include org workspaces with zero pending rows and " + "zero in-flight slots." + ), + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportDispatchDiagnosticsResponse: + """Live eval slot usage and fair-dispatch state for operators. + + Org admins use this to diagnose cross-workspace starvation (e.g. one + workspace's 10k run blocking another's pending eval rows) by inspecting + Redis in-flight counters, pending dispatch rows, and scheduler cursors. + """ + del api_key + payload = build_call_import_dispatch_diagnostics( + db, + organization_id, + workspace_id=workspace_id, + include_idle_workspaces=include_idle_workspaces, + ) + return CallImportDispatchDiagnosticsResponse.model_validate(payload) + + +@router.get( + "/datasets", + response_model=List[str], + operation_id="listCallImportDatasets", +) +async def list_call_import_datasets( + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + db: Session = Depends(get_db), +) -> List[str]: + """Return the distinct, non-null dataset labels in use for the active + workspace. + + Scoped per-workspace so each workspace's Dataset dropdown only shows + its own segregation labels. + """ + rows = ( + db.query(CallImport.dataset) + .filter( + CallImport.organization_id == organization_id, + CallImport.workspace_id == workspace_id, + CallImport.dataset.isnot(None), + CallImport.dataset != "", + ) + .distinct() + .order_by(CallImport.dataset.asc()) + .all() + ) + return [row[0] for row in rows if row[0]] + + +@router.get( + "/diarisation-prompt-default", + response_model=CallImportDiarisationPromptDefaultResponse, + operation_id="getCallImportDiarisationPromptDefault", +) +async def get_call_import_diarisation_prompt_default( + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), +) -> CallImportDiarisationPromptDefaultResponse: + """Return the canonical LLM diariser prompt. + + The Transcribe / Run Evaluation modals call this on open so they + can pre-fill the prompt textarea. Returning the constant from the + backend (rather than hard-coding it in the frontend) keeps the + fallback used by the worker and the placeholder shown in the UI + in lock-step — operators always see the *actual* default they'd + get if they leave the field blank. + + Registered before ``GET /{call_import_id}`` so the static path is + not mistaken for a UUID import id (which would 422). + """ + del api_key, organization_id + from app.workers.tasks.helpers.llm_diarisation import ( + DEFAULT_DIARIZATION_PROMPT, + ) + + return CallImportDiarisationPromptDefaultResponse( + prompt=DEFAULT_DIARIZATION_PROMPT + ) + + +@router.patch( + "/{call_import_id}", + response_model=CallImportResponse, + operation_id="updateCallImport", +) +async def update_call_import( + call_import_id: UUID, + payload: CallImportUpdate, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportResponse: + """Edit dataset / tag assignments (and schema, pre-import) on a batch. + + ``dataset = ""`` clears the label; ``tag_ids = []`` removes all tag + assignments. Fields omitted from the body are left untouched. + + ``schema_id`` is only honoured while the batch is in + ``uploaded`` / ``mapped`` state — once rows have been materialised + the schema is locked. Changing the schema resets any persisted + mapping (the user must re-MAP) and rewinds status to ``uploaded``. + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + body = payload.model_dump(exclude_unset=True) + if "dataset" in body: + call_import.dataset = _normalize_dataset(body["dataset"]) + + if "tag_ids" in body: + tag_ids = body["tag_ids"] or [] + call_import.tags = _resolve_tags(db, organization_id, tag_ids) + + if "schema_id" in body and body["schema_id"] is not None: + if call_import.status not in ( + CallImportStatus.UPLOADED, + CallImportStatus.MAPPED, + ): + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + f"Cannot reassign schema on a batch in status " + f"'{call_import.status.value}'." + ), + ) + new_schema = _resolve_schema( + db, organization_id, workspace_id, body["schema_id"] + ) + if call_import.schema_id != new_schema.id: + # Switching schemas invalidates the persisted mapping — + # parameter names won't line up with the new schema, so + # reset to UPLOADED and force a fresh MAP. + call_import.schema_id = new_schema.id + call_import.parameter_mapping = {} + call_import.skipped_columns = [] + call_import.sheet_name = None + call_import.status = CallImportStatus.UPLOADED + + stamp_call_import_actor(call_import, principal) + db.commit() + db.refresh(call_import) + return _serialize_call_import(db, call_import) + + +@router.get( + "/{call_import_id}", + response_model=CallImportDetailResponse, + operation_id="getCallImportDetail", +) +async def get_call_import_detail( + call_import_id: UUID, + row_limit: int = Query(500, ge=0, le=5000), + row_offset: int = Query(0, ge=0), + q: Optional[str] = Query( + None, + description=( + "Optional case-insensitive substring filter on " + "``conversation_id``. When set, ``filtered_total_rows`` in " + "the response reflects the post-filter row count so the UI " + "can paginate against the filtered slice." + ), + ), + diarised_status: Optional[str] = Query( + None, + description=( + "Optional filter on ``CallImportRow.diarised_transcript_status``. " + "Accepts one of ``pending``, ``running``, ``completed``, " + "``failed``. When set, ``filtered_total_rows`` reflects the " + "post-filter row count (combined with the ``q`` filter when " + "both are supplied) so the UI can paginate against the same " + "slice it's displaying." + ), + pattern="^(pending|running|completed|failed)$", + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportDetailResponse: + """Fetch a single import batch with a slice of its rows. + + ``row_limit=0`` is intentionally allowed so callers that only need the + batch metadata (e.g. the evaluation-detail page rendering the parent's + column mapping) can skip the rows payload entirely. + """ + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + if call_import.status == CallImportStatus.PROCESSING: + from app.services.call_imports.bulk_ops import rollup_call_import_batch_status + + prior_status = call_import.status + rollup_call_import_batch_status(db, call_import) + if call_import.status != prior_status: + db.commit() + db.refresh(call_import) + + search_term = (q or "").strip() + diarised_status_filter = (diarised_status or "").strip() or None + filtered_total_rows: Optional[int] = None + has_row_filters = bool(search_term or diarised_status_filter) + + if has_row_filters: + if is_sharding_enabled(): + from app.db_sharding.scatter_gather import count_call_import_rows_filtered + + filtered_total_rows = count_call_import_rows_filtered( + db, + call_import.id, + search_term=search_term, + diarised_status_filter=diarised_status_filter, + ) + else: + rows_query = db.query(CallImportRow).filter( + CallImportRow.call_import_id == call_import.id + ) + if search_term: + rows_query = rows_query.filter( + CallImportRow.conversation_id.ilike(f"%{search_term}%") + ) + if diarised_status_filter: + rows_query = rows_query.filter( + CallImportRow.diarised_transcript_status == diarised_status_filter + ) + filtered_total_rows = rows_query.count() + + if row_limit == 0: + rows: List[CallImportRow] = [] + elif is_sharding_enabled(): + from app.db_sharding.scatter_gather import ( + fetch_call_import_rows_filtered_page, + fetch_call_import_rows_page, + ) + + if has_row_filters: + rows = fetch_call_import_rows_filtered_page( + db, + call_import.id, + search_term=search_term, + diarised_status_filter=diarised_status_filter, + offset=row_offset, + limit=row_limit, + ) + else: + rows = fetch_call_import_rows_page( + db, + call_import.id, + offset=row_offset, + limit=row_limit, + ) + else: + rows_query = db.query(CallImportRow).filter( + CallImportRow.call_import_id == call_import.id + ) + if search_term: + rows_query = rows_query.filter( + CallImportRow.conversation_id.ilike(f"%{search_term}%") + ) + if diarised_status_filter: + rows_query = rows_query.filter( + CallImportRow.diarised_transcript_status == diarised_status_filter + ) + rows = ( + rows_query.order_by(CallImportRow.row_index) + .offset(row_offset) + .limit(row_limit) + .all() + ) + + # Batch-wide diarisation status aggregate. One ``GROUP BY`` query + # across the whole batch — much cheaper than paging through every + # row to recount on the client and lets the UI render a + # transcribe/diarise progress bar without a separate roundtrip. + if is_sharding_enabled(): + from app.db_sharding.scatter_gather import aggregate_diarised_transcript_counts + + diarised_status_counts = aggregate_diarised_transcript_counts( + db, call_import.id + ) + else: + diarised_status_counts: Dict[str, int] = {} + for status_value, count in ( + db.query(CallImportRow.diarised_transcript_status, func.count()) + .filter(CallImportRow.call_import_id == call_import.id) + .group_by(CallImportRow.diarised_transcript_status) + .all() + ): + if isinstance(status_value, str): + diarised_status_counts[status_value] = int(count or 0) + + detail = CallImportDetailResponse.model_validate( + _serialize_call_import(db, call_import).model_dump() + ) + detail.rows = [CallImportRowResponse.model_validate(r) for r in rows] + detail.filtered_total_rows = filtered_total_rows + detail.diarised_pending_rows = diarised_status_counts.get("pending", 0) + detail.diarised_running_rows = diarised_status_counts.get("running", 0) + detail.diarised_completed_rows = diarised_status_counts.get("completed", 0) + detail.diarised_failed_rows = diarised_status_counts.get("failed", 0) + return detail + + +@router.get( + "/{call_import_id}/row-ids", + response_model=CallImportRowIdsResponse, + operation_id="listCallImportRowIds", +) +async def list_call_import_row_ids( + call_import_id: UUID, + q: Optional[str] = Query( + None, + description=( + "Optional case-insensitive substring filter on " + "``conversation_id``. Same semantics as the detail endpoint." + ), + ), + diarised_status: Optional[str] = Query( + None, + description=( + "Optional filter on ``CallImportRow.diarised_transcript_status``. " + "Accepts ``pending`` / ``running`` / ``completed`` / ``failed``." + ), + pattern="^(pending|running|completed|failed)$", + ), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportRowIdsResponse: + """Return every matching ``CallImportRow.id`` for cross-page bulk select. + + Lightweight companion to ``GET /{call_import_id}`` — the detail + endpoint caps ``row_limit`` at 5000 and ships the entire row body + on each page, so harvesting ids that way is wasteful when the + user just wants to bulk-delete or bulk-transcribe everything that + matches the current filters. This endpoint applies the same ``q`` + and ``diarised_status`` filters and returns only the ids. + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + search_term = (q or "").strip() + status_filter = (diarised_status or "").strip() or None + + if is_sharding_enabled(): + from app.db_sharding.scatter_gather import list_call_import_row_ids_filtered + + ids = list_call_import_row_ids_filtered( + db, + call_import.id, + search_term=search_term, + diarised_status_filter=status_filter, + ) + return CallImportRowIdsResponse(ids=ids, total=len(ids)) + + rows_query = db.query(CallImportRow.id).filter( + CallImportRow.call_import_id == call_import.id + ) + if search_term: + rows_query = rows_query.filter( + CallImportRow.conversation_id.ilike(f"%{search_term}%") + ) + if status_filter: + rows_query = rows_query.filter( + CallImportRow.diarised_transcript_status == status_filter + ) + + ids = [ + row_id + for (row_id,) in rows_query.order_by(CallImportRow.row_index).all() + ] + return CallImportRowIdsResponse(ids=ids, total=len(ids)) + + +def _revoke_pending_tasks(rows: List[CallImportRow]) -> None: + """Best-effort revoke of in-flight Celery tasks for the given rows. + + Failures are logged and swallowed — Celery's control plane is async and + best-effort by design, and we always do an idempotent S3 cleanup + afterwards so a missed revoke can't leak storage. + """ + task_ids = [ + r.celery_task_id + for r in rows + if r.celery_task_id + and r.status in (CallImportRowStatus.PENDING, CallImportRowStatus.PROCESSING) + ] + if not task_ids: + return + + try: + from app.workers.celery_app import celery_app + + celery_app.control.revoke(task_ids, terminate=False) + logger.info("Revoked {} pending call-import tasks", len(task_ids)) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to revoke pending call-import tasks: {}", exc) + + +def _delete_s3_objects( + organization_id: UUID, + call_import_id: UUID, + rows: List[CallImportRow], +) -> tuple[int, int]: + """Delete every recording associated with ``rows`` plus a prefix sweep. + + The prefix sweep also cleans up the staged source file written at + UPLOAD time (``…/call_imports/{id}/source.{csv,xlsx}``) — both the + per-row recording keys and the source artefact share the same + organization-scoped prefix, so a single sweep covers them all. + + Returns ``(deleted_count, error_count)``. Never raises — callers proceed + with the DB delete regardless; orphans, if any, can be cleaned up by + re-running the same delete (it's idempotent). + """ + from app.services.storage.s3_service import s3_service + + if not s3_service.is_enabled(): + return 0, 0 + + keys = [r.recording_s3_key for r in rows if r.recording_s3_key] + deleted = 0 + errors = 0 + + if keys: + try: + d, errs = s3_service.delete_keys(keys) + deleted += d + errors += len(errs) + if errs: + logger.warning( + "S3 bulk-delete reported {} errors for call_import {}", + len(errs), + call_import_id, + ) + except Exception as exc: # noqa: BLE001 + logger.exception( + "Bulk S3 delete failed for call_import {}: {}", call_import_id, exc + ) + errors += len(keys) + + # Belt-and-braces sweep: catch anything that landed under the import's + # prefix but never made it into a row's recording_s3_key (narrow + # window where the S3 upload succeeded but the DB commit didn't). + sweep_prefix = ( + f"{s3_service.prefix}organizations/{organization_id}/" + f"call_imports/{call_import_id}/" + ) + try: + d, errs = s3_service.delete_keys_by_prefix(sweep_prefix) + deleted += d + errors += len(errs) + except Exception as exc: # noqa: BLE001 + logger.exception( + "S3 prefix sweep failed for {}: {}", sweep_prefix, exc + ) + + return deleted, errors + + +@router.delete( + "/{call_import_id}", + response_model=CallImportDeleteResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="deleteCallImport", +) +async def delete_call_import( + call_import_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportDeleteResponse: + """Delete a call-import batch asynchronously. + + Flips the batch to ``deleting`` and enqueues background teardown so + large imports (thousands of rows + S3 objects) do not block the API. + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + return CallImportDeleteResponse( + id=call_import_id, + status="completed", + ) + + if call_import.status == CallImportStatus.DELETING: + return CallImportDeleteResponse( + id=call_import.id, + status="accepted", + ) + + call_import.status = CallImportStatus.DELETING + call_import.error_message = None + stamp_call_import_actor(call_import, principal) + db.commit() + + from app.workers.tasks.call_import_bulk_ops import delete_call_import_task + + delete_call_import_task.delay( + str(call_import_id), + str(organization_id), + ) + + return CallImportDeleteResponse( + id=call_import.id, + status="accepted", + ) + + +def _locate_call_import_row_or_404( + catalog_db: Session, + *, + call_import_id: UUID, + row_id: UUID, + organization_id: UUID, +) -> Tuple[Session, CallImportRow, Optional[Session]]: + """Find a call import row on the correct DB session for mutation. + + When sharding is enabled rows live on shard databases; ``get_db`` only + opens the catalog. Returns ``(row_db, row, extra_catalog_to_close)`` + where ``extra_catalog_to_close`` is the catalog session opened by + :func:`locate_call_import_row` (distinct from the route's catalog + session) and must be closed via :func:`close_row_sessions`. + """ + from app.db_sharding.row_ops import close_row_sessions, locate_call_import_row + + try: + row_db, located_catalog, row, _shard_id = locate_call_import_row(row_id) + except LookupError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import row not found", + ) from None + if ( + row.call_import_id != call_import_id + or row.organization_id != organization_id + ): + close_row_sessions( + row_db, + located_catalog if located_catalog is not row_db else None, + ) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import row not found", + ) + extra_catalog = located_catalog if located_catalog is not row_db else None + return row_db, row, extra_catalog + + +@router.delete( + "/{call_import_id}/rows/{row_id}", + status_code=status.HTTP_204_NO_CONTENT, + operation_id="deleteCallImportRow", +) +async def delete_call_import_row( + call_import_id: UUID, + row_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> Response: + """Delete a single CallImportRow and its S3 recording. + + The parent ``CallImport`` is left in place. After deletion we recompute + its ``total_rows`` / ``completed_rows`` / ``failed_rows`` / ``status`` + so the UI's progress bar stays consistent with reality. + """ + from app.services.storage.s3_service import s3_service + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + from app.db_sharding.row_ops import close_row_sessions + + row_db, row, extra_catalog = _locate_call_import_row_or_404( + db, + call_import_id=call_import.id, + row_id=row_id, + organization_id=organization_id, + ) + try: + _revoke_pending_tasks([row]) + + if row.recording_s3_key and s3_service.is_enabled(): + try: + s3_service.delete_file_by_key(row.recording_s3_key) + except Exception as exc: # noqa: BLE001 — best-effort, DB is source of truth + logger.warning( + "Failed to delete S3 object {} for row {}: {}", + row.recording_s3_key, + row.id, + exc, + ) + + row_db.delete(row) + row_db.commit() + + _recompute_call_import_counters(db, call_import) + stamp_call_import_actor(call_import, principal) + db.commit() + finally: + close_row_sessions(row_db, extra_catalog) + + logger.info( + "Deleted call_import_row {} (call_import={}, org={})", + row_id, + call_import.id, + organization_id, + ) + + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +def _recompute_call_import_counters( + db: Session, call_import: CallImport +) -> None: + """Resync ``total/completed/failed_rows`` + status on the parent batch. + + Called after row-level mutations (single delete, bulk delete) so the + UI's progress bar stays consistent with the actual row set. The + rules mirror :func:`delete_call_import_row` so behavior doesn't + diverge between the per-row and bulk paths. + """ + + from app.services.call_imports.bulk_ops import rollup_call_import_batch_status + + rollup_call_import_batch_status(db, call_import) + + +@router.post( + "/{call_import_id}/retry-failed", + response_model=CallImportRetryFailedRowsResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="retryFailedCallImportRows", +) +async def retry_failed_call_import_rows( + call_import_id: UUID, + payload: Optional[CallImportRetryFailedRowsRequest] = Body(None), + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportRetryFailedRowsResponse: + """Re-enqueue every failed import row in this batch. + + Useful when transient provider issues are resolved and the operator wants + a one-click "try failed downloads again" pass without re-uploading the CSV. + + Pass ``provider`` + ``telephony_integration_id`` (or both omitted for + direct-URL retry) to change how recordings are fetched on this pass. + When the body is omitted entirely, the batch keeps its existing pinned + credentials. + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + if payload is not None: + if payload.telephony_integration_id is not None: + integration = _resolve_telephony_integration( + db, + organization_id, + payload.telephony_integration_id, + payload.provider or "", + ) + call_import.provider = integration.provider + call_import.telephony_integration_id = integration.id + if (integration.provider or "").lower() == "exotel": + schema = _resolve_schema( + db, + organization_id, + call_import.workspace_id, + call_import.schema_id, + ) + _validate_exotel_import_ready( + list(schema.parameters), + dict(call_import.parameter_mapping or {}), + ) + else: + call_import.provider = None + call_import.telephony_integration_id = None + db.flush() + + failed_rows = ( + db.query(CallImportRow) + .filter( + CallImportRow.call_import_id == call_import.id, + CallImportRow.status == CallImportRowStatus.FAILED, + ) + .order_by(CallImportRow.row_index.asc()) + .all() + ) + if not failed_rows: + return CallImportRetryFailedRowsResponse( + requeued=0, + enqueue_failed=0, + skipped=0, + ) + + from app.workers.concurrency.fair_import_dispatch import ( + schedule_fair_import_dispatch, + ) + + # Reset rows to pending BEFORE enqueue so the UI reflects "retry in + # progress" immediately even if the worker queue is backlogged. + for row in failed_rows: + row.status = CallImportRowStatus.PENDING + row.error_message = None + row.celery_task_id = None + + db.flush() + _recompute_call_import_counters(db, call_import) + stamp_call_import_actor(call_import, principal) + db.commit() + + try: + schedule_fair_import_dispatch(max_workspace_turns=999) + requeued = len(failed_rows) + enqueue_failed = 0 + skipped = 0 + except Exception as exc: # noqa: BLE001 + logger.exception( + "Failed to schedule fair import dispatch for import {}", + call_import.id, + ) + requeued = 0 + enqueue_failed = len(failed_rows) + skipped = 0 + for row in failed_rows: + db.refresh(row) + if row.status != CallImportRowStatus.PENDING: + skipped += 1 + enqueue_failed -= 1 + continue + row.status = CallImportRowStatus.FAILED + row.error_message = f"Failed to enqueue retry: {exc}" + db.flush() + _recompute_call_import_counters(db, call_import) + db.commit() + + return CallImportRetryFailedRowsResponse( + requeued=requeued, + enqueue_failed=enqueue_failed, + skipped=skipped, + ) + + +@router.post( + "/{call_import_id}/rows/bulk-delete", + response_model=CallImportRowBulkDeleteResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="bulkDeleteCallImportRows", +) +async def bulk_delete_call_import_rows( + call_import_id: UUID, + payload: CallImportRowBulkDelete, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportRowBulkDeleteResponse: + """Delete multiple ``CallImportRow`` rows in one request. + + Unknown / cross-tenant row ids are silently skipped — the response + reports how many actually went away so a UI that holds onto stale + ids (e.g. after another tab already deleted a row) doesn't 404 + the entire bulk action. + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + if not payload.row_ids: + return CallImportRowBulkDeleteResponse(deleted=0, status="completed") + + from app.workers.tasks.call_import_bulk_ops import bulk_delete_call_import_rows_task + + row_id_strs = [str(rid) for rid in payload.row_ids] + + stamp_call_import_actor(call_import, principal) + db.commit() + + bulk_delete_call_import_rows_task.delay( + str(call_import_id), + str(organization_id), + row_id_strs, + ) + + return CallImportRowBulkDeleteResponse(deleted=0, status="accepted") + + +# --------------------------------------------------------------------------- +# Diarization / transcription endpoints +# --------------------------------------------------------------------------- + + +def _select_rows_for_transcription( + db: Session, + call_import: CallImport, + payload: CallImportTranscribeRequest, + requested_row_ids: Optional[List[UUID]] = None, +) -> tuple[List[CallImportRow], Dict[str, int]]: + """Pick which rows to enqueue for diarisation (delegates to bulk_ops).""" + from app.services.call_imports.bulk_ops import select_rows_for_transcription + + try: + return select_rows_for_transcription( + db, call_import, payload, requested_row_ids=requested_row_ids + ) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + + +@router.post( + "/{call_import_id}/transcribe", + response_model=CallImportTranscribeResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="transcribeCallImport", +) +async def transcribe_call_import( + call_import_id: UUID, + payload: CallImportTranscribeRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportTranscribeResponse: + """Fan out diarization tasks for many rows in a single call. + + Returns a summary with how many rows were queued and how many were + skipped (broken down by reason) so the UI can show a meaningful + toast even when nothing actually got enqueued (e.g. "All 12 rows + already have transcripts"). + """ + + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + from app.workers.tasks.call_import_bulk_ops import bulk_diarize_call_import_task + + stamp_call_import_actor(call_import, principal) + db.commit() + + bulk_diarize_call_import_task.delay( + str(call_import_id), + str(organization_id), + payload.model_dump(mode="json"), + [str(rid) for rid in payload.row_ids] if payload.row_ids else None, + ) + + return CallImportTranscribeResponse( + queued=0, + skipped_rows=0, + skipped_reason_counts={}, + accepted=True, + ) + + +@router.post( + "/{call_import_id}/rows/{row_id}/transcribe", + response_model=CallImportTranscribeResponse, + status_code=status.HTTP_202_ACCEPTED, + operation_id="transcribeCallImportRow", +) +async def transcribe_call_import_row( + call_import_id: UUID, + row_id: UUID, + payload: CallImportTranscribeRequest, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportTranscribeResponse: + """Diarize / transcribe a single row. + + Thin wrapper over the batch endpoint that hard-codes a single + ``row_ids`` filter. Skip counts still surface so the UI can render + "Skipped — transcript present" diagnostics consistently. + """ + + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + from app.services.call_imports.bulk_ops import execute_bulk_diarization + + try: + result = execute_bulk_diarization( + db, + call_import, + payload, + requested_row_ids=[row_id], + ) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(exc), + ) from exc + + stamp_call_import_actor(call_import, principal) + db.commit() + + return CallImportTranscribeResponse( + queued=result.queued, + skipped_rows=result.skipped_rows, + skipped_reason_counts=result.skipped_reason_counts, + ) + + +# --------------------------------------------------------------------------- +# Cancel-in-flight diarisation +# --------------------------------------------------------------------------- +# +# Long-running multimodal LLM diarisation calls (especially LLM-only mode on +# slow audio) can sit in ``pending`` / ``running`` for tens of minutes when an +# upstream provider stalls. Without an abort affordance the operator's only +# recourse is to wait for Celery's ``time_limit`` to fire — which can be +# several minutes — or to manually mutate the DB. These helpers + the two +# endpoints below give the UI a first-class "Stop diarisation" button. +# +# Why ``terminate=True``: the legacy ``_revoke_pending_tasks`` helper uses +# ``terminate=False`` because it's called from delete-flow paths where the +# task may simply not get to run (a worker pulls it off the queue and drops +# it). For a user-initiated cancel we want SIGTERM to interrupt the worker +# mid-LLM call so the audio HTTP request actually aborts. ``terminate=True`` +# routes SIGTERM to the executing process; ``signal="SIGTERM"`` is the +# default but we spell it out so the intent is obvious to reviewers. + +# Sentinel error message stamped on cancelled rows. Read by the transcribe +# worker's finaliser (see ``app/workers/tasks/transcribe_call_import_row.py``) +# to detect a row that was cancelled mid-flight and AVOID overwriting it +# with whatever partial result the worker had managed to compute before the +# SIGTERM landed. +CANCELLED_BY_USER_ERROR: str = "Diarisation cancelled by user" + + +def _cancellable_diarisation_states() -> Tuple[str, ...]: + """States that a diarisation row can be cancelled from. + + Kept as a tiny helper so adding a future ``"queued"`` / ``"retrying"`` + state only needs one edit. + """ + return ("pending", "running") + + +def _revoke_diarisation_task(row: CallImportRow) -> None: + """Best-effort revoke of a single row's diarisation Celery task. + + Always swallows control-plane exceptions — Celery's control bus is + inherently best-effort and a missed revoke is not catastrophic + because the DB row is already flipped to ``failed`` by the caller + before this runs (so the UI immediately reflects the cancel; if + the task happens to finish anyway, the worker's finaliser skips + over the row via :data:`CANCELLED_BY_USER_ERROR`). + """ + task_id = (row.celery_task_id or "").strip() + if not task_id: + return + try: + from app.workers.celery_app import celery_app + + celery_app.control.revoke( + task_id, terminate=True, signal="SIGTERM" + ) + logger.info( + "Revoked diarisation task {} for call-import row {}", + task_id, + row.id, + ) + except Exception as exc: # noqa: BLE001 — revoke is best-effort + logger.warning( + "Failed to revoke diarisation task {} for row {}: {}", + task_id, + row.id, + exc, + ) + + +def _apply_diarisation_cancel(rows: List[CallImportRow]) -> Tuple[int, int]: + """Cancel diarisation on every cancellable row in ``rows``. + + Returns ``(cancelled, skipped)`` so the caller can build a typed + response without re-querying the DB. The caller is responsible for + ``db.commit()`` after this returns — we deliberately don't commit + here so a batch endpoint can flush all rows in one transaction. + """ + cancellable_states = _cancellable_diarisation_states() + cancelled = 0 + skipped = 0 + for row in rows: + if (row.diarised_transcript_status or "").lower() not in cancellable_states: + skipped += 1 + continue + # Flip the row state BEFORE we revoke so the UI's next poll + # already shows the cancel, even if Celery's control plane is + # slow to ack. + row.diarised_transcript_status = "failed" + row.diarised_transcript_error = CANCELLED_BY_USER_ERROR + _revoke_diarisation_task(row) + # Drop the task id so a follow-up retry (or a stale poll) can't + # accidentally re-revoke or get confused. + row.celery_task_id = None + cancelled += 1 + return cancelled, skipped + + +@router.post( + "/{call_import_id}/rows/{row_id}/cancel-diarisation", + response_model=CallImportRowResponse, + status_code=status.HTTP_200_OK, + operation_id="cancelCallImportRowDiarisation", +) +async def cancel_call_import_row_diarisation( + call_import_id: UUID, + row_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportRowResponse: + """Abort an in-flight (or queued) diarisation for a single row. + + Idempotent: calling on a row that's already terminal (``completed`` + / ``failed`` / ``idle``) returns the row unchanged with a 200, so + the UI can fire this from a "Stop" button without having to + pre-check the state. + + Race notes: + + * The row's ``diarised_transcript_status`` is flipped to ``failed`` + with :data:`CANCELLED_BY_USER_ERROR` BEFORE the Celery revoke, + so the polling UI sees the cancel immediately. + * If the worker happens to finish between our DB flip and the + SIGTERM landing, its finaliser will detect the cancelled + sentinel on the row and skip its own status / score writes + (see :mod:`app.workers.tasks.transcribe_call_import_row`). + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + from app.db_sharding.row_ops import close_row_sessions + + row_db, row, extra_catalog = _locate_call_import_row_or_404( + db, + call_import_id=call_import_id, + row_id=row_id, + organization_id=organization_id, + ) + try: + _apply_diarisation_cancel([row]) + row_db.commit() + stamp_call_import_actor(call_import, principal) + db.commit() + row_db.refresh(row) + return CallImportRowResponse.model_validate(row) + finally: + close_row_sessions(row_db, extra_catalog) + + +@router.post( + "/{call_import_id}/cancel-diarisation", + response_model=CallImportCancelDiarisationResponse, + status_code=status.HTTP_200_OK, + operation_id="cancelCallImportDiarisation", +) +async def cancel_call_import_diarisation( + call_import_id: UUID, + payload: Optional[CallImportCancelDiarisationRequest] = None, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportCancelDiarisationResponse: + """Abort in-flight diarisation for many rows in a single call. + + Default body (no ``row_ids``) cancels every row in this import + whose ``diarised_transcript_status`` is ``pending`` or + ``running`` — the "stop everything" button. Pass ``row_ids`` to + scope the cancel to the rows the operator has selected. + + Returns ``(cancelled, skipped)`` so the UI can render a tight + toast ("Cancelled 3 rows · 1 skipped (already completed)"). + """ + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + base_query = db.query(CallImportRow).filter( + CallImportRow.call_import_id == call_import_id + ) + + requested_ids = ( + payload.row_ids if payload and payload.row_ids is not None else None + ) + if requested_ids is not None: + if not requested_ids: + # Empty list is "no rows requested" — treat as a no-op + # 200 rather than 400 so the UI can pass through an empty + # selection without a special-case. + return CallImportCancelDiarisationResponse(cancelled=0, skipped=0) + rows = base_query.filter(CallImportRow.id.in_(requested_ids)).all() + found_ids = {r.id for r in rows} + # Treat requested-but-not-found ids as ``skipped`` so the UI's + # numbers reconcile (a stale selection that includes deleted + # rows shouldn't 404 the whole call). + missing = [rid for rid in requested_ids if rid not in found_ids] + skipped_missing = len(missing) + else: + # Implicit "cancel every cancellable row in this import" path. + rows = base_query.filter( + CallImportRow.diarised_transcript_status.in_( + list(_cancellable_diarisation_states()) + ) + ).all() + skipped_missing = 0 + + cancelled, skipped = _apply_diarisation_cancel(rows) + stamp_call_import_actor(call_import, principal) + db.commit() + return CallImportCancelDiarisationResponse( + cancelled=cancelled, + skipped=skipped + skipped_missing, + ) + + +def _render_diarised_segments_text( + segments: Optional[List[Dict[str, Any]]], + *, + swap: bool = False, +) -> str: + """Render ``CallImportRow.diarised_segments`` as ``: `` lines. + + Mirrors the worker's ``_render_turns_as_text`` (kept duplicated so + the route doesn't need to import a Celery task module just to + rebuild the rendered transcript). Only ``agent`` and ``user`` are + swapped — multi-party calls keep their ``speaker_N`` labels through + a swap so we don't silently collapse a third speaker into the user + side. + """ + if not segments: + return "" + out: List[str] = [] + for turn in segments: + if not isinstance(turn, dict): + continue + speaker = (turn.get("speaker") or "").strip() + text = (turn.get("text") or "").strip() + if not speaker or not text: + continue + if swap: + if speaker == "agent": + speaker = "user" + elif speaker == "user": + speaker = "agent" + out.append(f"{speaker}: {text}") + return "\n".join(out) + + +@router.post( + "/{call_import_id}/rows/{row_id}/diarised-speaker-swap", + response_model=CallImportRowResponse, + operation_id="toggleCallImportRowSpeakerSwap", +) +async def toggle_call_import_row_speaker_swap( + call_import_id: UUID, + row_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), + db: Session = Depends(get_db), +) -> CallImportRowResponse: + """Flip the user <-> agent mapping on a diarised row. + + The worker's "first speaker is the agent" heuristic is right most of + the time but does fail on inbound recordings where the customer + greets first, on recordings where the agent stays silent for the + intro, etc. Rather than rerun the (expensive) STT + pyannote + pipeline for those cases, we let reviewers flip the mapping in + place: the structured ``diarised_segments`` are the source of truth + and we re-render the plain-text ``diarised_transcript`` from them + with the swap applied. The next CSV export will then show the + corrected labels. + + Returns the updated row so the frontend can refresh without an + extra round-trip. + """ + + del api_key + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + from app.db_sharding.row_ops import close_row_sessions + + row_db, row, extra_catalog = _locate_call_import_row_or_404( + db, + call_import_id=call_import_id, + row_id=row_id, + organization_id=organization_id, + ) + try: + segments = ( + row.diarised_segments if isinstance(row.diarised_segments, list) else None + ) + if not segments: + # Without structured turns the swap toggle would have nothing to + # re-render — surface a clear error rather than silently + # flipping a flag the UI never read. + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=( + "This row has no structured diarised segments to swap. " + "Re-run diarisation to generate per-speaker turns first." + ), + ) + + new_swap = not bool(row.diarised_speaker_swap) + row.diarised_speaker_swap = new_swap + row.diarised_transcript = ( + _render_diarised_segments_text(segments, swap=new_swap) or None + ) + row_db.commit() + stamp_call_import_actor(call_import, principal) + db.commit() + row_db.refresh(row) + return CallImportRowResponse.model_validate(row) + finally: + close_row_sessions(row_db, extra_catalog) + + +# --------------------------------------------------------------------------- +# Cross-run insights for the import detail page +# --------------------------------------------------------------------------- + + +@router.get( + "/{call_import_id}/insights", + response_model=CallImportInsightsResponse, + operation_id="getCallImportInsights", +) +async def get_call_import_insights( + call_import_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportInsightsResponse: + """Aggregate signals across every evaluation run on this import. + + Powers the Insights tab on the call-import detail page: returns + per-metric "latest run" summaries plus a trend series of mean values + across runs so the UI can render a small line chart per metric. Also + bundles transcript coverage stats since those are the cheapest + pre-eval health-check (e.g. "30 of 50 rows still missing + transcripts"). + """ + + del api_key + + from app.models.database import ( + CallImportEvaluation, + CallImportEvaluationRow, + Metric, + ) + + call_import = ( + db.query(CallImport) + .filter( + CallImport.id == call_import_id, + CallImport.organization_id == organization_id, + ) + .first() + ) + if not call_import: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Call import not found", + ) + + rows = ( + db.query(CallImportRow) + .filter(CallImportRow.call_import_id == call_import_id) + .all() + ) + # A row "has a transcript" if EITHER the production (CSV) or the + # diarised (worker) column is populated — the insights tile reports + # the union so users see total coverage regardless of which source + # produced the value. + rows_with_transcript = sum( + 1 + for r in rows + if (r.transcript or "").strip() + or (r.diarised_transcript or "").strip() + ) + rows_without_transcript = len(rows) - rows_with_transcript + source_counts: Dict[str, int] = {} + for r in rows: + has_production = bool((r.transcript or "").strip()) + has_diarised = bool((r.diarised_transcript or "").strip()) + if has_production: + key = r.transcript_source or "csv" + source_counts[key] = source_counts.get(key, 0) + 1 + if has_diarised: + source_counts["diarised"] = source_counts.get("diarised", 0) + 1 + + evaluations = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .order_by(CallImportEvaluation.created_at.asc()) + .all() + ) + + # Defer heavy lifting to the aggregation helper so this endpoint and + # the per-run aggregate endpoint share the exact same metric + # bucketing math (no chance of "trend" disagreeing with "latest" on + # the same data set). + from app.api.v1.routes.call_import_evaluations import ( + _compute_metric_aggregates, + ) + + metric_history: Dict[str, List[CallImportInsightsRunPoint]] = {} + metric_meta: Dict[str, Metric] = {} + metric_latest: Dict[str, CallImportMetricAggregate] = {} + + for evaluation in evaluations: + eval_rows = ( + db.query(CallImportEvaluationRow) + .filter(CallImportEvaluationRow.evaluation_id == evaluation.id) + .all() + ) + aggregates = _compute_metric_aggregates(db, evaluation, eval_rows) + for agg in aggregates: + if agg.metric_id not in metric_meta: + # ``agg.metric_id`` is normally a UUID string, but the + # aggregator also emits ids that surface in row scores + # without a matching ``Metric`` row (e.g. a metric the + # user deleted mid-run, or LLM-discovered slugs). Those + # are not valid UUIDs, so coerce defensively and skip + # the metric registry lookup when the cast fails — the + # ``meta is None`` branch below already handles the + # display via the values stored on ``agg`` itself. + try: + metric_uuid = UUID(agg.metric_id) + except (ValueError, AttributeError, TypeError): + metric_uuid = None + if metric_uuid is not None: + metric_obj = ( + db.query(Metric) + .filter( + Metric.id == metric_uuid, + Metric.organization_id == organization_id, + ) + .first() + ) + if metric_obj is not None: + metric_meta[agg.metric_id] = metric_obj + history = metric_history.setdefault(agg.metric_id, []) + history.append( + CallImportInsightsRunPoint( + evaluation_id=evaluation.id, + name=evaluation.name, + created_at=evaluation.created_at, + mean=agg.mean, + completed_rows=agg.count, + ) + ) + metric_latest[agg.metric_id] = agg + + metrics_payload: List[CallImportInsightsMetric] = [] + for metric_id, latest in metric_latest.items(): + meta = metric_meta.get(metric_id) + metrics_payload.append( + CallImportInsightsMetric( + metric_id=metric_id, + metric_name=(meta.name if meta else latest.metric_name), + metric_type=(meta.metric_type if meta else latest.metric_type), + latest=latest, + trend=metric_history.get(metric_id, []), + ) + ) + + return CallImportInsightsResponse( + call_import_id=call_import_id, + total_rows=len(rows), + rows_with_transcript=rows_with_transcript, + rows_without_transcript=rows_without_transcript, + transcript_source_counts=source_counts, + evaluation_count=len(evaluations), + metrics=metrics_payload, + ) + + +from app.core.auth.capabilities import CALLS_DELETE, CALLS_IMPORT, CALLS_VIEW +from app.core.auth.workspace_route_capabilities import apply_workspace_route_capabilities + +apply_workspace_route_capabilities( + router, + view_capability=CALLS_VIEW, + manage_capability=CALLS_IMPORT, + delete_capability=CALLS_DELETE, +) diff --git a/app/config.py b/app/config.py index 91ecdbb3..94b51f18 100644 --- a/app/config.py +++ b/app/config.py @@ -107,7 +107,7 @@ class Settings(BaseSettings): RATE_LIMIT_PER_MINUTE: int = 60 # Authentication - AUTH_PROVIDERS: List[str] = ["api_key"] + AUTH_PROVIDERS: Annotated[List[str], NoDecode] = ["api_key"] AUTH_LOCAL_ALLOW_SIGNUP: bool = True AUTH_LOCAL_TOKEN_TTL_MINUTES: int = 15 AUTH_REFRESH_TOKEN_TTL_DAYS: int = 7 diff --git a/app/migrations/059_call_import_audit_users.py b/app/migrations/059_call_import_audit_users.py new file mode 100644 index 00000000..41ec06db --- /dev/null +++ b/app/migrations/059_call_import_audit_users.py @@ -0,0 +1,69 @@ +""" +Migration: last_updated_by_user_id on call imports and evaluations. + +Supports surfacing who created / last modified a batch or evaluation run +via FK to users (email resolved at read time). +""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Add last_updated_by_user_id to call_imports and call_import_evaluations" +) + + +def _column_exists(db: Session, table: str, column: str) -> bool: + row = db.execute( + text( + """ + SELECT 1 + FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + return row is not None + + +def _table_exists(db: Session, table: str) -> bool: + row = db.execute( + text( + "SELECT 1 FROM information_schema.tables WHERE table_name = :t" + ), + {"t": table}, + ).first() + return row is not None + + +def upgrade(db: Session): + for table in ("call_imports", "call_import_evaluations"): + if not _table_exists(db, table): + print(f"{table} does not exist, skipping...") + continue + if _column_exists(db, table, "last_updated_by_user_id"): + print(f"{table}.last_updated_by_user_id already exists, skipping...") + continue + db.execute( + text( + f""" + ALTER TABLE {table} + ADD COLUMN last_updated_by_user_id UUID NULL + REFERENCES users(id) ON DELETE SET NULL + """ + ) + ) + print(f"Added {table}.last_updated_by_user_id") + + +def downgrade(db: Session): + for table in ("call_import_evaluations", "call_imports"): + if not _table_exists(db, table): + continue + if not _column_exists(db, table, "last_updated_by_user_id"): + continue + db.execute( + text(f"ALTER TABLE {table} DROP COLUMN last_updated_by_user_id") + ) + print(f"Dropped {table}.last_updated_by_user_id") diff --git a/app/models/database.py b/app/models/database.py index a21a4819..3ce548de 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -1,2729 +1,2735 @@ -"""SQLAlchemy database models.""" - -from sqlalchemy import ( - BigInteger, - Boolean, - Column, - Date, - DateTime, - DDL, - Enum, - event, - Float, - ForeignKey, - Integer, - JSON, - String, - Text, - UniqueConstraint, - select, - text, -) -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import relationship -from sqlalchemy.sql import func -import uuid -import enum -from app.models.enums import ( - EvaluationType, EvaluationStatus, EvaluatorResultStatus, RoleEnum, InvitationStatus, - LanguageEnum, CallTypeEnum, CallMediumEnum, GenderEnum, AccentEnum, BackgroundNoiseEnum, - IntegrationPlatform, ModelProvider, VoiceBundleType, TestAgentConversationStatus, - MetricType, MetricCategory, MetricTrigger, CallRecordingStatus, AlertMetricType, AlertAggregation, - AlertOperator, AlertNotifyFrequency, AlertStatus, AlertHistoryStatus, CronJobStatus, - PromptOptimizationStatus, CallImportStatus, CallImportRowStatus, -) - -def get_enum_values(enum_class): - """Helper to get values from enum class for SQLAlchemy.""" - return [e.value for e in enum_class] - -from app.database import Base - - -# Enums moved to enums.py - - -class Organization(Base): - """Organization model for multi-tenancy.""" - - __tablename__ = "organizations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(255), nullable=False) - voice_playground_threshold_overrides = Column(JSON, nullable=True) - # AlignEval-style judge alignment thresholds. - # Shape: {"min_labels_to_evaluate": int, "min_labels_to_optimize": int} - # Falls back to system defaults (20 / 50) when null. - judge_alignment_settings = Column(JSON, nullable=True) - # Per-org LLM gateway overrides (enabled, gateway_type, base_url, keys). - llm_gateway_settings = Column(JSON, nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - # Relationships - api_keys = relationship("APIKey", back_populates="organization") - members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") - invitations = relationship("Invitation", back_populates="organization", cascade="all, delete-orphan") - workspaces = relationship( - "Workspace", - back_populates="organization", - cascade="all, delete-orphan", - ) - workspace_roles = relationship( - "WorkspaceRole", - back_populates="organization", - cascade="all, delete-orphan", - ) - - -class Workspace(Base): - """Workspace - in-org isolation boundary for call imports and metrics. - - Every organization has at least one workspace (``is_default = True``, - seeded by migration 033). Users pick an "active workspace" in the UI; - list endpoints filter by it so users only see calls/metrics from the - project they're currently working in. Access is governed by - ``workspace_members`` and org-scoped ``workspace_roles`` (capability - bundles); org admins implicitly access all workspaces. - """ - - __tablename__ = "workspaces" - __table_args__ = ( - UniqueConstraint("organization_id", "slug", name="uq_workspaces_org_slug"), - ) - - # ``server_default`` is required so that raw-SQL INSERTs (e.g. the - # per-org Default seed in migration 033) can omit ``id`` and let the - # database fill it in. Without it, ``create_all`` produces a column - # with NOT NULL but no DEFAULT, and the migration crashes with - # ``null value in column "id"``. - id = Column( - UUID(as_uuid=True), - primary_key=True, - default=uuid.uuid4, - server_default=text("gen_random_uuid()"), - ) - organization_id = Column( - UUID(as_uuid=True), - ForeignKey("organizations.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - name = Column(String(255), nullable=False) - slug = Column(String(255), nullable=False) - # At most one default per org. Enforced on Postgres by the partial - # unique index attached via the after_create event below; on - # SQLite (test runs) we rely on the route-level _check_slug_unique - # check + the Default-workspace conftest fixture instead, because - # SQLite doesn't support partial indexes the same way. - is_default = Column(Boolean, nullable=False, default=False, server_default="false") - # Reusable PDF/report branding metadata scoped to this workspace. Images - # live in S3. Shape: {"heading": str|null, "images": [{id, s3_key, - # content_type, filename, size_bytes, updated_at}, ...]}. - report_branding = Column(JSON, nullable=True) - created_by_user_id = Column( - UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True - ) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - organization = relationship("Organization", back_populates="workspaces") - members = relationship( - "WorkspaceMember", - back_populates="workspace", - cascade="all, delete-orphan", - ) - - -class WorkspaceRole(Base): - """Org-scoped workspace role (system or custom) as a capability bundle.""" - - __tablename__ = "workspace_roles" - __table_args__ = ( - UniqueConstraint("organization_id", "name", name="uq_workspace_roles_org_name"), - ) - - id = Column( - UUID(as_uuid=True), - primary_key=True, - default=uuid.uuid4, - server_default=text("gen_random_uuid()"), - ) - organization_id = Column( - UUID(as_uuid=True), - ForeignKey("organizations.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - name = Column(String(255), nullable=False) - description = Column(Text, nullable=True) - capabilities = Column(JSON, nullable=False, default=list) - is_system = Column(Boolean, nullable=False, default=False, server_default="false") - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - organization = relationship("Organization", back_populates="workspace_roles") - members = relationship("WorkspaceMember", back_populates="role") - - -class WorkspaceMember(Base): - """User membership in a workspace with an assigned workspace role.""" - - __tablename__ = "workspace_members" - __table_args__ = ( - UniqueConstraint("workspace_id", "user_id", name="uq_workspace_members_ws_user"), - ) - - id = Column( - UUID(as_uuid=True), - primary_key=True, - default=uuid.uuid4, - server_default=text("gen_random_uuid()"), - ) - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - user_id = Column( - UUID(as_uuid=True), - ForeignKey("users.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - role_id = Column( - UUID(as_uuid=True), - ForeignKey("workspace_roles.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - added_by_user_id = Column( - UUID(as_uuid=True), - ForeignKey("users.id", ondelete="SET NULL"), - nullable=True, - ) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - workspace = relationship("Workspace", back_populates="members") - user = relationship("User", foreign_keys=[user_id]) - role = relationship("WorkspaceRole", back_populates="members") - added_by = relationship("User", foreign_keys=[added_by_user_id]) - - -# Partial unique index: "at most one default workspace per org". This -# is attached as an after_create event (rather than declared in -# ``__table_args__``) because SQLAlchemy's ``Index(..., -# postgresql_where=...)`` silently degrades to a *full* unique index on -# SQLite - which then forbids any second workspace per org and breaks -# the test suite. ``execute_if(dialect="postgresql")`` makes this DDL -# a no-op on SQLite while still emitting it on Postgres (prod, CI). -event.listen( - Workspace.__table__, - "after_create", - DDL( - "CREATE UNIQUE INDEX IF NOT EXISTS uq_workspaces_org_default " - "ON workspaces (organization_id) WHERE is_default" - ).execute_if(dialect="postgresql"), -) - - -class User(Base): - """User model for authentication and profile management.""" - - __tablename__ = "users" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - email = Column(String(255), unique=True, nullable=False, index=True) - name = Column(String(255), nullable=True) - first_name = Column(String(255), nullable=True) - last_name = Column(String(255), nullable=True) - password_hash = Column(String(255), nullable=True) # Nullable for users created via invitation - external_id = Column(String(255), unique=True, nullable=True, index=True) - auth_provider = Column(String(50), nullable=True) - mfa_enabled = Column(Boolean, default=False, nullable=False) - last_login_at = Column(DateTime(timezone=True), nullable=True) - is_active = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - # Relationships - organization_memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan") - api_keys = relationship("APIKey", back_populates="user") - invitations = relationship("Invitation", back_populates="invited_user", foreign_keys="Invitation.invited_user_id") - refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan") - - -class RefreshToken(Base): - """Opaque refresh token for extending local-password sessions.""" - - __tablename__ = "refresh_tokens" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True) - token_hash = Column(String(64), unique=True, nullable=False, index=True) - expires_at = Column(DateTime(timezone=True), nullable=False) - revoked_at = Column(DateTime(timezone=True), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - user = relationship("User", back_populates="refresh_tokens") - - -class OrganizationMember(Base): - """Organization membership with role.""" - - __tablename__ = "organization_members" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) - role = Column(String, nullable=False, default=RoleEnum.READER.value) - - # User preferences for this organization - default_agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True, index=True) - - joined_at = Column(DateTime(timezone=True), server_default=func.now()) - - # Unique constraint: one membership per user per organization - __table_args__ = ( - UniqueConstraint('organization_id', 'user_id', name='uq_org_user'), - ) - - # Relationships - organization = relationship("Organization", back_populates="members") - user = relationship("User", back_populates="organization_memberships") - default_agent = relationship("Agent", foreign_keys=[default_agent_id]) - - -class Invitation(Base): - """Invitation model for inviting users to organizations.""" - - __tablename__ = "invitations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - invited_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True) # Null if user doesn't exist yet - invited_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - email = Column(String(255), nullable=False) # Email of invited user - role = Column(String, nullable=False, default=RoleEnum.READER.value) - status = Column(String, nullable=False, default=InvitationStatus.PENDING.value) - - - - token = Column(String(255), unique=True, nullable=False, index=True) # Invitation token - expires_at = Column(DateTime(timezone=True), nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - accepted_at = Column(DateTime(timezone=True), nullable=True) - - # Relationships - organization = relationship("Organization", back_populates="invitations") - invited_user = relationship("User", foreign_keys=[invited_user_id], back_populates="invitations") - invited_by = relationship("User", foreign_keys=[invited_by_id]) - - -class APIKey(Base): - """API Key model for authentication.""" - - __tablename__ = "api_keys" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - key = Column(String(255), unique=True, nullable=False, index=True) - name = Column(String(255), nullable=True) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True) # Optional: link to user - is_active = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - last_used = Column(DateTime(timezone=True), nullable=True) - - # Relationships - organization = relationship("Organization", back_populates="api_keys") - user = relationship("User", back_populates="api_keys") - - -class AudioFile(Base): - """Audio file model.""" - - __tablename__ = "audio_files" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - filename = Column(String(255), nullable=False) - file_path = Column(String(512), nullable=False) - file_size = Column(Integer, nullable=False) # Size in bytes - duration = Column(Float, nullable=True) # Duration in seconds - sample_rate = Column(Integer, nullable=True) - channels = Column(Integer, nullable=True) - format = Column(String(10), nullable=False) # wav, mp3, flac, etc. - uploaded_at = Column(DateTime(timezone=True), server_default=func.now()) - - # Relationships - evaluations = relationship("Evaluation", back_populates="audio_file") - - -class Evaluation(Base): - """Evaluation job model.""" - - __tablename__ = "evaluations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every legacy audio evaluation belongs to a - # workspace within its org. Stamped from the X-Workspace-Id header - # (falling back to the org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - audio_id = Column(UUID(as_uuid=True), ForeignKey("audio_files.id"), nullable=False) - reference_text = Column(String, nullable=True) # For WER calculation - evaluation_type = Column(String, nullable=False) - model_name = Column(String(100), nullable=True) - status = Column(String, default=EvaluationStatus.PENDING.value, nullable=False) - - - - metrics_requested = Column(JSON, nullable=True) # List of requested metrics - created_at = Column(DateTime(timezone=True), server_default=func.now()) - started_at = Column(DateTime(timezone=True), nullable=True) - completed_at = Column(DateTime(timezone=True), nullable=True) - error_message = Column(String, nullable=True) - - # Relationships - audio_file = relationship("AudioFile", back_populates="evaluations") - result = relationship("EvaluationResult", back_populates="evaluation", uselist=False) - - -class EvaluationResult(Base): - """Evaluation result model.""" - - __tablename__ = "evaluation_results" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - evaluation_id = Column(UUID(as_uuid=True), ForeignKey("evaluations.id"), nullable=False, unique=True) - # Workspace isolation: mirrors the parent Evaluation's workspace. - # Denormalized for fast filter-by-workspace listings without a join. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - transcript = Column(String, nullable=True) - metrics = Column(JSON, nullable=False) # {"wer": 0.05, "latency_ms": 1250, ...} - raw_output = Column(JSON, nullable=True) # Full model output - processing_time = Column(Float, nullable=True) # Processing time in seconds - model_used = Column(String(100), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - # Relationships - evaluation = relationship("Evaluation", back_populates="result") - - -# ============================================ -# VAIOPS MODELS - Voice AI Ops -# ============================================ - -# Enums moved to enums.py - - -class Agent(Base): - """Test Agent - The voice AI agent being evaluated""" - __tablename__ = "agents" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - agent_id = Column(String(6), unique=True, nullable=True, index=True) # 6-digit ID - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every agent belongs to a workspace within its - # org. Stamped from the X-Workspace-Id header (falling back to the - # org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - name = Column(String, nullable=False) - phone_number = Column(String, nullable=True) # Optional, required only for phone_call - language = Column(String, nullable=False, default=LanguageEnum.ENGLISH.value) - description = Column(String) - provider_prompt = Column(Text, nullable=True) - provider_prompt_synced_at = Column(DateTime(timezone=True), nullable=True) - call_type = Column(String, nullable=False, default=CallTypeEnum.OUTBOUND.value) - call_medium = Column(String, nullable=False, default=CallMediumEnum.PHONE_CALL.value) - telephony_phone_number_id = Column( - UUID(as_uuid=True), - ForeignKey("telephony_phone_numbers.id", ondelete="SET NULL"), - nullable=True, - index=True, - ) - - - - - # Voice configuration - either voice_bundle_id OR ai_provider_id OR voice_ai_integration_id (mutually exclusive) - voice_bundle_id = Column(UUID(as_uuid=True), ForeignKey("voicebundles.id"), nullable=True, index=True) - ai_provider_id = Column(UUID(as_uuid=True), ForeignKey("aiproviders.id"), nullable=True, index=True) - - # Voice AI agent integration (Retell, Vapi, etc.) - voice_ai_integration_id = Column(UUID(as_uuid=True), ForeignKey("integrations.id"), nullable=True, index=True) - voice_ai_agent_id = Column(String, nullable=True) # Agent ID from the external provider (Retell/Vapi) - prompt_variables = Column(JSON, nullable=True) - silence_hangup_secs = Column(Integer, nullable=False, server_default="15") - - created_at = Column(DateTime, server_default=func.now()) - updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) - created_by = Column(String) - - -class Persona(Base): - """Persona - TTS provider-tied voice identity for testing""" - __tablename__ = "personas" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every persona belongs to a workspace within - # its org. Stamped from the X-Workspace-Id header (falling back to - # the org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - name = Column(String, nullable=False) - gender = Column(String, nullable=False, default=GenderEnum.NEUTRAL.value) - tts_provider = Column(String(100), nullable=True) - tts_voice_id = Column(String(255), nullable=True) - tts_voice_name = Column(String(255), nullable=True) - is_custom = Column(Boolean, default=False) - description = Column(Text, nullable=True) - tts_config = Column(JSON, nullable=True) - llm_temperature = Column(Float, nullable=True) - llm_max_tokens = Column(Integer, nullable=True) - response_delay_ms = Column(Integer, nullable=True) - max_turns = Column(Integer, nullable=True) - allow_interruptions = Column(Boolean, nullable=True) - - created_at = Column(DateTime, server_default=func.now()) - updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) - created_by = Column(String) - - -class Scenario(Base): - """Scenario - The conversation scenario/test case""" - __tablename__ = "scenarios" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every scenario belongs to a workspace within - # its org. Stamped from the X-Workspace-Id header (falling back to - # the org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True, index=True) - name = Column(String, nullable=False) - description = Column(String) - required_info = Column(JSON) - - created_at = Column(DateTime, server_default=func.now()) - updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) - created_by = Column(String) - - -# Enums moved to enums.py - - -class Integration(Base): - """Integration model for connecting with external voice AI platforms.""" - __tablename__ = "integrations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - platform = Column(String, nullable=False) - - - - name = Column(String, nullable=True) # Optional friendly name - api_key = Column(String, nullable=False) # Encrypted Private API key for the platform - public_key = Column(String, nullable=True) # Optional Public API key (e.g. for Vapi) - is_active = Column(Boolean, default=True, nullable=False) - # Multiple credentials per (org, platform) are allowed. is_default marks - # the row used when a caller does not explicitly select a credential. - # A partial unique index in migration 028 enforces at most one default - # per (org, platform) at the DB level. - is_default = Column(Boolean, default=False, nullable=False) - # inherit | gateway | direct — per-credential LLM routing override - routing_mode = Column(String(20), nullable=False, default="inherit", server_default="inherit") - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - last_tested_at = Column(DateTime(timezone=True), nullable=True) # When API key was last validated - - -class ManualTranscription(Base): - """Manual transcription model for storing transcriptions from S3 audio files.""" - - __tablename__ = "manual_transcriptions" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - name = Column(String(255), nullable=True) # User-friendly name for the transcription - audio_file_key = Column(String(512), nullable=False) # S3 key or file path - transcript = Column(String, nullable=False) # Full transcript text - speaker_segments = Column(JSON, nullable=True) # List of segments with speaker labels: [{"speaker": "Speaker 1", "text": "...", "start": 0.0, "end": 5.2}] - stt_model = Column(String(100), nullable=True) # STT model used (e.g., "whisper-1", "google-speech-v2") - stt_provider = Column(String, nullable=True) # Provider used - - - - language = Column(String(10), nullable=True) # Detected or specified language - processing_time = Column(Float, nullable=True) # Processing time in seconds - raw_output = Column(JSON, nullable=True) # Full model output for reference - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class ConversationEvaluation(Base): - """Conversation evaluation model for evaluating manual transcriptions against agent objectives.""" - - __tablename__ = "conversation_evaluations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - transcription_id = Column(UUID(as_uuid=True), ForeignKey("manual_transcriptions.id"), nullable=False, index=True) - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False, index=True) - - # Evaluation results - objective_achieved = Column(Boolean, nullable=False) # Binary: was the conversation objective achieved? - objective_achieved_reason = Column(String, nullable=True) # Explanation for the binary result - additional_metrics = Column(JSON, nullable=True) # Additional evaluation metrics (e.g., professionalism, clarity, etc.) - overall_score = Column(Float, nullable=True) # Overall score (0.0 to 1.0) - - # LLM metadata - llm_provider = Column(Enum(ModelProvider, native_enum=False), nullable=True) - - llm_model = Column(String(100), nullable=True) - llm_response = Column(JSON, nullable=True) # Full LLM response for reference - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class AIProvider(Base): - """AI Provider - Stores API keys for different AI platforms.""" - __tablename__ = "aiproviders" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - provider = Column(String, nullable=False) - - - - api_key = Column(String, nullable=False) # Encrypted API key - name = Column(String, nullable=True) # Optional friendly name - # Azure OpenAI resource endpoint (e.g. https://my-resource.openai.azure.com). - # Only used when provider is azure; other providers ignore this column. - endpoint_url = Column(String, nullable=True) - is_active = Column(Boolean, default=True, nullable=False) - # Multiple AIProvider rows per (org, provider) are allowed. is_default - # marks the row resolved when no explicit credential id is selected. - # A partial unique index in migration 028 enforces at most one default. - is_default = Column(Boolean, default=False, nullable=False) - # inherit | gateway | direct — per-credential LLM routing override - routing_mode = Column(String(20), nullable=False, default="inherit", server_default="inherit") - # Bifrost custom model ID used when routing via gateway - gateway_model = Column(String(255), nullable=True) - # inherit | litellm_shim | native_openai — Bifrost API surface override - gateway_interface = Column(String(20), nullable=False, default="inherit", server_default="inherit") - # Optional per-credential Bifrost/gateway base URL override - gateway_base_url = Column(String(512), nullable=True) - # Optional auth header for Bifrost (e.g. x-bf-vk, Authorization, x-api-key) - gateway_auth_header = Column(String(64), nullable=True) - # Env var name whose value is sent as the gateway auth secret - gateway_auth_secret_env = Column(String(128), nullable=True) - # Encrypted inline gateway auth secret (alternative to env var) - gateway_auth_secret = Column(String, nullable=True) - # Arbitrary HTTP headers sent with gateway-routed LiteLLM calls - gateway_extra_headers = Column(JSON, nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - last_tested_at = Column(DateTime(timezone=True), nullable=True) # When API key was last validated - - -# Enums moved to enums.py - - -class VoiceBundle(Base): - """VoiceBundle - Composable unit combining STT, LLM, and TTS for voice AI testing, or S2S models.""" - __tablename__ = "voicebundles" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - name = Column(String, nullable=False) - description = Column(String, nullable=True) - - # Bundle type: either STT+LLM+TTS or S2S - # Using String instead of Enum to avoid SQLAlchemy enum conversion issues - # The enum conversion is handled in the Pydantic schemas - bundle_type = Column(String(50), nullable=False, default=VoiceBundleType.STT_LLM_TTS.value) - - # STT Configuration (references AIProvider via provider name) - required for STT_LLM_TTS, optional for S2S - stt_provider = Column(String, nullable=True) - # Optional explicit credential row (aiproviders.id or integrations.id). - # When NULL the credential resolver picks the default row for the - # provider. No FK is set because the target table varies by provider. - stt_credential_id = Column(UUID(as_uuid=True), nullable=True) - - stt_model = Column(String, nullable=True) # e.g., "whisper-1", "google-speech-v2" - - # LLM Configuration (references AIProvider via provider name) - required for STT_LLM_TTS, optional for S2S - llm_provider = Column(String, nullable=True) - llm_credential_id = Column(UUID(as_uuid=True), nullable=True) - - llm_model = Column(String, nullable=True) # e.g., "gpt-4", "claude-3-opus" - llm_temperature = Column(Float, nullable=True, default=0.7) - llm_max_tokens = Column(Integer, nullable=True) - llm_config = Column(JSON, nullable=True) # Additional LLM configuration (extensible) - - # TTS Configuration (references AIProvider via provider name) - required for STT_LLM_TTS, optional for S2S - tts_provider = Column(String, nullable=True) - tts_credential_id = Column(UUID(as_uuid=True), nullable=True) - - tts_model = Column(String, nullable=True) # e.g., "tts-1", "neural-voice" - tts_voice = Column(String, nullable=True) # Voice selection if applicable - tts_config = Column(JSON, nullable=True) # Additional TTS configuration (extensible) - - # S2S Configuration - required for S2S type, optional for STT_LLM_TTS - s2s_provider = Column(String, nullable=True) - s2s_credential_id = Column(UUID(as_uuid=True), nullable=True) - - - - s2s_model = Column(String, nullable=True) # e.g., "gpt-4o-transcribe", speech-to-speech model - s2s_config = Column(JSON, nullable=True) # Additional S2S configuration (extensible) - - # Additional configuration for extensibility - extra_metadata = Column(JSON, nullable=True) # For future extensions (renamed from 'metadata' to avoid SQLAlchemy conflict) - - is_active = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -# Enums moved to enums.py - - -class TestAgentConversation(Base): - """Test Agent Conversation - Records conversations between test AI agent and voice AI agent.""" - __tablename__ = "test_agent_conversations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every playground conversation belongs to a - # workspace within its org. Stamped from the X-Workspace-Id header - # (falling back to the org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - # Configuration - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False) - persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=False) - scenario_id = Column(UUID(as_uuid=True), ForeignKey("scenarios.id"), nullable=False) - voice_bundle_id = Column(UUID(as_uuid=True), ForeignKey("voicebundles.id"), nullable=True) - - # Conversation data - status = Column(String, nullable=False, default=TestAgentConversationStatus.INITIALIZING.value) - - - - live_transcription = Column(JSON, nullable=True) # Array of conversation turns with timestamps - conversation_audio_key = Column(String, nullable=True) # S3 key for recorded conversation audio - full_transcript = Column(String, nullable=True) # Full conversation transcript - - # Metadata - started_at = Column(DateTime(timezone=True), server_default=func.now()) - ended_at = Column(DateTime(timezone=True), nullable=True) - duration_seconds = Column(Float, nullable=True) - - # Additional metadata - conversation_metadata = Column(JSON, nullable=True) # Additional conversation metadata - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -class EvaluatorSuite(Base): - """Evaluator suite — one agent + one persona + N scenario combinations.""" - - __tablename__ = "evaluator_suites" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - name = Column(String, nullable=True) - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False) - persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=False) - metric_ids = Column(JSON, nullable=True) - llm_provider = Column(String, nullable=True) - llm_model = Column(String, nullable=True) - llm_config = Column(JSON, nullable=True) - tags = Column(JSON, nullable=True) - default_runs_per_combination = Column(Integer, nullable=False, default=1) - round_robin_index = Column(Integer, nullable=False, default=0) - is_active = Column(Boolean, nullable=False, default=False) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -class Evaluator(Base): - """Evaluator - Configuration for testing agents with specific persona and scenario combinations, or custom prompt evaluators.""" - __tablename__ = "evaluators" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - evaluator_id = Column(String(6), unique=True, nullable=False, index=True) # 6-digit ID - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every evaluator belongs to a workspace within - # its org. Stamped from the X-Workspace-Id header (falling back to - # the org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - # Display name (required for custom evaluators, optional for standard) - name = Column(String, nullable=True) - - # Parent suite (nullable for legacy/custom evaluators) - suite_id = Column( - UUID(as_uuid=True), - ForeignKey("evaluator_suites.id", ondelete="CASCADE"), - nullable=True, - index=True, - ) - - # Standard evaluator configuration (nullable for custom evaluators) - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=True) - persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=True) - scenario_id = Column(UUID(as_uuid=True), ForeignKey("scenarios.id"), nullable=True) - - # Custom evaluator prompt (used instead of agent/persona/scenario) - custom_prompt = Column(Text, nullable=True) - - # Custom evaluator metric selection. When set, the worker filters the - # enabled-org metrics down to only these IDs (list of metric UUID strings). - # Standard evaluators leave this NULL and use all enabled agent metrics. - metric_ids = Column(JSON, nullable=True) - - # LLM configuration for evaluation (overrides hardcoded defaults) - llm_provider = Column(String, nullable=True) # e.g. "openai", "anthropic", "google" - llm_model = Column(String, nullable=True) # e.g. "gpt-4.1", "claude-sonnet-4-20250514" - llm_config = Column(JSON, nullable=True) - - # Tags for categorization - tags = Column(JSON, nullable=True) # Array of tag strings - - # Metadata - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -# Enums moved to enums.py - - -class Metric(Base): - """Metric - Configuration for evaluation metrics. - - Supports a 2-level hierarchy via ``parent_metric_id``: a "category" - parent metric (e.g. "Call Outcome") owns N child sub-metric labels - (e.g. "happy_completion", "angry_hangup"). ``selection_mode`` is set - only on parents and controls how the LLM scores children together - (``single_choice`` = pick exactly one; ``multi_label`` = independent - yes/no with logical consistency). Children are always boolean. - """ - __tablename__ = "metrics" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: two-shape column. - # - # * ``workspace_id = `` — workspace-scoped metric. Only - # visible inside that workspace (the default behavior; existing - # rows all look like this). - # * ``workspace_id IS NULL`` — org-shared metric. Surfaces in - # every workspace's listing under this org so users don't have - # to recreate the same metric per workspace. - # - # Children always inherit their parent's ``workspace_id`` (including - # NULL) so a category metric's whole subtree shares one scope; the - # add-child / promote-discovered endpoints enforce this. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=True, - index=True, - ) - - # Basic information - name = Column(String, nullable=False) - description = Column(String, nullable=True) - # Free-form illustrative example used to sharpen the LLM judge's - # rubric. Today this is consumed by child sub-labels of a - # categorization parent metric so each label can carry "what does - # this look like in a transcript?" text alongside the rubric in - # ``description``. The column lives on every Metric row for - # forward-compat: a standalone metric could later surface its own - # example without another migration. - example = Column(Text, nullable=True) - - # Configuration - metric_type = Column(String, nullable=False, default=MetricType.RATING.value) - metric_category = Column( - String(30), - nullable=False, - default=MetricCategory.QUALITY.value, - server_default=MetricCategory.QUALITY.value, - ) - trigger = Column(String, nullable=False, default=MetricTrigger.ALWAYS.value) - metric_origin = Column(String(30), nullable=False, default="default") - supported_surfaces = Column(JSON, nullable=False, default=list) # ["agent", "voice_playground", "blind_test"] - enabled_surfaces = Column(JSON, nullable=False, default=list) # subset of supported_surfaces - custom_data_type = Column(String(30), nullable=True) # "boolean" | "enum" | "number_range" - custom_config = Column(JSON, nullable=True) # enum options / number range config - tags = Column(JSON, nullable=True) # ["tone", "latency", ...] - - # Hierarchy: NULL = standalone or parent. When set, this row is a - # child sub-metric of the referenced parent. ON DELETE CASCADE so - # deleting a category removes its children atomically. - parent_metric_id = Column( - UUID(as_uuid=True), - ForeignKey("metrics.id", ondelete="CASCADE"), - nullable=True, - index=True, - ) - # Set only on parent rows (``parent_metric_id IS NULL``). Either - # ``single_choice`` or ``multi_label``. NULL = legacy / non-hierarchical - # metric (no children). - selection_mode = Column(String(20), nullable=True) - - # When true on a parent metric (any selection_mode), the LLM is - # invited during call-import evaluation to emit additional - # candidate sub-labels beyond the user-defined children. The - # candidates surface in a "Discovered labels" panel where the user - # manually promotes them into real child Metric rows. For - # ``single_choice`` parents the discovered entries are - # supplemental — the chosen child is still picked from the - # predefined children so the exactly-one-true invariant holds. - # The validator rejects this flag on standalone / child metrics. - allow_discovery = Column( - Boolean, nullable=False, default=False, server_default="false" - ) - - # When True, this metric is a "transcript-compare judge": the - # call-import evaluator feeds BOTH the production transcript - # (``call_import_rows.transcript``, CSV-supplied) and the diarised - # transcript (``call_import_rows.diarised_transcript``, worker- - # produced by the STT/diarisation pipeline) to the LLM as a - # labeled pair instead of feeding one transcript. The parent - # evaluation's ``CallImportEvaluation.transcript_source`` is - # ignored for these metrics — they always read both columns. - # Rows where either transcript is missing are skipped per-metric - # with ``skipped="comparison_missing_transcript"`` so the rest of - # the row's metrics still produce scores. The Pydantic validator - # rejects ``compare_transcripts`` combined with ``parent_metric_id`` - # or ``selection_mode`` (i.e. it can't simultaneously be part of - # a parent/child hierarchy). The call-import worker also - # auto-promotes a metric to comparison mode when its description - # references the production / diarised transcripts in well-known - # phrases (see ``_metric_text_references_production`` in - # ``app.workers.tasks.evaluate_call_import_row``). - compare_transcripts = Column( - Boolean, nullable=False, default=False, server_default="false" - ) - - parent = relationship( - "Metric", - remote_side=[id], - backref="children", - ) - - # When true, the LLM-judge is asked to also return a short free-form - # rationale alongside the value (stored under ``metric_scores[id].rationale``). - # Adds a second " - LLM Rationale" column in the call-import CSV export. - capture_rationale = Column(Boolean, nullable=False, default=False) - - enabled = Column(Boolean, nullable=False, default=True) - - # Metadata - is_default = Column(Boolean, nullable=False, default=False) # Pre-defined metrics - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -class EvaluatorResult(Base): - """EvaluatorResult - Results from running an evaluator with transcription and metric evaluations.""" - __tablename__ = "evaluator_results" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - result_id = Column(String(6), unique=True, nullable=False, index=True) # 6-digit ID - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every evaluator result belongs to a workspace - # within its org. Stamped from the active workspace at creation time - # (either the X-Workspace-Id header or the org's Default workspace). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - # References - evaluator_id = Column(UUID(as_uuid=True), ForeignKey("evaluators.id"), nullable=True, index=True) # Optional - can be None for test calls without persona/scenario - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=True) # Nullable for custom evaluators - persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=True) # Optional - can be None for test calls - scenario_id = Column(UUID(as_uuid=True), ForeignKey("scenarios.id"), nullable=True) # Optional - can be None for test calls - - # Result data - name = Column(String, nullable=True) # Scenario name or test call name (optional) - timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) - duration_seconds = Column(Float, nullable=True) # Call duration - status = Column(String(20), nullable=False, default=EvaluatorResultStatus.QUEUED.value) - - # Audio and transcription - audio_s3_key = Column(String, nullable=True) # S3 key for audio file - transcription = Column(String, nullable=True) # Full transcription - speaker_segments = Column(JSON, nullable=True) # List of segments with speaker labels: [{"speaker": "Speaker 1", "text": "...", "start": 0.0, "end": 5.2}] - - # Metric scores - JSON object with metric_id as key and score as value - # Format: {"metric_id_1": {"value": 85, "type": "rating"}, "metric_id_2": {"value": true, "type": "boolean"}} - metric_scores = Column(JSON, nullable=True) - - # Celery task tracking - celery_task_id = Column(String, nullable=True, index=True) # Celery task ID for tracking - - # Error information - error_message = Column(String, nullable=True) - - # Call event tracking (similar to CallRecording) - call_event = Column(String, nullable=True, index=True) # Latest call event (e.g., call_started, call_ended) - provider_call_id = Column(String, nullable=True, index=True) # Provider's call_id (e.g., Retell call_id) - provider_platform = Column(String, nullable=True) # e.g., "retell", "vapi" - call_data = Column(JSON, nullable=True) # Full call details from provider (like CallRecording) - - # Data-plane shard routing (payload rows on shard DBs when sharding enabled) - shard_id = Column(String(64), nullable=True, index=True) - - # Metadata - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -# Enums moved to enums.py - - -class CallRecordingSource(str, enum.Enum): - """Source of the call recording data.""" - - PLAYGROUND = "playground" - WEBHOOK = "webhook" - - -class CallRecording(Base): - """Call Recording model for tracking voice provider calls.""" - __tablename__ = "call_recordings" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every recording belongs to a workspace within - # its org. For playground-origin rows this is stamped from the active - # workspace at creation time; for webhook-origin rows the worker - # looks up the recording's agent and inherits its workspace_id. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - call_short_id = Column(String(6), unique=True, nullable=False, index=True) # 6-digit ID - status = Column(Enum(CallRecordingStatus), nullable=False, default=CallRecordingStatus.PENDING, index=True) - call_event = Column(String, nullable=True, index=True) # Latest webhook event (e.g., call_started, call_ended) - source = Column(Enum(CallRecordingSource), nullable=False, default=CallRecordingSource.PLAYGROUND, index=True) - call_data = Column(JSON, nullable=True) # JSON blob for provider response - provider_call_id = Column(String, nullable=True, index=True) # Provider's call_id (e.g., Retell call_id) - provider_platform = Column(String, nullable=True) # e.g., "retell", "vapi" - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=True) # Reference to our agent - - # Link to EvaluatorResult for metric evaluations - evaluator_result_id = Column( - UUID(as_uuid=True), - ForeignKey("evaluator_results.id", ondelete="SET NULL"), - nullable=True, - index=True, - ) - - shard_id = Column(String(64), nullable=True, index=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class EvaluatorResultPayload(Base): - """Heavy evaluator result fields stored on data shards when sharding is enabled.""" - - __tablename__ = "evaluator_result_payloads" - - evaluator_result_id = Column(UUID(as_uuid=True), primary_key=True) - workspace_id = Column(UUID(as_uuid=True), nullable=False, index=True) - audio_s3_key = Column(String, nullable=True) - transcription = Column(String, nullable=True) - speaker_segments = Column(JSON, nullable=True) - metric_scores = Column(JSON, nullable=True) - call_data = Column(JSON, nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class CallRecordingPayload(Base): - """Heavy call recording fields stored on data shards when sharding is enabled.""" - - __tablename__ = "call_recording_payloads" - - call_recording_id = Column(UUID(as_uuid=True), primary_key=True) - workspace_id = Column(UUID(as_uuid=True), nullable=False, index=True) - call_data = Column(JSON, nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class Alert(Base): - """Alert model for configuring monitoring alerts.""" - __tablename__ = "alerts" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - - # Basic information - name = Column(String(255), nullable=False) - description = Column(String, nullable=True) - - # Metric condition configuration - metric_type = Column(String, nullable=False, default=AlertMetricType.NUMBER_OF_CALLS.value) - aggregation = Column(String, nullable=False, default=AlertAggregation.SUM.value) - operator = Column(String, nullable=False, default=AlertOperator.GREATER_THAN.value) - threshold_value = Column(Float, nullable=False) - time_window_minutes = Column(Integer, nullable=False, default=60) # Time window for aggregation - - # Agent selection (JSON array of agent UUIDs, null means all agents) - agent_ids = Column(JSON, nullable=True) - - # Notification configuration - notify_frequency = Column(String, nullable=False, default=AlertNotifyFrequency.IMMEDIATE.value) - notify_emails = Column(JSON, nullable=True) # Array of email addresses - notify_webhooks = Column(JSON, nullable=True) # Array of webhook URLs (Slack, etc.) - - # Status - status = Column(String, nullable=False, default=AlertStatus.ACTIVE.value) - - # Metadata - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - # Relationships - alert_history = relationship("AlertHistory", back_populates="alert", cascade="all, delete-orphan") - - -class AlertHistory(Base): - """Alert history model for tracking triggered alerts.""" - __tablename__ = "alert_history" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - alert_id = Column(UUID(as_uuid=True), ForeignKey("alerts.id"), nullable=False, index=True) - - # Trigger information - triggered_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) - triggered_value = Column(Float, nullable=False) # The actual value that triggered the alert - threshold_value = Column(Float, nullable=False) # The threshold at time of trigger - - # Status tracking - status = Column(String, nullable=False, default=AlertHistoryStatus.TRIGGERED.value) - - # Notification tracking - notified_at = Column(DateTime(timezone=True), nullable=True) - notification_details = Column(JSON, nullable=True) # Details of sent notifications - - # Resolution - acknowledged_at = Column(DateTime(timezone=True), nullable=True) - acknowledged_by = Column(String, nullable=True) - resolved_at = Column(DateTime(timezone=True), nullable=True) - resolved_by = Column(String, nullable=True) - resolution_notes = Column(String, nullable=True) - - # Additional context - context_data = Column(JSON, nullable=True) # Additional data about the trigger - - # Metadata - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - # Relationships - alert = relationship("Alert", back_populates="alert_history") - - -class CronJob(Base): - """Cron job model for scheduling automated evaluator runs.""" - __tablename__ = "cron_jobs" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - - # Basic information - name = Column(String(255), nullable=False) - cron_expression = Column(String(100), nullable=False) # e.g., "0 9 * * 1-5" - timezone = Column(String(100), nullable=False, default="UTC") - - # Run configuration - max_runs = Column(Integer, nullable=False, default=10) - current_runs = Column(Integer, nullable=False, default=0) - - # Evaluators to trigger (JSON array of evaluator UUIDs) - evaluator_ids = Column(JSON, nullable=False) - - # Status - status = Column(String, nullable=False, default=CronJobStatus.ACTIVE.value) - - # Run tracking - next_run_at = Column(DateTime(timezone=True), nullable=True) - last_run_at = Column(DateTime(timezone=True), nullable=True) - - # Metadata - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - -class TTSComparisonStatus(str, enum.Enum): - PENDING = "pending" - GENERATING = "generating" - EVALUATING = "evaluating" - COMPLETED = "completed" - FAILED = "failed" - - -class TTSSampleStatus(str, enum.Enum): - PENDING = "pending" - GENERATING = "generating" - COMPLETED = "completed" - FAILED = "failed" - - -class TTSReportJobStatus(str, enum.Enum): - PENDING = "pending" - PROCESSING = "processing" - COMPLETED = "completed" - FAILED = "failed" - - -class TTSComparison(Base): - """TTS Comparison session for A/B testing voice providers.""" - __tablename__ = "tts_comparisons" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every voice playground comparison belongs to - # a workspace within its org. Children (samples, report jobs, blind - # test shares) inherit this workspace_id. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - simulation_id = Column(String(6), unique=True, index=True, nullable=True) - - name = Column(String(255), nullable=True) - status = Column(String(50), nullable=False, default=TTSComparisonStatus.PENDING.value) - - # 'benchmark' = traditional TTS A/B benchmark (provider-generated audio). - # 'blind_test_only' = standalone blind test built from existing recordings - # / uploads / past TTS samples; no TTS generation happens. - mode = Column(String(32), nullable=False, default="benchmark") - - provider_a = Column(String(100), nullable=True) - model_a = Column(String(100), nullable=True) - voices_a = Column(JSON, nullable=True) - - provider_b = Column(String(100), nullable=True) - model_b = Column(String(100), nullable=True) - voices_b = Column(JSON, nullable=True) - - sample_texts = Column(JSON, nullable=False) - num_runs = Column(Integer, nullable=False, default=1) - - blind_test_results = Column(JSON, nullable=True) - evaluation_summary = Column(JSON, nullable=True) - - eval_stt_provider = Column(String(100), nullable=True) - eval_stt_model = Column(String(100), nullable=True) - - celery_task_id = Column(String, nullable=True, index=True) - error_message = Column(String, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - samples = relationship("TTSSample", back_populates="comparison", cascade="all, delete-orphan") - - -class TTSSample(Base): - """Individual TTS audio sample within a comparison.""" - __tablename__ = "tts_samples" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - comparison_id = Column(UUID(as_uuid=True), ForeignKey("tts_comparisons.id", ondelete="CASCADE"), nullable=False, index=True) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: mirrors the parent TTSComparison's workspace. - # Denormalized for fast filter-by-workspace listings without a join. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - provider = Column(String(100), nullable=True) - model = Column(String(100), nullable=True) - voice_id = Column(String(255), nullable=True) - voice_name = Column(String(255), nullable=True) - side = Column(String(1), nullable=True) # "A" or "B" - sample_index = Column(Integer, nullable=False) - run_index = Column(Integer, nullable=False, default=0) - - # 'tts' (default, audio is synthesized by a provider), 'recording' (audio - # is reused from a CallImportRow recording), or 'upload' (audio was - # uploaded by the user). Non-tts samples are marked completed up-front - # by the API and skipped by the generation worker. - source_type = Column(String(32), nullable=False, default="tts") - # When source_type == 'recording', references CallImportRow.id (no FK - # constraint to keep cascading deletes simple if a call import is later - # removed; the audio_s3_key is what's actually used). - source_ref_id = Column(UUID(as_uuid=True), nullable=True) - - text = Column(String, nullable=False) - audio_s3_key = Column(String(512), nullable=True) - duration_seconds = Column(Float, nullable=True) - latency_ms = Column(Float, nullable=True) - ttfb_ms = Column(Float, nullable=True) - - evaluation_metrics = Column(JSON, nullable=True) - status = Column(String(50), nullable=False, default=TTSSampleStatus.PENDING.value) - error_message = Column(String, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - comparison = relationship("TTSComparison", back_populates="samples") - - -class TTSReportJob(Base): - """Asynchronous PDF report generation jobs for Voice Playground.""" - __tablename__ = "tts_report_jobs" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: mirrors the parent TTSComparison's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - comparison_id = Column(UUID(as_uuid=True), ForeignKey("tts_comparisons.id", ondelete="CASCADE"), nullable=False, index=True) - - status = Column(String(50), nullable=False, default=TTSReportJobStatus.PENDING.value) - format = Column(String(20), nullable=False, default="pdf") - filename = Column(String(255), nullable=True) - s3_key = Column(String(512), nullable=True) - error_message = Column(String, nullable=True) - celery_task_id = Column(String, nullable=True, index=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - comparison = relationship("TTSComparison") - - -class TTSBlindTestShareStatus(str, enum.Enum): - OPEN = "open" - CLOSED = "closed" - - -class TTSBlindTestShare(Base): - """A publicly sharable blind test for a TTSComparison. - - The share_token is the capability: anyone holding it can open the public - form and submit a response. Each comparison has at most one share row. - """ - __tablename__ = "tts_blind_test_shares" - __table_args__ = ( - UniqueConstraint("comparison_id", name="uq_blind_test_shares_comparison"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - comparison_id = Column( - UUID(as_uuid=True), - ForeignKey("tts_comparisons.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: mirrors the parent TTSComparison's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - share_token = Column(String(64), unique=True, nullable=False, index=True) - - title = Column(String(255), nullable=False) - description = Column(Text, nullable=True) - - # Internal notes visible only to the share creator (e.g. which voice - # corresponds to which side, source notes for standalone blind tests). - # Never exposed via the public blind test payload. - creator_notes = Column(Text, nullable=True) - - # JSON list: [{ "key": str, "label": str, "type": "rating"|"comment", "scale": int? }] - custom_metrics = Column(JSON, nullable=False) - - status = Column(String(20), nullable=False, default=TTSBlindTestShareStatus.OPEN.value) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - closed_at = Column(DateTime(timezone=True), nullable=True) - created_by = Column(String, nullable=True) - - comparison = relationship("TTSComparison") - responses = relationship( - "TTSBlindTestResponse", - back_populates="share", - cascade="all, delete-orphan", - ) - - -class TTSBlindTestResponse(Base): - """A single rater's submission against a TTSBlindTestShare.""" - __tablename__ = "tts_blind_test_responses" - __table_args__ = ( - UniqueConstraint("share_id", "rater_email", name="uq_blind_test_response_share_email"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - share_id = Column( - UUID(as_uuid=True), - ForeignKey("tts_blind_test_shares.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - # Workspace isolation: mirrors the parent TTSBlindTestShare's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - rater_name = Column(String(255), nullable=False) - rater_email = Column(String(320), nullable=False, index=True) - - # JSON list keyed by sample_index. Server stores in TRUE A/B orientation - # (already de-flipped from whatever the rater's UI showed): - # [{ - # "sample_index": int, - # "preferred": "A" | "B", - # "ratings_a": { metric_key: number }, - # "ratings_b": { metric_key: number }, - # "comment": str? - # }] - responses = Column(JSON, nullable=False) - - ip = Column(String(64), nullable=True) - user_agent = Column(String(512), nullable=True) - - submitted_at = Column(DateTime(timezone=True), server_default=func.now()) - - share = relationship("TTSBlindTestShare", back_populates="responses") - - -class PromptPartial(Base): - """Prompt Partial - Reusable prompt templates with version history.""" - __tablename__ = "prompt_partials" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every prompt partial belongs to a workspace - # within its org. Versions inherit this workspace_id. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - name = Column(String(255), nullable=False) - description = Column(String, nullable=True) - content = Column(Text, nullable=False) - tags = Column(JSON, nullable=True) - current_version = Column(Integer, nullable=False, default=1) - # Cached LLM-generated flowchart for imported production agent prompts. - # Shape: AgentFlowGraph JSON (nodes[], edges[]). - agent_flowchart = Column(JSON, nullable=True) - agent_flowchart_status = Column(String(20), nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - versions = relationship("PromptPartialVersion", back_populates="prompt_partial", cascade="all, delete-orphan", order_by="PromptPartialVersion.version.desc()") - - -class PromptPartialVersion(Base): - """Version history for a prompt partial.""" - __tablename__ = "prompt_partial_versions" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - prompt_partial_id = Column(UUID(as_uuid=True), ForeignKey("prompt_partials.id", ondelete="CASCADE"), nullable=False, index=True) - # Workspace isolation: mirrors the parent PromptPartial's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - version = Column(Integer, nullable=False) - content = Column(Text, nullable=False) - change_summary = Column(String, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - created_by = Column(String, nullable=True) - - prompt_partial = relationship("PromptPartial", back_populates="versions") - - __table_args__ = ( - UniqueConstraint('prompt_partial_id', 'version', name='uq_prompt_partial_version'), - ) - - -class CustomTTSVoice(Base): - """Organization-scoped custom TTS voice metadata.""" - __tablename__ = "custom_tts_voices" - __table_args__ = ( - UniqueConstraint("organization_id", "provider", "voice_id", name="uq_custom_tts_voice_org_provider_voice_id"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - provider = Column(String(100), nullable=False, index=True) - voice_id = Column(String(255), nullable=False) - name = Column(String(255), nullable=False) - gender = Column(String(50), nullable=True) - accent = Column(String(100), nullable=True) - description = Column(Text, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - -class PromptOptimizationRun(Base): - """A single GEPA prompt optimization run for an agent.""" - __tablename__ = "prompt_optimization_runs" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every optimization run belongs to a workspace - # within its org. Candidates inherit this workspace_id. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False, index=True) - evaluator_id = Column(UUID(as_uuid=True), ForeignKey("evaluators.id"), nullable=True) - voice_bundle_id = Column(UUID(as_uuid=True), ForeignKey("voicebundles.id"), nullable=True) - - seed_prompt = Column(Text, nullable=False) - best_prompt = Column(Text, nullable=True) - best_score = Column(Float, nullable=True) - - status = Column(String(20), nullable=False, default=PromptOptimizationStatus.PENDING.value) - config = Column(JSON, nullable=True) - reflection_trace = Column(JSON, nullable=True) - metric_history = Column(JSON, nullable=True) - - num_iterations = Column(Integer, nullable=True) - num_metric_calls = Column(Integer, nullable=True) - - celery_task_id = Column(String, nullable=True, index=True) - error_message = Column(Text, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - candidates = relationship("PromptOptimizationCandidate", back_populates="optimization_run", cascade="all, delete-orphan") - - -class PromptOptimizationCandidate(Base): - """A candidate prompt generated during an optimization run.""" - __tablename__ = "prompt_optimization_candidates" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - optimization_run_id = Column(UUID(as_uuid=True), ForeignKey("prompt_optimization_runs.id", ondelete="CASCADE"), nullable=False, index=True) - # Workspace isolation: mirrors the parent PromptOptimizationRun's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - prompt_text = Column(Text, nullable=False) - score = Column(Float, nullable=True) - metric_breakdown = Column(JSON, nullable=True) - reflection_summary = Column(Text, nullable=True) - - parent_candidate_id = Column(UUID(as_uuid=True), ForeignKey("prompt_optimization_candidates.id"), nullable=True) - - is_accepted = Column(Boolean, nullable=False, default=False) - pushed_to_provider_at = Column(DateTime(timezone=True), nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - optimization_run = relationship("PromptOptimizationRun", back_populates="candidates") - - -class TelephonyIntegration(Base): - """Per-organization telephony provider credentials and configuration. - - Multiple rows per (organization_id, provider) are allowed so that an - organization can keep several Plivo / Exotel accounts side-by-side. - A partial unique index in migration 028 enforces at most one row with - is_default = TRUE per (org, provider); resolution falls back to that - default row when the caller does not pin a specific credential. - """ - - __tablename__ = "telephony_integrations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - provider = Column(String(50), nullable=False, default="plivo") - name = Column(String(255), nullable=True) # Optional friendly name to disambiguate multiple credentials - - auth_id = Column(String(255), nullable=False) - auth_token = Column(String(512), nullable=False) - - verify_app_uuid = Column(String(255), nullable=True) - voice_app_id = Column(String(255), nullable=True) - sip_domain = Column(String(255), nullable=True) - masking_config = Column(JSON, nullable=True) - - is_active = Column(Boolean, default=True, nullable=False) - is_default = Column(Boolean, default=False, nullable=False) - last_tested_at = Column(DateTime(timezone=True), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class TelephonyPhoneNumber(Base): - """Inventory of telephony phone numbers owned by an organization.""" - - __tablename__ = "telephony_phone_numbers" - __table_args__ = ( - UniqueConstraint("organization_id", "phone_number", name="uq_telephony_number_org_phone"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - telephony_integration_id = Column( - UUID(as_uuid=True), ForeignKey("telephony_integrations.id"), nullable=True, index=True - ) - - phone_number = Column(String(20), nullable=False, index=True) - country_iso2 = Column(String(2), nullable=True) - region = Column(String(100), nullable=True) - number_type = Column(String(20), nullable=True) - capabilities = Column(JSON, nullable=True) - provider_app_id = Column(String(255), nullable=True) - - is_masking_pool = Column(Boolean, default=False, nullable=False) - inbound_enabled = Column(Boolean, default=True, nullable=False) - outbound_enabled = Column(Boolean, default=True, nullable=False) - source = Column(String(20), nullable=False, default="imported") - agent_id = Column( - UUID(as_uuid=True), - ForeignKey( - "agents.id", - ondelete="SET NULL", - use_alter=True, - name="fk_telephony_phone_numbers_agent_id", - ), - nullable=True, - index=True, - ) - is_active = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class TelephonyDialTarget(Base): - """Org-scoped saved destination numbers for outbound test calls.""" - - __tablename__ = "telephony_dial_targets" - __table_args__ = ( - UniqueConstraint("organization_id", "phone_number", name="uq_telephony_dial_target_org_phone"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - phone_number = Column(String(20), nullable=False, index=True) - label = Column(String(255), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class TelephonyVerifySession(Base): - """Tracks voice OTP verification sessions via telephony provider.""" - - __tablename__ = "telephony_verify_sessions" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - provider_session_uuid = Column(String(255), nullable=False, unique=True, index=True) - recipient_number = Column(String(20), nullable=False) - channel = Column(String(10), nullable=False, default="voice") - status = Column(String(20), nullable=False, default="pending") - initiated_by = Column(String(255), nullable=True) - verify_app_uuid = Column(String(255), nullable=True) - verified_at = Column(DateTime(timezone=True), nullable=True) - expires_at = Column(DateTime(timezone=True), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class TelephonyMaskedSession(Base): - """Number-masking session between two parties through a middle number.""" - - __tablename__ = "telephony_masked_sessions" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - telephony_integration_id = Column(UUID(as_uuid=True), ForeignKey("telephony_integrations.id"), nullable=False) - masked_number_id = Column( - UUID(as_uuid=True), ForeignKey("telephony_phone_numbers.id"), nullable=False, index=True - ) - masked_number = Column(String(20), nullable=False) - party_a_number = Column(String(20), nullable=False) - party_b_number = Column(String(20), nullable=False) - status = Column(String(20), nullable=False, default="active") - expires_at = Column(DateTime(timezone=True), nullable=True) - ended_at = Column(DateTime(timezone=True), nullable=True) - session_metadata = Column("metadata", JSON, nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - -class CallImportSchema(Base): - """Reusable Input Parameter schema for the call-uploads flow. - - A schema is workspace-scoped: users define a named bundle of typed - Input Parameters once (e.g. "Standard Voice QA" with conversation_id + - recording_url + transcript + agent_name) and then map those parameters - to CSV/Excel headers each time they upload a new batch. - - Every schema MUST contain exactly one parameter with - ``type='conversation_id'`` and ``is_required=True`` - that's the - mandatory identity field every imported row needs. The invariant is - enforced in app code on create/update (no DB-level CHECK because the - parent + children are written across two tables in one transaction). - """ - - __tablename__ = "call_import_schemas" - __table_args__ = ( - # Case-insensitive uniqueness is enforced via the matching partial - # index on ``LOWER(name)`` in the migration; this constraint here - # would be case-sensitive and is intentionally omitted to avoid - # confusing the user. - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column( - UUID(as_uuid=True), - ForeignKey("organizations.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - name = Column(String(255), nullable=False) - description = Column(Text, nullable=True) - created_by_user_id = Column( - UUID(as_uuid=True), - ForeignKey("users.id", ondelete="SET NULL"), - nullable=True, - ) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - parameters = relationship( - "CallImportSchemaParameter", - back_populates="schema", - cascade="all, delete-orphan", - order_by="CallImportSchemaParameter.ordering", - ) - - -class CallImportSchemaParameter(Base): - """A single typed parameter inside a :class:`CallImportSchema`. - - ``type`` is one of the strings tracked by - :data:`app.models.enums.CallImportParameterType`. ``conversation_id`` - is reserved for the mandatory identity parameter every schema must - contain. - """ - - __tablename__ = "call_import_schema_parameters" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - schema_id = Column( - UUID(as_uuid=True), - ForeignKey("call_import_schemas.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - name = Column(String(255), nullable=False) - type = Column(String(32), nullable=False) - description = Column(Text, nullable=True) - is_required = Column(Boolean, nullable=False, default=False) - # Stable ordering so the UI renders parameters in the order the - # schema author defined them (matters when conversation_id is pinned - # first and the user re-orders the rest). - ordering = Column(Integer, nullable=False, default=0) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - schema = relationship("CallImportSchema", back_populates="parameters") - - -class CallImport(Base): - """Batch record for a CSV-driven call import job.""" - - __tablename__ = "call_imports" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - # Workspace isolation: every imported batch belongs to a workspace - # within its org. The /upload endpoint stamps it from the active - # workspace header (or the org's Default if absent). - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - created_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) - - # Telephony provider key (e.g. ``'exotel'``, ``'plivo'``). In the - # legacy one-shot ``POST /upload`` endpoint this is supplied with the - # file; in the three-stage flow (UPLOAD -> MAP -> IMPORT) the value - # isn't known until the IMPORT stage, so the column is nullable for - # ``uploaded`` / ``mapped`` batches. - provider = Column(String(50), nullable=True, default="exotel") - # Pin a specific telephony credential for this batch so the worker - # downloads recordings using *that* row instead of the org default. - # NULL preserves legacy behavior (resolve by provider + default). - telephony_integration_id = Column( - UUID(as_uuid=True), - ForeignKey("telephony_integrations.id", ondelete="SET NULL"), - nullable=True, - index=True, - ) - original_filename = Column(String(512), nullable=True) - # When the source file was a multi-sheet Excel workbook, this records - # the worksheet the rows came from (one batch per sheet). NULL for CSV - # uploads since CSV has no sheet concept. - sheet_name = Column(String(255), nullable=True) - - # --- Source-file staging (UPLOAD stage) --------------------------- - # The raw CSV / Excel file is stored in S3 between stages so the - # user can come back later to MAP and IMPORT without re-uploading. - # ``source_s3_key`` is NULL on legacy batches that were imported via - # the one-shot endpoint (those batches stay read-only post-import). - source_s3_key = Column(Text, nullable=True) - source_format = Column(String(16), nullable=True) - source_size_bytes = Column(BigInteger, nullable=True) - source_content_type = Column(String(255), nullable=True) - - # Snapshot of the file's sheets + headers captured at UPLOAD time - # so the MAP UI doesn't need to re-fetch the source bytes from S3. - # Shape: ``[{"name": str, "headers": [str, ...], "row_count": int}, ...]``. - available_sheets = Column(JSON, nullable=True) - - # User's explicit "drop these columns" decision captured at MAP - # time. Was validation-only and ephemeral in the legacy flow; now - # persisted so the IMPORT stage can re-parse the file with the same - # mapping/skip intent. - skipped_columns = Column(JSON, nullable=False, default=list) - # Rows skipped at parse time (missing/invalid conversation_id or URL). - # Shape: ``[{"source_row": int, "reason": str, "message": str}, ...]``. - source_row_skips = Column(JSON, nullable=False, default=list) - - # Free-text high-level segregation label. Powers the "Dataset" filter - # at the top of the imports page; multiple imports can share a value. - dataset = Column(String(255), nullable=True, index=True) - - # Reusable Input Parameter schema this batch was uploaded against. - # NULL on legacy batches uploaded before the schema-driven flow - # shipped; those still render via ``column_mapping`` + ``extra_columns`` - # + ``custom_column_mapping`` below. - schema_id = Column( - UUID(as_uuid=True), - ForeignKey("call_import_schemas.id", ondelete="RESTRICT"), - nullable=True, - index=True, - ) - # New schema-driven mapping: ``{schema_parameter_name: csv_header}``. - # Populated for new uploads; empty dict on legacy batches. - parameter_mapping = Column(JSON, nullable=False, default=dict) - - # Legacy free-form mapping (pre-schema-flow). Kept on the model so - # batches that were uploaded before the schema feature shipped still - # render correctly on the detail page; new uploads stop writing here. - # Keys: external_call_id (required), transcript, recording_url. - # (DB column ``external_call_id`` is now ``conversation_id``; this - # JSON key stays as-is for historical batches.) - # Values: original CSV header strings (preserve user casing for export). - column_mapping = Column(JSON, nullable=False, default=dict) - # Ordered list of additional CSV header strings the uploader wants - # preserved verbatim into the evaluation export CSV. - extra_columns = Column(JSON, nullable=False, default=list) - # User-defined ``{custom_field_name: csv_header}`` mappings on top of - # the three system fields above. Cells from the mapped CSV columns are - # preserved per row (keyed by the CSV header in ``raw_columns``) and - # surface in the evaluation export under the uploader-chosen name. - custom_column_mapping = Column(JSON, nullable=False, default=dict) - - total_rows = Column(Integer, nullable=False, default=0) - completed_rows = Column(Integer, nullable=False, default=0) - failed_rows = Column(Integer, nullable=False, default=0) - - status = Column( - Enum(CallImportStatus, values_callable=get_enum_values), - nullable=False, - default=CallImportStatus.PENDING, - index=True, - ) - error_message = Column(Text, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - rows = relationship( - "CallImportRow", - back_populates="call_import", - cascade="all, delete-orphan", - order_by="CallImportRow.row_index", - ) - tags = relationship( - "CallImportTag", - secondary="call_import_tag_assignments", - backref="call_imports", - lazy="selectin", - ) - evaluations = relationship( - "CallImportEvaluation", - back_populates="call_import", - cascade="all, delete-orphan", - ) - - -class CallImportShardSlice(Base): - """Registry row: which shard stores a slice of rows for an import.""" - - __tablename__ = "call_import_shard_slices" - - call_import_id = Column( - UUID(as_uuid=True), - ForeignKey("call_imports.id", ondelete="CASCADE"), - primary_key=True, - ) - slice_id = Column(Integer, primary_key=True) - shard_id = Column(String(64), nullable=False, index=True) - row_index_min = Column(Integer, nullable=False) - row_index_max = Column(Integer, nullable=False) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - -class CallImportRow(Base): - """A single row within a CallImport batch (one CSV line / one external call).""" - - __tablename__ = "call_import_rows" - __table_args__ = ( - UniqueConstraint("call_import_id", "row_index", name="uq_call_import_row_index"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - call_import_id = Column( - UUID(as_uuid=True), - ForeignKey("call_imports.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - row_index = Column(Integer, nullable=False) - # Was historically named ``external_call_id``; renamed to - # ``conversation_id`` so the new schema-driven upload flow can refer - # to it by a single canonical name across the schema definition, - # exports, and downstream evaluation tables. - conversation_id = Column(String(255), nullable=False, index=True) - # Supplied via CSV for Exotel credentialed imports (required per row). - # Nullable in the schema for legacy rows imported before recording_url - # was mandatory on every Exotel upload. - recording_url = Column(Text, nullable=True) - # Date-only call recording date supplied by the import schema. Used - # for historical report comparisons without timezone/time ambiguity. - recording_date = Column(Date, nullable=True, index=True) - # The "production" transcript: the value supplied via the CSV - # upload mapping. Never overwritten by the diarisation worker — - # the worker writes its output into ``diarised_transcript`` so - # the user keeps both versions side by side. - transcript = Column(Text, nullable=True) - # Snapshot of the original CSV row keyed by the user's headers so the - # evaluation export can reproduce every column the uploader supplied - # (mapped + extra). NULL on legacy rows imported before this column. - raw_columns = Column(JSON, nullable=True) - - # Where the value in ``transcript`` came from. ``csv`` = supplied via - # the upload mapping, ``edited`` = manually changed in the UI. NULL - # on rows that have never had a production transcript. - # (Worker-produced transcripts now live in ``diarised_transcript`` - # and are tracked via ``diarised_transcript_*`` metadata below.) - transcript_source = Column(String(20), nullable=True) - # Provider/model recorded by the (legacy) post-hoc transcription - # worker. New worker runs leave these NULL and write into the - # ``diarised_transcript_*`` columns instead; kept on the model for - # backwards compatibility with pre-split rows that still carry the - # original transcription metadata here. - transcript_provider = Column(String(50), nullable=True) - transcript_model = Column(String(100), nullable=True) - # Lifecycle status for the legacy transcription workflow itself, - # independent of the row's recording-fetch ``status``. ``idle`` = - # no transcribe task has touched this column. New diarisation runs - # update ``diarised_transcript_status`` instead. - transcript_status = Column( - String(20), - nullable=False, - default="idle", - ) - transcript_error = Column(Text, nullable=True) - transcribed_at = Column(DateTime(timezone=True), nullable=True) - - # The "diarised" transcript: produced by the post-hoc - # transcription/diarisation worker. Stored separately so a manual - # diarisation run never clobbers the production transcript above. - # Evaluations can be configured to score against either column - # (see ``CallImportEvaluation.transcript_source``). - diarised_transcript = Column(Text, nullable=True) - # Provider/model the diarisation worker used. Surfaced in the UI - # as "Diarised via deepgram/nova-2" next to the diarised - # transcript section. - diarised_transcript_provider = Column(String(50), nullable=True) - diarised_transcript_model = Column(String(100), nullable=True) - # Lifecycle status for the diarisation workflow. - # ``idle`` = no diarisation task has run; ``pending``/``running`` = - # a Celery task is queued or in flight; ``completed``/``failed`` = - # terminal. Independent of ``transcript_status`` so the two - # transcripts can be in different lifecycle states. - diarised_transcript_status = Column( - String(20), - nullable=False, - default="idle", - server_default="idle", - ) - diarised_transcript_error = Column(Text, nullable=True) - diarised_at = Column(DateTime(timezone=True), nullable=True) - - # Structured speaker turns produced by the diarisation worker — - # ``[{ "speaker": "agent"|"user"|"speaker_3", "text": "...", - # "start": float, "end": float, "raw_speaker": "Speaker 1" }, ...]`` - # The plain-text ``diarised_transcript`` above is a rendered view - # of this list (``: `` per line). When the worker - # cannot recover structured turns (no pyannote token / single- - # speaker recording / provider that doesn't surface segments) this - # column stays NULL and the plain-text path is still populated. - diarised_segments = Column(JSON, nullable=True) - # When True the ``agent`` <-> ``user`` mapping inside - # ``diarised_segments`` is inverted at render / export time. The - # worker writes the canonical mapping using the "first speaker is - # the agent" heuristic; reviewers can flip the toggle from the row - # detail panel without re-running diarisation. - diarised_speaker_swap = Column( - Boolean, - nullable=False, - default=False, - server_default="false", - ) - # LLM that turned the STT plain-text output into structured - # ``diarised_segments``. The legacy diarisation worker used - # pyannote and left these NULL; the current path always runs an - # LLM with the operator-supplied (or default) ``diarised_prompt`` - # below, and records exactly which model + prompt produced each - # row so reviewers can reproduce a specific run. - diarised_llm_provider = Column(String(50), nullable=True) - diarised_llm_model = Column(String(100), nullable=True) - diarised_llm_credential_id = Column(UUID(as_uuid=True), nullable=True) - diarised_prompt = Column(Text, nullable=True) - # Which diarisation pipeline produced this row's turns. - # * ``"stt_llm"`` (default) — two-stage: STT then LLM diariser. - # ``diarised_transcript_provider``/``_model`` describe the STT - # side; ``diarised_llm_provider``/``_model`` the LLM side. - # * ``"llm_only"`` — single-stage: audio fed straight to a - # multimodal LLM. ``diarised_transcript_provider`` is stamped - # with the sentinel ``"llm_only"``; the real model is on - # ``diarised_llm_*``. - # Persisting it on the row (not just the run) lets the row detail - # panel render the right "Diarised via …" label even for ad-hoc - # standalone transcribes (no parent evaluation). - transcribe_mode = Column( - String(20), - nullable=False, - default="stt_llm", - server_default="stt_llm", - ) - - status = Column( - Enum(CallImportRowStatus, values_callable=get_enum_values), - nullable=False, - default=CallImportRowStatus.PENDING, - index=True, - ) - - recording_s3_key = Column(String(1024), nullable=True) - recording_content_type = Column(String(128), nullable=True) - recording_size_bytes = Column(Integer, nullable=True) - - error_message = Column(Text, nullable=True) - attempts = Column(Integer, nullable=False, default=0) - celery_task_id = Column(String(255), nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - call_import = relationship("CallImport", back_populates="rows") - - -@event.listens_for(CallImportRow, "before_insert") -def _call_import_row_fill_workspace_id(_mapper, connection, target): - """Denormalize workspace_id from the parent import when omitted.""" - if target.workspace_id is not None or target.call_import_id is None: - return - workspace_id = connection.execute( - select(CallImport.workspace_id).where( - CallImport.id == target.call_import_id - ) - ).scalar_one_or_none() - if workspace_id is not None: - target.workspace_id = workspace_id - - -class CallImportTag(Base): - """User-defined tag that can be attached to one or more call imports. - - Tags coexist with the free-text ``CallImport.dataset`` column: dataset - is the primary high-level segregation, tags are an optional secondary - classification (an import can have many tags). - """ - - __tablename__ = "call_import_tags" - __table_args__ = ( - UniqueConstraint("organization_id", "name", name="uq_call_import_tag_org_name"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column( - UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True - ) - name = Column(String(255), nullable=False) - color = Column(String(32), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - -class CallImportTagAssignment(Base): - """Many-to-many join table between CallImport and CallImportTag.""" - - __tablename__ = "call_import_tag_assignments" - - call_import_id = Column( - UUID(as_uuid=True), - ForeignKey("call_imports.id", ondelete="CASCADE"), - primary_key=True, - ) - tag_id = Column( - UUID(as_uuid=True), - ForeignKey("call_import_tags.id", ondelete="CASCADE"), - primary_key=True, - index=True, - ) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - -class CallImportEvaluation(Base): - """Parent record for an evaluation run over a CallImport batch. - - A user picks a subset of org ``Metric`` rows and triggers an evaluation; - we fan out one ``CallImportEvaluationRow`` per source row and roll up - counters as workers finish. Status mirrors ``CallImportStatus`` plus a - ``RUNNING`` value so the UI can distinguish "queued" from "in flight". - """ - - __tablename__ = "call_import_evaluations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - call_import_id = Column( - UUID(as_uuid=True), - ForeignKey("call_imports.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - organization_id = Column( - UUID(as_uuid=True), - ForeignKey("organizations.id"), - nullable=False, - index=True, - ) - # Workspace isolation: mirrors the parent CallImport's workspace. - # Denormalized for fast filter-by-workspace listings without a join. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - created_by_user_id = Column( - UUID(as_uuid=True), ForeignKey("users.id"), nullable=True - ) - - # Optional user-supplied label for this run. Lets the UI surface - # something more meaningful than the UUID prefix (e.g. "March QA pass"). - name = Column(String(255), nullable=True) - - # JSON list of Metric UUID strings selected for this run. Stored as text - # in JSON so we don't have to deal with PG arrays of UUIDs / cascade - # delete policies when metrics are removed; the loader filters for - # still-existing org metrics at run time. - selected_metric_ids = Column(JSON, nullable=False, default=list) - # Hierarchy grouping snapshot: ``{parent_id_str: [child_id_str, ...]}``. - # Captures which children belong to which parent for THIS run so the UI - # / aggregator can reconstruct the tree even when the user selected - # only a subset of children, or after metrics are deleted / renamed. - # NULL on legacy rows means "no hierarchy" → fall back to flat - # ``selected_metric_ids`` semantics. - selected_metric_groups = Column(JSON, nullable=True) - # User-driven merges of LLM-discovered candidate sub-labels for - # ``allow_discovery`` parents. Shape: - # ``{"": {"": "", ...}}``. - # Populated via ``POST .../discovered-labels/merge``; consulted by - # the discovered-labels aggregator, the flow graph builder, and the - # worker so that rows finishing AFTER a merge cannot reintroduce - # the merged-away slug. Empty dict on fresh rows. - discovered_label_aliases = Column( - JSON, nullable=False, default=dict, server_default="{}" - ) - - # Per-run opt-in for top-level metric discovery. When True, the LLM - # is asked to propose brand-new top-level metrics (boolean / rating / - # category) observed in the transcripts in addition to scoring the - # ``selected_metric_ids`` for the row. Candidates surface in a - # "Discovered metrics" panel on the evaluation's Flow tab and can - # be promoted into real standalone ``Metric`` rows via - # ``POST /metrics/from-discovered``. Defaults to False so existing - # evaluation creation payloads keep their previous behaviour. - discover_new_metrics = Column( - Boolean, nullable=False, default=False, server_default="false" - ) - # Flat slug-to-slug redirect map for user merges + tombstones of - # discovered top-level metric candidates. Mirrors - # ``discovered_label_aliases`` but is NOT nested per parent — - # top-level metric discovery is not scoped to any parent. Shape:: - # - # {"": "", ...} - # - # An empty-string value tombstones the slug so workers finishing - # later can't re-introduce it. - discovered_metric_aliases = Column( - JSON, nullable=False, default=dict, server_default="{}" - ) - - # Run-level LLM config picked from the Run Evaluation modal. NULL on - # legacy rows means "use the historical OpenAI/gpt-4o default" — the - # worker checks for this and falls back accordingly. ``llm_credential_id`` - # pins a specific AIProvider row when the org has multiple credentials - # for the same provider. - llm_provider = Column(String(50), nullable=True) - llm_model = Column(String(100), nullable=True) - llm_credential_id = Column( - UUID(as_uuid=True), - ForeignKey("aiproviders.id", ondelete="SET NULL"), - nullable=True, - ) - llm_config = Column(JSON, nullable=True) - # Optional per-metric LLM override: - # ``{"": {"provider": "...", "model": "...", "credential_id": "..."}}``. - # Each entry overrides the run-level default for that metric only; - # missing keys = use run-level default. Stored as JSON so the UI can - # round-trip arbitrary {provider, model} pairs without migrations. - metric_llm_overrides = Column(JSON, nullable=True) - - # When ``auto_transcribe`` was set on the create payload, record the - # STT provider/model used so the UI can show "Auto-transcribed via - # deepgram/nova-2" on the evaluation header. ``stt_credential_id`` is - # untyped (no FK) because STT keys may live in either ``aiproviders`` - # (OpenAI) or ``integrations`` (Deepgram, ElevenLabs) — the - # transcription service handles the lookup. - stt_provider = Column(String(50), nullable=True) - stt_model = Column(String(100), nullable=True) - stt_credential_id = Column(UUID(as_uuid=True), nullable=True) - - # Run-level LLM diariser config. Used when the create-run / - # retry-run paths chain a ``transcribe_call_import_row_task`` - # because the row is missing a diarised transcript. Persisted on - # the run so a retry uses the same diariser the original create - # call picked (unless the retry payload explicitly overrides). - diarisation_llm_provider = Column(String(50), nullable=True) - diarisation_llm_model = Column(String(100), nullable=True) - diarisation_llm_credential_id = Column(UUID(as_uuid=True), nullable=True) - diarisation_prompt = Column(Text, nullable=True) - # Mode the run was *created* with for its auto-transcribe step. - # Retry chains read this to decide whether to enqueue an STT+LLM - # transcribe or a single-stage multimodal LLM transcribe — without - # it we'd have to infer the mode from "stt_provider is NULL", which - # would silently break legacy rows that simply never configured - # auto-transcribe. See migration 041 for the column DDL. - transcribe_mode = Column( - String(20), - nullable=False, - default="stt_llm", - server_default="stt_llm", - ) - - # Which of the two transcripts on each ``CallImportRow`` this run - # scored against. ``'production'`` reads ``CallImportRow.transcript`` - # (the CSV-supplied value); ``'diarised'`` reads - # ``CallImportRow.diarised_transcript`` (the worker output). When - # the user ticks both checkboxes in the Run Evaluation modal we - # create two ``CallImportEvaluation`` rows — one per source — so - # the two scorings can be compared side-by-side. Defaults to - # ``'production'`` so legacy runs (which always read the single - # historical ``transcript`` column) keep their semantics. - transcript_source = Column( - String(20), - nullable=False, - default="production", - server_default="production", - ) - - # Cached LLM-generated TLDR rendered above the Visualizations charts. - # Populated lazily by ``POST /evaluations/{eval_id}/insights`` so we - # never auto-burn LLM tokens on page load. Shape:: - # {"narrative": str, "patterns": [str, ...], - # "generated_at": iso8601, "generated_at_completed_rows": int, - # "provider": str, "model": str} - # NULL on rows that have never been summarised. - tldr_summary = Column(JSON, nullable=True) - - # Cached LLM-generated user insights for External Audit PDF section 03. - # Populated by a background Celery job triggered alongside TLDR generation. - # Shape: EvaluationUserInsightsState JSON (status, insights[], progress, …). - user_insights = Column(JSON, nullable=True) - - # Cached per-metric failure clustering for internal diagnostics PDF/UI. - # Shape: EvaluationMetricClustersState JSON (status, groups[], …). - metric_clusters = Column(JSON, nullable=True) - - # Cached LLM-generated prompt improvement suggestions keyed to an - # imported agent (PromptPartial tagged __imported_agent__). - # Shape: EvaluationPromptImprovementsState JSON. - prompt_improvements = Column(JSON, nullable=True) - - # Cached LLM explanations for week-over-week metric deltas keyed by - # baseline evaluation id + completed row counts. - period_delta_explanations = Column(JSON, nullable=True) - - status = Column(String(20), nullable=False, default="pending", index=True) - - total_rows = Column(Integer, nullable=False, default=0) - completed_rows = Column(Integer, nullable=False, default=0) - failed_rows = Column(Integer, nullable=False, default=0) - # Flexprice pass-level delta billing watermark: rows already emitted - # on ``call_import.evaluation_completed`` for this evaluation run. - billed_completed_rows = Column( - Integer, nullable=False, default=0, server_default="0" - ) - error_message = Column(Text, nullable=True) - celery_group_id = Column(String(255), nullable=True) - - started_at = Column(DateTime(timezone=True), nullable=True) - finished_at = Column(DateTime(timezone=True), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - call_import = relationship("CallImport", back_populates="evaluations") - row_results = relationship( - "CallImportEvaluationRow", - back_populates="evaluation", - cascade="all, delete-orphan", - ) - - -class CallImportEvaluationRow(Base): - """Per-source-row scoring output for a CallImportEvaluation parent.""" - - __tablename__ = "call_import_evaluation_rows" - __table_args__ = ( - UniqueConstraint( - "evaluation_id", "call_import_row_id", name="uq_call_import_evaluation_row" - ), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - evaluation_id = Column( - UUID(as_uuid=True), - ForeignKey("call_import_evaluations.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - call_import_row_id = Column( - UUID(as_uuid=True), - ForeignKey("call_import_rows.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - status = Column(String(20), nullable=False, default="pending", index=True) - # Same shape as EvaluatorResult.metric_scores: {metric_id_str: {value, type, metric_name, ...}} - metric_scores = Column(JSON, nullable=False, default=dict) - error_message = Column(Text, nullable=True) - celery_task_id = Column(String(255), nullable=True) - - started_at = Column(DateTime(timezone=True), nullable=True) - finished_at = Column(DateTime(timezone=True), nullable=True) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - evaluation = relationship("CallImportEvaluation", back_populates="row_results") - source_row = relationship("CallImportRow") - - -@event.listens_for(CallImportEvaluationRow, "before_insert") -def _call_import_evaluation_row_fill_workspace_id(_mapper, connection, target): - """Denormalize workspace_id from the parent evaluation when omitted.""" - if target.workspace_id is not None or target.evaluation_id is None: - return - workspace_id = connection.execute( - select(CallImportEvaluation.workspace_id).where( - CallImportEvaluation.id == target.evaluation_id - ) - ).scalar_one_or_none() - if workspace_id is not None: - target.workspace_id = workspace_id - - -class CallImportEvaluationReportSnapshot(Base): - """Persisted PDF-report aggregate used for period-over-period deltas.""" - - __tablename__ = "call_import_evaluation_report_snapshots" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - evaluation_id = Column( - UUID(as_uuid=True), - ForeignKey("call_import_evaluations.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - call_import_id = Column( - UUID(as_uuid=True), - ForeignKey("call_imports.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - organization_id = Column( - UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True - ) - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - period_label = Column(String(64), nullable=True, index=True) - period_start = Column(Date, nullable=True, index=True) - period_end = Column(Date, nullable=True, index=True) - report_config = Column(JSON, nullable=False, default=dict, server_default="{}") - selected_metric_ids = Column(JSON, nullable=False, default=list, server_default="[]") - metric_aggregates = Column(JSON, nullable=False, default=list, server_default="[]") - insight_aggregates = Column(JSON, nullable=False, default=list, server_default="[]") - narrative = Column(JSON, nullable=True) - total_calls = Column(Integer, nullable=False, default=0) - selected_metric_count = Column(Integer, nullable=False, default=0) - total_metric_count = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now() - ) - - -# --------------------------------------------------------------------------- -# Judge Alignment (AlignEval-style hybrid integration) -# -# Three tables back the "Judge Alignment" surface: -# - JudgeDataset: a labeled dataset materialised from one of three sources -# (voice transcripts, existing Metric/Evaluator outputs, -# or a generic CSV upload). Holds the dataset's source -# config + which fields play the role of input/output. -# - JudgeSample: one row in a dataset (input/output pair plus an -# optional binary pass/fail human label). -# - JudgeRun: a single run of an LLM-judge (existing Evaluator) over -# a subset of samples, with computed alignment metrics -# (precision/recall/F1/Cohen's kappa) and per-sample -# predictions. Optionally links to a GEPA optimization -# run when the user kicks off prompt tuning from a -# dataset. -# --------------------------------------------------------------------------- - - -class JudgeDataset(Base): - """Container for binary-labeled samples used to calibrate an LLM-judge.""" - - __tablename__ = "judge_datasets" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - organization_id = Column( - UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True - ) - # Workspace isolation: every judge dataset belongs to a workspace - # within its org. Samples and runs inherit this workspace_id. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - name = Column(String(255), nullable=False) - description = Column(Text, nullable=True) - - # One of: "transcript", "metric_output", "csv" - source_type = Column(String(32), nullable=False, index=True) - # Source-specific config. Examples: - # transcript: {"transcription_ids": [...]} or {"agent_id": "..."} - # metric_output: {"metric_id": "...", "evaluator_id": "..."} - # csv: {"s3_key": "...", "filename": "..."} - source_config = Column(JSON, nullable=False, default=dict) - - # Field roles - which textual content is "input" vs "output" for the judge. - # For voice transcripts both default to the transcript text but can be - # tightened (e.g. agent-only turns vs full conversation). - input_field = Column(String(64), nullable=False, default="input") - output_field = Column(String(64), nullable=False, default="output") - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - samples = relationship( - "JudgeSample", - back_populates="dataset", - cascade="all, delete-orphan", - order_by="JudgeSample.created_at", - ) - runs = relationship( - "JudgeRun", - back_populates="dataset", - cascade="all, delete-orphan", - order_by="JudgeRun.created_at.desc()", - ) - - -class JudgeSample(Base): - """One labelable input/output pair within a JudgeDataset.""" - - __tablename__ = "judge_samples" - __table_args__ = ( - UniqueConstraint("dataset_id", "external_id", name="uq_judge_samples_dataset_external"), - ) - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - dataset_id = Column( - UUID(as_uuid=True), - ForeignKey("judge_datasets.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - # Workspace isolation: mirrors the parent JudgeDataset's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - # Stable identifier within the source (e.g. transcription UUID, CSV row id). - # Used to dedupe re-imports and link back to the originating record. - external_id = Column(String(128), nullable=True, index=True) - - input_text = Column(Text, nullable=False) - output_text = Column(Text, nullable=False) - - # Binary human label: "pass" | "fail" | null (unlabeled). - # Stored as string (rather than enum) so it stays trivially extendable. - label = Column(String(16), nullable=True, index=True) - labeled_by = Column(String(255), nullable=True) - labeled_at = Column(DateTime(timezone=True), nullable=True) - - # Source-specific context (e.g. agent_id, original metric value, csv row). - extra = Column(JSON, nullable=True) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - - dataset = relationship("JudgeDataset", back_populates="samples") - - -class JudgeRun(Base): - """One execution of an LLM-judge against a JudgeDataset, with alignment metrics.""" - - __tablename__ = "judge_runs" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - dataset_id = Column( - UUID(as_uuid=True), - ForeignKey("judge_datasets.id", ondelete="CASCADE"), - nullable=False, - index=True, - ) - organization_id = Column( - UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True - ) - # Workspace isolation: mirrors the parent JudgeDataset's workspace. - workspace_id = Column( - UUID(as_uuid=True), - ForeignKey("workspaces.id", ondelete="RESTRICT"), - nullable=False, - index=True, - ) - - # Reuses the existing Evaluator row (its custom_prompt + llm_provider + llm_model - # define the judge under test). Nullable so a run may target an inline prompt - # in the future without inflating the Evaluator table. - evaluator_id = Column( - UUID(as_uuid=True), ForeignKey("evaluators.id", ondelete="SET NULL"), nullable=True, index=True - ) - - # Which subset was scored: "all" | "dev" | "test" - split = Column(String(16), nullable=False, default="all") - - # Snapshot of the model used (so a later Evaluator edit doesn't rewrite history). - llm_provider = Column(String(64), nullable=True) - llm_model = Column(String(128), nullable=True) - - # Computed alignment metrics: - # {"precision": float, "recall": float, "f1": float, "kappa": float, - # "tp": int, "fp": int, "tn": int, "fn": int, "n": int} - metrics = Column(JSON, nullable=True) - - # Per-sample predictions, keyed by sample_id (UUID string): - # {sample_id: {"prediction": "pass"|"fail", "explanation": str, "raw": str}} - predictions = Column(JSON, nullable=True) - - # Run lifecycle. - status = Column(String(20), nullable=False, default="pending", index=True) - error_message = Column(Text, nullable=True) - celery_task_id = Column(String, nullable=True, index=True) - - # Optional link to a GEPA optimization run kicked off from this dataset. - gepa_optimization_id = Column( - UUID(as_uuid=True), - ForeignKey("prompt_optimization_runs.id", ondelete="SET NULL"), - nullable=True, - index=True, - ) - - created_at = Column(DateTime(timezone=True), server_default=func.now()) - updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - created_by = Column(String, nullable=True) - - dataset = relationship("JudgeDataset", back_populates="runs") +"""SQLAlchemy database models.""" + +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + Date, + DateTime, + DDL, + Enum, + event, + Float, + ForeignKey, + Integer, + JSON, + String, + Text, + UniqueConstraint, + select, + text, +) +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +import uuid +import enum +from app.models.enums import ( + EvaluationType, EvaluationStatus, EvaluatorResultStatus, RoleEnum, InvitationStatus, + LanguageEnum, CallTypeEnum, CallMediumEnum, GenderEnum, AccentEnum, BackgroundNoiseEnum, + IntegrationPlatform, ModelProvider, VoiceBundleType, TestAgentConversationStatus, + MetricType, MetricCategory, MetricTrigger, CallRecordingStatus, AlertMetricType, AlertAggregation, + AlertOperator, AlertNotifyFrequency, AlertStatus, AlertHistoryStatus, CronJobStatus, + PromptOptimizationStatus, CallImportStatus, CallImportRowStatus, +) + +def get_enum_values(enum_class): + """Helper to get values from enum class for SQLAlchemy.""" + return [e.value for e in enum_class] + +from app.database import Base + + +# Enums moved to enums.py + + +class Organization(Base): + """Organization model for multi-tenancy.""" + + __tablename__ = "organizations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String(255), nullable=False) + voice_playground_threshold_overrides = Column(JSON, nullable=True) + # AlignEval-style judge alignment thresholds. + # Shape: {"min_labels_to_evaluate": int, "min_labels_to_optimize": int} + # Falls back to system defaults (20 / 50) when null. + judge_alignment_settings = Column(JSON, nullable=True) + # Per-org LLM gateway overrides (enabled, gateway_type, base_url, keys). + llm_gateway_settings = Column(JSON, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + api_keys = relationship("APIKey", back_populates="organization") + members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan") + invitations = relationship("Invitation", back_populates="organization", cascade="all, delete-orphan") + workspaces = relationship( + "Workspace", + back_populates="organization", + cascade="all, delete-orphan", + ) + workspace_roles = relationship( + "WorkspaceRole", + back_populates="organization", + cascade="all, delete-orphan", + ) + + +class Workspace(Base): + """Workspace - in-org isolation boundary for call imports and metrics. + + Every organization has at least one workspace (``is_default = True``, + seeded by migration 033). Users pick an "active workspace" in the UI; + list endpoints filter by it so users only see calls/metrics from the + project they're currently working in. Access is governed by + ``workspace_members`` and org-scoped ``workspace_roles`` (capability + bundles); org admins implicitly access all workspaces. + """ + + __tablename__ = "workspaces" + __table_args__ = ( + UniqueConstraint("organization_id", "slug", name="uq_workspaces_org_slug"), + ) + + # ``server_default`` is required so that raw-SQL INSERTs (e.g. the + # per-org Default seed in migration 033) can omit ``id`` and let the + # database fill it in. Without it, ``create_all`` produces a column + # with NOT NULL but no DEFAULT, and the migration crashes with + # ``null value in column "id"``. + id = Column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + server_default=text("gen_random_uuid()"), + ) + organization_id = Column( + UUID(as_uuid=True), + ForeignKey("organizations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name = Column(String(255), nullable=False) + slug = Column(String(255), nullable=False) + # At most one default per org. Enforced on Postgres by the partial + # unique index attached via the after_create event below; on + # SQLite (test runs) we rely on the route-level _check_slug_unique + # check + the Default-workspace conftest fixture instead, because + # SQLite doesn't support partial indexes the same way. + is_default = Column(Boolean, nullable=False, default=False, server_default="false") + # Reusable PDF/report branding metadata scoped to this workspace. Images + # live in S3. Shape: {"heading": str|null, "images": [{id, s3_key, + # content_type, filename, size_bytes, updated_at}, ...]}. + report_branding = Column(JSON, nullable=True) + created_by_user_id = Column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + organization = relationship("Organization", back_populates="workspaces") + members = relationship( + "WorkspaceMember", + back_populates="workspace", + cascade="all, delete-orphan", + ) + + +class WorkspaceRole(Base): + """Org-scoped workspace role (system or custom) as a capability bundle.""" + + __tablename__ = "workspace_roles" + __table_args__ = ( + UniqueConstraint("organization_id", "name", name="uq_workspace_roles_org_name"), + ) + + id = Column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + server_default=text("gen_random_uuid()"), + ) + organization_id = Column( + UUID(as_uuid=True), + ForeignKey("organizations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + capabilities = Column(JSON, nullable=False, default=list) + is_system = Column(Boolean, nullable=False, default=False, server_default="false") + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + organization = relationship("Organization", back_populates="workspace_roles") + members = relationship("WorkspaceMember", back_populates="role") + + +class WorkspaceMember(Base): + """User membership in a workspace with an assigned workspace role.""" + + __tablename__ = "workspace_members" + __table_args__ = ( + UniqueConstraint("workspace_id", "user_id", name="uq_workspace_members_ws_user"), + ) + + id = Column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + server_default=text("gen_random_uuid()"), + ) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + user_id = Column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + role_id = Column( + UUID(as_uuid=True), + ForeignKey("workspace_roles.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + added_by_user_id = Column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + ) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + workspace = relationship("Workspace", back_populates="members") + user = relationship("User", foreign_keys=[user_id]) + role = relationship("WorkspaceRole", back_populates="members") + added_by = relationship("User", foreign_keys=[added_by_user_id]) + + +# Partial unique index: "at most one default workspace per org". This +# is attached as an after_create event (rather than declared in +# ``__table_args__``) because SQLAlchemy's ``Index(..., +# postgresql_where=...)`` silently degrades to a *full* unique index on +# SQLite - which then forbids any second workspace per org and breaks +# the test suite. ``execute_if(dialect="postgresql")`` makes this DDL +# a no-op on SQLite while still emitting it on Postgres (prod, CI). +event.listen( + Workspace.__table__, + "after_create", + DDL( + "CREATE UNIQUE INDEX IF NOT EXISTS uq_workspaces_org_default " + "ON workspaces (organization_id) WHERE is_default" + ).execute_if(dialect="postgresql"), +) + + +class User(Base): + """User model for authentication and profile management.""" + + __tablename__ = "users" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + email = Column(String(255), unique=True, nullable=False, index=True) + name = Column(String(255), nullable=True) + first_name = Column(String(255), nullable=True) + last_name = Column(String(255), nullable=True) + password_hash = Column(String(255), nullable=True) # Nullable for users created via invitation + external_id = Column(String(255), unique=True, nullable=True, index=True) + auth_provider = Column(String(50), nullable=True) + mfa_enabled = Column(Boolean, default=False, nullable=False) + last_login_at = Column(DateTime(timezone=True), nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + organization_memberships = relationship("OrganizationMember", back_populates="user", cascade="all, delete-orphan") + api_keys = relationship("APIKey", back_populates="user") + invitations = relationship("Invitation", back_populates="invited_user", foreign_keys="Invitation.invited_user_id") + refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan") + + +class RefreshToken(Base): + """Opaque refresh token for extending local-password sessions.""" + + __tablename__ = "refresh_tokens" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True) + token_hash = Column(String(64), unique=True, nullable=False, index=True) + expires_at = Column(DateTime(timezone=True), nullable=False) + revoked_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User", back_populates="refresh_tokens") + + +class OrganizationMember(Base): + """Organization membership with role.""" + + __tablename__ = "organization_members" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) + role = Column(String, nullable=False, default=RoleEnum.READER.value) + + # User preferences for this organization + default_agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True, index=True) + + joined_at = Column(DateTime(timezone=True), server_default=func.now()) + + # Unique constraint: one membership per user per organization + __table_args__ = ( + UniqueConstraint('organization_id', 'user_id', name='uq_org_user'), + ) + + # Relationships + organization = relationship("Organization", back_populates="members") + user = relationship("User", back_populates="organization_memberships") + default_agent = relationship("Agent", foreign_keys=[default_agent_id]) + + +class Invitation(Base): + """Invitation model for inviting users to organizations.""" + + __tablename__ = "invitations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + invited_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True) # Null if user doesn't exist yet + invited_by_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + email = Column(String(255), nullable=False) # Email of invited user + role = Column(String, nullable=False, default=RoleEnum.READER.value) + status = Column(String, nullable=False, default=InvitationStatus.PENDING.value) + + + + token = Column(String(255), unique=True, nullable=False, index=True) # Invitation token + expires_at = Column(DateTime(timezone=True), nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + accepted_at = Column(DateTime(timezone=True), nullable=True) + + # Relationships + organization = relationship("Organization", back_populates="invitations") + invited_user = relationship("User", foreign_keys=[invited_user_id], back_populates="invitations") + invited_by = relationship("User", foreign_keys=[invited_by_id]) + + +class APIKey(Base): + """API Key model for authentication.""" + + __tablename__ = "api_keys" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + key = Column(String(255), unique=True, nullable=False, index=True) + name = Column(String(255), nullable=True) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True, index=True) # Optional: link to user + is_active = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + last_used = Column(DateTime(timezone=True), nullable=True) + + # Relationships + organization = relationship("Organization", back_populates="api_keys") + user = relationship("User", back_populates="api_keys") + + +class AudioFile(Base): + """Audio file model.""" + + __tablename__ = "audio_files" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + filename = Column(String(255), nullable=False) + file_path = Column(String(512), nullable=False) + file_size = Column(Integer, nullable=False) # Size in bytes + duration = Column(Float, nullable=True) # Duration in seconds + sample_rate = Column(Integer, nullable=True) + channels = Column(Integer, nullable=True) + format = Column(String(10), nullable=False) # wav, mp3, flac, etc. + uploaded_at = Column(DateTime(timezone=True), server_default=func.now()) + + # Relationships + evaluations = relationship("Evaluation", back_populates="audio_file") + + +class Evaluation(Base): + """Evaluation job model.""" + + __tablename__ = "evaluations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every legacy audio evaluation belongs to a + # workspace within its org. Stamped from the X-Workspace-Id header + # (falling back to the org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + audio_id = Column(UUID(as_uuid=True), ForeignKey("audio_files.id"), nullable=False) + reference_text = Column(String, nullable=True) # For WER calculation + evaluation_type = Column(String, nullable=False) + model_name = Column(String(100), nullable=True) + status = Column(String, default=EvaluationStatus.PENDING.value, nullable=False) + + + + metrics_requested = Column(JSON, nullable=True) # List of requested metrics + created_at = Column(DateTime(timezone=True), server_default=func.now()) + started_at = Column(DateTime(timezone=True), nullable=True) + completed_at = Column(DateTime(timezone=True), nullable=True) + error_message = Column(String, nullable=True) + + # Relationships + audio_file = relationship("AudioFile", back_populates="evaluations") + result = relationship("EvaluationResult", back_populates="evaluation", uselist=False) + + +class EvaluationResult(Base): + """Evaluation result model.""" + + __tablename__ = "evaluation_results" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + evaluation_id = Column(UUID(as_uuid=True), ForeignKey("evaluations.id"), nullable=False, unique=True) + # Workspace isolation: mirrors the parent Evaluation's workspace. + # Denormalized for fast filter-by-workspace listings without a join. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + transcript = Column(String, nullable=True) + metrics = Column(JSON, nullable=False) # {"wer": 0.05, "latency_ms": 1250, ...} + raw_output = Column(JSON, nullable=True) # Full model output + processing_time = Column(Float, nullable=True) # Processing time in seconds + model_used = Column(String(100), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + # Relationships + evaluation = relationship("Evaluation", back_populates="result") + + +# ============================================ +# VAIOPS MODELS - Voice AI Ops +# ============================================ + +# Enums moved to enums.py + + +class Agent(Base): + """Test Agent - The voice AI agent being evaluated""" + __tablename__ = "agents" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + agent_id = Column(String(6), unique=True, nullable=True, index=True) # 6-digit ID + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every agent belongs to a workspace within its + # org. Stamped from the X-Workspace-Id header (falling back to the + # org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + name = Column(String, nullable=False) + phone_number = Column(String, nullable=True) # Optional, required only for phone_call + language = Column(String, nullable=False, default=LanguageEnum.ENGLISH.value) + description = Column(String) + provider_prompt = Column(Text, nullable=True) + provider_prompt_synced_at = Column(DateTime(timezone=True), nullable=True) + call_type = Column(String, nullable=False, default=CallTypeEnum.OUTBOUND.value) + call_medium = Column(String, nullable=False, default=CallMediumEnum.PHONE_CALL.value) + telephony_phone_number_id = Column( + UUID(as_uuid=True), + ForeignKey("telephony_phone_numbers.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + + + + + # Voice configuration - either voice_bundle_id OR ai_provider_id OR voice_ai_integration_id (mutually exclusive) + voice_bundle_id = Column(UUID(as_uuid=True), ForeignKey("voicebundles.id"), nullable=True, index=True) + ai_provider_id = Column(UUID(as_uuid=True), ForeignKey("aiproviders.id"), nullable=True, index=True) + + # Voice AI agent integration (Retell, Vapi, etc.) + voice_ai_integration_id = Column(UUID(as_uuid=True), ForeignKey("integrations.id"), nullable=True, index=True) + voice_ai_agent_id = Column(String, nullable=True) # Agent ID from the external provider (Retell/Vapi) + prompt_variables = Column(JSON, nullable=True) + silence_hangup_secs = Column(Integer, nullable=False, server_default="15") + + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + created_by = Column(String) + + +class Persona(Base): + """Persona - TTS provider-tied voice identity for testing""" + __tablename__ = "personas" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every persona belongs to a workspace within + # its org. Stamped from the X-Workspace-Id header (falling back to + # the org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + name = Column(String, nullable=False) + gender = Column(String, nullable=False, default=GenderEnum.NEUTRAL.value) + tts_provider = Column(String(100), nullable=True) + tts_voice_id = Column(String(255), nullable=True) + tts_voice_name = Column(String(255), nullable=True) + is_custom = Column(Boolean, default=False) + description = Column(Text, nullable=True) + tts_config = Column(JSON, nullable=True) + llm_temperature = Column(Float, nullable=True) + llm_max_tokens = Column(Integer, nullable=True) + response_delay_ms = Column(Integer, nullable=True) + max_turns = Column(Integer, nullable=True) + allow_interruptions = Column(Boolean, nullable=True) + + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + created_by = Column(String) + + +class Scenario(Base): + """Scenario - The conversation scenario/test case""" + __tablename__ = "scenarios" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every scenario belongs to a workspace within + # its org. Stamped from the X-Workspace-Id header (falling back to + # the org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True, index=True) + name = Column(String, nullable=False) + description = Column(String) + required_info = Column(JSON) + + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + created_by = Column(String) + + +# Enums moved to enums.py + + +class Integration(Base): + """Integration model for connecting with external voice AI platforms.""" + __tablename__ = "integrations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + platform = Column(String, nullable=False) + + + + name = Column(String, nullable=True) # Optional friendly name + api_key = Column(String, nullable=False) # Encrypted Private API key for the platform + public_key = Column(String, nullable=True) # Optional Public API key (e.g. for Vapi) + is_active = Column(Boolean, default=True, nullable=False) + # Multiple credentials per (org, platform) are allowed. is_default marks + # the row used when a caller does not explicitly select a credential. + # A partial unique index in migration 028 enforces at most one default + # per (org, platform) at the DB level. + is_default = Column(Boolean, default=False, nullable=False) + # inherit | gateway | direct — per-credential LLM routing override + routing_mode = Column(String(20), nullable=False, default="inherit", server_default="inherit") + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + last_tested_at = Column(DateTime(timezone=True), nullable=True) # When API key was last validated + + +class ManualTranscription(Base): + """Manual transcription model for storing transcriptions from S3 audio files.""" + + __tablename__ = "manual_transcriptions" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + name = Column(String(255), nullable=True) # User-friendly name for the transcription + audio_file_key = Column(String(512), nullable=False) # S3 key or file path + transcript = Column(String, nullable=False) # Full transcript text + speaker_segments = Column(JSON, nullable=True) # List of segments with speaker labels: [{"speaker": "Speaker 1", "text": "...", "start": 0.0, "end": 5.2}] + stt_model = Column(String(100), nullable=True) # STT model used (e.g., "whisper-1", "google-speech-v2") + stt_provider = Column(String, nullable=True) # Provider used + + + + language = Column(String(10), nullable=True) # Detected or specified language + processing_time = Column(Float, nullable=True) # Processing time in seconds + raw_output = Column(JSON, nullable=True) # Full model output for reference + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class ConversationEvaluation(Base): + """Conversation evaluation model for evaluating manual transcriptions against agent objectives.""" + + __tablename__ = "conversation_evaluations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + transcription_id = Column(UUID(as_uuid=True), ForeignKey("manual_transcriptions.id"), nullable=False, index=True) + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False, index=True) + + # Evaluation results + objective_achieved = Column(Boolean, nullable=False) # Binary: was the conversation objective achieved? + objective_achieved_reason = Column(String, nullable=True) # Explanation for the binary result + additional_metrics = Column(JSON, nullable=True) # Additional evaluation metrics (e.g., professionalism, clarity, etc.) + overall_score = Column(Float, nullable=True) # Overall score (0.0 to 1.0) + + # LLM metadata + llm_provider = Column(Enum(ModelProvider, native_enum=False), nullable=True) + + llm_model = Column(String(100), nullable=True) + llm_response = Column(JSON, nullable=True) # Full LLM response for reference + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class AIProvider(Base): + """AI Provider - Stores API keys for different AI platforms.""" + __tablename__ = "aiproviders" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + provider = Column(String, nullable=False) + + + + api_key = Column(String, nullable=False) # Encrypted API key + name = Column(String, nullable=True) # Optional friendly name + # Azure OpenAI resource endpoint (e.g. https://my-resource.openai.azure.com). + # Only used when provider is azure; other providers ignore this column. + endpoint_url = Column(String, nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + # Multiple AIProvider rows per (org, provider) are allowed. is_default + # marks the row resolved when no explicit credential id is selected. + # A partial unique index in migration 028 enforces at most one default. + is_default = Column(Boolean, default=False, nullable=False) + # inherit | gateway | direct — per-credential LLM routing override + routing_mode = Column(String(20), nullable=False, default="inherit", server_default="inherit") + # Bifrost custom model ID used when routing via gateway + gateway_model = Column(String(255), nullable=True) + # inherit | litellm_shim | native_openai — Bifrost API surface override + gateway_interface = Column(String(20), nullable=False, default="inherit", server_default="inherit") + # Optional per-credential Bifrost/gateway base URL override + gateway_base_url = Column(String(512), nullable=True) + # Optional auth header for Bifrost (e.g. x-bf-vk, Authorization, x-api-key) + gateway_auth_header = Column(String(64), nullable=True) + # Env var name whose value is sent as the gateway auth secret + gateway_auth_secret_env = Column(String(128), nullable=True) + # Encrypted inline gateway auth secret (alternative to env var) + gateway_auth_secret = Column(String, nullable=True) + # Arbitrary HTTP headers sent with gateway-routed LiteLLM calls + gateway_extra_headers = Column(JSON, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + last_tested_at = Column(DateTime(timezone=True), nullable=True) # When API key was last validated + + +# Enums moved to enums.py + + +class VoiceBundle(Base): + """VoiceBundle - Composable unit combining STT, LLM, and TTS for voice AI testing, or S2S models.""" + __tablename__ = "voicebundles" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + name = Column(String, nullable=False) + description = Column(String, nullable=True) + + # Bundle type: either STT+LLM+TTS or S2S + # Using String instead of Enum to avoid SQLAlchemy enum conversion issues + # The enum conversion is handled in the Pydantic schemas + bundle_type = Column(String(50), nullable=False, default=VoiceBundleType.STT_LLM_TTS.value) + + # STT Configuration (references AIProvider via provider name) - required for STT_LLM_TTS, optional for S2S + stt_provider = Column(String, nullable=True) + # Optional explicit credential row (aiproviders.id or integrations.id). + # When NULL the credential resolver picks the default row for the + # provider. No FK is set because the target table varies by provider. + stt_credential_id = Column(UUID(as_uuid=True), nullable=True) + + stt_model = Column(String, nullable=True) # e.g., "whisper-1", "google-speech-v2" + + # LLM Configuration (references AIProvider via provider name) - required for STT_LLM_TTS, optional for S2S + llm_provider = Column(String, nullable=True) + llm_credential_id = Column(UUID(as_uuid=True), nullable=True) + + llm_model = Column(String, nullable=True) # e.g., "gpt-4", "claude-3-opus" + llm_temperature = Column(Float, nullable=True, default=0.7) + llm_max_tokens = Column(Integer, nullable=True) + llm_config = Column(JSON, nullable=True) # Additional LLM configuration (extensible) + + # TTS Configuration (references AIProvider via provider name) - required for STT_LLM_TTS, optional for S2S + tts_provider = Column(String, nullable=True) + tts_credential_id = Column(UUID(as_uuid=True), nullable=True) + + tts_model = Column(String, nullable=True) # e.g., "tts-1", "neural-voice" + tts_voice = Column(String, nullable=True) # Voice selection if applicable + tts_config = Column(JSON, nullable=True) # Additional TTS configuration (extensible) + + # S2S Configuration - required for S2S type, optional for STT_LLM_TTS + s2s_provider = Column(String, nullable=True) + s2s_credential_id = Column(UUID(as_uuid=True), nullable=True) + + + + s2s_model = Column(String, nullable=True) # e.g., "gpt-4o-transcribe", speech-to-speech model + s2s_config = Column(JSON, nullable=True) # Additional S2S configuration (extensible) + + # Additional configuration for extensibility + extra_metadata = Column(JSON, nullable=True) # For future extensions (renamed from 'metadata' to avoid SQLAlchemy conflict) + + is_active = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +# Enums moved to enums.py + + +class TestAgentConversation(Base): + """Test Agent Conversation - Records conversations between test AI agent and voice AI agent.""" + __tablename__ = "test_agent_conversations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every playground conversation belongs to a + # workspace within its org. Stamped from the X-Workspace-Id header + # (falling back to the org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + # Configuration + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False) + persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=False) + scenario_id = Column(UUID(as_uuid=True), ForeignKey("scenarios.id"), nullable=False) + voice_bundle_id = Column(UUID(as_uuid=True), ForeignKey("voicebundles.id"), nullable=True) + + # Conversation data + status = Column(String, nullable=False, default=TestAgentConversationStatus.INITIALIZING.value) + + + + live_transcription = Column(JSON, nullable=True) # Array of conversation turns with timestamps + conversation_audio_key = Column(String, nullable=True) # S3 key for recorded conversation audio + full_transcript = Column(String, nullable=True) # Full conversation transcript + + # Metadata + started_at = Column(DateTime(timezone=True), server_default=func.now()) + ended_at = Column(DateTime(timezone=True), nullable=True) + duration_seconds = Column(Float, nullable=True) + + # Additional metadata + conversation_metadata = Column(JSON, nullable=True) # Additional conversation metadata + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +class EvaluatorSuite(Base): + """Evaluator suite — one agent + one persona + N scenario combinations.""" + + __tablename__ = "evaluator_suites" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + name = Column(String, nullable=True) + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False) + persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=False) + metric_ids = Column(JSON, nullable=True) + llm_provider = Column(String, nullable=True) + llm_model = Column(String, nullable=True) + llm_config = Column(JSON, nullable=True) + tags = Column(JSON, nullable=True) + default_runs_per_combination = Column(Integer, nullable=False, default=1) + round_robin_index = Column(Integer, nullable=False, default=0) + is_active = Column(Boolean, nullable=False, default=False) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +class Evaluator(Base): + """Evaluator - Configuration for testing agents with specific persona and scenario combinations, or custom prompt evaluators.""" + __tablename__ = "evaluators" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + evaluator_id = Column(String(6), unique=True, nullable=False, index=True) # 6-digit ID + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every evaluator belongs to a workspace within + # its org. Stamped from the X-Workspace-Id header (falling back to + # the org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + # Display name (required for custom evaluators, optional for standard) + name = Column(String, nullable=True) + + # Parent suite (nullable for legacy/custom evaluators) + suite_id = Column( + UUID(as_uuid=True), + ForeignKey("evaluator_suites.id", ondelete="CASCADE"), + nullable=True, + index=True, + ) + + # Standard evaluator configuration (nullable for custom evaluators) + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=True) + persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=True) + scenario_id = Column(UUID(as_uuid=True), ForeignKey("scenarios.id"), nullable=True) + + # Custom evaluator prompt (used instead of agent/persona/scenario) + custom_prompt = Column(Text, nullable=True) + + # Custom evaluator metric selection. When set, the worker filters the + # enabled-org metrics down to only these IDs (list of metric UUID strings). + # Standard evaluators leave this NULL and use all enabled agent metrics. + metric_ids = Column(JSON, nullable=True) + + # LLM configuration for evaluation (overrides hardcoded defaults) + llm_provider = Column(String, nullable=True) # e.g. "openai", "anthropic", "google" + llm_model = Column(String, nullable=True) # e.g. "gpt-4.1", "claude-sonnet-4-20250514" + llm_config = Column(JSON, nullable=True) + + # Tags for categorization + tags = Column(JSON, nullable=True) # Array of tag strings + + # Metadata + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +# Enums moved to enums.py + + +class Metric(Base): + """Metric - Configuration for evaluation metrics. + + Supports a 2-level hierarchy via ``parent_metric_id``: a "category" + parent metric (e.g. "Call Outcome") owns N child sub-metric labels + (e.g. "happy_completion", "angry_hangup"). ``selection_mode`` is set + only on parents and controls how the LLM scores children together + (``single_choice`` = pick exactly one; ``multi_label`` = independent + yes/no with logical consistency). Children are always boolean. + """ + __tablename__ = "metrics" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: two-shape column. + # + # * ``workspace_id = `` — workspace-scoped metric. Only + # visible inside that workspace (the default behavior; existing + # rows all look like this). + # * ``workspace_id IS NULL`` — org-shared metric. Surfaces in + # every workspace's listing under this org so users don't have + # to recreate the same metric per workspace. + # + # Children always inherit their parent's ``workspace_id`` (including + # NULL) so a category metric's whole subtree shares one scope; the + # add-child / promote-discovered endpoints enforce this. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=True, + index=True, + ) + + # Basic information + name = Column(String, nullable=False) + description = Column(String, nullable=True) + # Free-form illustrative example used to sharpen the LLM judge's + # rubric. Today this is consumed by child sub-labels of a + # categorization parent metric so each label can carry "what does + # this look like in a transcript?" text alongside the rubric in + # ``description``. The column lives on every Metric row for + # forward-compat: a standalone metric could later surface its own + # example without another migration. + example = Column(Text, nullable=True) + + # Configuration + metric_type = Column(String, nullable=False, default=MetricType.RATING.value) + metric_category = Column( + String(30), + nullable=False, + default=MetricCategory.QUALITY.value, + server_default=MetricCategory.QUALITY.value, + ) + trigger = Column(String, nullable=False, default=MetricTrigger.ALWAYS.value) + metric_origin = Column(String(30), nullable=False, default="default") + supported_surfaces = Column(JSON, nullable=False, default=list) # ["agent", "voice_playground", "blind_test"] + enabled_surfaces = Column(JSON, nullable=False, default=list) # subset of supported_surfaces + custom_data_type = Column(String(30), nullable=True) # "boolean" | "enum" | "number_range" + custom_config = Column(JSON, nullable=True) # enum options / number range config + tags = Column(JSON, nullable=True) # ["tone", "latency", ...] + + # Hierarchy: NULL = standalone or parent. When set, this row is a + # child sub-metric of the referenced parent. ON DELETE CASCADE so + # deleting a category removes its children atomically. + parent_metric_id = Column( + UUID(as_uuid=True), + ForeignKey("metrics.id", ondelete="CASCADE"), + nullable=True, + index=True, + ) + # Set only on parent rows (``parent_metric_id IS NULL``). Either + # ``single_choice`` or ``multi_label``. NULL = legacy / non-hierarchical + # metric (no children). + selection_mode = Column(String(20), nullable=True) + + # When true on a parent metric (any selection_mode), the LLM is + # invited during call-import evaluation to emit additional + # candidate sub-labels beyond the user-defined children. The + # candidates surface in a "Discovered labels" panel where the user + # manually promotes them into real child Metric rows. For + # ``single_choice`` parents the discovered entries are + # supplemental — the chosen child is still picked from the + # predefined children so the exactly-one-true invariant holds. + # The validator rejects this flag on standalone / child metrics. + allow_discovery = Column( + Boolean, nullable=False, default=False, server_default="false" + ) + + # When True, this metric is a "transcript-compare judge": the + # call-import evaluator feeds BOTH the production transcript + # (``call_import_rows.transcript``, CSV-supplied) and the diarised + # transcript (``call_import_rows.diarised_transcript``, worker- + # produced by the STT/diarisation pipeline) to the LLM as a + # labeled pair instead of feeding one transcript. The parent + # evaluation's ``CallImportEvaluation.transcript_source`` is + # ignored for these metrics — they always read both columns. + # Rows where either transcript is missing are skipped per-metric + # with ``skipped="comparison_missing_transcript"`` so the rest of + # the row's metrics still produce scores. The Pydantic validator + # rejects ``compare_transcripts`` combined with ``parent_metric_id`` + # or ``selection_mode`` (i.e. it can't simultaneously be part of + # a parent/child hierarchy). The call-import worker also + # auto-promotes a metric to comparison mode when its description + # references the production / diarised transcripts in well-known + # phrases (see ``_metric_text_references_production`` in + # ``app.workers.tasks.evaluate_call_import_row``). + compare_transcripts = Column( + Boolean, nullable=False, default=False, server_default="false" + ) + + parent = relationship( + "Metric", + remote_side=[id], + backref="children", + ) + + # When true, the LLM-judge is asked to also return a short free-form + # rationale alongside the value (stored under ``metric_scores[id].rationale``). + # Adds a second " - LLM Rationale" column in the call-import CSV export. + capture_rationale = Column(Boolean, nullable=False, default=False) + + enabled = Column(Boolean, nullable=False, default=True) + + # Metadata + is_default = Column(Boolean, nullable=False, default=False) # Pre-defined metrics + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +class EvaluatorResult(Base): + """EvaluatorResult - Results from running an evaluator with transcription and metric evaluations.""" + __tablename__ = "evaluator_results" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + result_id = Column(String(6), unique=True, nullable=False, index=True) # 6-digit ID + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every evaluator result belongs to a workspace + # within its org. Stamped from the active workspace at creation time + # (either the X-Workspace-Id header or the org's Default workspace). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + # References + evaluator_id = Column(UUID(as_uuid=True), ForeignKey("evaluators.id"), nullable=True, index=True) # Optional - can be None for test calls without persona/scenario + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=True) # Nullable for custom evaluators + persona_id = Column(UUID(as_uuid=True), ForeignKey("personas.id"), nullable=True) # Optional - can be None for test calls + scenario_id = Column(UUID(as_uuid=True), ForeignKey("scenarios.id"), nullable=True) # Optional - can be None for test calls + + # Result data + name = Column(String, nullable=True) # Scenario name or test call name (optional) + timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + duration_seconds = Column(Float, nullable=True) # Call duration + status = Column(String(20), nullable=False, default=EvaluatorResultStatus.QUEUED.value) + + # Audio and transcription + audio_s3_key = Column(String, nullable=True) # S3 key for audio file + transcription = Column(String, nullable=True) # Full transcription + speaker_segments = Column(JSON, nullable=True) # List of segments with speaker labels: [{"speaker": "Speaker 1", "text": "...", "start": 0.0, "end": 5.2}] + + # Metric scores - JSON object with metric_id as key and score as value + # Format: {"metric_id_1": {"value": 85, "type": "rating"}, "metric_id_2": {"value": true, "type": "boolean"}} + metric_scores = Column(JSON, nullable=True) + + # Celery task tracking + celery_task_id = Column(String, nullable=True, index=True) # Celery task ID for tracking + + # Error information + error_message = Column(String, nullable=True) + + # Call event tracking (similar to CallRecording) + call_event = Column(String, nullable=True, index=True) # Latest call event (e.g., call_started, call_ended) + provider_call_id = Column(String, nullable=True, index=True) # Provider's call_id (e.g., Retell call_id) + provider_platform = Column(String, nullable=True) # e.g., "retell", "vapi" + call_data = Column(JSON, nullable=True) # Full call details from provider (like CallRecording) + + # Data-plane shard routing (payload rows on shard DBs when sharding enabled) + shard_id = Column(String(64), nullable=True, index=True) + + # Metadata + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +# Enums moved to enums.py + + +class CallRecordingSource(str, enum.Enum): + """Source of the call recording data.""" + + PLAYGROUND = "playground" + WEBHOOK = "webhook" + + +class CallRecording(Base): + """Call Recording model for tracking voice provider calls.""" + __tablename__ = "call_recordings" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every recording belongs to a workspace within + # its org. For playground-origin rows this is stamped from the active + # workspace at creation time; for webhook-origin rows the worker + # looks up the recording's agent and inherits its workspace_id. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + call_short_id = Column(String(6), unique=True, nullable=False, index=True) # 6-digit ID + status = Column(Enum(CallRecordingStatus), nullable=False, default=CallRecordingStatus.PENDING, index=True) + call_event = Column(String, nullable=True, index=True) # Latest webhook event (e.g., call_started, call_ended) + source = Column(Enum(CallRecordingSource), nullable=False, default=CallRecordingSource.PLAYGROUND, index=True) + call_data = Column(JSON, nullable=True) # JSON blob for provider response + provider_call_id = Column(String, nullable=True, index=True) # Provider's call_id (e.g., Retell call_id) + provider_platform = Column(String, nullable=True) # e.g., "retell", "vapi" + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=True) # Reference to our agent + + # Link to EvaluatorResult for metric evaluations + evaluator_result_id = Column( + UUID(as_uuid=True), + ForeignKey("evaluator_results.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + + shard_id = Column(String(64), nullable=True, index=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class EvaluatorResultPayload(Base): + """Heavy evaluator result fields stored on data shards when sharding is enabled.""" + + __tablename__ = "evaluator_result_payloads" + + evaluator_result_id = Column(UUID(as_uuid=True), primary_key=True) + workspace_id = Column(UUID(as_uuid=True), nullable=False, index=True) + audio_s3_key = Column(String, nullable=True) + transcription = Column(String, nullable=True) + speaker_segments = Column(JSON, nullable=True) + metric_scores = Column(JSON, nullable=True) + call_data = Column(JSON, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class CallRecordingPayload(Base): + """Heavy call recording fields stored on data shards when sharding is enabled.""" + + __tablename__ = "call_recording_payloads" + + call_recording_id = Column(UUID(as_uuid=True), primary_key=True) + workspace_id = Column(UUID(as_uuid=True), nullable=False, index=True) + call_data = Column(JSON, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class Alert(Base): + """Alert model for configuring monitoring alerts.""" + __tablename__ = "alerts" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + + # Basic information + name = Column(String(255), nullable=False) + description = Column(String, nullable=True) + + # Metric condition configuration + metric_type = Column(String, nullable=False, default=AlertMetricType.NUMBER_OF_CALLS.value) + aggregation = Column(String, nullable=False, default=AlertAggregation.SUM.value) + operator = Column(String, nullable=False, default=AlertOperator.GREATER_THAN.value) + threshold_value = Column(Float, nullable=False) + time_window_minutes = Column(Integer, nullable=False, default=60) # Time window for aggregation + + # Agent selection (JSON array of agent UUIDs, null means all agents) + agent_ids = Column(JSON, nullable=True) + + # Notification configuration + notify_frequency = Column(String, nullable=False, default=AlertNotifyFrequency.IMMEDIATE.value) + notify_emails = Column(JSON, nullable=True) # Array of email addresses + notify_webhooks = Column(JSON, nullable=True) # Array of webhook URLs (Slack, etc.) + + # Status + status = Column(String, nullable=False, default=AlertStatus.ACTIVE.value) + + # Metadata + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + # Relationships + alert_history = relationship("AlertHistory", back_populates="alert", cascade="all, delete-orphan") + + +class AlertHistory(Base): + """Alert history model for tracking triggered alerts.""" + __tablename__ = "alert_history" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + alert_id = Column(UUID(as_uuid=True), ForeignKey("alerts.id"), nullable=False, index=True) + + # Trigger information + triggered_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + triggered_value = Column(Float, nullable=False) # The actual value that triggered the alert + threshold_value = Column(Float, nullable=False) # The threshold at time of trigger + + # Status tracking + status = Column(String, nullable=False, default=AlertHistoryStatus.TRIGGERED.value) + + # Notification tracking + notified_at = Column(DateTime(timezone=True), nullable=True) + notification_details = Column(JSON, nullable=True) # Details of sent notifications + + # Resolution + acknowledged_at = Column(DateTime(timezone=True), nullable=True) + acknowledged_by = Column(String, nullable=True) + resolved_at = Column(DateTime(timezone=True), nullable=True) + resolved_by = Column(String, nullable=True) + resolution_notes = Column(String, nullable=True) + + # Additional context + context_data = Column(JSON, nullable=True) # Additional data about the trigger + + # Metadata + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + alert = relationship("Alert", back_populates="alert_history") + + +class CronJob(Base): + """Cron job model for scheduling automated evaluator runs.""" + __tablename__ = "cron_jobs" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + + # Basic information + name = Column(String(255), nullable=False) + cron_expression = Column(String(100), nullable=False) # e.g., "0 9 * * 1-5" + timezone = Column(String(100), nullable=False, default="UTC") + + # Run configuration + max_runs = Column(Integer, nullable=False, default=10) + current_runs = Column(Integer, nullable=False, default=0) + + # Evaluators to trigger (JSON array of evaluator UUIDs) + evaluator_ids = Column(JSON, nullable=False) + + # Status + status = Column(String, nullable=False, default=CronJobStatus.ACTIVE.value) + + # Run tracking + next_run_at = Column(DateTime(timezone=True), nullable=True) + last_run_at = Column(DateTime(timezone=True), nullable=True) + + # Metadata + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + +class TTSComparisonStatus(str, enum.Enum): + PENDING = "pending" + GENERATING = "generating" + EVALUATING = "evaluating" + COMPLETED = "completed" + FAILED = "failed" + + +class TTSSampleStatus(str, enum.Enum): + PENDING = "pending" + GENERATING = "generating" + COMPLETED = "completed" + FAILED = "failed" + + +class TTSReportJobStatus(str, enum.Enum): + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class TTSComparison(Base): + """TTS Comparison session for A/B testing voice providers.""" + __tablename__ = "tts_comparisons" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every voice playground comparison belongs to + # a workspace within its org. Children (samples, report jobs, blind + # test shares) inherit this workspace_id. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + simulation_id = Column(String(6), unique=True, index=True, nullable=True) + + name = Column(String(255), nullable=True) + status = Column(String(50), nullable=False, default=TTSComparisonStatus.PENDING.value) + + # 'benchmark' = traditional TTS A/B benchmark (provider-generated audio). + # 'blind_test_only' = standalone blind test built from existing recordings + # / uploads / past TTS samples; no TTS generation happens. + mode = Column(String(32), nullable=False, default="benchmark") + + provider_a = Column(String(100), nullable=True) + model_a = Column(String(100), nullable=True) + voices_a = Column(JSON, nullable=True) + + provider_b = Column(String(100), nullable=True) + model_b = Column(String(100), nullable=True) + voices_b = Column(JSON, nullable=True) + + sample_texts = Column(JSON, nullable=False) + num_runs = Column(Integer, nullable=False, default=1) + + blind_test_results = Column(JSON, nullable=True) + evaluation_summary = Column(JSON, nullable=True) + + eval_stt_provider = Column(String(100), nullable=True) + eval_stt_model = Column(String(100), nullable=True) + + celery_task_id = Column(String, nullable=True, index=True) + error_message = Column(String, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + samples = relationship("TTSSample", back_populates="comparison", cascade="all, delete-orphan") + + +class TTSSample(Base): + """Individual TTS audio sample within a comparison.""" + __tablename__ = "tts_samples" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + comparison_id = Column(UUID(as_uuid=True), ForeignKey("tts_comparisons.id", ondelete="CASCADE"), nullable=False, index=True) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: mirrors the parent TTSComparison's workspace. + # Denormalized for fast filter-by-workspace listings without a join. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + provider = Column(String(100), nullable=True) + model = Column(String(100), nullable=True) + voice_id = Column(String(255), nullable=True) + voice_name = Column(String(255), nullable=True) + side = Column(String(1), nullable=True) # "A" or "B" + sample_index = Column(Integer, nullable=False) + run_index = Column(Integer, nullable=False, default=0) + + # 'tts' (default, audio is synthesized by a provider), 'recording' (audio + # is reused from a CallImportRow recording), or 'upload' (audio was + # uploaded by the user). Non-tts samples are marked completed up-front + # by the API and skipped by the generation worker. + source_type = Column(String(32), nullable=False, default="tts") + # When source_type == 'recording', references CallImportRow.id (no FK + # constraint to keep cascading deletes simple if a call import is later + # removed; the audio_s3_key is what's actually used). + source_ref_id = Column(UUID(as_uuid=True), nullable=True) + + text = Column(String, nullable=False) + audio_s3_key = Column(String(512), nullable=True) + duration_seconds = Column(Float, nullable=True) + latency_ms = Column(Float, nullable=True) + ttfb_ms = Column(Float, nullable=True) + + evaluation_metrics = Column(JSON, nullable=True) + status = Column(String(50), nullable=False, default=TTSSampleStatus.PENDING.value) + error_message = Column(String, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + comparison = relationship("TTSComparison", back_populates="samples") + + +class TTSReportJob(Base): + """Asynchronous PDF report generation jobs for Voice Playground.""" + __tablename__ = "tts_report_jobs" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: mirrors the parent TTSComparison's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + comparison_id = Column(UUID(as_uuid=True), ForeignKey("tts_comparisons.id", ondelete="CASCADE"), nullable=False, index=True) + + status = Column(String(50), nullable=False, default=TTSReportJobStatus.PENDING.value) + format = Column(String(20), nullable=False, default="pdf") + filename = Column(String(255), nullable=True) + s3_key = Column(String(512), nullable=True) + error_message = Column(String, nullable=True) + celery_task_id = Column(String, nullable=True, index=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + comparison = relationship("TTSComparison") + + +class TTSBlindTestShareStatus(str, enum.Enum): + OPEN = "open" + CLOSED = "closed" + + +class TTSBlindTestShare(Base): + """A publicly sharable blind test for a TTSComparison. + + The share_token is the capability: anyone holding it can open the public + form and submit a response. Each comparison has at most one share row. + """ + __tablename__ = "tts_blind_test_shares" + __table_args__ = ( + UniqueConstraint("comparison_id", name="uq_blind_test_shares_comparison"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + comparison_id = Column( + UUID(as_uuid=True), + ForeignKey("tts_comparisons.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: mirrors the parent TTSComparison's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + share_token = Column(String(64), unique=True, nullable=False, index=True) + + title = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + + # Internal notes visible only to the share creator (e.g. which voice + # corresponds to which side, source notes for standalone blind tests). + # Never exposed via the public blind test payload. + creator_notes = Column(Text, nullable=True) + + # JSON list: [{ "key": str, "label": str, "type": "rating"|"comment", "scale": int? }] + custom_metrics = Column(JSON, nullable=False) + + status = Column(String(20), nullable=False, default=TTSBlindTestShareStatus.OPEN.value) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + closed_at = Column(DateTime(timezone=True), nullable=True) + created_by = Column(String, nullable=True) + + comparison = relationship("TTSComparison") + responses = relationship( + "TTSBlindTestResponse", + back_populates="share", + cascade="all, delete-orphan", + ) + + +class TTSBlindTestResponse(Base): + """A single rater's submission against a TTSBlindTestShare.""" + __tablename__ = "tts_blind_test_responses" + __table_args__ = ( + UniqueConstraint("share_id", "rater_email", name="uq_blind_test_response_share_email"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + share_id = Column( + UUID(as_uuid=True), + ForeignKey("tts_blind_test_shares.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + # Workspace isolation: mirrors the parent TTSBlindTestShare's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + rater_name = Column(String(255), nullable=False) + rater_email = Column(String(320), nullable=False, index=True) + + # JSON list keyed by sample_index. Server stores in TRUE A/B orientation + # (already de-flipped from whatever the rater's UI showed): + # [{ + # "sample_index": int, + # "preferred": "A" | "B", + # "ratings_a": { metric_key: number }, + # "ratings_b": { metric_key: number }, + # "comment": str? + # }] + responses = Column(JSON, nullable=False) + + ip = Column(String(64), nullable=True) + user_agent = Column(String(512), nullable=True) + + submitted_at = Column(DateTime(timezone=True), server_default=func.now()) + + share = relationship("TTSBlindTestShare", back_populates="responses") + + +class PromptPartial(Base): + """Prompt Partial - Reusable prompt templates with version history.""" + __tablename__ = "prompt_partials" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every prompt partial belongs to a workspace + # within its org. Versions inherit this workspace_id. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + name = Column(String(255), nullable=False) + description = Column(String, nullable=True) + content = Column(Text, nullable=False) + tags = Column(JSON, nullable=True) + current_version = Column(Integer, nullable=False, default=1) + # Cached LLM-generated flowchart for imported production agent prompts. + # Shape: AgentFlowGraph JSON (nodes[], edges[]). + agent_flowchart = Column(JSON, nullable=True) + agent_flowchart_status = Column(String(20), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + versions = relationship("PromptPartialVersion", back_populates="prompt_partial", cascade="all, delete-orphan", order_by="PromptPartialVersion.version.desc()") + + +class PromptPartialVersion(Base): + """Version history for a prompt partial.""" + __tablename__ = "prompt_partial_versions" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + prompt_partial_id = Column(UUID(as_uuid=True), ForeignKey("prompt_partials.id", ondelete="CASCADE"), nullable=False, index=True) + # Workspace isolation: mirrors the parent PromptPartial's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + version = Column(Integer, nullable=False) + content = Column(Text, nullable=False) + change_summary = Column(String, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + created_by = Column(String, nullable=True) + + prompt_partial = relationship("PromptPartial", back_populates="versions") + + __table_args__ = ( + UniqueConstraint('prompt_partial_id', 'version', name='uq_prompt_partial_version'), + ) + + +class CustomTTSVoice(Base): + """Organization-scoped custom TTS voice metadata.""" + __tablename__ = "custom_tts_voices" + __table_args__ = ( + UniqueConstraint("organization_id", "provider", "voice_id", name="uq_custom_tts_voice_org_provider_voice_id"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + provider = Column(String(100), nullable=False, index=True) + voice_id = Column(String(255), nullable=False) + name = Column(String(255), nullable=False) + gender = Column(String(50), nullable=True) + accent = Column(String(100), nullable=True) + description = Column(Text, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + +class PromptOptimizationRun(Base): + """A single GEPA prompt optimization run for an agent.""" + __tablename__ = "prompt_optimization_runs" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every optimization run belongs to a workspace + # within its org. Candidates inherit this workspace_id. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False, index=True) + evaluator_id = Column(UUID(as_uuid=True), ForeignKey("evaluators.id"), nullable=True) + voice_bundle_id = Column(UUID(as_uuid=True), ForeignKey("voicebundles.id"), nullable=True) + + seed_prompt = Column(Text, nullable=False) + best_prompt = Column(Text, nullable=True) + best_score = Column(Float, nullable=True) + + status = Column(String(20), nullable=False, default=PromptOptimizationStatus.PENDING.value) + config = Column(JSON, nullable=True) + reflection_trace = Column(JSON, nullable=True) + metric_history = Column(JSON, nullable=True) + + num_iterations = Column(Integer, nullable=True) + num_metric_calls = Column(Integer, nullable=True) + + celery_task_id = Column(String, nullable=True, index=True) + error_message = Column(Text, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + candidates = relationship("PromptOptimizationCandidate", back_populates="optimization_run", cascade="all, delete-orphan") + + +class PromptOptimizationCandidate(Base): + """A candidate prompt generated during an optimization run.""" + __tablename__ = "prompt_optimization_candidates" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + optimization_run_id = Column(UUID(as_uuid=True), ForeignKey("prompt_optimization_runs.id", ondelete="CASCADE"), nullable=False, index=True) + # Workspace isolation: mirrors the parent PromptOptimizationRun's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + prompt_text = Column(Text, nullable=False) + score = Column(Float, nullable=True) + metric_breakdown = Column(JSON, nullable=True) + reflection_summary = Column(Text, nullable=True) + + parent_candidate_id = Column(UUID(as_uuid=True), ForeignKey("prompt_optimization_candidates.id"), nullable=True) + + is_accepted = Column(Boolean, nullable=False, default=False) + pushed_to_provider_at = Column(DateTime(timezone=True), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + optimization_run = relationship("PromptOptimizationRun", back_populates="candidates") + + +class TelephonyIntegration(Base): + """Per-organization telephony provider credentials and configuration. + + Multiple rows per (organization_id, provider) are allowed so that an + organization can keep several Plivo / Exotel accounts side-by-side. + A partial unique index in migration 028 enforces at most one row with + is_default = TRUE per (org, provider); resolution falls back to that + default row when the caller does not pin a specific credential. + """ + + __tablename__ = "telephony_integrations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + provider = Column(String(50), nullable=False, default="plivo") + name = Column(String(255), nullable=True) # Optional friendly name to disambiguate multiple credentials + + auth_id = Column(String(255), nullable=False) + auth_token = Column(String(512), nullable=False) + + verify_app_uuid = Column(String(255), nullable=True) + voice_app_id = Column(String(255), nullable=True) + sip_domain = Column(String(255), nullable=True) + masking_config = Column(JSON, nullable=True) + + is_active = Column(Boolean, default=True, nullable=False) + is_default = Column(Boolean, default=False, nullable=False) + last_tested_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class TelephonyPhoneNumber(Base): + """Inventory of telephony phone numbers owned by an organization.""" + + __tablename__ = "telephony_phone_numbers" + __table_args__ = ( + UniqueConstraint("organization_id", "phone_number", name="uq_telephony_number_org_phone"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + telephony_integration_id = Column( + UUID(as_uuid=True), ForeignKey("telephony_integrations.id"), nullable=True, index=True + ) + + phone_number = Column(String(20), nullable=False, index=True) + country_iso2 = Column(String(2), nullable=True) + region = Column(String(100), nullable=True) + number_type = Column(String(20), nullable=True) + capabilities = Column(JSON, nullable=True) + provider_app_id = Column(String(255), nullable=True) + + is_masking_pool = Column(Boolean, default=False, nullable=False) + inbound_enabled = Column(Boolean, default=True, nullable=False) + outbound_enabled = Column(Boolean, default=True, nullable=False) + source = Column(String(20), nullable=False, default="imported") + agent_id = Column( + UUID(as_uuid=True), + ForeignKey( + "agents.id", + ondelete="SET NULL", + use_alter=True, + name="fk_telephony_phone_numbers_agent_id", + ), + nullable=True, + index=True, + ) + is_active = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class TelephonyDialTarget(Base): + """Org-scoped saved destination numbers for outbound test calls.""" + + __tablename__ = "telephony_dial_targets" + __table_args__ = ( + UniqueConstraint("organization_id", "phone_number", name="uq_telephony_dial_target_org_phone"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + phone_number = Column(String(20), nullable=False, index=True) + label = Column(String(255), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class TelephonyVerifySession(Base): + """Tracks voice OTP verification sessions via telephony provider.""" + + __tablename__ = "telephony_verify_sessions" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + provider_session_uuid = Column(String(255), nullable=False, unique=True, index=True) + recipient_number = Column(String(20), nullable=False) + channel = Column(String(10), nullable=False, default="voice") + status = Column(String(20), nullable=False, default="pending") + initiated_by = Column(String(255), nullable=True) + verify_app_uuid = Column(String(255), nullable=True) + verified_at = Column(DateTime(timezone=True), nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class TelephonyMaskedSession(Base): + """Number-masking session between two parties through a middle number.""" + + __tablename__ = "telephony_masked_sessions" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + telephony_integration_id = Column(UUID(as_uuid=True), ForeignKey("telephony_integrations.id"), nullable=False) + masked_number_id = Column( + UUID(as_uuid=True), ForeignKey("telephony_phone_numbers.id"), nullable=False, index=True + ) + masked_number = Column(String(20), nullable=False) + party_a_number = Column(String(20), nullable=False) + party_b_number = Column(String(20), nullable=False) + status = Column(String(20), nullable=False, default="active") + expires_at = Column(DateTime(timezone=True), nullable=True) + ended_at = Column(DateTime(timezone=True), nullable=True) + session_metadata = Column("metadata", JSON, nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + +class CallImportSchema(Base): + """Reusable Input Parameter schema for the call-uploads flow. + + A schema is workspace-scoped: users define a named bundle of typed + Input Parameters once (e.g. "Standard Voice QA" with conversation_id + + recording_url + transcript + agent_name) and then map those parameters + to CSV/Excel headers each time they upload a new batch. + + Every schema MUST contain exactly one parameter with + ``type='conversation_id'`` and ``is_required=True`` - that's the + mandatory identity field every imported row needs. The invariant is + enforced in app code on create/update (no DB-level CHECK because the + parent + children are written across two tables in one transaction). + """ + + __tablename__ = "call_import_schemas" + __table_args__ = ( + # Case-insensitive uniqueness is enforced via the matching partial + # index on ``LOWER(name)`` in the migration; this constraint here + # would be case-sensitive and is intentionally omitted to avoid + # confusing the user. + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column( + UUID(as_uuid=True), + ForeignKey("organizations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + created_by_user_id = Column( + UUID(as_uuid=True), + ForeignKey("users.id", ondelete="SET NULL"), + nullable=True, + ) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + parameters = relationship( + "CallImportSchemaParameter", + back_populates="schema", + cascade="all, delete-orphan", + order_by="CallImportSchemaParameter.ordering", + ) + + +class CallImportSchemaParameter(Base): + """A single typed parameter inside a :class:`CallImportSchema`. + + ``type`` is one of the strings tracked by + :data:`app.models.enums.CallImportParameterType`. ``conversation_id`` + is reserved for the mandatory identity parameter every schema must + contain. + """ + + __tablename__ = "call_import_schema_parameters" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + schema_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_schemas.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name = Column(String(255), nullable=False) + type = Column(String(32), nullable=False) + description = Column(Text, nullable=True) + is_required = Column(Boolean, nullable=False, default=False) + # Stable ordering so the UI renders parameters in the order the + # schema author defined them (matters when conversation_id is pinned + # first and the user re-orders the rest). + ordering = Column(Integer, nullable=False, default=0) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + schema = relationship("CallImportSchema", back_populates="parameters") + + +class CallImport(Base): + """Batch record for a CSV-driven call import job.""" + + __tablename__ = "call_imports" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + # Workspace isolation: every imported batch belongs to a workspace + # within its org. The /upload endpoint stamps it from the active + # workspace header (or the org's Default if absent). + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + created_by_user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + last_updated_by_user_id = Column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # Telephony provider key (e.g. ``'exotel'``, ``'plivo'``). In the + # legacy one-shot ``POST /upload`` endpoint this is supplied with the + # file; in the three-stage flow (UPLOAD -> MAP -> IMPORT) the value + # isn't known until the IMPORT stage, so the column is nullable for + # ``uploaded`` / ``mapped`` batches. + provider = Column(String(50), nullable=True, default="exotel") + # Pin a specific telephony credential for this batch so the worker + # downloads recordings using *that* row instead of the org default. + # NULL preserves legacy behavior (resolve by provider + default). + telephony_integration_id = Column( + UUID(as_uuid=True), + ForeignKey("telephony_integrations.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + original_filename = Column(String(512), nullable=True) + # When the source file was a multi-sheet Excel workbook, this records + # the worksheet the rows came from (one batch per sheet). NULL for CSV + # uploads since CSV has no sheet concept. + sheet_name = Column(String(255), nullable=True) + + # --- Source-file staging (UPLOAD stage) --------------------------- + # The raw CSV / Excel file is stored in S3 between stages so the + # user can come back later to MAP and IMPORT without re-uploading. + # ``source_s3_key`` is NULL on legacy batches that were imported via + # the one-shot endpoint (those batches stay read-only post-import). + source_s3_key = Column(Text, nullable=True) + source_format = Column(String(16), nullable=True) + source_size_bytes = Column(BigInteger, nullable=True) + source_content_type = Column(String(255), nullable=True) + + # Snapshot of the file's sheets + headers captured at UPLOAD time + # so the MAP UI doesn't need to re-fetch the source bytes from S3. + # Shape: ``[{"name": str, "headers": [str, ...], "row_count": int}, ...]``. + available_sheets = Column(JSON, nullable=True) + + # User's explicit "drop these columns" decision captured at MAP + # time. Was validation-only and ephemeral in the legacy flow; now + # persisted so the IMPORT stage can re-parse the file with the same + # mapping/skip intent. + skipped_columns = Column(JSON, nullable=False, default=list) + # Rows skipped at parse time (missing/invalid conversation_id or URL). + # Shape: ``[{"source_row": int, "reason": str, "message": str}, ...]``. + source_row_skips = Column(JSON, nullable=False, default=list) + + # Free-text high-level segregation label. Powers the "Dataset" filter + # at the top of the imports page; multiple imports can share a value. + dataset = Column(String(255), nullable=True, index=True) + + # Reusable Input Parameter schema this batch was uploaded against. + # NULL on legacy batches uploaded before the schema-driven flow + # shipped; those still render via ``column_mapping`` + ``extra_columns`` + # + ``custom_column_mapping`` below. + schema_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_schemas.id", ondelete="RESTRICT"), + nullable=True, + index=True, + ) + # New schema-driven mapping: ``{schema_parameter_name: csv_header}``. + # Populated for new uploads; empty dict on legacy batches. + parameter_mapping = Column(JSON, nullable=False, default=dict) + + # Legacy free-form mapping (pre-schema-flow). Kept on the model so + # batches that were uploaded before the schema feature shipped still + # render correctly on the detail page; new uploads stop writing here. + # Keys: external_call_id (required), transcript, recording_url. + # (DB column ``external_call_id`` is now ``conversation_id``; this + # JSON key stays as-is for historical batches.) + # Values: original CSV header strings (preserve user casing for export). + column_mapping = Column(JSON, nullable=False, default=dict) + # Ordered list of additional CSV header strings the uploader wants + # preserved verbatim into the evaluation export CSV. + extra_columns = Column(JSON, nullable=False, default=list) + # User-defined ``{custom_field_name: csv_header}`` mappings on top of + # the three system fields above. Cells from the mapped CSV columns are + # preserved per row (keyed by the CSV header in ``raw_columns``) and + # surface in the evaluation export under the uploader-chosen name. + custom_column_mapping = Column(JSON, nullable=False, default=dict) + + total_rows = Column(Integer, nullable=False, default=0) + completed_rows = Column(Integer, nullable=False, default=0) + failed_rows = Column(Integer, nullable=False, default=0) + + status = Column( + Enum(CallImportStatus, values_callable=get_enum_values), + nullable=False, + default=CallImportStatus.PENDING, + index=True, + ) + error_message = Column(Text, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + rows = relationship( + "CallImportRow", + back_populates="call_import", + cascade="all, delete-orphan", + order_by="CallImportRow.row_index", + ) + tags = relationship( + "CallImportTag", + secondary="call_import_tag_assignments", + backref="call_imports", + lazy="selectin", + ) + evaluations = relationship( + "CallImportEvaluation", + back_populates="call_import", + cascade="all, delete-orphan", + ) + + +class CallImportShardSlice(Base): + """Registry row: which shard stores a slice of rows for an import.""" + + __tablename__ = "call_import_shard_slices" + + call_import_id = Column( + UUID(as_uuid=True), + ForeignKey("call_imports.id", ondelete="CASCADE"), + primary_key=True, + ) + slice_id = Column(Integer, primary_key=True) + shard_id = Column(String(64), nullable=False, index=True) + row_index_min = Column(Integer, nullable=False) + row_index_max = Column(Integer, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + +class CallImportRow(Base): + """A single row within a CallImport batch (one CSV line / one external call).""" + + __tablename__ = "call_import_rows" + __table_args__ = ( + UniqueConstraint("call_import_id", "row_index", name="uq_call_import_row_index"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + call_import_id = Column( + UUID(as_uuid=True), + ForeignKey("call_imports.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + row_index = Column(Integer, nullable=False) + # Was historically named ``external_call_id``; renamed to + # ``conversation_id`` so the new schema-driven upload flow can refer + # to it by a single canonical name across the schema definition, + # exports, and downstream evaluation tables. + conversation_id = Column(String(255), nullable=False, index=True) + # Supplied via CSV for Exotel credentialed imports (required per row). + # Nullable in the schema for legacy rows imported before recording_url + # was mandatory on every Exotel upload. + recording_url = Column(Text, nullable=True) + # Date-only call recording date supplied by the import schema. Used + # for historical report comparisons without timezone/time ambiguity. + recording_date = Column(Date, nullable=True, index=True) + # The "production" transcript: the value supplied via the CSV + # upload mapping. Never overwritten by the diarisation worker — + # the worker writes its output into ``diarised_transcript`` so + # the user keeps both versions side by side. + transcript = Column(Text, nullable=True) + # Snapshot of the original CSV row keyed by the user's headers so the + # evaluation export can reproduce every column the uploader supplied + # (mapped + extra). NULL on legacy rows imported before this column. + raw_columns = Column(JSON, nullable=True) + + # Where the value in ``transcript`` came from. ``csv`` = supplied via + # the upload mapping, ``edited`` = manually changed in the UI. NULL + # on rows that have never had a production transcript. + # (Worker-produced transcripts now live in ``diarised_transcript`` + # and are tracked via ``diarised_transcript_*`` metadata below.) + transcript_source = Column(String(20), nullable=True) + # Provider/model recorded by the (legacy) post-hoc transcription + # worker. New worker runs leave these NULL and write into the + # ``diarised_transcript_*`` columns instead; kept on the model for + # backwards compatibility with pre-split rows that still carry the + # original transcription metadata here. + transcript_provider = Column(String(50), nullable=True) + transcript_model = Column(String(100), nullable=True) + # Lifecycle status for the legacy transcription workflow itself, + # independent of the row's recording-fetch ``status``. ``idle`` = + # no transcribe task has touched this column. New diarisation runs + # update ``diarised_transcript_status`` instead. + transcript_status = Column( + String(20), + nullable=False, + default="idle", + ) + transcript_error = Column(Text, nullable=True) + transcribed_at = Column(DateTime(timezone=True), nullable=True) + + # The "diarised" transcript: produced by the post-hoc + # transcription/diarisation worker. Stored separately so a manual + # diarisation run never clobbers the production transcript above. + # Evaluations can be configured to score against either column + # (see ``CallImportEvaluation.transcript_source``). + diarised_transcript = Column(Text, nullable=True) + # Provider/model the diarisation worker used. Surfaced in the UI + # as "Diarised via deepgram/nova-2" next to the diarised + # transcript section. + diarised_transcript_provider = Column(String(50), nullable=True) + diarised_transcript_model = Column(String(100), nullable=True) + # Lifecycle status for the diarisation workflow. + # ``idle`` = no diarisation task has run; ``pending``/``running`` = + # a Celery task is queued or in flight; ``completed``/``failed`` = + # terminal. Independent of ``transcript_status`` so the two + # transcripts can be in different lifecycle states. + diarised_transcript_status = Column( + String(20), + nullable=False, + default="idle", + server_default="idle", + ) + diarised_transcript_error = Column(Text, nullable=True) + diarised_at = Column(DateTime(timezone=True), nullable=True) + + # Structured speaker turns produced by the diarisation worker — + # ``[{ "speaker": "agent"|"user"|"speaker_3", "text": "...", + # "start": float, "end": float, "raw_speaker": "Speaker 1" }, ...]`` + # The plain-text ``diarised_transcript`` above is a rendered view + # of this list (``: `` per line). When the worker + # cannot recover structured turns (no pyannote token / single- + # speaker recording / provider that doesn't surface segments) this + # column stays NULL and the plain-text path is still populated. + diarised_segments = Column(JSON, nullable=True) + # When True the ``agent`` <-> ``user`` mapping inside + # ``diarised_segments`` is inverted at render / export time. The + # worker writes the canonical mapping using the "first speaker is + # the agent" heuristic; reviewers can flip the toggle from the row + # detail panel without re-running diarisation. + diarised_speaker_swap = Column( + Boolean, + nullable=False, + default=False, + server_default="false", + ) + # LLM that turned the STT plain-text output into structured + # ``diarised_segments``. The legacy diarisation worker used + # pyannote and left these NULL; the current path always runs an + # LLM with the operator-supplied (or default) ``diarised_prompt`` + # below, and records exactly which model + prompt produced each + # row so reviewers can reproduce a specific run. + diarised_llm_provider = Column(String(50), nullable=True) + diarised_llm_model = Column(String(100), nullable=True) + diarised_llm_credential_id = Column(UUID(as_uuid=True), nullable=True) + diarised_prompt = Column(Text, nullable=True) + # Which diarisation pipeline produced this row's turns. + # * ``"stt_llm"`` (default) — two-stage: STT then LLM diariser. + # ``diarised_transcript_provider``/``_model`` describe the STT + # side; ``diarised_llm_provider``/``_model`` the LLM side. + # * ``"llm_only"`` — single-stage: audio fed straight to a + # multimodal LLM. ``diarised_transcript_provider`` is stamped + # with the sentinel ``"llm_only"``; the real model is on + # ``diarised_llm_*``. + # Persisting it on the row (not just the run) lets the row detail + # panel render the right "Diarised via …" label even for ad-hoc + # standalone transcribes (no parent evaluation). + transcribe_mode = Column( + String(20), + nullable=False, + default="stt_llm", + server_default="stt_llm", + ) + + status = Column( + Enum(CallImportRowStatus, values_callable=get_enum_values), + nullable=False, + default=CallImportRowStatus.PENDING, + index=True, + ) + + recording_s3_key = Column(String(1024), nullable=True) + recording_content_type = Column(String(128), nullable=True) + recording_size_bytes = Column(Integer, nullable=True) + + error_message = Column(Text, nullable=True) + attempts = Column(Integer, nullable=False, default=0) + celery_task_id = Column(String(255), nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + call_import = relationship("CallImport", back_populates="rows") + + +@event.listens_for(CallImportRow, "before_insert") +def _call_import_row_fill_workspace_id(_mapper, connection, target): + """Denormalize workspace_id from the parent import when omitted.""" + if target.workspace_id is not None or target.call_import_id is None: + return + workspace_id = connection.execute( + select(CallImport.workspace_id).where( + CallImport.id == target.call_import_id + ) + ).scalar_one_or_none() + if workspace_id is not None: + target.workspace_id = workspace_id + + +class CallImportTag(Base): + """User-defined tag that can be attached to one or more call imports. + + Tags coexist with the free-text ``CallImport.dataset`` column: dataset + is the primary high-level segregation, tags are an optional secondary + classification (an import can have many tags). + """ + + __tablename__ = "call_import_tags" + __table_args__ = ( + UniqueConstraint("organization_id", "name", name="uq_call_import_tag_org_name"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column( + UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True + ) + name = Column(String(255), nullable=False) + color = Column(String(32), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + +class CallImportTagAssignment(Base): + """Many-to-many join table between CallImport and CallImportTag.""" + + __tablename__ = "call_import_tag_assignments" + + call_import_id = Column( + UUID(as_uuid=True), + ForeignKey("call_imports.id", ondelete="CASCADE"), + primary_key=True, + ) + tag_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_tags.id", ondelete="CASCADE"), + primary_key=True, + index=True, + ) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + +class CallImportEvaluation(Base): + """Parent record for an evaluation run over a CallImport batch. + + A user picks a subset of org ``Metric`` rows and triggers an evaluation; + we fan out one ``CallImportEvaluationRow`` per source row and roll up + counters as workers finish. Status mirrors ``CallImportStatus`` plus a + ``RUNNING`` value so the UI can distinguish "queued" from "in flight". + """ + + __tablename__ = "call_import_evaluations" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + call_import_id = Column( + UUID(as_uuid=True), + ForeignKey("call_imports.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + organization_id = Column( + UUID(as_uuid=True), + ForeignKey("organizations.id"), + nullable=False, + index=True, + ) + # Workspace isolation: mirrors the parent CallImport's workspace. + # Denormalized for fast filter-by-workspace listings without a join. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + created_by_user_id = Column( + UUID(as_uuid=True), ForeignKey("users.id"), nullable=True + ) + last_updated_by_user_id = Column( + UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True + ) + + # Optional user-supplied label for this run. Lets the UI surface + # something more meaningful than the UUID prefix (e.g. "March QA pass"). + name = Column(String(255), nullable=True) + + # JSON list of Metric UUID strings selected for this run. Stored as text + # in JSON so we don't have to deal with PG arrays of UUIDs / cascade + # delete policies when metrics are removed; the loader filters for + # still-existing org metrics at run time. + selected_metric_ids = Column(JSON, nullable=False, default=list) + # Hierarchy grouping snapshot: ``{parent_id_str: [child_id_str, ...]}``. + # Captures which children belong to which parent for THIS run so the UI + # / aggregator can reconstruct the tree even when the user selected + # only a subset of children, or after metrics are deleted / renamed. + # NULL on legacy rows means "no hierarchy" → fall back to flat + # ``selected_metric_ids`` semantics. + selected_metric_groups = Column(JSON, nullable=True) + # User-driven merges of LLM-discovered candidate sub-labels for + # ``allow_discovery`` parents. Shape: + # ``{"": {"": "", ...}}``. + # Populated via ``POST .../discovered-labels/merge``; consulted by + # the discovered-labels aggregator, the flow graph builder, and the + # worker so that rows finishing AFTER a merge cannot reintroduce + # the merged-away slug. Empty dict on fresh rows. + discovered_label_aliases = Column( + JSON, nullable=False, default=dict, server_default="{}" + ) + + # Per-run opt-in for top-level metric discovery. When True, the LLM + # is asked to propose brand-new top-level metrics (boolean / rating / + # category) observed in the transcripts in addition to scoring the + # ``selected_metric_ids`` for the row. Candidates surface in a + # "Discovered metrics" panel on the evaluation's Flow tab and can + # be promoted into real standalone ``Metric`` rows via + # ``POST /metrics/from-discovered``. Defaults to False so existing + # evaluation creation payloads keep their previous behaviour. + discover_new_metrics = Column( + Boolean, nullable=False, default=False, server_default="false" + ) + # Flat slug-to-slug redirect map for user merges + tombstones of + # discovered top-level metric candidates. Mirrors + # ``discovered_label_aliases`` but is NOT nested per parent — + # top-level metric discovery is not scoped to any parent. Shape:: + # + # {"": "", ...} + # + # An empty-string value tombstones the slug so workers finishing + # later can't re-introduce it. + discovered_metric_aliases = Column( + JSON, nullable=False, default=dict, server_default="{}" + ) + + # Run-level LLM config picked from the Run Evaluation modal. NULL on + # legacy rows means "use the historical OpenAI/gpt-4o default" — the + # worker checks for this and falls back accordingly. ``llm_credential_id`` + # pins a specific AIProvider row when the org has multiple credentials + # for the same provider. + llm_provider = Column(String(50), nullable=True) + llm_model = Column(String(100), nullable=True) + llm_credential_id = Column( + UUID(as_uuid=True), + ForeignKey("aiproviders.id", ondelete="SET NULL"), + nullable=True, + ) + llm_config = Column(JSON, nullable=True) + # Optional per-metric LLM override: + # ``{"": {"provider": "...", "model": "...", "credential_id": "..."}}``. + # Each entry overrides the run-level default for that metric only; + # missing keys = use run-level default. Stored as JSON so the UI can + # round-trip arbitrary {provider, model} pairs without migrations. + metric_llm_overrides = Column(JSON, nullable=True) + + # When ``auto_transcribe`` was set on the create payload, record the + # STT provider/model used so the UI can show "Auto-transcribed via + # deepgram/nova-2" on the evaluation header. ``stt_credential_id`` is + # untyped (no FK) because STT keys may live in either ``aiproviders`` + # (OpenAI) or ``integrations`` (Deepgram, ElevenLabs) — the + # transcription service handles the lookup. + stt_provider = Column(String(50), nullable=True) + stt_model = Column(String(100), nullable=True) + stt_credential_id = Column(UUID(as_uuid=True), nullable=True) + + # Run-level LLM diariser config. Used when the create-run / + # retry-run paths chain a ``transcribe_call_import_row_task`` + # because the row is missing a diarised transcript. Persisted on + # the run so a retry uses the same diariser the original create + # call picked (unless the retry payload explicitly overrides). + diarisation_llm_provider = Column(String(50), nullable=True) + diarisation_llm_model = Column(String(100), nullable=True) + diarisation_llm_credential_id = Column(UUID(as_uuid=True), nullable=True) + diarisation_prompt = Column(Text, nullable=True) + # Mode the run was *created* with for its auto-transcribe step. + # Retry chains read this to decide whether to enqueue an STT+LLM + # transcribe or a single-stage multimodal LLM transcribe — without + # it we'd have to infer the mode from "stt_provider is NULL", which + # would silently break legacy rows that simply never configured + # auto-transcribe. See migration 041 for the column DDL. + transcribe_mode = Column( + String(20), + nullable=False, + default="stt_llm", + server_default="stt_llm", + ) + + # Which of the two transcripts on each ``CallImportRow`` this run + # scored against. ``'production'`` reads ``CallImportRow.transcript`` + # (the CSV-supplied value); ``'diarised'`` reads + # ``CallImportRow.diarised_transcript`` (the worker output). When + # the user ticks both checkboxes in the Run Evaluation modal we + # create two ``CallImportEvaluation`` rows — one per source — so + # the two scorings can be compared side-by-side. Defaults to + # ``'production'`` so legacy runs (which always read the single + # historical ``transcript`` column) keep their semantics. + transcript_source = Column( + String(20), + nullable=False, + default="production", + server_default="production", + ) + + # Cached LLM-generated TLDR rendered above the Visualizations charts. + # Populated lazily by ``POST /evaluations/{eval_id}/insights`` so we + # never auto-burn LLM tokens on page load. Shape:: + # {"narrative": str, "patterns": [str, ...], + # "generated_at": iso8601, "generated_at_completed_rows": int, + # "provider": str, "model": str} + # NULL on rows that have never been summarised. + tldr_summary = Column(JSON, nullable=True) + + # Cached LLM-generated user insights for External Audit PDF section 03. + # Populated by a background Celery job triggered alongside TLDR generation. + # Shape: EvaluationUserInsightsState JSON (status, insights[], progress, …). + user_insights = Column(JSON, nullable=True) + + # Cached per-metric failure clustering for internal diagnostics PDF/UI. + # Shape: EvaluationMetricClustersState JSON (status, groups[], …). + metric_clusters = Column(JSON, nullable=True) + + # Cached LLM-generated prompt improvement suggestions keyed to an + # imported agent (PromptPartial tagged __imported_agent__). + # Shape: EvaluationPromptImprovementsState JSON. + prompt_improvements = Column(JSON, nullable=True) + + # Cached LLM explanations for week-over-week metric deltas keyed by + # baseline evaluation id + completed row counts. + period_delta_explanations = Column(JSON, nullable=True) + + status = Column(String(20), nullable=False, default="pending", index=True) + + total_rows = Column(Integer, nullable=False, default=0) + completed_rows = Column(Integer, nullable=False, default=0) + failed_rows = Column(Integer, nullable=False, default=0) + # Flexprice pass-level delta billing watermark: rows already emitted + # on ``call_import.evaluation_completed`` for this evaluation run. + billed_completed_rows = Column( + Integer, nullable=False, default=0, server_default="0" + ) + error_message = Column(Text, nullable=True) + celery_group_id = Column(String(255), nullable=True) + + started_at = Column(DateTime(timezone=True), nullable=True) + finished_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + call_import = relationship("CallImport", back_populates="evaluations") + row_results = relationship( + "CallImportEvaluationRow", + back_populates="evaluation", + cascade="all, delete-orphan", + ) + + +class CallImportEvaluationRow(Base): + """Per-source-row scoring output for a CallImportEvaluation parent.""" + + __tablename__ = "call_import_evaluation_rows" + __table_args__ = ( + UniqueConstraint( + "evaluation_id", "call_import_row_id", name="uq_call_import_evaluation_row" + ), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + evaluation_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_evaluations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + call_import_row_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_rows.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + status = Column(String(20), nullable=False, default="pending", index=True) + # Same shape as EvaluatorResult.metric_scores: {metric_id_str: {value, type, metric_name, ...}} + metric_scores = Column(JSON, nullable=False, default=dict) + error_message = Column(Text, nullable=True) + celery_task_id = Column(String(255), nullable=True) + + started_at = Column(DateTime(timezone=True), nullable=True) + finished_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + evaluation = relationship("CallImportEvaluation", back_populates="row_results") + source_row = relationship("CallImportRow") + + +@event.listens_for(CallImportEvaluationRow, "before_insert") +def _call_import_evaluation_row_fill_workspace_id(_mapper, connection, target): + """Denormalize workspace_id from the parent evaluation when omitted.""" + if target.workspace_id is not None or target.evaluation_id is None: + return + workspace_id = connection.execute( + select(CallImportEvaluation.workspace_id).where( + CallImportEvaluation.id == target.evaluation_id + ) + ).scalar_one_or_none() + if workspace_id is not None: + target.workspace_id = workspace_id + + +class CallImportEvaluationReportSnapshot(Base): + """Persisted PDF-report aggregate used for period-over-period deltas.""" + + __tablename__ = "call_import_evaluation_report_snapshots" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + evaluation_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_evaluations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + call_import_id = Column( + UUID(as_uuid=True), + ForeignKey("call_imports.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + organization_id = Column( + UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True + ) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + period_label = Column(String(64), nullable=True, index=True) + period_start = Column(Date, nullable=True, index=True) + period_end = Column(Date, nullable=True, index=True) + report_config = Column(JSON, nullable=False, default=dict, server_default="{}") + selected_metric_ids = Column(JSON, nullable=False, default=list, server_default="[]") + metric_aggregates = Column(JSON, nullable=False, default=list, server_default="[]") + insight_aggregates = Column(JSON, nullable=False, default=list, server_default="[]") + narrative = Column(JSON, nullable=True) + total_calls = Column(Integer, nullable=False, default=0) + selected_metric_count = Column(Integer, nullable=False, default=0) + total_metric_count = Column(Integer, nullable=False, default=0) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + +# --------------------------------------------------------------------------- +# Judge Alignment (AlignEval-style hybrid integration) +# +# Three tables back the "Judge Alignment" surface: +# - JudgeDataset: a labeled dataset materialised from one of three sources +# (voice transcripts, existing Metric/Evaluator outputs, +# or a generic CSV upload). Holds the dataset's source +# config + which fields play the role of input/output. +# - JudgeSample: one row in a dataset (input/output pair plus an +# optional binary pass/fail human label). +# - JudgeRun: a single run of an LLM-judge (existing Evaluator) over +# a subset of samples, with computed alignment metrics +# (precision/recall/F1/Cohen's kappa) and per-sample +# predictions. Optionally links to a GEPA optimization +# run when the user kicks off prompt tuning from a +# dataset. +# --------------------------------------------------------------------------- + + +class JudgeDataset(Base): + """Container for binary-labeled samples used to calibrate an LLM-judge.""" + + __tablename__ = "judge_datasets" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column( + UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True + ) + # Workspace isolation: every judge dataset belongs to a workspace + # within its org. Samples and runs inherit this workspace_id. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + + # One of: "transcript", "metric_output", "csv" + source_type = Column(String(32), nullable=False, index=True) + # Source-specific config. Examples: + # transcript: {"transcription_ids": [...]} or {"agent_id": "..."} + # metric_output: {"metric_id": "...", "evaluator_id": "..."} + # csv: {"s3_key": "...", "filename": "..."} + source_config = Column(JSON, nullable=False, default=dict) + + # Field roles - which textual content is "input" vs "output" for the judge. + # For voice transcripts both default to the transcript text but can be + # tightened (e.g. agent-only turns vs full conversation). + input_field = Column(String(64), nullable=False, default="input") + output_field = Column(String(64), nullable=False, default="output") + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + samples = relationship( + "JudgeSample", + back_populates="dataset", + cascade="all, delete-orphan", + order_by="JudgeSample.created_at", + ) + runs = relationship( + "JudgeRun", + back_populates="dataset", + cascade="all, delete-orphan", + order_by="JudgeRun.created_at.desc()", + ) + + +class JudgeSample(Base): + """One labelable input/output pair within a JudgeDataset.""" + + __tablename__ = "judge_samples" + __table_args__ = ( + UniqueConstraint("dataset_id", "external_id", name="uq_judge_samples_dataset_external"), + ) + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + dataset_id = Column( + UUID(as_uuid=True), + ForeignKey("judge_datasets.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + # Workspace isolation: mirrors the parent JudgeDataset's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + # Stable identifier within the source (e.g. transcription UUID, CSV row id). + # Used to dedupe re-imports and link back to the originating record. + external_id = Column(String(128), nullable=True, index=True) + + input_text = Column(Text, nullable=False) + output_text = Column(Text, nullable=False) + + # Binary human label: "pass" | "fail" | null (unlabeled). + # Stored as string (rather than enum) so it stays trivially extendable. + label = Column(String(16), nullable=True, index=True) + labeled_by = Column(String(255), nullable=True) + labeled_at = Column(DateTime(timezone=True), nullable=True) + + # Source-specific context (e.g. agent_id, original metric value, csv row). + extra = Column(JSON, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + dataset = relationship("JudgeDataset", back_populates="samples") + + +class JudgeRun(Base): + """One execution of an LLM-judge against a JudgeDataset, with alignment metrics.""" + + __tablename__ = "judge_runs" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + dataset_id = Column( + UUID(as_uuid=True), + ForeignKey("judge_datasets.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + organization_id = Column( + UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True + ) + # Workspace isolation: mirrors the parent JudgeDataset's workspace. + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + + # Reuses the existing Evaluator row (its custom_prompt + llm_provider + llm_model + # define the judge under test). Nullable so a run may target an inline prompt + # in the future without inflating the Evaluator table. + evaluator_id = Column( + UUID(as_uuid=True), ForeignKey("evaluators.id", ondelete="SET NULL"), nullable=True, index=True + ) + + # Which subset was scored: "all" | "dev" | "test" + split = Column(String(16), nullable=False, default="all") + + # Snapshot of the model used (so a later Evaluator edit doesn't rewrite history). + llm_provider = Column(String(64), nullable=True) + llm_model = Column(String(128), nullable=True) + + # Computed alignment metrics: + # {"precision": float, "recall": float, "f1": float, "kappa": float, + # "tp": int, "fp": int, "tn": int, "fn": int, "n": int} + metrics = Column(JSON, nullable=True) + + # Per-sample predictions, keyed by sample_id (UUID string): + # {sample_id: {"prediction": "pass"|"fail", "explanation": str, "raw": str}} + predictions = Column(JSON, nullable=True) + + # Run lifecycle. + status = Column(String(20), nullable=False, default="pending", index=True) + error_message = Column(Text, nullable=True) + celery_task_id = Column(String, nullable=True, index=True) + + # Optional link to a GEPA optimization run kicked off from this dataset. + gepa_optimization_id = Column( + UUID(as_uuid=True), + ForeignKey("prompt_optimization_runs.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + created_by = Column(String, nullable=True) + + dataset = relationship("JudgeDataset", back_populates="runs") diff --git a/app/models/schemas.py b/app/models/schemas.py index 78dd14f9..de5c203e 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -3168,6 +3168,8 @@ class CallImportResponse(BaseModel): error_message: Optional[str] = None created_at: datetime updated_at: datetime + created_by_email: Optional[str] = None + last_updated_by_email: Optional[str] = None model_config = ConfigDict(from_attributes=True) @@ -3981,6 +3983,8 @@ class CallImportEvaluationResponse(BaseModel): finished_at: Optional[datetime] = None created_at: datetime updated_at: datetime + created_by_email: Optional[str] = None + last_updated_by_email: Optional[str] = None # Cached LLM-generated TLDR for the Visualizations tab. Lazily # populated by ``POST /evaluations/{eval_id}/insights``; ``None`` # for runs the user has not summarised yet. ``is_stale`` on the diff --git a/app/services/call_imports/audit.py b/app/services/call_imports/audit.py new file mode 100644 index 00000000..6479ef18 --- /dev/null +++ b/app/services/call_imports/audit.py @@ -0,0 +1,99 @@ +"""Actor stamping and email resolution for call import audit fields.""" + +from __future__ import annotations + +from typing import Dict, Iterable, Optional, Set, Tuple +from uuid import UUID + +from sqlalchemy.orm import Session + +from app.core.auth.principal import Principal +from app.models.database import CallImport, CallImportEvaluation, User + + +def stamp_call_import_actor( + call_import: CallImport, + principal: Principal, + *, + creating: bool = False, +) -> None: + if creating and principal.user_id is not None: + call_import.created_by_user_id = principal.user_id + if principal.user_id is not None: + call_import.last_updated_by_user_id = principal.user_id + + +def stamp_evaluation_actor( + evaluation: CallImportEvaluation, + principal: Principal, + *, + creating: bool = False, +) -> None: + if creating and principal.user_id is not None: + evaluation.created_by_user_id = principal.user_id + if principal.user_id is not None: + evaluation.last_updated_by_user_id = principal.user_id + + +def user_ids_from_call_imports(imports: Iterable[CallImport]) -> Set[UUID]: + ids: Set[UUID] = set() + for row in imports: + if row.created_by_user_id is not None: + ids.add(row.created_by_user_id) + if row.last_updated_by_user_id is not None: + ids.add(row.last_updated_by_user_id) + return ids + + +def user_ids_from_evaluations( + evaluations: Iterable[CallImportEvaluation], +) -> Set[UUID]: + ids: Set[UUID] = set() + for row in evaluations: + if row.created_by_user_id is not None: + ids.add(row.created_by_user_id) + if row.last_updated_by_user_id is not None: + ids.add(row.last_updated_by_user_id) + return ids + + +def emails_for_user_ids(db: Session, user_ids: Iterable[UUID]) -> Dict[UUID, str]: + unique = {uid for uid in user_ids if uid is not None} + if not unique: + return {} + rows = db.query(User.id, User.email).filter(User.id.in_(unique)).all() + return {row.id: row.email for row in rows if row.email} + + +def actor_emails_for_call_import( + call_import: CallImport, + email_by_id: Dict[UUID, str], +) -> Tuple[Optional[str], Optional[str]]: + created = ( + email_by_id.get(call_import.created_by_user_id) + if call_import.created_by_user_id + else None + ) + updated = ( + email_by_id.get(call_import.last_updated_by_user_id) + if call_import.last_updated_by_user_id + else None + ) + return created, updated + + +def actor_emails_for_evaluation( + evaluation: CallImportEvaluation, + email_by_id: Dict[UUID, str], +) -> Tuple[Optional[str], Optional[str]]: + created = ( + email_by_id.get(evaluation.created_by_user_id) + if evaluation.created_by_user_id + else None + ) + updated = ( + email_by_id.get(evaluation.last_updated_by_user_id) + if evaluation.last_updated_by_user_id + else None + ) + return created, updated diff --git a/env.example b/env.example index ff2f226a..c434c850 100644 --- a/env.example +++ b/env.example @@ -41,7 +41,7 @@ RATE_LIMIT_PER_MINUTE=60 # Comma-separated or JSON list. See config.yml.example for full docs. # OSS default: api_key,local_password # Enterprise SSO: api_key,external_oidc (Okta / Azure AD / Google / Cognito / Auth0 / ...) -AUTH_PROVIDERS=api_key,local_password +AUTH_PROVIDERS=["api_key","local_password"] # Local password (HS256 Bearer tokens signed with SECRET_KEY) AUTH_LOCAL_TOKEN_TTL_MINUTES=15 diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 91fbc9c5..39c0fed7 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -1,2132 +1,2136 @@ -// API Types matching the backend schemas - -export type { LLMGenerationConfig } from '../config/llmGenerationParams' -import type { LLMGenerationConfig } from '../config/llmGenerationParams' - -export enum EvaluationType { - ASR = 'asr', - TTS = 'tts', -} - -export enum EvaluationStatus { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -export interface AudioFile { - id: string - filename: string - format: string - file_size: number - duration?: number | null - sample_rate?: number | null - channels?: number | null - uploaded_at: string -} - -export interface Evaluation { - id: string - audio_id: string - reference_text?: string | null - evaluation_type: EvaluationType - model_name?: string | null - status: EvaluationStatus - metrics_requested?: string[] | null - created_at: string - started_at?: string | null - completed_at?: string | null - error_message?: string | null -} - -export interface DashboardSummary { - evaluations: { - total: number - completed: number - pending: number - failed: number - } - resources: { - agents: number - personas: number - scenarios: number - integrations: number - voice_bundles: number - } - setup_progress: { - has_integration: boolean - has_voice_bundle: boolean - has_agent: boolean - has_evaluation: boolean - } - metrics: { - total: number - enabled: number - } - call_imports: { - total: number - } - call_import_evaluations: { - total: number - completed: number - running: number - failed: number - } - recent_evaluations: Evaluation[] -} - -export interface ModelConfigEntry { - provider: string - model_type: string - description?: string - featured?: boolean - featured_rank?: number - highlights?: string[] -} - -export interface EvaluationCreate { - audio_id: string - reference_text?: string | null - evaluation_type: EvaluationType - model_name?: string | null - metrics?: string[] -} - -export interface EvaluationResult { - evaluation_id: string - status: EvaluationStatus - transcript?: string | null - metrics: Record - processing_time?: number | null - model_used?: string | null - created_at: string -} - -export interface BatchEvaluationResult { - processed_files: number - failed_files: number - aggregated_metrics?: Record | null - individual_results: EvaluationResult[] -} - -/** Voice agent evaluator run (evaluator_results table). */ -export type EvaluatorResultStatus = - | 'queued' - | 'call_initiating' - | 'call_connecting' - | 'call_in_progress' - | 'call_ended' - | 'transcribing' - | 'evaluating' - | 'fetching_details' - | 'completed' - | 'failed' - -export interface EvaluatorResultMetricScore { - value: unknown - type: string - metric_name: string - parent_metric_id?: string | null -} - -export interface EvaluatorResultRow { - id: string - result_id: string - name: string | null - evaluator_id: string | null - agent_id?: string | null - persona_id?: string | null - scenario_id?: string | null - suite_id?: string | null - timestamp: string - duration_seconds: number | null - status: EvaluatorResultStatus - metric_scores: Record | null - error_message: string | null - agent?: { id: string; name: string } | null - scenario?: { id: string; name: string } | null -} - -export interface EvaluatorResultListResponse { - items: EvaluatorResultRow[] - total: number -} - -export interface EvaluatorResultCounts { - total: number - completed: number - failed: number - in_progress: number - last_run_at?: string | null -} - -export interface EvaluatorResultsScenarioSummary { - scenario_id: string - scenario_name: string - counts: EvaluatorResultCounts -} - -export interface EvaluatorResultsSuiteSummary { - suite_id: string - suite_name?: string | null - agent_id: string - persona_id?: string | null - counts: EvaluatorResultCounts - scenarios?: EvaluatorResultsScenarioSummary[] | null -} - -export interface EvaluatorResultsAgentSummary { - agent_id: string - agent_name: string - counts: EvaluatorResultCounts - suites?: EvaluatorResultsSuiteSummary[] | null -} - -export interface EvaluatorResultsOverviewResponse { - workspace_counts: EvaluatorResultCounts - agents: EvaluatorResultsAgentSummary[] - unassigned: { - counts: EvaluatorResultCounts - recent_result_ids: string[] - } -} - -export interface ListEvaluatorResultsParams { - skip?: number - limit?: number - evaluatorId?: string - agentId?: string - suiteId?: string - scenarioId?: string - status?: 'completed' | 'failed' | 'in_progress' - unassignedOnly?: boolean - playground?: boolean - testAgentsOnly?: boolean -} - -export interface APIKey { - id: string - key: string - name?: string | null - is_active: boolean - created_at: string - last_used?: string | null - message?: string -} - -export interface MessageResponse { - message: string -} - -// IAM & User Types -export enum Role { - READER = 'reader', - WRITER = 'writer', - ADMIN = 'admin', -} - -export enum InvitationStatus { - PENDING = 'pending', - ACCEPTED = 'accepted', - DECLINED = 'declined', - EXPIRED = 'expired', -} - -export interface User { - id: string - email: string - name?: string | null - is_active: boolean - created_at: string -} - -export interface OrganizationMember { - id: string - user_id: string - organization_id: string - role: Role - joined_at: string - user: User -} - -export interface Invitation { - id: string - organization_id: string - email: string - role: Role - status: InvitationStatus - expires_at: string - created_at: string - organization_name?: string | null -} - -export interface InvitationCreate { - email: string - role: Role -} - -export interface RoleUpdate { - role: Role -} - -export interface Profile { - id: string - email: string - name?: string | null - first_name?: string | null - last_name?: string | null - created_at: string - organizations: Array<{ - id: string - name: string - role: string - joined_at: string - }> -} - -export interface UserUpdate { - name?: string | null - first_name?: string | null - last_name?: string | null - email?: string | null -} - -export interface UserPreferences { - theme?: string - notifications_enabled?: boolean - email_notifications?: boolean - default_language?: string - [key: string]: any -} - -export interface UserPreferencesUpdate { - theme?: string - notifications_enabled?: boolean - email_notifications?: boolean - default_language?: string - [key: string]: any -} - -// Integration Types -export enum IntegrationPlatform { - RETELL = 'retell', - VAPI = 'vapi', - CARTESIA = 'cartesia', - ELEVENLABS = 'elevenlabs', - DEEPGRAM = 'deepgram', - MURF = 'murf', - SARVAM = 'sarvam', - VOICEMAKER = 'voicemaker', - SMALLEST = 'smallest', -} - -export enum TelephonyProvider { - PLIVO = 'plivo', - EXOTEL = 'exotel', - VOBIZ = 'vobiz', -} - -export type CredentialRoutingMode = 'inherit' | 'gateway' | 'direct' -export type GatewayInterfaceMode = 'inherit' | 'litellm_shim' | 'native_openai' - -export type EffectiveCredentialRouting = - | 'inherit' - | 'direct' - | 'gateway' - | 'bifrost' - | 'litellm_proxy' - -export interface Integration { - id: string - organization_id: string - platform: IntegrationPlatform - name?: string | null - public_key?: string | null - is_active: boolean - /** True if this row is the default credential for (org, platform). */ - is_default?: boolean - routing_mode?: CredentialRoutingMode - effective_routing?: EffectiveCredentialRouting - created_at: string - updated_at: string - last_tested_at?: string | null -} - -export interface IntegrationCreate { - platform: IntegrationPlatform - api_key: string - public_key?: string - name?: string | null - routing_mode?: CredentialRoutingMode - /** Mark the new credential as the default for (org, platform). */ - is_default?: boolean -} - -// VoiceBundle Types -export enum ModelProvider { - OPENAI = 'openai', - ANTHROPIC = 'anthropic', - GOOGLE = 'google', - XAI = 'xai', - FIREWORKS = 'fireworks', - COHERE = 'cohere', - MISTRAL = 'mistral', - META = 'meta', - TOGETHER = 'together', - PERPLEXITY = 'perplexity', - AZURE = 'azure', - AWS = 'aws', - DEEPGRAM = 'deepgram', - CARTESIA = 'cartesia', - ELEVENLABS = 'elevenlabs', - MURF = 'murf', - CUSTOM = 'custom', - SARVAM = 'sarvam', - VOICEMAKER = 'voicemaker', - SMALLEST = 'smallest', -} - -// AI Provider Types -export interface AIProvider { - id: string - provider: ModelProvider - api_key?: string | null - name?: string | null - endpoint_url?: string | null - is_active: boolean - /** True if this row is the default credential for (org, provider). */ - is_default?: boolean - routing_mode?: CredentialRoutingMode - gateway_model?: string | null - gateway_interface?: GatewayInterfaceMode - gateway_base_url?: string | null - gateway_auth_header?: string | null - gateway_auth_secret_env?: string | null - has_gateway_auth_secret?: boolean - gateway_extra_headers?: Record | null - /** True when provider secrets are resolved by the Bifrost gateway. */ - gateway_managed?: boolean - effective_routing?: EffectiveCredentialRouting - effective_gateway_interface?: 'litellm_shim' | 'native_openai' - created_at: string - updated_at: string - last_tested_at?: string | null -} - -export interface AIProviderCreate { - provider: ModelProvider - api_key?: string | null - name?: string | null - endpoint_url?: string | null - routing_mode?: CredentialRoutingMode - gateway_model?: string | null - gateway_interface?: GatewayInterfaceMode - gateway_base_url?: string | null - gateway_auth_header?: string | null - gateway_auth_secret_env?: string | null - gateway_auth_secret?: string | null - gateway_extra_headers?: Record | null - /** Mark the new credential as the default for (org, provider). */ - is_default?: boolean -} - -export interface AIProviderUpdate { - api_key?: string | null - name?: string | null - endpoint_url?: string | null - is_active?: boolean - routing_mode?: CredentialRoutingMode - gateway_model?: string | null - gateway_interface?: GatewayInterfaceMode - gateway_base_url?: string | null - gateway_auth_header?: string | null - gateway_auth_secret_env?: string | null - gateway_auth_secret?: string | null - clear_gateway_auth_secret?: boolean - gateway_extra_headers?: Record | null -} - -export enum VoiceBundleType { - STT_LLM_TTS = 'stt_llm_tts', - S2S = 's2s', -} - -export interface VoiceBundle { - id: string - name: string - description?: string | null - bundle_type: VoiceBundleType - stt_provider?: ModelProvider | null - stt_model?: string | null - /** - * Optional explicit AIProvider/Integration row id for STT. When null the - * runtime resolver picks the default credential for stt_provider. - */ - stt_credential_id?: string | null - llm_provider?: ModelProvider | null - llm_model?: string | null - llm_temperature?: number | null - llm_max_tokens?: number | null - llm_config?: Record | null - llm_credential_id?: string | null - tts_provider?: ModelProvider | null - tts_model?: string | null - tts_voice?: string | null - tts_config?: Record | null - tts_credential_id?: string | null - s2s_provider?: ModelProvider | null - s2s_model?: string | null - s2s_config?: Record | null - s2s_credential_id?: string | null - extra_metadata?: Record | null - is_active: boolean - created_at: string - updated_at: string - created_by?: string | null -} - -export interface VoiceBundleCreate { - name: string - description?: string | null - bundle_type?: VoiceBundleType - stt_provider?: ModelProvider | null - stt_model?: string | null - stt_credential_id?: string | null - llm_provider?: ModelProvider | null - llm_model?: string | null - llm_temperature?: number | null - llm_max_tokens?: number | null - llm_config?: Record | null - llm_credential_id?: string | null - tts_provider?: ModelProvider | null - tts_model?: string | null - tts_voice?: string | null - tts_config?: Record | null - tts_credential_id?: string | null - s2s_provider?: ModelProvider | null - s2s_model?: string | null - s2s_config?: Record | null - s2s_credential_id?: string | null - extra_metadata?: Record | null -} - -// Test Agent Types -export interface AgentPhoneAssignmentConflict { - agent_id: string - agent_name: string - phone_number: string -} - -export interface AgentPhoneAssignmentCheckResponse { - available: boolean - phone_number?: string | null - conflict?: AgentPhoneAssignmentConflict | null -} - -export interface TestAgent { - id: string - agent_id?: string | null - name: string - phone_number?: string | null - telephony_phone_number_id?: string | null - language: string - description: string | null - prompt_variables?: Record | null - silence_hangup_secs?: number - call_type: string - call_medium: string - voice_bundle_id?: string | null - voice_ai_integration_id?: string | null - voice_ai_agent_id?: string | null - provider_prompt?: string | null - provider_prompt_synced_at?: string | null - created_at: string - updated_at: string -} - -// Test Agent Conversation Types -export interface TestAgentConversation { - id: string - organization_id: string - agent_id: string - persona_id: string - scenario_id: string - voice_bundle_id: string - status: string - live_transcription?: Array<{ - speaker: string - text: string - timestamp: number - audio_segment_key?: string - }> | null - conversation_audio_key?: string | null - full_transcript?: string | null - started_at: string - ended_at?: string | null - duration_seconds?: number | null - conversation_metadata?: Record | null - created_at: string - updated_at: string - created_by?: string | null -} - -export interface TestAgentConversationCreate { - agent_id: string - persona_id: string - scenario_id: string - voice_bundle_id: string - conversation_metadata?: Record | null -} - -export interface TestAgentConversationUpdate { - status?: string | null - live_transcription?: Array> | null - full_transcript?: string | null - conversation_metadata?: Record | null -} - -export interface VoiceBundleUpdate { - name?: string - description?: string | null - stt_provider?: ModelProvider - stt_model?: string - stt_credential_id?: string | null - llm_provider?: ModelProvider - llm_model?: string - llm_temperature?: number | null - llm_max_tokens?: number | null - llm_config?: Record | null - llm_credential_id?: string | null - tts_provider?: ModelProvider - tts_model?: string - tts_voice?: string | null - tts_config?: Record | null - tts_credential_id?: string | null - s2s_provider?: ModelProvider | null - s2s_model?: string | null - s2s_config?: Record | null - s2s_credential_id?: string | null - extra_metadata?: Record | null - is_active?: boolean -} - -// Data Sources Types -export interface S3ConnectionTest { - bucket_name: string - region?: string - access_key_id: string - secret_access_key: string - endpoint_url?: string | null -} - -export interface S3ConnectionTestResponse { - success: boolean - message: string - bucket_name?: string | null -} - -export interface S3FileInfo { - key: string - filename: string - size: number - last_modified: string -} - -export interface S3FolderInfo { - name: string - path: string -} - -export interface S3ListFilesResponse { - files: S3FileInfo[] - total: number - prefix?: string | null -} - -export interface S3BrowseResponse { - folders: S3FolderInfo[] - files: S3FileInfo[] - current_path: string - organization_id: string -} - -export interface S3Status { - enabled: boolean - provider?: 's3' | 'gcs' | string - error?: string | null -} - -// Alert Types -export enum AlertMetricType { - NUMBER_OF_CALLS = 'number_of_calls', - CALL_DURATION = 'call_duration', - ERROR_RATE = 'error_rate', - SUCCESS_RATE = 'success_rate', - LATENCY = 'latency', - CUSTOM = 'custom', -} - -export enum AlertAggregation { - SUM = 'sum', - AVG = 'avg', - COUNT = 'count', - MIN = 'min', - MAX = 'max', -} - -export enum AlertOperator { - GREATER_THAN = '>', - LESS_THAN = '<', - GREATER_THAN_OR_EQUAL = '>=', - LESS_THAN_OR_EQUAL = '<=', - EQUAL = '=', - NOT_EQUAL = '!=', -} - -export enum AlertNotifyFrequency { - IMMEDIATE = 'immediate', - HOURLY = 'hourly', - DAILY = 'daily', - WEEKLY = 'weekly', -} - -export enum AlertStatus { - ACTIVE = 'active', - PAUSED = 'paused', - DISABLED = 'disabled', -} - -export enum AlertHistoryStatus { - TRIGGERED = 'triggered', - NOTIFIED = 'notified', - ACKNOWLEDGED = 'acknowledged', - RESOLVED = 'resolved', -} - -export interface Alert { - id: string - organization_id: string - name: string - description?: string | null - metric_type: AlertMetricType - aggregation: AlertAggregation - operator: AlertOperator - threshold_value: number - time_window_minutes: number - agent_ids?: string[] | null - notify_frequency: AlertNotifyFrequency - notify_emails?: string[] | null - notify_webhooks?: string[] | null - status: AlertStatus - created_at: string - updated_at: string - created_by?: string | null -} - -export interface AlertCreate { - name: string - description?: string | null - metric_type?: AlertMetricType - aggregation?: AlertAggregation - operator?: AlertOperator - threshold_value: number - time_window_minutes?: number - agent_ids?: string[] | null - notify_frequency?: AlertNotifyFrequency - notify_emails?: string[] - notify_webhooks?: string[] -} - -export interface AlertUpdate { - name?: string - description?: string | null - metric_type?: AlertMetricType - aggregation?: AlertAggregation - operator?: AlertOperator - threshold_value?: number - time_window_minutes?: number - agent_ids?: string[] | null - notify_frequency?: AlertNotifyFrequency - notify_emails?: string[] - notify_webhooks?: string[] - status?: AlertStatus -} - -export interface AlertHistoryItem { - id: string - organization_id: string - alert_id: string - triggered_at: string - triggered_value: number - threshold_value: number - status: AlertHistoryStatus - notified_at?: string | null - notification_details?: Record | null - acknowledged_at?: string | null - acknowledged_by?: string | null - resolved_at?: string | null - resolved_by?: string | null - resolution_notes?: string | null - context_data?: Record | null - created_at: string - updated_at: string - alert?: Alert -} - - -// Cron Job Types -export enum CronJobStatus { - ACTIVE = 'active', - PAUSED = 'paused', - COMPLETED = 'completed', -} - -export interface CronJob { - id: string - organization_id: string - name: string - cron_expression: string - timezone: string - max_runs: number - current_runs: number - evaluator_ids: string[] - status: CronJobStatus - next_run_at?: string | null - last_run_at?: string | null - created_at: string - updated_at: string - created_by?: string | null -} - -export interface CronJobCreate { - name: string - cron_expression: string - timezone: string - max_runs: number - evaluator_ids: string[] -} - -export interface CronJobUpdate { - name?: string - cron_expression?: string - timezone?: string - max_runs?: number - evaluator_ids?: string[] - status?: CronJobStatus -} - -// --- Call Imports --- - -/** - * Lifecycle for a call-import batch. - * - * - ``uploaded`` : file landed in S3, no mapping yet. - * - ``mapped`` : user picked a schema + sheet + column mapping; no - * rows materialised yet, no worker enqueued. - * - ``processing`` : rows materialised + workers enqueued. - * - ``pending`` : transient state used by the legacy one-shot - * ``POST /upload`` endpoint just before transitioning - * to ``processing``. - */ -export type CallImportStatus = - | 'pending' - | 'uploaded' - | 'mapped' - | 'processing' - | 'completed' - | 'partial' - | 'failed' - | 'deleting' - -export type CallImportRowStatus = - | 'pending' - | 'processing' - | 'completed' - | 'failed' - -/** Where the value in `transcript` came from. */ -export type CallImportTranscriptSource = - | 'csv' - | 'transcribed' - | 'edited' - | null -/** Lifecycle status for the post-hoc transcription workflow itself. */ -export type CallImportTranscriptStatus = - | 'idle' - | 'pending' - | 'running' - | 'completed' - | 'failed' - | null - -/** - * Which transcript an evaluation run scored against. - * - `production`: the CSV-supplied value on `CallImportRow.transcript`. - * - `diarised`: the worker-produced value on `CallImportRow.diarised_transcript`. - */ -export type CallImportEvaluationTranscriptSource = 'production' | 'diarised' - -/** - * One contiguous turn inside ``CallImportRow.diarised_segments``. - * - * The diarisation worker rewrites each pyannote ``Speaker N`` label - * into ``agent`` / ``user`` (first speaker = agent heuristic). Anything - * beyond two distinct speakers keeps a generic ``speaker_N`` label so - * multi-party recordings still render every voice. - */ -export interface CallImportDiarisedSegment { - speaker: string - text: string - start: number - end: number - /** Original pyannote label (``Speaker 1`` / ``Speaker 2`` / ...). */ - raw_speaker: string -} - -export interface CallImportRow { - id: string - row_index: number - /** Mandatory identifier per row. Renamed from ``external_call_id``. */ - conversation_id: string - recording_url: string | null - recording_date: string | null - /** Production transcript — the value supplied via the CSV upload. */ - transcript: string | null - /** Provenance of the stored production transcript (csv = CSV upload, edited = manual edit). */ - transcript_source: CallImportTranscriptSource - /** Legacy: provider recorded by the original transcription worker before the split. */ - transcript_provider: string | null - transcript_model: string | null - transcript_status: CallImportTranscriptStatus - transcript_error: string | null - transcribed_at: string | null - /** Diarised transcript — produced by the post-hoc diarisation worker. */ - diarised_transcript: string | null - /** Provider used by the diarisation worker (e.g. "deepgram"). */ - diarised_transcript_provider: string | null - diarised_transcript_model: string | null - diarised_transcript_status: CallImportTranscriptStatus - diarised_transcript_error: string | null - diarised_at: string | null - /** - * Structured speaker turns produced by the diarisation worker. Each - * entry is a single contiguous turn shaped as - * `{ speaker: 'agent' | 'user' | 'speaker_N', text, start, end, - * raw_speaker }`. ``diarised_transcript`` is a rendered - * `: ` view of this list with - * ``diarised_speaker_swap`` applied. ``null`` on legacy rows that - * were diarised before structured turns were persisted (or when the - * STT provider didn't surface segments). - */ - diarised_segments: CallImportDiarisedSegment[] | null - /** - * When ``true`` the ``agent`` <-> ``user`` mapping inside - * ``diarised_segments`` is inverted in the rendered transcript / - * CSV export. The worker writes the canonical mapping using a - * "first speaker is the agent" heuristic; reviewers can flip the - * toggle from the row detail panel without re-running diarisation. - */ - diarised_speaker_swap: boolean - /** - * LLM that turned the STT plain-text output into structured - * ``diarised_segments``. NULL on legacy rows (pre-LLM-diariser). - */ - diarised_llm_provider: string | null - diarised_llm_model: string | null - /** - * Exact prompt the LLM diariser ran with. Useful for the modal to - * pre-fill its textarea when the operator wants to iterate on a - * previously-diarised row. - */ - diarised_prompt: string | null - /** - * Diarisation pipeline that produced this row's turns. - * - `stt_llm` (default) — two-stage STT then LLM diariser. - * - `llm_only` — single-stage multimodal LLM (audio in). - * Read-only; written by the worker on each diarisation. - */ - transcribe_mode?: 'stt_llm' | 'llm_only' - /** - * Per-row preservation of the mapped source cells. Values land here - * as whatever type the schema parameter coerced them to — - * strings (text / url / conversation_id / recording_url / - * recording_date / transcript / datetime), numbers, booleans, or - * ``null`` for blanks. Always - * coerce with ``String(value)`` before string operations. - */ - raw_columns: Record | null - status: CallImportRowStatus - recording_s3_key: string | null - recording_content_type: string | null - recording_size_bytes: number | null - error_message: string | null - attempts: number - created_at: string - updated_at: string -} - -export interface CallImportTag { - id: string - name: string - color: string | null - created_at: string - updated_at: string -} - -/** - * Parameter type tag on a Call Import schema parameter. - * - * - ``conversation_id``: mandatory identifier (one per schema). - * - ``recording_url``: feeds ``CallImportRow.recording_url``. - * - ``recording_date``: date-only call recording date used for reports. - * - ``transcript``: feeds ``CallImportRow.transcript``. - * - ``text`` / ``number`` / ``boolean`` / ``datetime`` / ``url``: - * generic typed fields preserved per row in ``raw_columns`` and - * surfaced in the evaluation export under the parameter's name. - */ -export type CallImportSchemaParameterType = - | 'conversation_id' - | 'recording_url' - | 'recording_date' - | 'transcript' - | 'text' - | 'number' - | 'boolean' - | 'datetime' - | 'url' - -export interface CallImportSchemaParameter { - id?: string - name: string - type: CallImportSchemaParameterType - description: string | null - is_required: boolean - ordering?: number -} - -export interface CallImportSchema { - id: string - organization_id: string - workspace_id: string - name: string - description: string | null - parameters: CallImportSchemaParameter[] - /** How many CallImport batches reference this schema. */ - usage_count: number - created_at: string - updated_at: string -} - -export interface CallImportSchemaListResponse { - items: CallImportSchema[] - total: number -} - -export interface CallImportSchemaCreate { - name: string - description?: string | null - parameters: Array> -} - -export interface CallImportSchemaUpdate { - name?: string - description?: string | null - parameters?: Array> -} - -/** - * In-org Workspace - the active workspace scopes call imports and - * metrics in the UI. The org's Default workspace is auto-seeded by - * migration 033 and cannot be deleted. - */ -export interface Workspace { - id: string - organization_id: string - name: string - slug: string - is_default: boolean - created_at: string - updated_at: string - role_id?: string | null - role_name?: string | null - capabilities?: string[] -} - -export interface WorkspaceRole { - id: string - organization_id: string - name: string - description?: string | null - capabilities: string[] - is_system: boolean - created_at: string - updated_at: string -} - -export interface WorkspaceMember { - id: string - workspace_id: string - user_id: string - role_id: string - role_name: string - user_email: string - user_name?: string | null - added_by_user_id?: string | null - created_at: string -} - -export interface CapabilityInfo { - key: string - label: string -} - -export interface CapabilityDomain { - key: string - label: string - capabilities: CapabilityInfo[] -} - -export interface WorkspaceRoleCreate { - name: string - description?: string | null - capabilities: string[] -} - -export interface WorkspaceRoleUpdate { - name?: string - description?: string | null - capabilities?: string[] -} - -export interface CallImportSourceRowSkip { - source_row: number - reason: string - message: string -} - -export interface CallImport { - id: string - organization_id: string - /** Workspace this import belongs to. */ - workspace_id: string - /** - * Telephony provider key. ``null`` until the IMPORT stage in the - * staged flow (which is the first step that knows the provider). - * Always populated on post-import batches and on legacy one-shot - * uploads. - */ - provider: string | null - telephony_integration_id: string | null - original_filename: string | null - /** - * For Excel uploads, which worksheet this batch came from. ``null`` - * for CSV uploads (CSV files have no sheet concept) and for any - * imports created before multi-sheet support landed. - */ - sheet_name: string | null - /** Optional free-text dataset label (high-level segregation filter). */ - dataset: string | null - /** Tags currently attached to this import. Empty array if untagged. */ - tags: CallImportTag[] - /** - * Reusable Input Parameter schema the batch was uploaded against. - * NULL on legacy batches uploaded before the schema-driven flow shipped. - */ - schema_id: string | null - /** - * Schema-driven mapping: ``{parameter_name: csv_header}``. Empty on - * legacy batches; check ``column_mapping`` / ``extra_columns`` / - * ``custom_column_mapping`` instead for those. - */ - parameter_mapping: Record - /** Legacy free-form mapping kept for batches uploaded before schemas. */ - column_mapping: Record - /** Legacy extra-column list kept for backwards-compat. */ - extra_columns: string[] - /** Legacy uploader-named columns kept for backwards-compat. */ - custom_column_mapping: Record - /** - * Source headers the uploader explicitly skipped, captured at the - * MAP stage. Empty for legacy one-shot uploads where the value was - * ephemeral. - */ - skipped_columns: string[] - /** - * Source rows skipped at parse time (missing/invalid conversation ID or URL). - */ - source_row_skips?: CallImportSourceRowSkip[] - /** S3 key for the staged source file. ``null`` on legacy batches. */ - source_s3_key: string | null - /** ``'csv'`` / ``'xlsx'`` for staged files, or ``'audio'`` for manual uploads. */ - source_format: string | null - source_size_bytes: number | null - source_content_type: string | null - /** - * Snapshot of the file's sheets + headers captured at UPLOAD time so - * the MAP UI can render without re-fetching the source from S3. - * ``null`` on legacy batches. - */ - available_sheets: CallImportPreviewSheet[] | null - total_rows: number - completed_rows: number - failed_rows: number - status: CallImportStatus - error_message: string | null - created_at: string - updated_at: string -} - -export interface CallImportDetail extends CallImport { - rows: CallImportRow[] - /** - * Total row count *after* applying the optional ``q`` search filter. - * ``null`` when no filter is active — paginate against ``total_rows`` - * in that case. - */ - filtered_total_rows: number | null - /** - * Batch-wide aggregates of ``CallImportRow.diarised_transcript_status``. - * The ``idle`` bucket (rows never touched by the transcribe/diarise - * worker) is implicit: ``total_rows - (pending + running + completed - * + failed)``. Lets the UI render a transcribe-and-diarise progress - * bar without paginating through every row. - */ - diarised_pending_rows: number - diarised_running_rows: number - diarised_completed_rows: number - diarised_failed_rows: number -} - -export interface CallImportListResponse { - items: CallImport[] - total: number - page: number - page_size: number -} - -export interface CallImportUploadResponse { - id: string - total_rows: number - status: CallImportStatus - dataset: string | null - tags: CallImportTag[] - message: string -} - -/** One worksheet (or one CSV file synthesized as a single sheet). */ -export interface CallImportPreviewSheet { - /** Sheet name for xlsx; filename for csv. */ - name: string - /** Column headers from the first non-empty row. */ - headers: string[] - /** Approximate count of data rows (excludes the header row). */ - row_count: number -} - -/** - * Sheets / headers extracted server-side from an uploaded CSV or Excel - * workbook. Drives the modal's column-mapping UI without forcing the - * frontend to parse the file itself. - */ -export interface CallImportPreviewResponse { - /** ``'csv'`` or ``'xlsx'``. */ - format: 'csv' | 'xlsx' - sheets: CallImportPreviewSheet[] -} - -export type MetricSelectionMode = 'single_choice' | 'multi_label' - -export interface CallImportMetricSummary { - id: string - name: string - metric_type: string | null - description: string | null - parent_metric_id?: string | null - selection_mode?: MetricSelectionMode | null - /** Only meaningful on multi_label parents; gates the Discovered - * Labels panel on the Flow tab. Defaults to false. */ - allow_discovery?: boolean -} - -/** Per-metric LLM override (provider+model+optional credential + generation params). */ -export interface CallImportEvaluationLLMOverride { - provider?: string | null - model?: string | null - credential_id?: string | null - llm_config?: LLMGenerationConfig | null -} - -export interface CallImportEvaluation { - id: string - call_import_id: string - organization_id: string - /** User-supplied label for the run; null when not named. */ - name: string | null - selected_metric_ids: string[] - /** parent_id -> [child_id, ...] snapshot captured at run time. */ - selected_metric_groups?: Record | null - metrics: CallImportMetricSummary[] - status: 'pending' | 'running' | 'completed' | 'partial' | 'failed' - total_rows: number - completed_rows: number - failed_rows: number - error_message: string | null - /** Run-level LLM provider chosen by the user (null = legacy default). */ - llm_provider: string | null - llm_model: string | null - llm_credential_id: string | null - llm_config?: LLMGenerationConfig | null - metric_llm_overrides: Record | null - stt_provider: string | null - stt_model: string | null - stt_credential_id: string | null - /** - * Run-level LLM diariser config used when the worker auto-diarises - * rows that are missing a diarised transcript. - */ - diarisation_llm_provider?: string | null - diarisation_llm_model?: string | null - diarisation_llm_credential_id?: string | null - diarisation_prompt?: string | null - /** - * Diarisation pipeline shape this run was created with. - * - `stt_llm` (default) — STT then an LLM diariser over the text. - * - `llm_only` — audio fed directly to a multimodal diariser LLM. - * Surfaced so the retry / re-run UI can preselect the right mode. - */ - transcribe_mode?: 'stt_llm' | 'llm_only' - /** - * Which transcript column this run scored against. - * Defaults to `production` on legacy runs. - */ - transcript_source: CallImportEvaluationTranscriptSource - /** - * Other evaluation ids created in the same Run Evaluation request. - * Populated only on the POST response when the user ticked both - * Production and Diarised. Empty array on all other reads. - */ - sibling_evaluation_ids: string[] - started_at: string | null - finished_at: string | null - created_at: string - updated_at: string - /** - * Cached LLM-generated TLDR rendered above the Visualizations tab. - * Populated lazily via ``POST /evaluations/{id}/insights``; null on - * runs the user has not summarised yet. - */ - tldr_summary?: EvaluationTldrSummary | null - user_insights?: EvaluationUserInsightsState | null - metric_clusters?: EvaluationMetricClustersState | null - /** - * True when the user opted into top-level metric discovery on the - * Run Evaluation modal. Gates the Discovered metrics panel on the - * evaluation detail Flow tab. - */ - discover_new_metrics?: boolean - /** - * Set while a bulk background operation (abort, force-fail, retry) is - * still running. The UI disables other mutating actions until cleared. - */ - bulk_operation?: 'abort' | 'force_fail_pending' | 'retry' | null -} - -/** - * LLM-generated narrative + bullet patterns for a single evaluation - * run. Cached on the evaluation row so re-opening the Visualizations - * tab doesn't auto-burn LLM tokens. ``is_stale`` is computed by the - * backend at read time when ``completed_rows`` has grown since the - * summary was generated. - */ -export interface EvaluationTldrSummary { - narrative: string - patterns: string[] - metric_insights?: Record - generated_at: string - generated_at_completed_rows: number - provider?: string | null - model?: string | null - is_stale: boolean -} - -export interface UserInsightCategory { - label: string - count: number - share_pct: number -} - -export interface UserInsightEvidenceTurn { - speaker: string - text: string -} - -export interface UserInsightEvidence { - conversation_id?: string | null - quote: string - turns?: UserInsightEvidenceTurn[] -} - -export interface EvaluationUserInsightItem { - id: string - title: string - categories: UserInsightCategory[] - observation: string - evidence: UserInsightEvidence -} - -export interface EvaluationUserInsightsState { - status: 'idle' | 'running' | 'completed' | 'failed' - insights: EvaluationUserInsightItem[] - overview?: string | null - generated_at?: string | null - generated_at_completed_rows: number - progress?: { completed_llm_calls: number; total_llm_calls: number } | null - provider?: string | null - model?: string | null - llm_calls_used: number - max_llm_calls?: number | null - error_message?: string | null - is_stale: boolean -} - -export type MetricClusterGapLabel = - | 'LOGIC_GAP' - | 'UNDERSPEC' - | 'EXISTS_NO_TRIGGER' - | 'MISSING' - -export interface MetricSubCluster { - label: string - count: number - share_pct: number -} - -export interface MetricClusterEvidenceTurn { - speaker: string - text: string -} - -export interface MetricClusterEvidence { - conversation_id?: string | null - evaluation_row_id?: string | null - quote: string - turns?: MetricClusterEvidenceTurn[] -} - -export interface MetricCluster { - id: string - label: string - gap_label: MetricClusterGapLabel - level: number - count: number - share_pct: number - sub_clusters: MetricSubCluster[] - observation: string - failure_reason?: string - evidence: MetricClusterEvidence - is_discovered: boolean -} - -export interface MetricClusterGroup { - metric_id: string - metric_name: string - flagged_count: number - failure_reason?: string - clusters: MetricCluster[] -} - -export interface DiscoveredProblemCluster { - id: string - label: string - gap_label: MetricClusterGapLabel - count: number - share_pct: number - observation: string - failure_reason?: string - evidence: MetricClusterEvidence -} - -export interface RcaRepeatedPatternRow { - metric_id: string - metric_name: string - top_rca_patterns: string - evidence_share_pct: number - evidence_calls: number - evidence_cluster_count?: number - failure_reason: string -} - -export interface RcaMetricHotspotRow { - metric_id: string - metric_name: string - description: string - metric_rate_pct: number - flagged_calls: number -} - -export interface RcaPromptAreaRow { - label: string - share_pct: number - gap_label: MetricClusterGapLabel -} - -export interface MetricClustersRcaSummary { - total_clusters: number - total_clustered_instances: number - total_flagged_instances?: number - analysed_calls: number - repeated_patterns: RcaRepeatedPatternRow[] - metric_hotspots: RcaMetricHotspotRow[] - prompt_areas: RcaPromptAreaRow[] -} - -export interface MetricFailurePolicy { - metric_id: string - failure_values: string[] - failure_child_names?: string[] - numeric_rule?: { op: 'lt' | 'lte' | 'gt' | 'gte'; threshold: number } | null -} - -export interface MetricFailurePolicyValueCount { - label: string - count: number -} - -export interface MetricFailurePolicyMetricPreview { - metric_id: string - metric_name: string - metric_type?: string | null - selection_mode?: string | null - is_multi_label_parent: boolean - value_counts: MetricFailurePolicyValueCount[] - child_names: string[] - row_count_by_value: Record - suggested_policy: MetricFailurePolicy - effective_policy: MetricFailurePolicy -} - -export interface MetricFailurePoliciesResponse { - previews: MetricFailurePolicyMetricPreview[] - policies: Record - source: 'inferred' | 'user' - updated_at?: string | null -} - -export interface MetricClusterEligibleRow { - evaluation_row_id: string - conversation_id?: string | null - row_index?: number | null - flagged_metric_names: string[] -} - -export interface MetricClusterEligibleRowsResponse { - items: MetricClusterEligibleRow[] - total: number -} - -export interface EvaluationMetricClustersState { - status: 'idle' | 'running' | 'completed' | 'failed' | 'cancelled' - groups: MetricClusterGroup[] - discovered_problems: DiscoveredProblemCluster[] - overview?: string | null - generated_at?: string | null - generated_at_completed_rows: number - progress?: { completed_llm_calls: number; total_llm_calls: number } | null - provider?: string | null - model?: string | null - llm_calls_used: number - max_llm_calls?: number | null - error_message?: string | null - is_stale: boolean - selected_evaluation_row_ids?: string[] - failure_policies?: Record - failure_policies_source?: 'inferred' | 'user' - failure_policies_updated_at?: string | null - rca_summary?: MetricClustersRcaSummary | null -} - -export interface AgentFlowNode { - id: string - label: string - node_type: 'start' | 'decision' | 'action' | 'terminal' - position_x?: number | null - position_y?: number | null - prompt_excerpt?: string | null - start_offset?: number | null - end_offset?: number | null -} - -export interface AgentFlowEdge { - source: string - target: string - condition?: string | null -} - -export interface AgentFlowGraph { - nodes: AgentFlowNode[] - edges: AgentFlowEdge[] - generated_at?: string | null - provider?: string | null - model?: string | null - layout_saved_at?: string | null - prompt_content_hash?: string | null - mapping_error?: string | null - generation_error?: string | null -} - -export interface ImportedAgent { - id: string - organization_id: string - name: string - description: string | null - content: string - tags: string[] | null - current_version: number - agent_flowchart?: AgentFlowGraph | null - agent_flowchart_status?: string | null - created_at: string - updated_at: string - created_by: string | null -} - -export interface ImportedAgentDetail extends ImportedAgent { - versions: PromptPartialVersion[] -} - -export interface MetricPartialChild { - name: string - description: string - example: string -} - -export interface MetricPartialContent { - schema_version: 1 - metric_kind: 'single' | 'category' - description: string - children?: MetricPartialChild[] -} - -export interface MetricPartial { - id: string - organization_id: string - name: string - description: string | null - content: string - tags: string[] | null - current_version: number - created_at: string - updated_at: string - created_by: string | null -} - -export interface MetricPartialDetail extends MetricPartial { - versions: PromptPartialVersion[] -} - -export interface PromptPartialVersion { - id: string - prompt_partial_id: string - version: number - content: string - change_summary: string | null - created_at: string - created_by: string | null -} - -export interface PromptImprovementSuggestion { - id: string - metric_id: string - metric_name: string - cluster_id: string - cluster_label: string - gap_label: MetricClusterGapLabel - share_pct: number - priority: 'high' | 'medium' | 'low' - change_type?: 'edit' | 'add' - target_section: string - anchor_excerpt?: string - current_gap: string - suggested_text: string - rationale: string - flow_node_id?: string - flow_node_label?: string -} - -export interface EvaluationPromptImprovementsState { - status: 'idle' | 'running' | 'completed' | 'failed' - imported_agent_id?: string | null - imported_agent_name?: string | null - suggestions: PromptImprovementSuggestion[] - overview?: string | null - generated_at?: string | null - generated_at_completed_rows: number - provider?: string | null - model?: string | null - error_message?: string | null - is_stale: boolean -} - -export interface MetricPeriodDelta { - label: string - detail: string - why?: string | null -} - -export interface CallImportEvaluationListResponse { - items: CallImportEvaluation[] - total: number -} - -export interface CallImportEvaluationBaselineCandidate { - evaluation_id: string - name: string - dataset: string - period_label: string | null - period_start: string | null - period_end: string | null - period_display: string - completed_rows: number - created_at: string - is_default: boolean -} - -export interface CallImportEvaluationBaselineCandidatesResponse { - items: CallImportEvaluationBaselineCandidate[] - default_evaluation_id: string | null -} - -export interface CallImportEvaluationRow { - id: string - evaluation_id: string - call_import_row_id: string - row_index: number | null - /** Mandatory identifier from the source batch (renamed from ``external_call_id``). */ - conversation_id: string | null - transcript: string | null - raw_columns: Record | null - recording_url: string | null - recording_date: string | null - /** - * S3 object key for the downloaded recording. Prefer this over - * ``recording_url`` for playback — we resolve it to a presigned URL - * so audio plays from our storage instead of the (often expired) - * provider URL. - */ - recording_s3_key: string | null - diarised_transcript_status?: string | null - diarised_transcript_error?: string | null - status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' - metric_scores: Record - error_message: string | null - started_at: string | null - finished_at: string | null - created_at: string - updated_at: string -} - -export interface CallImportEvaluationRowListResponse { - items: CallImportEvaluationRow[] - total: number - page: number - page_size: number -} - -// --- Retry (re-enqueue failed rows on an existing evaluation run) --- - -export interface CallImportEvaluationRetryRequest { - /** - * Restrict the retry to a specific subset of evaluation rows. - * When omitted, every row with status='failed' in this run is - * re-enqueued. - */ - eval_row_ids?: string[] - - /** - * Optional LLM overrides. When provided, persisted onto the run so - * future retries default to the new config. ``llm_provider`` and - * ``llm_model`` must be sent together — the backend 400s on - * half-configured input. - */ - llm_provider?: string - llm_model?: string - llm_credential_id?: string | null - - /** - * Optional STT overrides (only meaningful when the run scores the - * diarised transcript). Same paired-field rule as LLM. - */ - stt_provider?: string - stt_model?: string - stt_credential_id?: string | null - - /** - * When true, wipe the diarised transcript on every retried row so - * the (possibly new) STT runs from scratch. Only takes effect for - * diarised runs that have STT config. - */ - transcribe_overwrite?: boolean -} - -export interface CallImportEvaluationRetrySkippedItem { - eval_row_id: string - /** - * Why this row was not re-enqueued. Known values: - * - 'unknown' (id not in this run) - * - 'in_progress' (status is pending/running) - * - 'completed' (already successful) - * - 'source_row_missing' - */ - reason: 'unknown' | 'in_progress' | 'completed' | 'source_row_missing' -} - -export interface CallImportEvaluationRetryResponse { - requeued: number - /** - * Of those, how many were chained through a diarisation task first - * because the diarised transcript was missing. - */ - transcribe_requeued: number - skipped: CallImportEvaluationRetrySkippedItem[] -} - -export interface CallImportEvaluationBulkActionResponse { - accepted: boolean - target_count: number - evaluation_id: string -} - -// --- Diarization / transcription --- - -export interface CallImportTranscribeRequest { - /** - * Diarisation pipeline shape. - * - `stt_llm` (default) — STT produces plain text, then an LLM - * diariser splits it into agent/user turns. STT fields required. - * - `llm_only` — skip STT entirely and feed the audio bytes - * directly to a multimodal `diarization_llm_*` model along with - * `diarization_prompt`. STT fields MUST be omitted in this mode. - */ - mode?: 'stt_llm' | 'llm_only' - /** Required when `mode === 'stt_llm'`; must be null/omitted in `llm_only`. */ - stt_provider?: string | null - /** Required when `mode === 'stt_llm'`; must be null/omitted in `llm_only`. */ - stt_model?: string | null - credential_id?: string | null - language?: string | null - only_missing?: boolean - overwrite_existing?: boolean - row_ids?: string[] - /** - * LLM diariser. In `stt_llm` mode it splits the STT plain-text into - * agent/user turns; in `llm_only` mode it directly receives the - * audio along with `diarization_prompt`. Always required. - */ - diarization_llm_provider: string - diarization_llm_model: string - diarization_llm_credential_id?: string | null - /** - * Operator-supplied system prompt for the diariser LLM. NULL/empty - * means "fall back to the canonical default" (see - * ``getDiarisationDefaultPrompt``). - */ - diarization_prompt?: string | null -} - -export interface CallImportTranscribeResponse { - queued: number - skipped_rows: number - skipped_reason_counts: Record - accepted?: boolean -} - -export interface CallImportRowBulkDeleteResponse { - deleted: number - status?: 'completed' | 'accepted' -} - -export interface CallImportRetryFailedRowsResponse { - requeued: number - enqueue_failed: number - skipped: number -} - -export interface CallImportDiarisationPromptDefaultResponse { - prompt: string -} - -// --- Aggregation / visualization payloads --- - -export interface CallImportMetricHistogramBucket { - x0: number - x1: number - count: number -} - -export interface CallImportMetricValueCount { - label: string - count: number -} - -/** - * One unordered pair-count cell from a multi-label parent's - * co-occurrence matrix. ``a`` and ``b`` are child label names with - * ``a < b`` lexicographically; ``count`` is the number of rows on - * which both labels fired together. - */ -export interface CallImportMetricLabelPair { - a: string - b: string - count: number -} - -export interface CallImportMetricAggregate { - metric_id: string - metric_name: string - metric_type: string | null - metric_category?: 'quality' | 'user_insight' | string - /** - * True when the metric is a multi-label classifier parent. - * ``value_counts`` then lists per-child label tallies and one row - * may contribute to several labels, so the chart layout has to - * ignore the pie toggle (slices wouldn't sum to 100%) and the - * n-badge represents rows scored, not label occurrences. - */ - is_multi_label_parent?: boolean - count: number - skipped_count: number - error_count: number - mean: number | null - median: number | null - p25: number | null - p75: number | null - p95: number | null - min: number | null - max: number | null - stddev: number | null - histogram_buckets: CallImportMetricHistogramBucket[] - value_counts: CallImportMetricValueCount[] - /** - * Pairwise label intersections for multi-label parent metrics. - * Empty for everything else. The Visualizations tab reconstructs - * a square symmetric matrix from these unordered pairs to render - * the co-occurrence heatmap chart type. - */ - co_occurrence?: CallImportMetricLabelPair[] -} - -export interface CallImportEvaluationAggregateResponse { - evaluation_id: string - total_rows: number - completed_rows: number - failed_rows: number - metrics: CallImportMetricAggregate[] - period_deltas?: Record - baseline_evaluation_id?: string | null - failure_policies_source?: 'inferred' | 'user' | null -} - -export interface EvaluatorResultsAggregateResponse { - scope: string - suite_id?: string | null - agent_id?: string | null - scenario_id?: string | null - total_rows: number - completed_rows: number - failed_rows: number - metrics: CallImportMetricAggregate[] -} - -export interface CallImportInsightsRunPoint { - evaluation_id: string - name: string | null - created_at: string - mean: number | null - completed_rows: number -} - -export interface CallImportInsightsMetric { - metric_id: string - metric_name: string - metric_type: string | null - latest: CallImportMetricAggregate | null - trend: CallImportInsightsRunPoint[] -} - -export interface CallImportInsightsResponse { - call_import_id: string - total_rows: number - rows_with_transcript: number - rows_without_transcript: number - transcript_source_counts: Record - evaluation_count: number - metrics: CallImportInsightsMetric[] -} - -// --- Metrics hierarchy + flow visualization --- - -export interface MetricSummary { - id: string - organization_id: string - name: string - description: string | null - metric_type: string - metric_category?: 'quality' | 'user_insight' | string - trigger: string - enabled: boolean - is_default: boolean - metric_origin: string - supported_surfaces: string[] - enabled_surfaces: string[] - custom_data_type: string | null - custom_config: Record | null - tags: string[] | null - capture_rationale: boolean - parent_metric_id: string | null - selection_mode: MetricSelectionMode | null - allow_discovery?: boolean - /** - * When true, this metric is a "transcript-compare judge": at - * call-import evaluation time the worker feeds BOTH the production - * transcript and the diarised transcript to the LLM as a labeled - * pair, and the run's transcript_source toggle is ignored for this - * metric. Mutually exclusive with parent_metric_id and selection_mode - * — comparison metrics stay standalone. - */ - compare_transcripts?: boolean - children?: MetricSummary[] - created_at: string - updated_at: string - created_by: string | null -} - -export interface MetricChildDraft { - name: string - description?: string | null - enabled?: boolean - capture_rationale?: boolean | null - tags?: string[] | null -} - -export interface MetricCreateWithChildrenPayload { - name: string - description?: string | null - selection_mode: MetricSelectionMode - enabled?: boolean - supported_surfaces?: string[] - enabled_surfaces?: string[] - tags?: string[] | null - allow_discovery?: boolean - children: MetricChildDraft[] -} - -export interface MetricFlowNode { - id: string - label: string - count: number - is_terminal: boolean - is_discovered?: boolean -} - -export interface MetricFlowEdge { - source: string - target: string - count: number -} - -export interface MetricFlowResponse { - parent_metric_id: string - parent_metric_name: string - selection_mode: MetricSelectionMode | null - nodes: MetricFlowNode[] - edges: MetricFlowEdge[] - total_rows: number - rows_with_sequence: number -} - -export interface DiscoveredLabel { - key: string - name: string - description?: string | null - sample_rationale?: string | null - /** - * Up to 3 distinct LLM rationales captured for this candidate - * across rows. The Discovered Labels promote flow surfaces the - * first 2 as an ``Examples:`` block on the new sub-metric's - * rubric so the user starts with concrete cases in the prompt. - */ - examples?: string[] - count: number -} - -export interface DiscoveredLabelsResponse { - parent_metric_id: string - items: DiscoveredLabel[] -} - -/** - * One LLM-discovered candidate TOP-LEVEL metric aggregated across all - * rows of an evaluation. Mirrors :class:`DiscoveredLabel` but adds a - * ``suggested_type`` field — the LLM's guess at the best shape for - * the new metric — that the promote modal can pre-fill the type radio - * with. - */ -export interface DiscoveredMetric { - key: string - name: string - description?: string | null - suggested_type: 'boolean' | 'rating' | 'category' - sample_rationale?: string | null - examples?: string[] - count: number -} - -export interface DiscoveredMetricsResponse { - evaluation_id: string - items: DiscoveredMetric[] -} - -export interface ObservabilityCallAgent { - id: string - agent_id?: string | null - name: string -} - -export interface ObservabilityCallData { - startedAt?: string - started_at?: string - endedAt?: string - ended_at?: string - from_phone_number?: string - to_phone_number?: string - endedReason?: string - recording_s3_key?: string - recording_url?: string - duration_seconds?: number - agent_name?: string - _agent_ref?: string | number - direction?: string - messages?: Array<{ role: string; content: string; start_time?: number; end_time?: number }> - live_transcript?: Array<{ role: string; content: string; timestamp?: string; start_time?: number }> - metadata?: Record - call_short_id?: string -} - -export interface ObservabilityCall { - id: string - call_short_id: string - status?: string | null - call_event?: string | null - is_live?: boolean - direction?: string | null - source?: string | null - provider_platform?: string | null - provider_call_id?: string | null - agent_id?: string | null - agent?: ObservabilityCallAgent | null - created_at?: string | null - updated_at?: string | null - call_data?: ObservabilityCallData | null - live_transcript?: Array<{ role: string; content: string; timestamp?: string }> -} +// API Types matching the backend schemas + +export type { LLMGenerationConfig } from '../config/llmGenerationParams' +import type { LLMGenerationConfig } from '../config/llmGenerationParams' + +export enum EvaluationType { + ASR = 'asr', + TTS = 'tts', +} + +export enum EvaluationStatus { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', +} + +export interface AudioFile { + id: string + filename: string + format: string + file_size: number + duration?: number | null + sample_rate?: number | null + channels?: number | null + uploaded_at: string +} + +export interface Evaluation { + id: string + audio_id: string + reference_text?: string | null + evaluation_type: EvaluationType + model_name?: string | null + status: EvaluationStatus + metrics_requested?: string[] | null + created_at: string + started_at?: string | null + completed_at?: string | null + error_message?: string | null +} + +export interface DashboardSummary { + evaluations: { + total: number + completed: number + pending: number + failed: number + } + resources: { + agents: number + personas: number + scenarios: number + integrations: number + voice_bundles: number + } + setup_progress: { + has_integration: boolean + has_voice_bundle: boolean + has_agent: boolean + has_evaluation: boolean + } + metrics: { + total: number + enabled: number + } + call_imports: { + total: number + } + call_import_evaluations: { + total: number + completed: number + running: number + failed: number + } + recent_evaluations: Evaluation[] +} + +export interface ModelConfigEntry { + provider: string + model_type: string + description?: string + featured?: boolean + featured_rank?: number + highlights?: string[] +} + +export interface EvaluationCreate { + audio_id: string + reference_text?: string | null + evaluation_type: EvaluationType + model_name?: string | null + metrics?: string[] +} + +export interface EvaluationResult { + evaluation_id: string + status: EvaluationStatus + transcript?: string | null + metrics: Record + processing_time?: number | null + model_used?: string | null + created_at: string +} + +export interface BatchEvaluationResult { + processed_files: number + failed_files: number + aggregated_metrics?: Record | null + individual_results: EvaluationResult[] +} + +/** Voice agent evaluator run (evaluator_results table). */ +export type EvaluatorResultStatus = + | 'queued' + | 'call_initiating' + | 'call_connecting' + | 'call_in_progress' + | 'call_ended' + | 'transcribing' + | 'evaluating' + | 'fetching_details' + | 'completed' + | 'failed' + +export interface EvaluatorResultMetricScore { + value: unknown + type: string + metric_name: string + parent_metric_id?: string | null +} + +export interface EvaluatorResultRow { + id: string + result_id: string + name: string | null + evaluator_id: string | null + agent_id?: string | null + persona_id?: string | null + scenario_id?: string | null + suite_id?: string | null + timestamp: string + duration_seconds: number | null + status: EvaluatorResultStatus + metric_scores: Record | null + error_message: string | null + agent?: { id: string; name: string } | null + scenario?: { id: string; name: string } | null +} + +export interface EvaluatorResultListResponse { + items: EvaluatorResultRow[] + total: number +} + +export interface EvaluatorResultCounts { + total: number + completed: number + failed: number + in_progress: number + last_run_at?: string | null +} + +export interface EvaluatorResultsScenarioSummary { + scenario_id: string + scenario_name: string + counts: EvaluatorResultCounts +} + +export interface EvaluatorResultsSuiteSummary { + suite_id: string + suite_name?: string | null + agent_id: string + persona_id?: string | null + counts: EvaluatorResultCounts + scenarios?: EvaluatorResultsScenarioSummary[] | null +} + +export interface EvaluatorResultsAgentSummary { + agent_id: string + agent_name: string + counts: EvaluatorResultCounts + suites?: EvaluatorResultsSuiteSummary[] | null +} + +export interface EvaluatorResultsOverviewResponse { + workspace_counts: EvaluatorResultCounts + agents: EvaluatorResultsAgentSummary[] + unassigned: { + counts: EvaluatorResultCounts + recent_result_ids: string[] + } +} + +export interface ListEvaluatorResultsParams { + skip?: number + limit?: number + evaluatorId?: string + agentId?: string + suiteId?: string + scenarioId?: string + status?: 'completed' | 'failed' | 'in_progress' + unassignedOnly?: boolean + playground?: boolean + testAgentsOnly?: boolean +} + +export interface APIKey { + id: string + key: string + name?: string | null + is_active: boolean + created_at: string + last_used?: string | null + message?: string +} + +export interface MessageResponse { + message: string +} + +// IAM & User Types +export enum Role { + READER = 'reader', + WRITER = 'writer', + ADMIN = 'admin', +} + +export enum InvitationStatus { + PENDING = 'pending', + ACCEPTED = 'accepted', + DECLINED = 'declined', + EXPIRED = 'expired', +} + +export interface User { + id: string + email: string + name?: string | null + is_active: boolean + created_at: string +} + +export interface OrganizationMember { + id: string + user_id: string + organization_id: string + role: Role + joined_at: string + user: User +} + +export interface Invitation { + id: string + organization_id: string + email: string + role: Role + status: InvitationStatus + expires_at: string + created_at: string + organization_name?: string | null +} + +export interface InvitationCreate { + email: string + role: Role +} + +export interface RoleUpdate { + role: Role +} + +export interface Profile { + id: string + email: string + name?: string | null + first_name?: string | null + last_name?: string | null + created_at: string + organizations: Array<{ + id: string + name: string + role: string + joined_at: string + }> +} + +export interface UserUpdate { + name?: string | null + first_name?: string | null + last_name?: string | null + email?: string | null +} + +export interface UserPreferences { + theme?: string + notifications_enabled?: boolean + email_notifications?: boolean + default_language?: string + [key: string]: any +} + +export interface UserPreferencesUpdate { + theme?: string + notifications_enabled?: boolean + email_notifications?: boolean + default_language?: string + [key: string]: any +} + +// Integration Types +export enum IntegrationPlatform { + RETELL = 'retell', + VAPI = 'vapi', + CARTESIA = 'cartesia', + ELEVENLABS = 'elevenlabs', + DEEPGRAM = 'deepgram', + MURF = 'murf', + SARVAM = 'sarvam', + VOICEMAKER = 'voicemaker', + SMALLEST = 'smallest', +} + +export enum TelephonyProvider { + PLIVO = 'plivo', + EXOTEL = 'exotel', + VOBIZ = 'vobiz', +} + +export type CredentialRoutingMode = 'inherit' | 'gateway' | 'direct' +export type GatewayInterfaceMode = 'inherit' | 'litellm_shim' | 'native_openai' + +export type EffectiveCredentialRouting = + | 'inherit' + | 'direct' + | 'gateway' + | 'bifrost' + | 'litellm_proxy' + +export interface Integration { + id: string + organization_id: string + platform: IntegrationPlatform + name?: string | null + public_key?: string | null + is_active: boolean + /** True if this row is the default credential for (org, platform). */ + is_default?: boolean + routing_mode?: CredentialRoutingMode + effective_routing?: EffectiveCredentialRouting + created_at: string + updated_at: string + last_tested_at?: string | null +} + +export interface IntegrationCreate { + platform: IntegrationPlatform + api_key: string + public_key?: string + name?: string | null + routing_mode?: CredentialRoutingMode + /** Mark the new credential as the default for (org, platform). */ + is_default?: boolean +} + +// VoiceBundle Types +export enum ModelProvider { + OPENAI = 'openai', + ANTHROPIC = 'anthropic', + GOOGLE = 'google', + XAI = 'xai', + FIREWORKS = 'fireworks', + COHERE = 'cohere', + MISTRAL = 'mistral', + META = 'meta', + TOGETHER = 'together', + PERPLEXITY = 'perplexity', + AZURE = 'azure', + AWS = 'aws', + DEEPGRAM = 'deepgram', + CARTESIA = 'cartesia', + ELEVENLABS = 'elevenlabs', + MURF = 'murf', + CUSTOM = 'custom', + SARVAM = 'sarvam', + VOICEMAKER = 'voicemaker', + SMALLEST = 'smallest', +} + +// AI Provider Types +export interface AIProvider { + id: string + provider: ModelProvider + api_key?: string | null + name?: string | null + endpoint_url?: string | null + is_active: boolean + /** True if this row is the default credential for (org, provider). */ + is_default?: boolean + routing_mode?: CredentialRoutingMode + gateway_model?: string | null + gateway_interface?: GatewayInterfaceMode + gateway_base_url?: string | null + gateway_auth_header?: string | null + gateway_auth_secret_env?: string | null + has_gateway_auth_secret?: boolean + gateway_extra_headers?: Record | null + /** True when provider secrets are resolved by the Bifrost gateway. */ + gateway_managed?: boolean + effective_routing?: EffectiveCredentialRouting + effective_gateway_interface?: 'litellm_shim' | 'native_openai' + created_at: string + updated_at: string + last_tested_at?: string | null +} + +export interface AIProviderCreate { + provider: ModelProvider + api_key?: string | null + name?: string | null + endpoint_url?: string | null + routing_mode?: CredentialRoutingMode + gateway_model?: string | null + gateway_interface?: GatewayInterfaceMode + gateway_base_url?: string | null + gateway_auth_header?: string | null + gateway_auth_secret_env?: string | null + gateway_auth_secret?: string | null + gateway_extra_headers?: Record | null + /** Mark the new credential as the default for (org, provider). */ + is_default?: boolean +} + +export interface AIProviderUpdate { + api_key?: string | null + name?: string | null + endpoint_url?: string | null + is_active?: boolean + routing_mode?: CredentialRoutingMode + gateway_model?: string | null + gateway_interface?: GatewayInterfaceMode + gateway_base_url?: string | null + gateway_auth_header?: string | null + gateway_auth_secret_env?: string | null + gateway_auth_secret?: string | null + clear_gateway_auth_secret?: boolean + gateway_extra_headers?: Record | null +} + +export enum VoiceBundleType { + STT_LLM_TTS = 'stt_llm_tts', + S2S = 's2s', +} + +export interface VoiceBundle { + id: string + name: string + description?: string | null + bundle_type: VoiceBundleType + stt_provider?: ModelProvider | null + stt_model?: string | null + /** + * Optional explicit AIProvider/Integration row id for STT. When null the + * runtime resolver picks the default credential for stt_provider. + */ + stt_credential_id?: string | null + llm_provider?: ModelProvider | null + llm_model?: string | null + llm_temperature?: number | null + llm_max_tokens?: number | null + llm_config?: Record | null + llm_credential_id?: string | null + tts_provider?: ModelProvider | null + tts_model?: string | null + tts_voice?: string | null + tts_config?: Record | null + tts_credential_id?: string | null + s2s_provider?: ModelProvider | null + s2s_model?: string | null + s2s_config?: Record | null + s2s_credential_id?: string | null + extra_metadata?: Record | null + is_active: boolean + created_at: string + updated_at: string + created_by?: string | null +} + +export interface VoiceBundleCreate { + name: string + description?: string | null + bundle_type?: VoiceBundleType + stt_provider?: ModelProvider | null + stt_model?: string | null + stt_credential_id?: string | null + llm_provider?: ModelProvider | null + llm_model?: string | null + llm_temperature?: number | null + llm_max_tokens?: number | null + llm_config?: Record | null + llm_credential_id?: string | null + tts_provider?: ModelProvider | null + tts_model?: string | null + tts_voice?: string | null + tts_config?: Record | null + tts_credential_id?: string | null + s2s_provider?: ModelProvider | null + s2s_model?: string | null + s2s_config?: Record | null + s2s_credential_id?: string | null + extra_metadata?: Record | null +} + +// Test Agent Types +export interface AgentPhoneAssignmentConflict { + agent_id: string + agent_name: string + phone_number: string +} + +export interface AgentPhoneAssignmentCheckResponse { + available: boolean + phone_number?: string | null + conflict?: AgentPhoneAssignmentConflict | null +} + +export interface TestAgent { + id: string + agent_id?: string | null + name: string + phone_number?: string | null + telephony_phone_number_id?: string | null + language: string + description: string | null + prompt_variables?: Record | null + silence_hangup_secs?: number + call_type: string + call_medium: string + voice_bundle_id?: string | null + voice_ai_integration_id?: string | null + voice_ai_agent_id?: string | null + provider_prompt?: string | null + provider_prompt_synced_at?: string | null + created_at: string + updated_at: string +} + +// Test Agent Conversation Types +export interface TestAgentConversation { + id: string + organization_id: string + agent_id: string + persona_id: string + scenario_id: string + voice_bundle_id: string + status: string + live_transcription?: Array<{ + speaker: string + text: string + timestamp: number + audio_segment_key?: string + }> | null + conversation_audio_key?: string | null + full_transcript?: string | null + started_at: string + ended_at?: string | null + duration_seconds?: number | null + conversation_metadata?: Record | null + created_at: string + updated_at: string + created_by?: string | null +} + +export interface TestAgentConversationCreate { + agent_id: string + persona_id: string + scenario_id: string + voice_bundle_id: string + conversation_metadata?: Record | null +} + +export interface TestAgentConversationUpdate { + status?: string | null + live_transcription?: Array> | null + full_transcript?: string | null + conversation_metadata?: Record | null +} + +export interface VoiceBundleUpdate { + name?: string + description?: string | null + stt_provider?: ModelProvider + stt_model?: string + stt_credential_id?: string | null + llm_provider?: ModelProvider + llm_model?: string + llm_temperature?: number | null + llm_max_tokens?: number | null + llm_config?: Record | null + llm_credential_id?: string | null + tts_provider?: ModelProvider + tts_model?: string + tts_voice?: string | null + tts_config?: Record | null + tts_credential_id?: string | null + s2s_provider?: ModelProvider | null + s2s_model?: string | null + s2s_config?: Record | null + s2s_credential_id?: string | null + extra_metadata?: Record | null + is_active?: boolean +} + +// Data Sources Types +export interface S3ConnectionTest { + bucket_name: string + region?: string + access_key_id: string + secret_access_key: string + endpoint_url?: string | null +} + +export interface S3ConnectionTestResponse { + success: boolean + message: string + bucket_name?: string | null +} + +export interface S3FileInfo { + key: string + filename: string + size: number + last_modified: string +} + +export interface S3FolderInfo { + name: string + path: string +} + +export interface S3ListFilesResponse { + files: S3FileInfo[] + total: number + prefix?: string | null +} + +export interface S3BrowseResponse { + folders: S3FolderInfo[] + files: S3FileInfo[] + current_path: string + organization_id: string +} + +export interface S3Status { + enabled: boolean + provider?: 's3' | 'gcs' | string + error?: string | null +} + +// Alert Types +export enum AlertMetricType { + NUMBER_OF_CALLS = 'number_of_calls', + CALL_DURATION = 'call_duration', + ERROR_RATE = 'error_rate', + SUCCESS_RATE = 'success_rate', + LATENCY = 'latency', + CUSTOM = 'custom', +} + +export enum AlertAggregation { + SUM = 'sum', + AVG = 'avg', + COUNT = 'count', + MIN = 'min', + MAX = 'max', +} + +export enum AlertOperator { + GREATER_THAN = '>', + LESS_THAN = '<', + GREATER_THAN_OR_EQUAL = '>=', + LESS_THAN_OR_EQUAL = '<=', + EQUAL = '=', + NOT_EQUAL = '!=', +} + +export enum AlertNotifyFrequency { + IMMEDIATE = 'immediate', + HOURLY = 'hourly', + DAILY = 'daily', + WEEKLY = 'weekly', +} + +export enum AlertStatus { + ACTIVE = 'active', + PAUSED = 'paused', + DISABLED = 'disabled', +} + +export enum AlertHistoryStatus { + TRIGGERED = 'triggered', + NOTIFIED = 'notified', + ACKNOWLEDGED = 'acknowledged', + RESOLVED = 'resolved', +} + +export interface Alert { + id: string + organization_id: string + name: string + description?: string | null + metric_type: AlertMetricType + aggregation: AlertAggregation + operator: AlertOperator + threshold_value: number + time_window_minutes: number + agent_ids?: string[] | null + notify_frequency: AlertNotifyFrequency + notify_emails?: string[] | null + notify_webhooks?: string[] | null + status: AlertStatus + created_at: string + updated_at: string + created_by?: string | null +} + +export interface AlertCreate { + name: string + description?: string | null + metric_type?: AlertMetricType + aggregation?: AlertAggregation + operator?: AlertOperator + threshold_value: number + time_window_minutes?: number + agent_ids?: string[] | null + notify_frequency?: AlertNotifyFrequency + notify_emails?: string[] + notify_webhooks?: string[] +} + +export interface AlertUpdate { + name?: string + description?: string | null + metric_type?: AlertMetricType + aggregation?: AlertAggregation + operator?: AlertOperator + threshold_value?: number + time_window_minutes?: number + agent_ids?: string[] | null + notify_frequency?: AlertNotifyFrequency + notify_emails?: string[] + notify_webhooks?: string[] + status?: AlertStatus +} + +export interface AlertHistoryItem { + id: string + organization_id: string + alert_id: string + triggered_at: string + triggered_value: number + threshold_value: number + status: AlertHistoryStatus + notified_at?: string | null + notification_details?: Record | null + acknowledged_at?: string | null + acknowledged_by?: string | null + resolved_at?: string | null + resolved_by?: string | null + resolution_notes?: string | null + context_data?: Record | null + created_at: string + updated_at: string + alert?: Alert +} + + +// Cron Job Types +export enum CronJobStatus { + ACTIVE = 'active', + PAUSED = 'paused', + COMPLETED = 'completed', +} + +export interface CronJob { + id: string + organization_id: string + name: string + cron_expression: string + timezone: string + max_runs: number + current_runs: number + evaluator_ids: string[] + status: CronJobStatus + next_run_at?: string | null + last_run_at?: string | null + created_at: string + updated_at: string + created_by?: string | null +} + +export interface CronJobCreate { + name: string + cron_expression: string + timezone: string + max_runs: number + evaluator_ids: string[] +} + +export interface CronJobUpdate { + name?: string + cron_expression?: string + timezone?: string + max_runs?: number + evaluator_ids?: string[] + status?: CronJobStatus +} + +// --- Call Imports --- + +/** + * Lifecycle for a call-import batch. + * + * - ``uploaded`` : file landed in S3, no mapping yet. + * - ``mapped`` : user picked a schema + sheet + column mapping; no + * rows materialised yet, no worker enqueued. + * - ``processing`` : rows materialised + workers enqueued. + * - ``pending`` : transient state used by the legacy one-shot + * ``POST /upload`` endpoint just before transitioning + * to ``processing``. + */ +export type CallImportStatus = + | 'pending' + | 'uploaded' + | 'mapped' + | 'processing' + | 'completed' + | 'partial' + | 'failed' + | 'deleting' + +export type CallImportRowStatus = + | 'pending' + | 'processing' + | 'completed' + | 'failed' + +/** Where the value in `transcript` came from. */ +export type CallImportTranscriptSource = + | 'csv' + | 'transcribed' + | 'edited' + | null +/** Lifecycle status for the post-hoc transcription workflow itself. */ +export type CallImportTranscriptStatus = + | 'idle' + | 'pending' + | 'running' + | 'completed' + | 'failed' + | null + +/** + * Which transcript an evaluation run scored against. + * - `production`: the CSV-supplied value on `CallImportRow.transcript`. + * - `diarised`: the worker-produced value on `CallImportRow.diarised_transcript`. + */ +export type CallImportEvaluationTranscriptSource = 'production' | 'diarised' + +/** + * One contiguous turn inside ``CallImportRow.diarised_segments``. + * + * The diarisation worker rewrites each pyannote ``Speaker N`` label + * into ``agent`` / ``user`` (first speaker = agent heuristic). Anything + * beyond two distinct speakers keeps a generic ``speaker_N`` label so + * multi-party recordings still render every voice. + */ +export interface CallImportDiarisedSegment { + speaker: string + text: string + start: number + end: number + /** Original pyannote label (``Speaker 1`` / ``Speaker 2`` / ...). */ + raw_speaker: string +} + +export interface CallImportRow { + id: string + row_index: number + /** Mandatory identifier per row. Renamed from ``external_call_id``. */ + conversation_id: string + recording_url: string | null + recording_date: string | null + /** Production transcript — the value supplied via the CSV upload. */ + transcript: string | null + /** Provenance of the stored production transcript (csv = CSV upload, edited = manual edit). */ + transcript_source: CallImportTranscriptSource + /** Legacy: provider recorded by the original transcription worker before the split. */ + transcript_provider: string | null + transcript_model: string | null + transcript_status: CallImportTranscriptStatus + transcript_error: string | null + transcribed_at: string | null + /** Diarised transcript — produced by the post-hoc diarisation worker. */ + diarised_transcript: string | null + /** Provider used by the diarisation worker (e.g. "deepgram"). */ + diarised_transcript_provider: string | null + diarised_transcript_model: string | null + diarised_transcript_status: CallImportTranscriptStatus + diarised_transcript_error: string | null + diarised_at: string | null + /** + * Structured speaker turns produced by the diarisation worker. Each + * entry is a single contiguous turn shaped as + * `{ speaker: 'agent' | 'user' | 'speaker_N', text, start, end, + * raw_speaker }`. ``diarised_transcript`` is a rendered + * `: ` view of this list with + * ``diarised_speaker_swap`` applied. ``null`` on legacy rows that + * were diarised before structured turns were persisted (or when the + * STT provider didn't surface segments). + */ + diarised_segments: CallImportDiarisedSegment[] | null + /** + * When ``true`` the ``agent`` <-> ``user`` mapping inside + * ``diarised_segments`` is inverted in the rendered transcript / + * CSV export. The worker writes the canonical mapping using a + * "first speaker is the agent" heuristic; reviewers can flip the + * toggle from the row detail panel without re-running diarisation. + */ + diarised_speaker_swap: boolean + /** + * LLM that turned the STT plain-text output into structured + * ``diarised_segments``. NULL on legacy rows (pre-LLM-diariser). + */ + diarised_llm_provider: string | null + diarised_llm_model: string | null + /** + * Exact prompt the LLM diariser ran with. Useful for the modal to + * pre-fill its textarea when the operator wants to iterate on a + * previously-diarised row. + */ + diarised_prompt: string | null + /** + * Diarisation pipeline that produced this row's turns. + * - `stt_llm` (default) — two-stage STT then LLM diariser. + * - `llm_only` — single-stage multimodal LLM (audio in). + * Read-only; written by the worker on each diarisation. + */ + transcribe_mode?: 'stt_llm' | 'llm_only' + /** + * Per-row preservation of the mapped source cells. Values land here + * as whatever type the schema parameter coerced them to — + * strings (text / url / conversation_id / recording_url / + * recording_date / transcript / datetime), numbers, booleans, or + * ``null`` for blanks. Always + * coerce with ``String(value)`` before string operations. + */ + raw_columns: Record | null + status: CallImportRowStatus + recording_s3_key: string | null + recording_content_type: string | null + recording_size_bytes: number | null + error_message: string | null + attempts: number + created_at: string + updated_at: string +} + +export interface CallImportTag { + id: string + name: string + color: string | null + created_at: string + updated_at: string +} + +/** + * Parameter type tag on a Call Import schema parameter. + * + * - ``conversation_id``: mandatory identifier (one per schema). + * - ``recording_url``: feeds ``CallImportRow.recording_url``. + * - ``recording_date``: date-only call recording date used for reports. + * - ``transcript``: feeds ``CallImportRow.transcript``. + * - ``text`` / ``number`` / ``boolean`` / ``datetime`` / ``url``: + * generic typed fields preserved per row in ``raw_columns`` and + * surfaced in the evaluation export under the parameter's name. + */ +export type CallImportSchemaParameterType = + | 'conversation_id' + | 'recording_url' + | 'recording_date' + | 'transcript' + | 'text' + | 'number' + | 'boolean' + | 'datetime' + | 'url' + +export interface CallImportSchemaParameter { + id?: string + name: string + type: CallImportSchemaParameterType + description: string | null + is_required: boolean + ordering?: number +} + +export interface CallImportSchema { + id: string + organization_id: string + workspace_id: string + name: string + description: string | null + parameters: CallImportSchemaParameter[] + /** How many CallImport batches reference this schema. */ + usage_count: number + created_at: string + updated_at: string +} + +export interface CallImportSchemaListResponse { + items: CallImportSchema[] + total: number +} + +export interface CallImportSchemaCreate { + name: string + description?: string | null + parameters: Array> +} + +export interface CallImportSchemaUpdate { + name?: string + description?: string | null + parameters?: Array> +} + +/** + * In-org Workspace - the active workspace scopes call imports and + * metrics in the UI. The org's Default workspace is auto-seeded by + * migration 033 and cannot be deleted. + */ +export interface Workspace { + id: string + organization_id: string + name: string + slug: string + is_default: boolean + created_at: string + updated_at: string + role_id?: string | null + role_name?: string | null + capabilities?: string[] +} + +export interface WorkspaceRole { + id: string + organization_id: string + name: string + description?: string | null + capabilities: string[] + is_system: boolean + created_at: string + updated_at: string +} + +export interface WorkspaceMember { + id: string + workspace_id: string + user_id: string + role_id: string + role_name: string + user_email: string + user_name?: string | null + added_by_user_id?: string | null + created_at: string +} + +export interface CapabilityInfo { + key: string + label: string +} + +export interface CapabilityDomain { + key: string + label: string + capabilities: CapabilityInfo[] +} + +export interface WorkspaceRoleCreate { + name: string + description?: string | null + capabilities: string[] +} + +export interface WorkspaceRoleUpdate { + name?: string + description?: string | null + capabilities?: string[] +} + +export interface CallImportSourceRowSkip { + source_row: number + reason: string + message: string +} + +export interface CallImport { + id: string + organization_id: string + /** Workspace this import belongs to. */ + workspace_id: string + /** + * Telephony provider key. ``null`` until the IMPORT stage in the + * staged flow (which is the first step that knows the provider). + * Always populated on post-import batches and on legacy one-shot + * uploads. + */ + provider: string | null + telephony_integration_id: string | null + original_filename: string | null + /** + * For Excel uploads, which worksheet this batch came from. ``null`` + * for CSV uploads (CSV files have no sheet concept) and for any + * imports created before multi-sheet support landed. + */ + sheet_name: string | null + /** Optional free-text dataset label (high-level segregation filter). */ + dataset: string | null + /** Tags currently attached to this import. Empty array if untagged. */ + tags: CallImportTag[] + /** + * Reusable Input Parameter schema the batch was uploaded against. + * NULL on legacy batches uploaded before the schema-driven flow shipped. + */ + schema_id: string | null + /** + * Schema-driven mapping: ``{parameter_name: csv_header}``. Empty on + * legacy batches; check ``column_mapping`` / ``extra_columns`` / + * ``custom_column_mapping`` instead for those. + */ + parameter_mapping: Record + /** Legacy free-form mapping kept for batches uploaded before schemas. */ + column_mapping: Record + /** Legacy extra-column list kept for backwards-compat. */ + extra_columns: string[] + /** Legacy uploader-named columns kept for backwards-compat. */ + custom_column_mapping: Record + /** + * Source headers the uploader explicitly skipped, captured at the + * MAP stage. Empty for legacy one-shot uploads where the value was + * ephemeral. + */ + skipped_columns: string[] + /** + * Source rows skipped at parse time (missing/invalid conversation ID or URL). + */ + source_row_skips?: CallImportSourceRowSkip[] + /** S3 key for the staged source file. ``null`` on legacy batches. */ + source_s3_key: string | null + /** ``'csv'`` / ``'xlsx'`` for staged files, or ``'audio'`` for manual uploads. */ + source_format: string | null + source_size_bytes: number | null + source_content_type: string | null + /** + * Snapshot of the file's sheets + headers captured at UPLOAD time so + * the MAP UI can render without re-fetching the source from S3. + * ``null`` on legacy batches. + */ + available_sheets: CallImportPreviewSheet[] | null + total_rows: number + completed_rows: number + failed_rows: number + status: CallImportStatus + error_message: string | null + created_at: string + updated_at: string + created_by_email?: string | null + last_updated_by_email?: string | null +} + +export interface CallImportDetail extends CallImport { + rows: CallImportRow[] + /** + * Total row count *after* applying the optional ``q`` search filter. + * ``null`` when no filter is active — paginate against ``total_rows`` + * in that case. + */ + filtered_total_rows: number | null + /** + * Batch-wide aggregates of ``CallImportRow.diarised_transcript_status``. + * The ``idle`` bucket (rows never touched by the transcribe/diarise + * worker) is implicit: ``total_rows - (pending + running + completed + * + failed)``. Lets the UI render a transcribe-and-diarise progress + * bar without paginating through every row. + */ + diarised_pending_rows: number + diarised_running_rows: number + diarised_completed_rows: number + diarised_failed_rows: number +} + +export interface CallImportListResponse { + items: CallImport[] + total: number + page: number + page_size: number +} + +export interface CallImportUploadResponse { + id: string + total_rows: number + status: CallImportStatus + dataset: string | null + tags: CallImportTag[] + message: string +} + +/** One worksheet (or one CSV file synthesized as a single sheet). */ +export interface CallImportPreviewSheet { + /** Sheet name for xlsx; filename for csv. */ + name: string + /** Column headers from the first non-empty row. */ + headers: string[] + /** Approximate count of data rows (excludes the header row). */ + row_count: number +} + +/** + * Sheets / headers extracted server-side from an uploaded CSV or Excel + * workbook. Drives the modal's column-mapping UI without forcing the + * frontend to parse the file itself. + */ +export interface CallImportPreviewResponse { + /** ``'csv'`` or ``'xlsx'``. */ + format: 'csv' | 'xlsx' + sheets: CallImportPreviewSheet[] +} + +export type MetricSelectionMode = 'single_choice' | 'multi_label' + +export interface CallImportMetricSummary { + id: string + name: string + metric_type: string | null + description: string | null + parent_metric_id?: string | null + selection_mode?: MetricSelectionMode | null + /** Only meaningful on multi_label parents; gates the Discovered + * Labels panel on the Flow tab. Defaults to false. */ + allow_discovery?: boolean +} + +/** Per-metric LLM override (provider+model+optional credential + generation params). */ +export interface CallImportEvaluationLLMOverride { + provider?: string | null + model?: string | null + credential_id?: string | null + llm_config?: LLMGenerationConfig | null +} + +export interface CallImportEvaluation { + id: string + call_import_id: string + organization_id: string + /** User-supplied label for the run; null when not named. */ + name: string | null + selected_metric_ids: string[] + /** parent_id -> [child_id, ...] snapshot captured at run time. */ + selected_metric_groups?: Record | null + metrics: CallImportMetricSummary[] + status: 'pending' | 'running' | 'completed' | 'partial' | 'failed' + total_rows: number + completed_rows: number + failed_rows: number + error_message: string | null + /** Run-level LLM provider chosen by the user (null = legacy default). */ + llm_provider: string | null + llm_model: string | null + llm_credential_id: string | null + llm_config?: LLMGenerationConfig | null + metric_llm_overrides: Record | null + stt_provider: string | null + stt_model: string | null + stt_credential_id: string | null + /** + * Run-level LLM diariser config used when the worker auto-diarises + * rows that are missing a diarised transcript. + */ + diarisation_llm_provider?: string | null + diarisation_llm_model?: string | null + diarisation_llm_credential_id?: string | null + diarisation_prompt?: string | null + /** + * Diarisation pipeline shape this run was created with. + * - `stt_llm` (default) — STT then an LLM diariser over the text. + * - `llm_only` — audio fed directly to a multimodal diariser LLM. + * Surfaced so the retry / re-run UI can preselect the right mode. + */ + transcribe_mode?: 'stt_llm' | 'llm_only' + /** + * Which transcript column this run scored against. + * Defaults to `production` on legacy runs. + */ + transcript_source: CallImportEvaluationTranscriptSource + /** + * Other evaluation ids created in the same Run Evaluation request. + * Populated only on the POST response when the user ticked both + * Production and Diarised. Empty array on all other reads. + */ + sibling_evaluation_ids: string[] + started_at: string | null + finished_at: string | null + created_at: string + updated_at: string + created_by_email?: string | null + last_updated_by_email?: string | null + /** + * Cached LLM-generated TLDR rendered above the Visualizations tab. + * Populated lazily via ``POST /evaluations/{id}/insights``; null on + * runs the user has not summarised yet. + */ + tldr_summary?: EvaluationTldrSummary | null + user_insights?: EvaluationUserInsightsState | null + metric_clusters?: EvaluationMetricClustersState | null + /** + * True when the user opted into top-level metric discovery on the + * Run Evaluation modal. Gates the Discovered metrics panel on the + * evaluation detail Flow tab. + */ + discover_new_metrics?: boolean + /** + * Set while a bulk background operation (abort, force-fail, retry) is + * still running. The UI disables other mutating actions until cleared. + */ + bulk_operation?: 'abort' | 'force_fail_pending' | 'retry' | null +} + +/** + * LLM-generated narrative + bullet patterns for a single evaluation + * run. Cached on the evaluation row so re-opening the Visualizations + * tab doesn't auto-burn LLM tokens. ``is_stale`` is computed by the + * backend at read time when ``completed_rows`` has grown since the + * summary was generated. + */ +export interface EvaluationTldrSummary { + narrative: string + patterns: string[] + metric_insights?: Record + generated_at: string + generated_at_completed_rows: number + provider?: string | null + model?: string | null + is_stale: boolean +} + +export interface UserInsightCategory { + label: string + count: number + share_pct: number +} + +export interface UserInsightEvidenceTurn { + speaker: string + text: string +} + +export interface UserInsightEvidence { + conversation_id?: string | null + quote: string + turns?: UserInsightEvidenceTurn[] +} + +export interface EvaluationUserInsightItem { + id: string + title: string + categories: UserInsightCategory[] + observation: string + evidence: UserInsightEvidence +} + +export interface EvaluationUserInsightsState { + status: 'idle' | 'running' | 'completed' | 'failed' + insights: EvaluationUserInsightItem[] + overview?: string | null + generated_at?: string | null + generated_at_completed_rows: number + progress?: { completed_llm_calls: number; total_llm_calls: number } | null + provider?: string | null + model?: string | null + llm_calls_used: number + max_llm_calls?: number | null + error_message?: string | null + is_stale: boolean +} + +export type MetricClusterGapLabel = + | 'LOGIC_GAP' + | 'UNDERSPEC' + | 'EXISTS_NO_TRIGGER' + | 'MISSING' + +export interface MetricSubCluster { + label: string + count: number + share_pct: number +} + +export interface MetricClusterEvidenceTurn { + speaker: string + text: string +} + +export interface MetricClusterEvidence { + conversation_id?: string | null + evaluation_row_id?: string | null + quote: string + turns?: MetricClusterEvidenceTurn[] +} + +export interface MetricCluster { + id: string + label: string + gap_label: MetricClusterGapLabel + level: number + count: number + share_pct: number + sub_clusters: MetricSubCluster[] + observation: string + failure_reason?: string + evidence: MetricClusterEvidence + is_discovered: boolean +} + +export interface MetricClusterGroup { + metric_id: string + metric_name: string + flagged_count: number + failure_reason?: string + clusters: MetricCluster[] +} + +export interface DiscoveredProblemCluster { + id: string + label: string + gap_label: MetricClusterGapLabel + count: number + share_pct: number + observation: string + failure_reason?: string + evidence: MetricClusterEvidence +} + +export interface RcaRepeatedPatternRow { + metric_id: string + metric_name: string + top_rca_patterns: string + evidence_share_pct: number + evidence_calls: number + evidence_cluster_count?: number + failure_reason: string +} + +export interface RcaMetricHotspotRow { + metric_id: string + metric_name: string + description: string + metric_rate_pct: number + flagged_calls: number +} + +export interface RcaPromptAreaRow { + label: string + share_pct: number + gap_label: MetricClusterGapLabel +} + +export interface MetricClustersRcaSummary { + total_clusters: number + total_clustered_instances: number + total_flagged_instances?: number + analysed_calls: number + repeated_patterns: RcaRepeatedPatternRow[] + metric_hotspots: RcaMetricHotspotRow[] + prompt_areas: RcaPromptAreaRow[] +} + +export interface MetricFailurePolicy { + metric_id: string + failure_values: string[] + failure_child_names?: string[] + numeric_rule?: { op: 'lt' | 'lte' | 'gt' | 'gte'; threshold: number } | null +} + +export interface MetricFailurePolicyValueCount { + label: string + count: number +} + +export interface MetricFailurePolicyMetricPreview { + metric_id: string + metric_name: string + metric_type?: string | null + selection_mode?: string | null + is_multi_label_parent: boolean + value_counts: MetricFailurePolicyValueCount[] + child_names: string[] + row_count_by_value: Record + suggested_policy: MetricFailurePolicy + effective_policy: MetricFailurePolicy +} + +export interface MetricFailurePoliciesResponse { + previews: MetricFailurePolicyMetricPreview[] + policies: Record + source: 'inferred' | 'user' + updated_at?: string | null +} + +export interface MetricClusterEligibleRow { + evaluation_row_id: string + conversation_id?: string | null + row_index?: number | null + flagged_metric_names: string[] +} + +export interface MetricClusterEligibleRowsResponse { + items: MetricClusterEligibleRow[] + total: number +} + +export interface EvaluationMetricClustersState { + status: 'idle' | 'running' | 'completed' | 'failed' | 'cancelled' + groups: MetricClusterGroup[] + discovered_problems: DiscoveredProblemCluster[] + overview?: string | null + generated_at?: string | null + generated_at_completed_rows: number + progress?: { completed_llm_calls: number; total_llm_calls: number } | null + provider?: string | null + model?: string | null + llm_calls_used: number + max_llm_calls?: number | null + error_message?: string | null + is_stale: boolean + selected_evaluation_row_ids?: string[] + failure_policies?: Record + failure_policies_source?: 'inferred' | 'user' + failure_policies_updated_at?: string | null + rca_summary?: MetricClustersRcaSummary | null +} + +export interface AgentFlowNode { + id: string + label: string + node_type: 'start' | 'decision' | 'action' | 'terminal' + position_x?: number | null + position_y?: number | null + prompt_excerpt?: string | null + start_offset?: number | null + end_offset?: number | null +} + +export interface AgentFlowEdge { + source: string + target: string + condition?: string | null +} + +export interface AgentFlowGraph { + nodes: AgentFlowNode[] + edges: AgentFlowEdge[] + generated_at?: string | null + provider?: string | null + model?: string | null + layout_saved_at?: string | null + prompt_content_hash?: string | null + mapping_error?: string | null + generation_error?: string | null +} + +export interface ImportedAgent { + id: string + organization_id: string + name: string + description: string | null + content: string + tags: string[] | null + current_version: number + agent_flowchart?: AgentFlowGraph | null + agent_flowchart_status?: string | null + created_at: string + updated_at: string + created_by: string | null +} + +export interface ImportedAgentDetail extends ImportedAgent { + versions: PromptPartialVersion[] +} + +export interface MetricPartialChild { + name: string + description: string + example: string +} + +export interface MetricPartialContent { + schema_version: 1 + metric_kind: 'single' | 'category' + description: string + children?: MetricPartialChild[] +} + +export interface MetricPartial { + id: string + organization_id: string + name: string + description: string | null + content: string + tags: string[] | null + current_version: number + created_at: string + updated_at: string + created_by: string | null +} + +export interface MetricPartialDetail extends MetricPartial { + versions: PromptPartialVersion[] +} + +export interface PromptPartialVersion { + id: string + prompt_partial_id: string + version: number + content: string + change_summary: string | null + created_at: string + created_by: string | null +} + +export interface PromptImprovementSuggestion { + id: string + metric_id: string + metric_name: string + cluster_id: string + cluster_label: string + gap_label: MetricClusterGapLabel + share_pct: number + priority: 'high' | 'medium' | 'low' + change_type?: 'edit' | 'add' + target_section: string + anchor_excerpt?: string + current_gap: string + suggested_text: string + rationale: string + flow_node_id?: string + flow_node_label?: string +} + +export interface EvaluationPromptImprovementsState { + status: 'idle' | 'running' | 'completed' | 'failed' + imported_agent_id?: string | null + imported_agent_name?: string | null + suggestions: PromptImprovementSuggestion[] + overview?: string | null + generated_at?: string | null + generated_at_completed_rows: number + provider?: string | null + model?: string | null + error_message?: string | null + is_stale: boolean +} + +export interface MetricPeriodDelta { + label: string + detail: string + why?: string | null +} + +export interface CallImportEvaluationListResponse { + items: CallImportEvaluation[] + total: number +} + +export interface CallImportEvaluationBaselineCandidate { + evaluation_id: string + name: string + dataset: string + period_label: string | null + period_start: string | null + period_end: string | null + period_display: string + completed_rows: number + created_at: string + is_default: boolean +} + +export interface CallImportEvaluationBaselineCandidatesResponse { + items: CallImportEvaluationBaselineCandidate[] + default_evaluation_id: string | null +} + +export interface CallImportEvaluationRow { + id: string + evaluation_id: string + call_import_row_id: string + row_index: number | null + /** Mandatory identifier from the source batch (renamed from ``external_call_id``). */ + conversation_id: string | null + transcript: string | null + raw_columns: Record | null + recording_url: string | null + recording_date: string | null + /** + * S3 object key for the downloaded recording. Prefer this over + * ``recording_url`` for playback — we resolve it to a presigned URL + * so audio plays from our storage instead of the (often expired) + * provider URL. + */ + recording_s3_key: string | null + diarised_transcript_status?: string | null + diarised_transcript_error?: string | null + status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' + metric_scores: Record + error_message: string | null + started_at: string | null + finished_at: string | null + created_at: string + updated_at: string +} + +export interface CallImportEvaluationRowListResponse { + items: CallImportEvaluationRow[] + total: number + page: number + page_size: number +} + +// --- Retry (re-enqueue failed rows on an existing evaluation run) --- + +export interface CallImportEvaluationRetryRequest { + /** + * Restrict the retry to a specific subset of evaluation rows. + * When omitted, every row with status='failed' in this run is + * re-enqueued. + */ + eval_row_ids?: string[] + + /** + * Optional LLM overrides. When provided, persisted onto the run so + * future retries default to the new config. ``llm_provider`` and + * ``llm_model`` must be sent together — the backend 400s on + * half-configured input. + */ + llm_provider?: string + llm_model?: string + llm_credential_id?: string | null + + /** + * Optional STT overrides (only meaningful when the run scores the + * diarised transcript). Same paired-field rule as LLM. + */ + stt_provider?: string + stt_model?: string + stt_credential_id?: string | null + + /** + * When true, wipe the diarised transcript on every retried row so + * the (possibly new) STT runs from scratch. Only takes effect for + * diarised runs that have STT config. + */ + transcribe_overwrite?: boolean +} + +export interface CallImportEvaluationRetrySkippedItem { + eval_row_id: string + /** + * Why this row was not re-enqueued. Known values: + * - 'unknown' (id not in this run) + * - 'in_progress' (status is pending/running) + * - 'completed' (already successful) + * - 'source_row_missing' + */ + reason: 'unknown' | 'in_progress' | 'completed' | 'source_row_missing' +} + +export interface CallImportEvaluationRetryResponse { + requeued: number + /** + * Of those, how many were chained through a diarisation task first + * because the diarised transcript was missing. + */ + transcribe_requeued: number + skipped: CallImportEvaluationRetrySkippedItem[] +} + +export interface CallImportEvaluationBulkActionResponse { + accepted: boolean + target_count: number + evaluation_id: string +} + +// --- Diarization / transcription --- + +export interface CallImportTranscribeRequest { + /** + * Diarisation pipeline shape. + * - `stt_llm` (default) — STT produces plain text, then an LLM + * diariser splits it into agent/user turns. STT fields required. + * - `llm_only` — skip STT entirely and feed the audio bytes + * directly to a multimodal `diarization_llm_*` model along with + * `diarization_prompt`. STT fields MUST be omitted in this mode. + */ + mode?: 'stt_llm' | 'llm_only' + /** Required when `mode === 'stt_llm'`; must be null/omitted in `llm_only`. */ + stt_provider?: string | null + /** Required when `mode === 'stt_llm'`; must be null/omitted in `llm_only`. */ + stt_model?: string | null + credential_id?: string | null + language?: string | null + only_missing?: boolean + overwrite_existing?: boolean + row_ids?: string[] + /** + * LLM diariser. In `stt_llm` mode it splits the STT plain-text into + * agent/user turns; in `llm_only` mode it directly receives the + * audio along with `diarization_prompt`. Always required. + */ + diarization_llm_provider: string + diarization_llm_model: string + diarization_llm_credential_id?: string | null + /** + * Operator-supplied system prompt for the diariser LLM. NULL/empty + * means "fall back to the canonical default" (see + * ``getDiarisationDefaultPrompt``). + */ + diarization_prompt?: string | null +} + +export interface CallImportTranscribeResponse { + queued: number + skipped_rows: number + skipped_reason_counts: Record + accepted?: boolean +} + +export interface CallImportRowBulkDeleteResponse { + deleted: number + status?: 'completed' | 'accepted' +} + +export interface CallImportRetryFailedRowsResponse { + requeued: number + enqueue_failed: number + skipped: number +} + +export interface CallImportDiarisationPromptDefaultResponse { + prompt: string +} + +// --- Aggregation / visualization payloads --- + +export interface CallImportMetricHistogramBucket { + x0: number + x1: number + count: number +} + +export interface CallImportMetricValueCount { + label: string + count: number +} + +/** + * One unordered pair-count cell from a multi-label parent's + * co-occurrence matrix. ``a`` and ``b`` are child label names with + * ``a < b`` lexicographically; ``count`` is the number of rows on + * which both labels fired together. + */ +export interface CallImportMetricLabelPair { + a: string + b: string + count: number +} + +export interface CallImportMetricAggregate { + metric_id: string + metric_name: string + metric_type: string | null + metric_category?: 'quality' | 'user_insight' | string + /** + * True when the metric is a multi-label classifier parent. + * ``value_counts`` then lists per-child label tallies and one row + * may contribute to several labels, so the chart layout has to + * ignore the pie toggle (slices wouldn't sum to 100%) and the + * n-badge represents rows scored, not label occurrences. + */ + is_multi_label_parent?: boolean + count: number + skipped_count: number + error_count: number + mean: number | null + median: number | null + p25: number | null + p75: number | null + p95: number | null + min: number | null + max: number | null + stddev: number | null + histogram_buckets: CallImportMetricHistogramBucket[] + value_counts: CallImportMetricValueCount[] + /** + * Pairwise label intersections for multi-label parent metrics. + * Empty for everything else. The Visualizations tab reconstructs + * a square symmetric matrix from these unordered pairs to render + * the co-occurrence heatmap chart type. + */ + co_occurrence?: CallImportMetricLabelPair[] +} + +export interface CallImportEvaluationAggregateResponse { + evaluation_id: string + total_rows: number + completed_rows: number + failed_rows: number + metrics: CallImportMetricAggregate[] + period_deltas?: Record + baseline_evaluation_id?: string | null + failure_policies_source?: 'inferred' | 'user' | null +} + +export interface EvaluatorResultsAggregateResponse { + scope: string + suite_id?: string | null + agent_id?: string | null + scenario_id?: string | null + total_rows: number + completed_rows: number + failed_rows: number + metrics: CallImportMetricAggregate[] +} + +export interface CallImportInsightsRunPoint { + evaluation_id: string + name: string | null + created_at: string + mean: number | null + completed_rows: number +} + +export interface CallImportInsightsMetric { + metric_id: string + metric_name: string + metric_type: string | null + latest: CallImportMetricAggregate | null + trend: CallImportInsightsRunPoint[] +} + +export interface CallImportInsightsResponse { + call_import_id: string + total_rows: number + rows_with_transcript: number + rows_without_transcript: number + transcript_source_counts: Record + evaluation_count: number + metrics: CallImportInsightsMetric[] +} + +// --- Metrics hierarchy + flow visualization --- + +export interface MetricSummary { + id: string + organization_id: string + name: string + description: string | null + metric_type: string + metric_category?: 'quality' | 'user_insight' | string + trigger: string + enabled: boolean + is_default: boolean + metric_origin: string + supported_surfaces: string[] + enabled_surfaces: string[] + custom_data_type: string | null + custom_config: Record | null + tags: string[] | null + capture_rationale: boolean + parent_metric_id: string | null + selection_mode: MetricSelectionMode | null + allow_discovery?: boolean + /** + * When true, this metric is a "transcript-compare judge": at + * call-import evaluation time the worker feeds BOTH the production + * transcript and the diarised transcript to the LLM as a labeled + * pair, and the run's transcript_source toggle is ignored for this + * metric. Mutually exclusive with parent_metric_id and selection_mode + * — comparison metrics stay standalone. + */ + compare_transcripts?: boolean + children?: MetricSummary[] + created_at: string + updated_at: string + created_by: string | null +} + +export interface MetricChildDraft { + name: string + description?: string | null + enabled?: boolean + capture_rationale?: boolean | null + tags?: string[] | null +} + +export interface MetricCreateWithChildrenPayload { + name: string + description?: string | null + selection_mode: MetricSelectionMode + enabled?: boolean + supported_surfaces?: string[] + enabled_surfaces?: string[] + tags?: string[] | null + allow_discovery?: boolean + children: MetricChildDraft[] +} + +export interface MetricFlowNode { + id: string + label: string + count: number + is_terminal: boolean + is_discovered?: boolean +} + +export interface MetricFlowEdge { + source: string + target: string + count: number +} + +export interface MetricFlowResponse { + parent_metric_id: string + parent_metric_name: string + selection_mode: MetricSelectionMode | null + nodes: MetricFlowNode[] + edges: MetricFlowEdge[] + total_rows: number + rows_with_sequence: number +} + +export interface DiscoveredLabel { + key: string + name: string + description?: string | null + sample_rationale?: string | null + /** + * Up to 3 distinct LLM rationales captured for this candidate + * across rows. The Discovered Labels promote flow surfaces the + * first 2 as an ``Examples:`` block on the new sub-metric's + * rubric so the user starts with concrete cases in the prompt. + */ + examples?: string[] + count: number +} + +export interface DiscoveredLabelsResponse { + parent_metric_id: string + items: DiscoveredLabel[] +} + +/** + * One LLM-discovered candidate TOP-LEVEL metric aggregated across all + * rows of an evaluation. Mirrors :class:`DiscoveredLabel` but adds a + * ``suggested_type`` field — the LLM's guess at the best shape for + * the new metric — that the promote modal can pre-fill the type radio + * with. + */ +export interface DiscoveredMetric { + key: string + name: string + description?: string | null + suggested_type: 'boolean' | 'rating' | 'category' + sample_rationale?: string | null + examples?: string[] + count: number +} + +export interface DiscoveredMetricsResponse { + evaluation_id: string + items: DiscoveredMetric[] +} + +export interface ObservabilityCallAgent { + id: string + agent_id?: string | null + name: string +} + +export interface ObservabilityCallData { + startedAt?: string + started_at?: string + endedAt?: string + ended_at?: string + from_phone_number?: string + to_phone_number?: string + endedReason?: string + recording_s3_key?: string + recording_url?: string + duration_seconds?: number + agent_name?: string + _agent_ref?: string | number + direction?: string + messages?: Array<{ role: string; content: string; start_time?: number; end_time?: number }> + live_transcript?: Array<{ role: string; content: string; timestamp?: string; start_time?: number }> + metadata?: Record + call_short_id?: string +} + +export interface ObservabilityCall { + id: string + call_short_id: string + status?: string | null + call_event?: string | null + is_live?: boolean + direction?: string | null + source?: string | null + provider_platform?: string | null + provider_call_id?: string | null + agent_id?: string | null + agent?: ObservabilityCallAgent | null + created_at?: string | null + updated_at?: string | null + call_data?: ObservabilityCallData | null + live_transcript?: Array<{ role: string; content: string; timestamp?: string }> +} diff --git a/tests/test_api/test_call_import_audit.py b/tests/test_api/test_call_import_audit.py new file mode 100644 index 00000000..60592ff0 --- /dev/null +++ b/tests/test_api/test_call_import_audit.py @@ -0,0 +1,78 @@ +"""Audit fields (created_by / last_updated_by email) on call imports.""" + +from uuid import uuid4 + +from app.models.database import CallImport, Workspace +from app.models.enums import CallImportStatus + + +def _ensure_default_workspace(db_session, org_id): + ws = ( + db_session.query(Workspace) + .filter(Workspace.organization_id == org_id, Workspace.is_default.is_(True)) + .first() + ) + if ws is None: + ws = Workspace( + organization_id=org_id, name="Default", slug="default", is_default=True + ) + db_session.add(ws) + db_session.commit() + return ws + + +def test_update_call_import_metadata_stamps_actor_emails( + authenticated_client, db_session, org_id, seed_org +): + workspace = _ensure_default_workspace(db_session, org_id) + call_import = CallImport( + id=uuid4(), + organization_id=org_id, + workspace_id=workspace.id, + provider="exotel", + original_filename="batch.csv", + total_rows=0, + completed_rows=0, + failed_rows=0, + status=CallImportStatus.COMPLETED, + dataset="before", + ) + db_session.add(call_import) + db_session.commit() + + response = authenticated_client.patch( + f"/api/v1/call-imports/{call_import.id}", + json={"dataset": "after"}, + ) + assert response.status_code == 200, response.text + body = response.json() + assert body["dataset"] == "after" + assert body["created_by_email"] is None + assert body["last_updated_by_email"] == "owner@example.com" + + +def test_list_call_imports_includes_actor_emails( + authenticated_client, db_session, org_id, seed_org +): + workspace = _ensure_default_workspace(db_session, org_id) + call_import = CallImport( + id=uuid4(), + organization_id=org_id, + workspace_id=workspace.id, + provider=None, + original_filename="listed.csv", + total_rows=0, + completed_rows=0, + failed_rows=0, + status=CallImportStatus.UPLOADED, + ) + db_session.add(call_import) + db_session.commit() + + listing = authenticated_client.get("/api/v1/call-imports") + assert listing.status_code == 200, listing.text + items = listing.json()["items"] + match = [item for item in items if item["id"] == str(call_import.id)] + assert len(match) == 1 + assert match[0]["created_by_email"] is None + assert match[0]["last_updated_by_email"] is None diff --git a/tests/test_api/test_call_import_evaluations.py b/tests/test_api/test_call_import_evaluations.py index faf3ff9d..43d9230b 100644 --- a/tests/test_api/test_call_import_evaluations.py +++ b/tests/test_api/test_call_import_evaluations.py @@ -1152,3 +1152,43 @@ def test_evaluation_retry_can_override_telephony_credentials( db_session.refresh(call_import) assert call_import.telephony_integration_id == right_integration.id assert call_import.provider == "exotel" + + +def test_create_evaluation_sets_actor_emails( + authenticated_client, db_session, org_id, seed_org +): + metric = _make_metric(db_session, org_id) + call_import, _rows = _make_call_import(db_session, org_id, rows=2) + + response = authenticated_client.post( + f"/api/v1/call-imports/{call_import.id}/evaluations", + json=_eval_body([metric.id]), + ) + assert response.status_code == 202, response.text + body = response.json() + assert body["created_by_email"] == "owner@example.com" + assert body["last_updated_by_email"] == "owner@example.com" + + +def test_update_evaluation_name_stamps_last_updated_by_email( + authenticated_client, db_session, org_id, seed_org +): + metric = _make_metric(db_session, org_id) + call_import, _rows = _make_call_import(db_session, org_id, rows=1) + + created = authenticated_client.post( + f"/api/v1/call-imports/{call_import.id}/evaluations", + json=_eval_body([metric.id]), + ) + assert created.status_code == 202, created.text + eval_id = created.json()["id"] + + patched = authenticated_client.patch( + f"/api/v1/call-imports/{call_import.id}/evaluations/{eval_id}", + json={"name": "Renamed run"}, + ) + assert patched.status_code == 200, patched.text + body = patched.json() + assert body["name"] == "Renamed run" + assert body["created_by_email"] == "owner@example.com" + assert body["last_updated_by_email"] == "owner@example.com" From 3115b13f8653173ae810970f52410efcfe583ac3 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 7 Aug 2026 19:46:46 +0530 Subject: [PATCH 03/55] feat: implement actor stamping for call import evaluations and user insights --- app/api/v1/routes/call_import_evaluations.py | 16 +++++++ ...st_call_import_evaluation_user_insights.py | 26 +++++++++++ ...st_call_import_evaluations_mapped_async.py | 45 +++++++++++++++++++ .../test_call_import_metric_clusters_rows.py | 40 +++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/app/api/v1/routes/call_import_evaluations.py b/app/api/v1/routes/call_import_evaluations.py index 95222d6d..12069420 100644 --- a/app/api/v1/routes/call_import_evaluations.py +++ b/app/api/v1/routes/call_import_evaluations.py @@ -35,6 +35,7 @@ from app.services.call_imports.audit import ( actor_emails_for_evaluation, emails_for_user_ids, + stamp_call_import_actor, stamp_evaluation_actor, user_ids_from_evaluations, ) @@ -1032,6 +1033,7 @@ async def create_call_import_evaluation( call_import.failed_rows = 0 call_import.error_message = None call_import.status = CallImportStatus.PROCESSING + stamp_call_import_actor(call_import, principal) db.commit() db.refresh(call_import) starting_from_mapped = True @@ -5139,6 +5141,9 @@ async def generate_call_import_evaluation_insights( summary = EvaluationTldrSummary.model_validate(task_result) db.refresh(evaluation) + stamp_evaluation_actor(evaluation, principal) + db.commit() + db.refresh(evaluation) from app.services.ai.llm_resolver import get_llm_provider_and_model @@ -5153,6 +5158,7 @@ async def generate_call_import_evaluation_insights( force=body.regenerate, max_llm_calls=body.max_llm_calls, db=db, + principal=principal, ) return summary @@ -5210,6 +5216,7 @@ def _enqueue_user_insights_job( force: bool = False, max_llm_calls: Optional[int] = None, db: Optional[Session] = None, + principal: Optional[Principal] = None, ) -> None: """Enqueue background user-insights generation unless already running.""" current = _user_insights_payload(evaluation) @@ -5241,6 +5248,8 @@ def _enqueue_user_insights_job( "error_message": None, } if db is not None: + if principal is not None: + stamp_evaluation_actor(evaluation, principal) flag_modified(evaluation, "user_insights") db.commit() @@ -5348,6 +5357,7 @@ async def generate_call_import_evaluation_user_insights( force=body.force or body.regenerate, max_llm_calls=body.max_llm_calls, db=db, + principal=principal, ) db.refresh(evaluation) @@ -5590,6 +5600,7 @@ def _enqueue_metric_clusters_job( selected_evaluation_row_ids: Optional[List[str]] = None, failure_policies: Optional[Dict[str, MetricFailurePolicy]] = None, db: Optional[Session] = None, + principal: Optional[Principal] = None, ) -> None: current = _metric_clusters_payload(evaluation) if current is not None and current.status == "running" and not force: @@ -5668,6 +5679,8 @@ def _enqueue_metric_clusters_job( **policy_blob, } if db is not None: + if principal is not None: + stamp_evaluation_actor(evaluation, principal) flag_modified(evaluation, "metric_clusters") db.commit() @@ -5689,6 +5702,8 @@ def _enqueue_metric_clusters_job( if db is not None and isinstance(evaluation.metric_clusters, dict): evaluation.metric_clusters["celery_task_id"] = async_result.id flag_modified(evaluation, "metric_clusters") + if principal is not None: + stamp_evaluation_actor(evaluation, principal) db.commit() @@ -6092,6 +6107,7 @@ async def generate_call_import_evaluation_metric_clusters( selected_evaluation_row_ids=selected_row_ids, failure_policies=merged_policies, db=db, + principal=principal, ) db.refresh(evaluation) diff --git a/tests/test_api/test_call_import_evaluation_user_insights.py b/tests/test_api/test_call_import_evaluation_user_insights.py index 1fd76744..4d9a1336 100644 --- a/tests/test_api/test_call_import_evaluation_user_insights.py +++ b/tests/test_api/test_call_import_evaluation_user_insights.py @@ -191,6 +191,32 @@ def test_post_user_insights_enqueues_task( assert response.json()["max_llm_calls"] == 100 +def test_post_user_insights_stamps_last_updated_by_email( + authenticated_client, + db_session, + org_id, + seed_org, + make_ai_provider, + stub_user_insights_worker, +): + make_ai_provider(provider="openai", is_active=True) + call_import, evaluation = _seed_evaluation(db_session, org_id) + evaluation.last_updated_by_user_id = None + db_session.commit() + + response = authenticated_client.post( + f"/api/v1/call-imports/{call_import.id}/evaluations/{evaluation.id}/user-insights", + json={"regenerate": True, "force": True}, + ) + assert response.status_code == 200, response.text + + detail = authenticated_client.get( + f"/api/v1/call-imports/{call_import.id}/evaluations/{evaluation.id}" + ) + assert detail.status_code == 200, detail.text + assert detail.json()["last_updated_by_email"] == "owner@example.com" + + def test_post_user_insights_requires_completed_rows( authenticated_client, db_session, org_id, seed_org ): diff --git a/tests/test_api/test_call_import_evaluations_mapped_async.py b/tests/test_api/test_call_import_evaluations_mapped_async.py index ac3035f9..f072dd68 100644 --- a/tests/test_api/test_call_import_evaluations_mapped_async.py +++ b/tests/test_api/test_call_import_evaluations_mapped_async.py @@ -117,3 +117,48 @@ def test_create_evaluation_from_mapped_enqueues_async_materialization( .first() ) assert refreshed_import.status == CallImportStatus.PROCESSING + + +def test_create_evaluation_from_mapped_stamps_parent_last_updated_by( + authenticated_client, + db_session, + org_id, + seed_org, + monkeypatch, +): + from tests.test_api.test_call_import_evaluations import ( + _eval_body, + _make_metric, + ) + + monkeypatch.setattr( + "app.api.v1.routes.call_imports._ensure_blob_storage_enabled", + lambda: None, + ) + + metric = _make_metric(db_session, org_id) + workspace = metric.workspace_id + call_import = _make_mapped_call_import(db_session, org_id, workspace) + call_import.last_updated_by_user_id = None + db_session.commit() + + delay_mock = MagicMock(return_value=MagicMock(id="async-task")) + fake_bulk_ops = types.ModuleType("app.workers.tasks.call_import_bulk_ops") + fake_bulk_ops.materialize_mapped_call_import_evaluation_task = MagicMock( + delay=delay_mock, + ) + monkeypatch.setitem( + sys.modules, + "app.workers.tasks.call_import_bulk_ops", + fake_bulk_ops, + ) + + response = authenticated_client.post( + f"/api/v1/call-imports/{call_import.id}/evaluations", + json=_eval_body([metric.id]), + ) + assert response.status_code == 202, response.text + + detail = authenticated_client.get(f"/api/v1/call-imports/{call_import.id}") + assert detail.status_code == 200, detail.text + assert detail.json()["last_updated_by_email"] == "owner@example.com" diff --git a/tests/test_api/test_call_import_metric_clusters_rows.py b/tests/test_api/test_call_import_metric_clusters_rows.py index a1d1bd31..6595b5ab 100644 --- a/tests/test_api/test_call_import_metric_clusters_rows.py +++ b/tests/test_api/test_call_import_metric_clusters_rows.py @@ -147,6 +147,46 @@ def fake_apply_async(*, kwargs=None, **_kw): assert len(captured["evaluation_row_ids"]) == 2 +def test_generate_metric_clusters_stamps_last_updated_by_email( + authenticated_client, + db_session, + org_id, + seed_org, + make_ai_provider, + monkeypatch, +): + make_ai_provider(provider="openai", is_active=True) + call_import, evaluation, _ = _seed_eval_with_rows( + db_session, + org_id, + rows=[ + {"conversation_id": "c0", "status": "completed", "score_value": 0.2}, + ], + ) + evaluation.last_updated_by_user_id = None + db_session.commit() + + def fake_apply_async(*, kwargs=None, **_kw): + return types.SimpleNamespace(id="cluster-task-1") + + monkeypatch.setattr( + "app.workers.tasks.generate_evaluation_metric_clusters.generate_evaluation_metric_clusters_task.apply_async", + fake_apply_async, + ) + + response = authenticated_client.post( + f"/api/v1/call-imports/{call_import.id}/evaluations/{evaluation.id}/metric-clusters", + json={"row_limit": 1}, + ) + assert response.status_code == 200, response.text + + detail = authenticated_client.get( + f"/api/v1/call-imports/{call_import.id}/evaluations/{evaluation.id}" + ) + assert detail.status_code == 200, detail.text + assert detail.json()["last_updated_by_email"] == "owner@example.com" + + def test_cancel_preserves_selected_row_ids_in_state( authenticated_client, db_session, org_id, seed_org, make_ai_provider, monkeypatch ): From 681fe3719682e6169deddefe17fc26f16ed4c722 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 7 Aug 2026 20:09:48 +0530 Subject: [PATCH 04/55] feat: enhance user insights with improved call import evaluations --- .gitignore | 1 - app/config.py | 31 ++++++++-- config.docker.yml | 137 --------------------------------------------- docker-compose.yml | 8 +++ env.example | 4 ++ 5 files changed, 39 insertions(+), 142 deletions(-) delete mode 100644 config.docker.yml diff --git a/.gitignore b/.gitignore index 3fb88623..106532ab 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ env/ ENV/ .venv config.yml -config.docker.yml # uv .python-version uv.lock diff --git a/app/config.py b/app/config.py index 94b51f18..0eec6b2a 100644 --- a/app/config.py +++ b/app/config.py @@ -1,6 +1,8 @@ """Configuration management using Pydantic settings.""" import json +import os +import re import yaml from pathlib import Path from typing import Annotated, Any, Dict, List, Optional, Union @@ -412,6 +414,19 @@ def apply_service_mode(mode: str) -> None: settings.SERVICE_MODE = normalized +_ENV_REF_PATTERN = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$") + + +def _expand_env_ref(value: Any) -> Any: + """Replace ``${VAR}`` with ``os.environ[VAR]`` when loading YAML secrets.""" + if not isinstance(value, str): + return value + match = _ENV_REF_PATTERN.match(value.strip()) + if not match: + return value + return os.environ.get(match.group(1), "") + + def load_config_from_file(config_path: str) -> None: """Load configuration from a YAML file and update global settings.""" import yaml @@ -577,9 +592,13 @@ def load_config_from_file(config_path: str) -> None: if "region" in s3_config: settings.S3_REGION = s3_config["region"] if "access_key_id" in s3_config: - settings.S3_ACCESS_KEY_ID = s3_config["access_key_id"] + resolved = _expand_env_ref(s3_config["access_key_id"]) + if resolved: + settings.S3_ACCESS_KEY_ID = resolved if "secret_access_key" in s3_config: - settings.S3_SECRET_ACCESS_KEY = s3_config["secret_access_key"] + resolved = _expand_env_ref(s3_config["secret_access_key"]) + if resolved: + settings.S3_SECRET_ACCESS_KEY = resolved if "endpoint_url" in s3_config: settings.S3_ENDPOINT_URL = s3_config["endpoint_url"] if "prefix" in s3_config: @@ -730,9 +749,13 @@ def load_config_from_file(config_path: str) -> None: if "region" in loki_s3: settings.LOKI_S3_REGION = loki_s3["region"] if "access_key_id" in loki_s3: - settings.LOKI_S3_ACCESS_KEY_ID = loki_s3["access_key_id"] + resolved = _expand_env_ref(loki_s3["access_key_id"]) + if resolved: + settings.LOKI_S3_ACCESS_KEY_ID = resolved if "secret_access_key" in loki_s3: - settings.LOKI_S3_SECRET_ACCESS_KEY = loki_s3["secret_access_key"] + resolved = _expand_env_ref(loki_s3["secret_access_key"]) + if resolved: + settings.LOKI_S3_SECRET_ACCESS_KEY = resolved if "prefix" in loki_s3: settings.LOKI_S3_PREFIX = loki_s3["prefix"] if "plivo" in config_data: diff --git a/config.docker.yml b/config.docker.yml deleted file mode 100644 index 6b84d505..00000000 --- a/config.docker.yml +++ /dev/null @@ -1,137 +0,0 @@ -# EfficientAI Docker Configuration File -# This config uses Docker service names for networking - -# Application Settings -app: - name: "EfficientAI Voice AI Evaluation Platform" - version: "0.1.0" - debug: true - secret_key: "your-secret-key-here-change-in-production" - -# Server Settings -server: - host: "0.0.0.0" - port: 8000 - -# Operational endpoints (/metrics). /health is always open for load balancers. -# Add VPC CIDRs here if Prometheus scrapes /metrics from inside the VPC. -operational: - public: false - trusted_ips: - - "10.0.0.0/8" - -# Database Configuration (Docker service name) -database: - url: "postgresql://efficientai:password@db:5432/efficientai" - -# Redis Configuration (Docker service name) -redis: - url: "redis://redis:6379/0" - -# Celery Configuration (Docker service names) -celery: - broker_url: "redis://redis:6379/0" - result_backend: "redis://redis:6379/0" - -# File Storage -storage: - upload_dir: "/app/uploads" - max_file_size_mb: 500 - allowed_audio_formats: - - "wav" - - "mp3" - - "flac" - - "m4a" - -# S3 Configuration (for data sources integration) -s3: - enabled: true - bucket_name: "voiceai-evals-test" - region: "us-east-1" - access_key_id: "${S3_ACCESS_KEY_ID}" - secret_access_key: "${S3_SECRET_ACCESS_KEY}" - endpoint_url: null - prefix: "audio/" - -# Speaker Diarization (pyannote.audio) -# Requires accepting model terms at https://huggingface.co/pyannote/speaker-diarization-3.1 -diarization: - huggingface_token: # Set your HuggingFace token here (e.g., "hf_xxxxx") - num_speakers: 2 # Force exact speaker count (2 = agent + customer). Set to null for auto-detect. - -# CORS Settings -cors: - origins: - - "http://localhost:3000" - - "http://localhost:8000" - -# API Settings -api: - prefix: "/api/v1" - key_header: "X-API-Key" - rate_limit_per_minute: 60 - -# Observability (Loki log aggregation) -# storage: "filesystem" (default, local Docker volume) or "s3" (durable, for production) -# multi_tenant: false (default, single tenant) or true (per-org log isolation) -observability: - enabled: false - loki: - enabled: false - url: "http://loki:3100" - storage: filesystem - multi_tenant: false - platform_tenant: "platform" - # s3: - # bucket_name: "my-logs-bucket" - # region: "us-east-1" - # access_key_id: "" - # secret_access_key: "" - # prefix: "logs/" - -auth: - providers: - - api_key - - local_password - # - external_oidc # requires EFFICIENTAI_LICENSE feature: oidc_sso - - local_password: - # Lifetime of the Bearer tokens minted at POST /auth/login (in minutes). - # Keep this short; clients re-authenticate silently. - token_ttl_minutes: 720 # 12 hours - # Turn this off in Cloud SaaS to block self-serve signup. - allow_signup: true - - # External OIDC (enterprise): bring your own IdP. Works with any - # OIDC-compliant provider. See README.md > Authentication & Deployment - # Recipes for copy-paste recipes for Okta, Azure AD, Google Workspace - # and AWS Cognito. - # - # oidc: - # issuer: "https://example.okta.com" # REQUIRED - # audience: "efficientai" # expected `aud` claim - # client_id: "0oa..." # SPA client id - # jwks_uri: "https://example.okta.com/oauth2/v1/keys" # optional, derived from issuer - # default_org_name: "Example Inc" # used when no org claim is present - # org_claim_path: ["https://efficientai.com/org"] # dotted path into the JWT to find the org name - -# Judge Alignment (AlignEval-style hybrid integration). -# Per-org thresholds and judge model selection are UI-driven; only the -# operator kill switch + CSV upload limit live here. -judge_alignment: - enabled: true - csv_max_rows: 5000 - -# LLM gateway (optional). Per-org overrides in Integrations UI. -# llm_gateway: -# enabled: false -# type: bifrost # bifrost | litellm_proxy -# base_url: "http://localhost:8080/litellm" -# virtual_key: null -# master_key: null -# passthrough_provider_keys: true - -# Enterprise License (JWT signed with RS256). Unlocks gated features like -# oidc_sso, mfa_enforce, audit_export, voice_playground, gepa_optimization. -# license: -# key: "eyJhbGciOi..." diff --git a/docker-compose.yml b/docker-compose.yml index 066b7832..552884f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,8 @@ services: context: . dockerfile: docker/Dockerfile.api container_name: efficientai_api + env_file: + - .env environment: DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} REDIS_URL: redis://redis:6379/0 @@ -95,6 +97,8 @@ services: args: INSTALL_EXTRAS: "qualitative-voice" container_name: efficientai_media + env_file: + - .env environment: DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} REDIS_URL: redis://redis:6379/0 @@ -126,6 +130,8 @@ services: args: INSTALL_EXTRAS: "qualitative-voice" container_name: efficientai_worker + env_file: + - .env environment: # Worker uses Docker network, so it reaches DB/Redis via service names DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} @@ -162,6 +168,8 @@ services: args: INSTALL_EXTRAS: "" container_name: efficientai_worker_imports + env_file: + - .env environment: DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} REDIS_URL: redis://redis:6379/0 diff --git a/env.example b/env.example index c434c850..0de4f151 100644 --- a/env.example +++ b/env.example @@ -24,6 +24,10 @@ UPLOAD_DIR=/app/uploads MAX_FILE_SIZE_MB=500 ALLOWED_AUDIO_FORMATS=wav,mp3,flac,m4a +# S3 (optional — used when config.docker.yml references ${S3_ACCESS_KEY_ID}) +# S3_ACCESS_KEY_ID= +# S3_SECRET_ACCESS_KEY= + # Celery Configuration CELERY_BROKER_URL=redis://redis:6379/0 CELERY_RESULT_BACKEND=redis://redis:6379/0 From 8786a43520d9968fb7109fc45ab3bb8c03e35660 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 7 Aug 2026 20:17:54 +0530 Subject: [PATCH 05/55] chore: add config.docker.yml to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 106532ab..3fb88623 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ env/ ENV/ .venv config.yml +config.docker.yml # uv .python-version uv.lock From 55b3bfd05dcd23be019233ba48b950d6167b7775 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 7 Aug 2026 22:52:50 +0530 Subject: [PATCH 06/55] feat: Cache evaluation PDF reports by content fingerprint and reuse S3 artifacts on preview --- app/api/v1/routes/call_import_evaluations.py | 338 +++++++++++++++++- .../060_call_import_evaluation_pdf_reports.py | 76 ++++ ...call_import_eval_pdf_report_cache_index.py | 137 +++++++ app/models/database.py | 44 +++ .../call_import_pdf_report_storage.py | 207 +++++++++++ app/services/storage/azure_blob_service.py | 27 +- app/services/storage/blob_storage_service.py | 14 +- app/services/storage/gcs_service.py | 24 +- app/services/storage/s3_service.py | 13 +- env.example | 2 + frontend/src/lib/api.ts | 32 +- .../CallImportEvaluationDetail.tsx | 229 ++++++++++-- frontend/src/types/api.ts | 29 ++ .../test_call_import_evaluation_pdf_report.py | 131 +++++++ 14 files changed, 1233 insertions(+), 70 deletions(-) create mode 100644 app/migrations/060_call_import_evaluation_pdf_reports.py create mode 100644 app/migrations/061_call_import_eval_pdf_report_cache_index.py create mode 100644 app/services/reporting/call_import_pdf_report_storage.py diff --git a/app/api/v1/routes/call_import_evaluations.py b/app/api/v1/routes/call_import_evaluations.py index 12069420..94c371d0 100644 --- a/app/api/v1/routes/call_import_evaluations.py +++ b/app/api/v1/routes/call_import_evaluations.py @@ -11,7 +11,7 @@ import re import statistics from typing import Any, Dict, Iterator, List, Literal, Optional, Set, Tuple -from uuid import UUID +from uuid import UUID, uuid4 from datetime import date, datetime, timedelta, timezone @@ -20,6 +20,7 @@ from loguru import logger from pydantic import BaseModel, Field, field_validator from sqlalchemy import desc, func, or_, text +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from sqlalchemy.orm.attributes import flag_modified @@ -45,6 +46,7 @@ CallImport, CallImportEvaluation, CallImportEvaluationReportSnapshot, + CallImportEvaluationPdfReport, CallImportEvaluationRow, CallImportRow, Metric, @@ -99,6 +101,15 @@ from app.services.reporting.call_import_evaluation_pdf_report import ( call_import_evaluation_pdf_report_service, ) +from app.services.reporting.call_import_pdf_report_storage import ( + build_pdf_report_s3_key, + compute_pdf_report_cache_fingerprint, + compute_pdf_report_config_fingerprint, + compute_pdf_report_content_fingerprint, + config_summary_from_report_config, + find_cached_pdf_report, + presigned_urls_for_pdf_report, +) from app.services.call_import_metric_clusters import ( METRIC_CLUSTERS_CANCELLED_BY_USER_ERROR, estimate_metric_clusters_llm_calls, @@ -159,6 +170,35 @@ def _clean_vendor_name(cls, value: str) -> str: return cleaned +class CallImportEvaluationPdfReportResponse(BaseModel): + id: str + filename: str + preview_url: Optional[str] = None + download_url: Optional[str] = None + created_at: datetime + created_by: Optional[str] = None + report_type: str + vendor_name: str + config_summary: Optional[str] = None + storage_available: bool = True + cache_hit: bool = False + + +class CallImportEvaluationPdfReportListItem(BaseModel): + id: str + filename: Optional[str] = None + vendor_name: str + report_type: str + created_by: Optional[str] = None + created_at: datetime + config_summary: Optional[str] = None + cache_fingerprint: Optional[str] = None + + +class CallImportEvaluationPdfReportListResponse(BaseModel): + items: List[CallImportEvaluationPdfReportListItem] + + class CallImportEvaluationBaselineCandidate(BaseModel): evaluation_id: str name: str @@ -2112,6 +2152,57 @@ def _report_filename_slug(value: str) -> str: return slug or "client" +def _pdf_report_actor(principal: Principal) -> tuple[Optional[str], Optional[UUID]]: + created_by = principal.email + if not created_by and principal.user_id: + created_by = str(principal.user_id) + return created_by, principal.user_id + + +def _pdf_report_response_from_row( + row: CallImportEvaluationPdfReport, + *, + cache_hit: bool = False, +) -> CallImportEvaluationPdfReportResponse: + filename = row.filename or "report.pdf" + preview_url, download_url = presigned_urls_for_pdf_report( + row.s3_key or "", + filename, + ) + return CallImportEvaluationPdfReportResponse( + id=str(row.id), + filename=filename, + preview_url=preview_url, + download_url=download_url, + created_at=row.created_at or datetime.now(timezone.utc), + created_by=row.created_by, + report_type=row.report_type, + vendor_name=row.vendor_name, + config_summary=config_summary_from_report_config( + row.report_config if isinstance(row.report_config, dict) else {} + ), + storage_available=bool(row.s3_key), + cache_hit=cache_hit, + ) + + +def _pdf_report_list_item_from_row( + row: CallImportEvaluationPdfReport, +) -> CallImportEvaluationPdfReportListItem: + return CallImportEvaluationPdfReportListItem( + id=str(row.id), + filename=row.filename, + vendor_name=row.vendor_name, + report_type=row.report_type, + created_by=row.created_by, + created_at=row.created_at or datetime.now(timezone.utc), + config_summary=config_summary_from_report_config( + row.report_config if isinstance(row.report_config, dict) else {} + ), + cache_fingerprint=row.cache_fingerprint, + ) + + def _report_branding_for_import_workspace( db: Session, organization_id: UUID, @@ -3405,8 +3496,9 @@ async def generate_call_import_evaluation_pdf_report( payload: CallImportEvaluationPdfReportRequest, api_key: str = Depends(get_api_key), organization_id: UUID = Depends(get_organization_id), + principal: Principal = Depends(get_principal), db: Session = Depends(get_db), -) -> StreamingResponse: +): del api_key call_import = _require_import(db, call_import_id, organization_id) @@ -3527,17 +3619,6 @@ async def generate_call_import_evaluation_pdf_report( cached_prompt_improvements, report_config, ) - narrative = _generate_report_narrative( - db, - organization_id, - metric_aggregates=metric_aggregates, - insight_aggregates=insight_aggregates if is_internal else [], - period_delta_by_metric=period_delta_by_metric, - evidence_samples=evidence_samples if is_internal else {}, - report_config=report_config, - ) - - generated_at = datetime.now(timezone.utc) branding_images, custom_heading = _report_branding_for_import_workspace( db, organization_id, @@ -3562,6 +3643,76 @@ async def generate_call_import_evaluation_pdf_report( pdf_aggregates, child_names_by_parent=pdf_child_map, ) + + from app.services.storage.s3_service import s3_service + + config_fingerprint = compute_pdf_report_config_fingerprint( + report_type=payload.report_type, + include_period_delta=bool(payload.include_period_delta), + include_weekly_delta=bool(payload.include_weekly_delta), + baseline_evaluation_id=payload.baseline_evaluation_id, + internal_brand_image_id=payload.internal_brand_image_id, + external_brand_image_id=payload.external_brand_image_id, + use_case=payload.use_case, + report_config=report_config, + report_heading=custom_heading, + vendor_name=payload.vendor_name, + platform_base_url=payload.platform_base_url, + period_label=period_label, + ) + content_fingerprint = compute_pdf_report_content_fingerprint( + evaluation_status=evaluation.status, + completed_rows=int(evaluation.completed_rows or 0), + total_rows=int(evaluation.total_rows or 0), + failed_rows=int(evaluation.failed_rows or 0), + metric_aggregates=metric_aggregates, + insight_aggregates=insight_aggregates, + period_delta_by_metric=period_delta_by_metric, + benchmark_context=benchmark_context, + metric_metadata=[ + { + "id": str(metric.id), + "name": metric.name, + "description": metric.description, + } + for metric in metrics + ], + failure_policies=failure_policies_for_pdf, + tldr_summary=cached_tldr_summary, + user_insights_for_pdf=generated_insights_for_pdf, + metric_clusters_for_pdf=metric_clusters_for_pdf, + prompt_improvements_for_pdf=prompt_improvements_for_pdf, + ) + cache_fingerprint = compute_pdf_report_cache_fingerprint( + config_fingerprint=config_fingerprint, + content_fingerprint=content_fingerprint, + ) + if s3_service.is_enabled(): + cached_pdf_report = find_cached_pdf_report( + db, + evaluation_id=evaluation.id, + organization_id=organization_id, + cache_fingerprint=cache_fingerprint, + ) + if cached_pdf_report is not None: + logger.info( + "Reusing stored PDF report {} for evaluation {} (cache fingerprint match)", + cached_pdf_report.id, + eval_id, + ) + return _pdf_report_response_from_row(cached_pdf_report, cache_hit=True) + + narrative = _generate_report_narrative( + db, + organization_id, + metric_aggregates=metric_aggregates, + insight_aggregates=insight_aggregates if is_internal else [], + period_delta_by_metric=period_delta_by_metric, + evidence_samples=evidence_samples if is_internal else {}, + report_config=report_config, + ) + + generated_at = datetime.now(timezone.utc) try: pdf_started = datetime.now(timezone.utc) pdf_bytes = await asyncio.to_thread( @@ -3635,17 +3786,168 @@ async def generate_call_import_evaluation_pdf_report( .count(), ) db.add(snapshot) - db.commit() + db.flush() filename = ( f"{_report_filename_slug(payload.vendor_name)}-" f"{payload.report_type}-quality-metric-audit-{eval_id}.pdf" ) - return StreamingResponse( - iter([pdf_bytes]), - media_type="application/pdf", - headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + + if not s3_service.is_enabled(): + db.commit() + return StreamingResponse( + iter([pdf_bytes]), + media_type="application/pdf", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) + + report_id = uuid4() + s3_key = build_pdf_report_s3_key( + organization_id=organization_id, + call_import_id=call_import.id, + evaluation_id=evaluation.id, + report_id=report_id, ) + try: + s3_service.upload_file_by_key( + file_content=pdf_bytes, + key=s3_key, + content_type="application/pdf", + ) + except Exception as exc: # noqa: BLE001 + logger.exception( + "Failed to upload PDF report for evaluation {} to object storage", + eval_id, + ) + db.rollback() + raise HTTPException( + status_code=500, + detail=f"Failed to store PDF report: {exc}", + ) from exc + + created_by, created_by_user_id = _pdf_report_actor(principal) + pdf_report = CallImportEvaluationPdfReport( + id=report_id, + evaluation_id=evaluation.id, + call_import_id=call_import.id, + organization_id=organization_id, + workspace_id=call_import.workspace_id, + snapshot_id=snapshot.id, + vendor_name=payload.vendor_name, + report_type=payload.report_type, + filename=filename, + s3_key=s3_key, + report_config=report_config, + cache_fingerprint=cache_fingerprint, + created_by=created_by, + created_by_user_id=created_by_user_id, + ) + db.add(pdf_report) + try: + db.commit() + except IntegrityError: + db.rollback() + try: + s3_service.delete_file_by_key(s3_key) + except Exception: # noqa: BLE001 + logger.warning( + "Failed to delete orphan PDF after cache race for evaluation {}", + eval_id, + ) + raced_winner = find_cached_pdf_report( + db, + evaluation_id=evaluation.id, + organization_id=organization_id, + cache_fingerprint=cache_fingerprint, + ) + if raced_winner is not None: + logger.info( + "PDF report cache race resolved for evaluation {} (winner {})", + eval_id, + raced_winner.id, + ) + return _pdf_report_response_from_row(raced_winner, cache_hit=True) + raise HTTPException( + status_code=500, + detail="Failed to store PDF report due to a concurrent duplicate request.", + ) from None + db.refresh(pdf_report) + return _pdf_report_response_from_row(pdf_report) + + +@router.get( + "/{eval_id}/pdf-reports", + response_model=CallImportEvaluationPdfReportListResponse, + operation_id="listCallImportEvaluationPdfReports", +) +async def list_call_import_evaluation_pdf_reports( + call_import_id: UUID, + eval_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationPdfReportListResponse: + del api_key + _require_import(db, call_import_id, organization_id) + evaluation = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.id == eval_id, + CallImportEvaluation.call_import_id == call_import_id, + CallImportEvaluation.organization_id == organization_id, + ) + .first() + ) + if not evaluation: + raise HTTPException(status_code=404, detail="Call import evaluation not found") + + rows = ( + db.query(CallImportEvaluationPdfReport) + .filter( + CallImportEvaluationPdfReport.evaluation_id == eval_id, + CallImportEvaluationPdfReport.organization_id == organization_id, + ) + .order_by(desc(CallImportEvaluationPdfReport.created_at)) + .all() + ) + return CallImportEvaluationPdfReportListResponse( + items=[_pdf_report_list_item_from_row(row) for row in rows], + ) + + +@router.get( + "/{eval_id}/pdf-reports/{report_id}", + response_model=CallImportEvaluationPdfReportResponse, + operation_id="getCallImportEvaluationPdfReport", +) +async def get_call_import_evaluation_pdf_report( + call_import_id: UUID, + eval_id: UUID, + report_id: UUID, + api_key: str = Depends(get_api_key), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +) -> CallImportEvaluationPdfReportResponse: + del api_key + _require_import(db, call_import_id, organization_id) + row = ( + db.query(CallImportEvaluationPdfReport) + .filter( + CallImportEvaluationPdfReport.id == report_id, + CallImportEvaluationPdfReport.evaluation_id == eval_id, + CallImportEvaluationPdfReport.call_import_id == call_import_id, + CallImportEvaluationPdfReport.organization_id == organization_id, + ) + .first() + ) + if not row: + raise HTTPException(status_code=404, detail="PDF report not found") + if not row.s3_key: + raise HTTPException( + status_code=404, + detail="PDF report file is not available in object storage", + ) + return _pdf_report_response_from_row(row) @router.patch( diff --git a/app/migrations/060_call_import_evaluation_pdf_reports.py b/app/migrations/060_call_import_evaluation_pdf_reports.py new file mode 100644 index 00000000..8f2c6012 --- /dev/null +++ b/app/migrations/060_call_import_evaluation_pdf_reports.py @@ -0,0 +1,76 @@ +"""Migration: stored PDF reports for call import evaluations.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = "Add call_import_evaluation_pdf_reports for S3-stored evaluation PDFs" + + +def _table_exists(db: Session, table_name: str) -> bool: + row = db.execute( + text( + """ + SELECT 1 + FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table_name}, + ).first() + return row is not None + + +def upgrade(db: Session): + if _table_exists(db, "call_import_evaluation_pdf_reports"): + print("call_import_evaluation_pdf_reports already exists, skipping") + db.commit() + return + + db.execute( + text( + """ + CREATE TABLE call_import_evaluation_pdf_reports ( + id UUID PRIMARY KEY, + evaluation_id UUID NOT NULL REFERENCES call_import_evaluations(id) ON DELETE CASCADE, + call_import_id UUID NOT NULL REFERENCES call_imports(id) ON DELETE CASCADE, + organization_id UUID NOT NULL REFERENCES organizations(id), + workspace_id UUID NOT NULL REFERENCES workspaces(id), + snapshot_id UUID REFERENCES call_import_evaluation_report_snapshots(id) ON DELETE SET NULL, + vendor_name VARCHAR(120) NOT NULL, + report_type VARCHAR(20) NOT NULL DEFAULT 'external', + filename VARCHAR(255), + s3_key VARCHAR(512), + report_config JSONB NOT NULL DEFAULT '{}'::jsonb, + cache_fingerprint VARCHAR(64), + created_by TEXT, + created_by_user_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_call_import_eval_pdf_reports_eval + ON call_import_evaluation_pdf_reports (evaluation_id, created_at DESC) + """ + ) + ) + db.execute( + text( + """ + CREATE UNIQUE INDEX IF NOT EXISTS + uq_call_import_eval_pdf_reports_eval_cache_fp + ON call_import_evaluation_pdf_reports (evaluation_id, cache_fingerprint) + WHERE cache_fingerprint IS NOT NULL + """ + ) + ) + print("Created call_import_evaluation_pdf_reports") + db.commit() + + +def downgrade(db: Session): + db.execute(text("DROP TABLE IF EXISTS call_import_evaluation_pdf_reports")) + db.commit() diff --git a/app/migrations/061_call_import_eval_pdf_report_cache_index.py b/app/migrations/061_call_import_eval_pdf_report_cache_index.py new file mode 100644 index 00000000..baa579b0 --- /dev/null +++ b/app/migrations/061_call_import_eval_pdf_report_cache_index.py @@ -0,0 +1,137 @@ +"""Migration: PDF report cache fingerprint column rename and lookup index.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Rename config_fingerprint to cache_fingerprint and add unique " + "(evaluation_id, cache_fingerprint) for scale-safe cache lookups" +) + + +def _table_exists(db: Session, table_name: str) -> bool: + row = db.execute( + text( + """ + SELECT 1 + FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table_name}, + ).first() + return row is not None + + +def _column_exists(db: Session, column_name: str) -> bool: + row = db.execute( + text( + """ + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'call_import_evaluation_pdf_reports' + AND column_name = :column_name + """ + ), + {"column_name": column_name}, + ).first() + return row is not None + + +def upgrade(db: Session): + if not _table_exists(db, "call_import_evaluation_pdf_reports"): + print("call_import_evaluation_pdf_reports missing, skipping 061") + db.commit() + return + + if _column_exists(db, "config_fingerprint") and not _column_exists( + db, "cache_fingerprint" + ): + db.execute( + text( + """ + ALTER TABLE call_import_evaluation_pdf_reports + RENAME COLUMN config_fingerprint TO cache_fingerprint + """ + ) + ) + print("Renamed config_fingerprint -> cache_fingerprint") + + if not _column_exists(db, "cache_fingerprint"): + print("cache_fingerprint column missing, skipping index work") + db.commit() + return + + db.execute( + text( + """ + DELETE FROM call_import_evaluation_pdf_reports stale + USING call_import_evaluation_pdf_reports keep + WHERE stale.evaluation_id = keep.evaluation_id + AND stale.cache_fingerprint = keep.cache_fingerprint + AND stale.cache_fingerprint IS NOT NULL + AND stale.id <> keep.id + AND ( + stale.created_at < keep.created_at + OR ( + stale.created_at = keep.created_at + AND stale.id::text < keep.id::text + ) + ) + """ + ) + ) + + db.execute( + text( + """ + DROP INDEX IF EXISTS ix_call_import_eval_pdf_reports_fingerprint + """ + ) + ) + db.execute( + text( + """ + CREATE UNIQUE INDEX IF NOT EXISTS + uq_call_import_eval_pdf_reports_eval_cache_fp + ON call_import_evaluation_pdf_reports (evaluation_id, cache_fingerprint) + WHERE cache_fingerprint IS NOT NULL + """ + ) + ) + print("Ensured unique (evaluation_id, cache_fingerprint) index") + db.commit() + + +def downgrade(db: Session): + if not _table_exists(db, "call_import_evaluation_pdf_reports"): + db.commit() + return + + db.execute( + text( + """ + DROP INDEX IF EXISTS uq_call_import_eval_pdf_reports_eval_cache_fp + """ + ) + ) + if _column_exists(db, "cache_fingerprint") and not _column_exists( + db, "config_fingerprint" + ): + db.execute( + text( + """ + ALTER TABLE call_import_evaluation_pdf_reports + RENAME COLUMN cache_fingerprint TO config_fingerprint + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_call_import_eval_pdf_reports_fingerprint + ON call_import_evaluation_pdf_reports (config_fingerprint) + """ + ) + ) + db.commit() diff --git a/app/models/database.py b/app/models/database.py index 3ce548de..68bcfebc 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -2552,6 +2552,50 @@ class CallImportEvaluationReportSnapshot(Base): ) +class CallImportEvaluationPdfReport(Base): + """Stored PDF artifact for a call import evaluation report generation.""" + + __tablename__ = "call_import_evaluation_pdf_reports" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + evaluation_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_evaluations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + call_import_id = Column( + UUID(as_uuid=True), + ForeignKey("call_imports.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + organization_id = Column( + UUID(as_uuid=True), ForeignKey("organizations.id"), nullable=False, index=True + ) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + snapshot_id = Column( + UUID(as_uuid=True), + ForeignKey("call_import_evaluation_report_snapshots.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + vendor_name = Column(String(120), nullable=False) + report_type = Column(String(20), nullable=False, default="external") + filename = Column(String(255), nullable=True) + s3_key = Column(String(512), nullable=True) + report_config = Column(JSON, nullable=False, default=dict, server_default="{}") + cache_fingerprint = Column(String(64), nullable=True) + created_by = Column(String, nullable=True) + created_by_user_id = Column(UUID(as_uuid=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + # --------------------------------------------------------------------------- # Judge Alignment (AlignEval-style hybrid integration) # diff --git a/app/services/reporting/call_import_pdf_report_storage.py b/app/services/reporting/call_import_pdf_report_storage.py new file mode 100644 index 00000000..17992d05 --- /dev/null +++ b/app/services/reporting/call_import_pdf_report_storage.py @@ -0,0 +1,207 @@ +"""Helpers for call import evaluation PDF report storage and config fingerprinting.""" + +from __future__ import annotations + +import hashlib +import json +from typing import TYPE_CHECKING, Any +from uuid import UUID + +from app.services.storage.s3_service import s3_service + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + from app.models.database import CallImportEvaluationPdfReport + + +def build_pdf_report_s3_key( + *, + organization_id: UUID, + call_import_id: UUID, + evaluation_id: UUID, + report_id: UUID, +) -> str: + prefix = s3_service.prefix or "" + return ( + f"{prefix}organizations/{organization_id}/call_imports/{call_import_id}/" + f"evaluations/{evaluation_id}/reports/{report_id}.pdf" + ) + + +def _canonicalize_report_config(value: Any) -> Any: + if isinstance(value, dict): + return {str(k): _canonicalize_report_config(v) for k, v in sorted(value.items())} + if isinstance(value, list): + normalized = [_canonicalize_report_config(item) for item in value] + try: + return sorted( + normalized, + key=lambda item: json.dumps(item, sort_keys=True, default=str), + ) + except TypeError: + return normalized + return value + + +def _fingerprint_digest(payload: dict[str, Any]) -> str: + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def _json_fingerprint_value(value: Any) -> Any: + if value is None: + return None + if hasattr(value, "model_dump"): + return value.model_dump(mode="json") + if isinstance(value, dict): + return {str(k): _json_fingerprint_value(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [_json_fingerprint_value(item) for item in value] + return value + + +def compute_pdf_report_config_fingerprint( + *, + report_type: str, + include_period_delta: bool, + include_weekly_delta: bool, + baseline_evaluation_id: str | None, + internal_brand_image_id: str | None, + external_brand_image_id: str | None, + use_case: str | None, + report_config: dict[str, Any], + report_heading: str | None, + vendor_name: str | None = None, + platform_base_url: str | None = None, + period_label: str | None = None, +) -> str: + payload = { + "report_type": report_type, + "include_period_delta": include_period_delta, + "include_weekly_delta": include_weekly_delta, + "baseline_evaluation_id": baseline_evaluation_id, + "internal_brand_image_id": internal_brand_image_id, + "external_brand_image_id": external_brand_image_id, + "use_case": use_case, + "report_config": _canonicalize_report_config(report_config or {}), + "report_heading": (report_heading or "").strip(), + "vendor_name": (vendor_name or "").strip(), + "platform_base_url": (platform_base_url or "").strip(), + "period_label": (period_label or "").strip(), + } + return _fingerprint_digest(payload) + + +def compute_pdf_report_content_fingerprint( + *, + evaluation_status: str, + completed_rows: int, + total_rows: int, + failed_rows: int, + metric_aggregates: list[dict[str, Any]], + insight_aggregates: list[dict[str, Any]], + period_delta_by_metric: dict[str, Any], + benchmark_context: Any, + metric_metadata: list[dict[str, Any]], + failure_policies: dict[str, Any], + tldr_summary: Any = None, + user_insights_for_pdf: Any = None, + metric_clusters_for_pdf: Any = None, + prompt_improvements_for_pdf: Any = None, +) -> str: + payload = { + "evaluation_status": evaluation_status, + "completed_rows": completed_rows, + "total_rows": total_rows, + "failed_rows": failed_rows, + "metric_aggregates": _canonicalize_report_config(metric_aggregates or []), + "insight_aggregates": _canonicalize_report_config(insight_aggregates or []), + "period_delta_by_metric": _canonicalize_report_config(period_delta_by_metric or {}), + "benchmark_context": _json_fingerprint_value(benchmark_context), + "metric_metadata": _canonicalize_report_config(metric_metadata or []), + "failure_policies": _json_fingerprint_value(failure_policies or {}), + "tldr_summary": _json_fingerprint_value(tldr_summary), + "user_insights_for_pdf": _json_fingerprint_value(user_insights_for_pdf), + "metric_clusters_for_pdf": _json_fingerprint_value(metric_clusters_for_pdf), + "prompt_improvements_for_pdf": _json_fingerprint_value(prompt_improvements_for_pdf), + } + return _fingerprint_digest(payload) + + +def compute_pdf_report_cache_fingerprint( + *, + config_fingerprint: str, + content_fingerprint: str, +) -> str: + combined = f"{config_fingerprint}:{content_fingerprint}" + return hashlib.sha256(combined.encode("utf-8")).hexdigest() + + +def find_cached_pdf_report( + db: "Session", + *, + evaluation_id: UUID, + organization_id: UUID, + cache_fingerprint: str, +) -> "CallImportEvaluationPdfReport | None": + from sqlalchemy import desc + + from app.models.database import CallImportEvaluationPdfReport + + if not cache_fingerprint: + return None + return ( + db.query(CallImportEvaluationPdfReport) + .filter( + CallImportEvaluationPdfReport.evaluation_id == evaluation_id, + CallImportEvaluationPdfReport.organization_id == organization_id, + CallImportEvaluationPdfReport.cache_fingerprint == cache_fingerprint, + CallImportEvaluationPdfReport.s3_key.isnot(None), + ) + .order_by(desc(CallImportEvaluationPdfReport.created_at)) + .first() + ) + + +def config_summary_from_report_config(report_config: dict[str, Any] | None) -> str: + cfg = report_config if isinstance(report_config, dict) else {} + quality_ids = cfg.get("quality_metric_ids") or [] + insight_ids = cfg.get("insights") or [] + user_insight_ids = cfg.get("user_insight_ids") or [] + metric_count = len(quality_ids) if isinstance(quality_ids, list) else 0 + insight_count = len(insight_ids) if isinstance(insight_ids, list) else 0 + user_count = len(user_insight_ids) if isinstance(user_insight_ids, list) else 0 + parts: list[str] = [] + if metric_count: + parts.append(f"{metric_count} quality metric{'s' if metric_count != 1 else ''}") + if insight_count: + parts.append(f"{insight_count} insight{'s' if insight_count != 1 else ''}") + if user_count: + parts.append(f"{user_count} user insight{'s' if user_count != 1 else ''}") + return ", ".join(parts) if parts else "default sections" + + +def presigned_urls_for_pdf_report( + s3_key: str, + filename: str, + *, + expiration: int = 3600, +) -> tuple[str | None, str | None]: + if not s3_key or not s3_service.is_enabled(): + return None, None + safe_name = filename.replace('"', "'") + try: + preview_url = s3_service.generate_presigned_url_by_key( + s3_key, + expiration=expiration, + response_content_disposition="inline", + ) + download_url = s3_service.generate_presigned_url_by_key( + s3_key, + expiration=expiration, + response_content_disposition=f'attachment; filename="{safe_name}"', + ) + return preview_url, download_url + except Exception: + return None, None diff --git a/app/services/storage/azure_blob_service.py b/app/services/storage/azure_blob_service.py index 3b1cb19e..9e6e8425 100644 --- a/app/services/storage/azure_blob_service.py +++ b/app/services/storage/azure_blob_service.py @@ -553,7 +553,13 @@ def generate_presigned_url( key = self._get_key(file_id, file_format) return self.generate_presigned_url_by_key(key, expiration=expiration) - def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str: + def generate_presigned_url_by_key( + self, + key: str, + expiration: int = 3600, + *, + response_content_disposition: str | None = None, + ) -> str: """Generate a SAS URL for temporary file access by key.""" self._ensure_initialized() if not self.is_enabled(): @@ -574,14 +580,17 @@ def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str ) try: - sas_token = generate_blob_sas( - account_name=account_name, - container_name=self.bucket_name, - blob_name=key, - account_key=account_key, - permission=BlobSasPermissions(read=True), - expiry=datetime.now(UTC) + timedelta(seconds=expiration), - ) + sas_kwargs: dict = { + "account_name": account_name, + "container_name": self.bucket_name, + "blob_name": key, + "account_key": account_key, + "permission": BlobSasPermissions(read=True), + "expiry": datetime.now(UTC) + timedelta(seconds=expiration), + } + if response_content_disposition: + sas_kwargs["content_disposition"] = response_content_disposition + sas_token = generate_blob_sas(**sas_kwargs) blob_client = self.container_client.get_blob_client(key) return f"{blob_client.url}?{sas_token}" except Exception as e: diff --git a/app/services/storage/blob_storage_service.py b/app/services/storage/blob_storage_service.py index 5f4b3bc4..03374464 100644 --- a/app/services/storage/blob_storage_service.py +++ b/app/services/storage/blob_storage_service.py @@ -128,8 +128,18 @@ def generate_presigned_url( ) -> str: return self._backend().generate_presigned_url(file_id, file_format, expiration) - def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str: - return self._backend().generate_presigned_url_by_key(key, expiration) + def generate_presigned_url_by_key( + self, + key: str, + expiration: int = 3600, + *, + response_content_disposition: str | None = None, + ) -> str: + return self._backend().generate_presigned_url_by_key( + key, + expiration, + response_content_disposition=response_content_disposition, + ) blob_storage_service = BlobStorageService() diff --git a/app/services/storage/gcs_service.py b/app/services/storage/gcs_service.py index 06a53550..07213e62 100644 --- a/app/services/storage/gcs_service.py +++ b/app/services/storage/gcs_service.py @@ -553,7 +553,13 @@ def generate_presigned_url( key = self._get_key(file_id, file_format) return self.generate_presigned_url_by_key(key, expiration=expiration) - def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str: + def generate_presigned_url_by_key( + self, + key: str, + expiration: int = 3600, + *, + response_content_disposition: str | None = None, + ) -> str: """Generate a signed URL for temporary file access by key.""" self._ensure_initialized() if not self.is_enabled(): @@ -567,23 +573,27 @@ def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str if credentials is None and iam_params is None: raise StorageError(_GCS_SIGNING_UNAVAILABLE_MSG) + signed_url_kwargs: dict = { + "version": "v4", + "expiration": timedelta(seconds=expiration), + "method": "GET", + } + if response_content_disposition: + signed_url_kwargs["response_disposition"] = response_content_disposition + try: blob = self.bucket.blob(key) if credentials is not None: url = blob.generate_signed_url( - version="v4", - expiration=timedelta(seconds=expiration), - method="GET", credentials=credentials, + **signed_url_kwargs, ) else: sa_email, access_token = iam_params url = blob.generate_signed_url( - version="v4", - expiration=timedelta(seconds=expiration), - method="GET", service_account_email=sa_email, access_token=access_token, + **signed_url_kwargs, ) return url except GoogleCloudError as e: diff --git a/app/services/storage/s3_service.py b/app/services/storage/s3_service.py index 1c22d168..b8376a85 100644 --- a/app/services/storage/s3_service.py +++ b/app/services/storage/s3_service.py @@ -478,7 +478,13 @@ def generate_presigned_url(self, file_id: uuid.UUID, file_format: str, expiratio except Exception as e: raise StorageError(f"Unexpected error generating presigned URL: {str(e)}") - def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str: + def generate_presigned_url_by_key( + self, + key: str, + expiration: int = 3600, + *, + response_content_disposition: str | None = None, + ) -> str: """Generate a presigned URL for temporary file access by key.""" self._ensure_initialized() if not self.is_enabled(): @@ -486,9 +492,12 @@ def generate_presigned_url_by_key(self, key: str, expiration: int = 3600) -> str raise StorageError(error_msg) try: + params: dict[str, str] = {"Bucket": self.bucket_name, "Key": key} + if response_content_disposition: + params["ResponseContentDisposition"] = response_content_disposition url = self.s3_client.generate_presigned_url( "get_object", - Params={"Bucket": self.bucket_name, "Key": key}, + Params=params, ExpiresIn=expiration, ) return url diff --git a/env.example b/env.example index 0de4f151..cd34c25a 100644 --- a/env.example +++ b/env.example @@ -25,6 +25,8 @@ MAX_FILE_SIZE_MB=500 ALLOWED_AUDIO_FORMATS=wav,mp3,flac,m4a # S3 (optional — used when config.docker.yml references ${S3_ACCESS_KEY_ID}) +# Required for call-import PDF report history (stored artifacts); without blob storage, +# PDF generation still streams inline but is not versioned. # S3_ACCESS_KEY_ID= # S3_SECRET_ACCESS_KEY= diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 04e1b025..15724db3 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -43,6 +43,8 @@ import type { CallImportPreviewResponse, CallImportEvaluation, CallImportEvaluationBaselineCandidatesResponse, + CallImportEvaluationPdfReport, + CallImportEvaluationPdfReportListResponse, CallImportEvaluationLLMOverride, CallImportEvaluationListResponse, CallImportEvaluationRow, @@ -2882,7 +2884,7 @@ class ApiClient { reportConfig?: Record platformBaseUrl?: string | null }, - ): Promise { + ): Promise { const response = await this.client.post( `/api/v1/call-imports/${callImportId}/evaluations/${evaluationId}/pdf-report`, { @@ -2897,7 +2899,33 @@ class ApiClient { report_config: options?.reportConfig || {}, platform_base_url: options?.platformBaseUrl || null, }, - { responseType: 'blob' }, + { responseType: 'arraybuffer' }, + ) + const contentType = String(response.headers['content-type'] || '') + if (contentType.includes('application/pdf')) { + return new Blob([response.data], { type: 'application/pdf' }) + } + const text = new TextDecoder().decode(response.data) + return JSON.parse(text) as CallImportEvaluationPdfReport + } + + async listCallImportEvaluationPdfReports( + callImportId: string, + evaluationId: string, + ): Promise { + const response = await this.client.get( + `/api/v1/call-imports/${callImportId}/evaluations/${evaluationId}/pdf-reports`, + ) + return response.data + } + + async getCallImportEvaluationPdfReport( + callImportId: string, + evaluationId: string, + reportId: string, + ): Promise { + const response = await this.client.get( + `/api/v1/call-imports/${callImportId}/evaluations/${evaluationId}/pdf-reports/${reportId}`, ) return response.data } diff --git a/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx b/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx index 0c42f91a..7e68664c 100644 --- a/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx +++ b/frontend/src/pages/callImports/CallImportEvaluationDetail.tsx @@ -435,6 +435,8 @@ export default function CallImportEvaluationDetail() { const [forceFailPendingOpen, setForceFailPendingOpen] = useState(false) const [downloadMenuOpen, setDownloadMenuOpen] = useState(false) const downloadMenuRef = useRef(null) + const [pdfReportMenuOpen, setPdfReportMenuOpen] = useState(false) + const pdfReportMenuRef = useRef(null) const [pdfReportOpen, setPdfReportOpen] = useState(false) const [pdfWizardStep, setPdfWizardStep] = useState(1) const [pdfUserInsightsTriggering, setPdfUserInsightsTriggering] = @@ -890,6 +892,12 @@ export default function CallImportEvaluationDetail() { }, }) + const pdfReportsQuery = useQuery({ + queryKey: ['call-import-evaluation-pdf-reports', activeWorkspaceId, id, evalId], + queryFn: () => apiClient.listCallImportEvaluationPdfReports(id!, evalId!), + enabled: pdfReportMenuOpen && !!id && !!evalId, + }) + useEffect(() => { if (!deepLinkConversationId && !deepLinkRowId) return if (deepLinkConversationId && searchQuery !== deepLinkConversationId) { @@ -1817,6 +1825,33 @@ export default function CallImportEvaluationDetail() { ) } + const openStoredPdfPreview = async (reportId: string) => { + if (!id || !evalId) return + try { + const report = await apiClient.getCallImportEvaluationPdfReport( + id, + evalId, + reportId, + ) + if (!report.preview_url) { + throw new Error('Preview URL is not available.') + } + if (pdfPreviewUrl?.startsWith('blob:')) { + window.URL.revokeObjectURL(pdfPreviewUrl) + } + setPdfPreviewUrl(report.preview_url) + setPdfPreviewFilename(report.filename || 'report.pdf') + setPdfPreviewOpen(true) + setPdfReportMenuOpen(false) + } catch (e: unknown) { + console.error('Failed to open stored PDF report', e) + showToast( + getApiErrorMessage(e, 'Failed to open PDF preview.'), + 'error', + ) + } + } + const handlePdfReportSubmit = async () => { if (!id || !evalId || pdfReportLoading) return const vendorName = pdfVendorName.trim() @@ -1827,20 +1862,45 @@ export default function CallImportEvaluationDetail() { setPdfReportLoadingAction('download') setPdfReportError(null) try { - const blob = await generatePdfReportBlob(vendorName) + const result = await generatePdfReportBlob(vendorName) const vendorSlug = vendorName .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'client' - const url = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `${vendorSlug}-${pdfReportType}-quality-metric-audit-${evalId}.pdf` - document.body.appendChild(link) - link.click() - link.remove() - window.URL.revokeObjectURL(url) + const defaultFilename = `${vendorSlug}-${pdfReportType}-quality-metric-audit-${evalId}.pdf` + + if (result instanceof Blob) { + const url = window.URL.createObjectURL(result) + const link = document.createElement('a') + link.href = url + link.download = defaultFilename + document.body.appendChild(link) + link.click() + link.remove() + window.URL.revokeObjectURL(url) + } else { + const downloadUrl = result.download_url + if (!downloadUrl) { + throw new Error('Download URL is not available.') + } + const link = document.createElement('a') + link.href = downloadUrl + link.download = result.filename || defaultFilename + link.target = '_blank' + link.rel = 'noopener noreferrer' + document.body.appendChild(link) + link.click() + link.remove() + await queryClient.invalidateQueries({ + queryKey: [ + 'call-import-evaluation-pdf-reports', + activeWorkspaceId, + id, + evalId, + ], + }) + } setPdfReportOpen(false) setPdfWizardStep(1) setPdfVendorName('') @@ -1866,18 +1926,43 @@ export default function CallImportEvaluationDetail() { setPdfReportLoadingAction('preview') setPdfReportError(null) try { - const blob = await generatePdfReportBlob(vendorName) + const result = await generatePdfReportBlob(vendorName) const vendorSlug = vendorName .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'client' - if (pdfPreviewUrl) window.URL.revokeObjectURL(pdfPreviewUrl) - const url = window.URL.createObjectURL(blob) - setPdfPreviewUrl(url) - setPdfPreviewFilename( - `${vendorSlug}-${pdfReportType}-quality-metric-audit-${evalId}.pdf`, - ) + + if (result instanceof Blob) { + if (pdfPreviewUrl?.startsWith('blob:')) { + window.URL.revokeObjectURL(pdfPreviewUrl) + } + const url = window.URL.createObjectURL(result) + setPdfPreviewUrl(url) + setPdfPreviewFilename( + `${vendorSlug}-${pdfReportType}-quality-metric-audit-${evalId}.pdf`, + ) + } else { + if (!result.preview_url) { + throw new Error('Preview URL is not available.') + } + if (pdfPreviewUrl?.startsWith('blob:')) { + window.URL.revokeObjectURL(pdfPreviewUrl) + } + setPdfPreviewUrl(result.preview_url) + setPdfPreviewFilename( + result.filename || + `${vendorSlug}-${pdfReportType}-quality-metric-audit-${evalId}.pdf`, + ) + await queryClient.invalidateQueries({ + queryKey: [ + 'call-import-evaluation-pdf-reports', + activeWorkspaceId, + id, + evalId, + ], + }) + } setPdfPreviewOpen(true) } catch (e: unknown) { console.error('Failed to preview PDF report', e) @@ -2165,6 +2250,20 @@ export default function CallImportEvaluationDetail() { return () => document.removeEventListener('mousedown', handleClickOutside) }, [downloadMenuOpen]) + useEffect(() => { + if (!pdfReportMenuOpen) return + const handleClickOutside = (event: MouseEvent) => { + if ( + pdfReportMenuRef.current && + !pdfReportMenuRef.current.contains(event.target as Node) + ) { + setPdfReportMenuOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [pdfReportMenuOpen]) + if (!id || !evalId) { return
Missing identifiers.
} @@ -2347,20 +2446,90 @@ export default function CallImportEvaluationDetail() { Re-run metrics )} - +
+ + {pdfReportMenuOpen && ( +
+
+ +
+
+ History +
+
+ {pdfReportsQuery.isLoading ? ( +

Loading…

+ ) : (pdfReportsQuery.data?.items?.length ?? 0) === 0 ? ( +

+ No stored reports yet. +

+ ) : ( +
    + {pdfReportsQuery.data!.items.map((item) => ( +
  • + +
  • + ))} +
+ )} +
+
+ )} +
+ + {!collapsed && ( +
+ {justSaved && ( +
+
+ +

+ Mapping saved. Continue to Run Evaluation below to + fetch recordings, diarize transcripts, and score each row. +

+
+
+ )}
@@ -300,9 +388,11 @@ export default function MappingPanel({ callImport }: MappingPanelProps) { isLoading={mappingMutation.isPending} disabled={!canSubmit} > - {isAlreadyMapped ? 'Save mapping' : 'Save and continue'} + {isMapped ? 'Save mapping' : 'Save and continue'}
+
+ )}
) } diff --git a/frontend/src/pages/callImports/components/RunEvaluationStep.tsx b/frontend/src/pages/callImports/components/RunEvaluationStep.tsx index 7fef9cf2..1a78da35 100644 --- a/frontend/src/pages/callImports/components/RunEvaluationStep.tsx +++ b/frontend/src/pages/callImports/components/RunEvaluationStep.tsx @@ -4,6 +4,7 @@ import Button from '../../../components/Button' interface RunEvaluationStepProps { onRunEvaluation: () => void disabled?: boolean + highlighted?: boolean } /** @@ -14,9 +15,16 @@ interface RunEvaluationStepProps { export default function RunEvaluationStep({ onRunEvaluation, disabled = false, + highlighted = false, }: RunEvaluationStepProps) { return ( -
+
From 7e09155b1cb4b37d57c9d69266b4f24eee7d8af8 Mon Sep 17 00:00:00 2001 From: M Sami Date: Wed, 12 Aug 2026 00:40:30 +0530 Subject: [PATCH 15/55] feat: integrate LLM usage tracking and reporting across various components --- .gitignore | 5 + app/api/v1/api.py | 2 + app/api/v1/routes/call_import_evaluations.py | 64 ++- app/api/v1/routes/chat.py | 38 +- app/api/v1/routes/org_usage.py | 426 +++++++++++++++ app/app_factory.py | 2 + app/cli.py | 16 +- app/core/usage_context_middleware.py | 43 ++ app/dependencies.py | 16 + app/migrations/062_llm_usage_daily.py | 92 ++++ app/models/database.py | 37 ++ app/services/ai/llm_gateway.py | 26 +- app/services/ai/llm_service.py | 24 + app/services/usage/__init__.py | 32 ++ app/services/usage/context.py | 177 +++++++ app/services/usage/llm_usage.py | 491 ++++++++++++++++++ app/services/usage/normalize.py | 99 ++++ app/services/usage/voice_usage_processor.py | 80 +++ app/services/voice_agent/bot_fast_api.py | 35 +- app/workers/config.py | 9 + app/workers/tasks/__init__.py | 2 + app/workers/tasks/evaluate_call_import_row.py | 21 + app/workers/tasks/flush_usage_counters.py | 18 + .../generate_evaluation_tldr_insights.py | 29 +- app/workers/tasks/process_evaluator_result.py | 22 + .../tasks/transcribe_call_import_row.py | 49 ++ frontend/src/App.tsx | 4 + frontend/src/components/Layout.tsx | 8 + frontend/src/lib/api.ts | 82 +++ frontend/src/pages/usage/Usage.tsx | 294 +++++++++++ tests/test_services/test_usage/__init__.py | 0 .../test_usage/test_llm_usage.py | 387 ++++++++++++++ 32 files changed, 2584 insertions(+), 46 deletions(-) create mode 100644 app/api/v1/routes/org_usage.py create mode 100644 app/core/usage_context_middleware.py create mode 100644 app/migrations/062_llm_usage_daily.py create mode 100644 app/services/usage/__init__.py create mode 100644 app/services/usage/context.py create mode 100644 app/services/usage/llm_usage.py create mode 100644 app/services/usage/normalize.py create mode 100644 app/services/usage/voice_usage_processor.py create mode 100644 app/workers/tasks/flush_usage_counters.py create mode 100644 frontend/src/pages/usage/Usage.tsx create mode 100644 tests/test_services/test_usage/__init__.py create mode 100644 tests/test_services/test_usage/test_llm_usage.py diff --git a/.gitignore b/.gitignore index df987a07..8a24e3a7 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,9 @@ frontend/dist/ # Enterprise license private keys enterprise/keys/*.pem +# Celery Beat persistent schedule (SQLite + WAL sidecars) +celerybeat-schedule +celerybeat-schedule-* +celerybeat-schedule.* + # \ No newline at end of file diff --git a/app/api/v1/api.py b/app/api/v1/api.py index d6dc6d9d..51d81945 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -45,6 +45,7 @@ workspace_iam, dashboard, llm_gateway, + org_usage, ) api_router = APIRouter() @@ -93,3 +94,4 @@ api_router.include_router(workspace_iam.router) api_router.include_router(dashboard.router) api_router.include_router(llm_gateway.router) +api_router.include_router(org_usage.router) diff --git a/app/api/v1/routes/call_import_evaluations.py b/app/api/v1/routes/call_import_evaluations.py index 109004a3..030c98ef 100644 --- a/app/api/v1/routes/call_import_evaluations.py +++ b/app/api/v1/routes/call_import_evaluations.py @@ -3702,16 +3702,32 @@ async def generate_call_import_evaluation_pdf_report( ) return _pdf_report_response_from_row(cached_pdf_report, cache_hit=True) - narrative = _generate_report_narrative( - db, - organization_id, - metric_aggregates=metric_aggregates, - insight_aggregates=insight_aggregates if is_internal else [], - period_delta_by_metric=period_delta_by_metric, - evidence_samples=evidence_samples if is_internal else {}, - report_config=report_config, + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, ) + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=getattr(call_import, "workspace_id", None) + or evaluation.workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, + resource_id=evaluation.id, + resource_type="call_import_evaluation", + ) + ): + narrative = _generate_report_narrative( + db, + organization_id, + metric_aggregates=metric_aggregates, + insight_aggregates=insight_aggregates if is_internal else [], + period_delta_by_metric=period_delta_by_metric, + evidence_samples=evidence_samples if is_internal else {}, + report_config=report_config, + ) + generated_at = datetime.now(timezone.utc) try: pdf_started = datetime.now(timezone.utc) @@ -5337,21 +5353,35 @@ def _generate_and_persist_tldr_summary( from app.services.ai.llm_resolver import get_llm_provider_and_model from app.services.ai.llm_service import llm_service + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) provider_enum, model_str = get_llm_provider_and_model( organization_id, db, provider, model ) try: - llm_result = llm_service.generate_response( - messages=messages, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - temperature=0.4, - max_tokens=1400, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=evaluation.workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, + resource_id=evaluation.id, + resource_type="call_import_evaluation", + ) + ): + llm_result = llm_service.generate_response( + messages=messages, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + temperature=0.4, + max_tokens=1400, + ) except Exception as e: logger.error(f"[CallImportInsights] LLM call failed: {e}") raise HTTPException( diff --git a/app/api/v1/routes/chat.py b/app/api/v1/routes/chat.py index a8317844..6fc026f8 100644 --- a/app/api/v1/routes/chat.py +++ b/app/api/v1/routes/chat.py @@ -47,20 +47,32 @@ async def chat_completion( ): """Generate a chat completion using the specified AI provider and model.""" try: - # Convert ChatMessage to dict format expected by LLM service - messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] - - result = llm_service.generate_response( - messages=messages, - llm_provider=request.provider, - llm_model=request.model, - organization_id=organization_id, - db=db, - llm_config=request.llm_config, - temperature=request.temperature, - max_tokens=request.max_tokens, - task_defaults={"temperature": 0.7}, + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, ) + + messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] + + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.CHAT, + ) + ): + result = llm_service.generate_response( + messages=messages, + llm_provider=request.provider, + llm_model=request.model, + organization_id=organization_id, + db=db, + llm_config=request.llm_config, + temperature=request.temperature, + max_tokens=request.max_tokens, + task_defaults={"temperature": 0.7}, + ) background_tasks.add_task( record_chat_completion, diff --git a/app/api/v1/routes/org_usage.py b/app/api/v1/routes/org_usage.py new file mode 100644 index 00000000..05f984a0 --- /dev/null +++ b/app/api/v1/routes/org_usage.py @@ -0,0 +1,426 @@ +"""Org-scoped LLM Usage API (tokens + call counts).""" + +from __future__ import annotations + +from datetime import date, datetime, timedelta, timezone +from typing import List, Literal, Optional +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel, Field +from sqlalchemy import func +from sqlalchemy.orm import Session + +from app.database import get_db +from app.dependencies import get_organization_id +from app.models.database import CallImportEvaluation, LLMUsageDaily, Workspace +from app.services.usage.llm_usage import flush_usage_to_catalog, merge_usage_totals + +router = APIRouter(prefix="/organizations/usage", tags=["Usage"]) + +GroupBy = Literal["workspace", "product_section", "model", "resource"] + +SECTION_LABELS = { + "call_import_evaluations": "Call Import Evaluations", + "call_imports": "Call Imports", + "playground": "Playground", + "voice_playground": "Voice Playground", + "evaluators": "Evaluators", + "metrics": "Metrics", + "chat": "Chat", + "judge_alignment": "Judge Alignment", + "prompt_optimization": "Prompt Optimization", + "personas": "Personas", + "agents": "Agents", + "prompt_partials": "Prompt Partials", + "conversation_evaluations": "Conversation Evaluations", + "telephony": "Telephony", + "test_agent": "Test Agent", + "other": "Other", +} + + +class UsageTotals(BaseModel): + prompt_tokens: int = 0 + completion_tokens: int = 0 + total_tokens: int = 0 + cache_read_tokens: int = 0 + cache_creation_tokens: int = 0 + reasoning_tokens: int = 0 + call_count: int = 0 + + +class UsageSummaryResponse(BaseModel): + start: date + end: date + totals: UsageTotals + last_updated_at: Optional[datetime] = None + + +class UsageBreakdownRow(BaseModel): + workspace_id: Optional[UUID] = None + workspace_name: Optional[str] = None + product_section: Optional[str] = None + product_section_label: Optional[str] = None + model: Optional[str] = None + resource_id: Optional[UUID] = None + resource_type: Optional[str] = None + resource_label: Optional[str] = None + prompt_tokens: int = 0 + completion_tokens: int = 0 + total_tokens: int = 0 + cache_read_tokens: int = 0 + cache_creation_tokens: int = 0 + reasoning_tokens: int = 0 + call_count: int = 0 + + +class UsageBreakdownResponse(BaseModel): + start: date + end: date + group_by: GroupBy + rows: List[UsageBreakdownRow] + total_count: int + last_updated_at: Optional[datetime] = None + + +class UsageFiltersResponse(BaseModel): + workspaces: List[dict] = Field(default_factory=list) + product_sections: List[dict] = Field(default_factory=list) + models: List[str] = Field(default_factory=list) + resources: List[dict] = Field(default_factory=list) + + +def _default_range() -> tuple[date, date]: + end = datetime.now(timezone.utc).date() + start = end - timedelta(days=29) + return start, end + + +def _apply_filters( + query, + *, + organization_id: UUID, + start: date, + end: date, + workspace_id: Optional[UUID], + product_section: Optional[str], + model: Optional[str], + resource_id: Optional[UUID], +): + query = query.filter( + LLMUsageDaily.organization_id == organization_id, + LLMUsageDaily.usage_date >= start, + LLMUsageDaily.usage_date <= end, + ) + if workspace_id is not None: + query = query.filter(LLMUsageDaily.workspace_id == workspace_id) + if product_section: + query = query.filter(LLMUsageDaily.product_section == product_section) + if model: + query = query.filter(LLMUsageDaily.model == model) + if resource_id is not None: + query = query.filter(LLMUsageDaily.resource_id == resource_id) + return query + + +def _last_updated(db: Session, organization_id: UUID) -> Optional[datetime]: + return ( + db.query(func.max(LLMUsageDaily.updated_at)) + .filter(LLMUsageDaily.organization_id == organization_id) + .scalar() + ) + + +@router.get("/summary", response_model=UsageSummaryResponse) +def get_usage_summary( + start: Optional[date] = Query(None), + end: Optional[date] = Query(None), + workspace_id: Optional[UUID] = Query(None), + product_section: Optional[str] = Query(None), + model: Optional[str] = Query(None), + resource_id: Optional[UUID] = Query(None), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + if start is None or end is None: + start, end = _default_range() + if end < start: + raise HTTPException(status_code=400, detail="end must be >= start") + + flush_usage_to_catalog(db, organization_id) + + query = _apply_filters( + db.query(LLMUsageDaily), + organization_id=organization_id, + start=start, + end=end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + resource_id=resource_id, + ) + rows = query.all() + totals = merge_usage_totals(rows) + return UsageSummaryResponse( + start=start, + end=end, + totals=UsageTotals(**totals), + last_updated_at=_last_updated(db, organization_id), + ) + + +@router.get("/breakdown", response_model=UsageBreakdownResponse) +def get_usage_breakdown( + start: Optional[date] = Query(None), + end: Optional[date] = Query(None), + group_by: GroupBy = Query("workspace"), + workspace_id: Optional[UUID] = Query(None), + product_section: Optional[str] = Query(None), + model: Optional[str] = Query(None), + resource_id: Optional[UUID] = Query(None), + limit: int = Query(100, ge=1, le=500), + offset: int = Query(0, ge=0), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + if start is None or end is None: + start, end = _default_range() + if end < start: + raise HTTPException(status_code=400, detail="end must be >= start") + + flush_usage_to_catalog(db, organization_id) + + dim = { + "workspace": LLMUsageDaily.workspace_id, + "product_section": LLMUsageDaily.product_section, + "model": LLMUsageDaily.model, + "resource": LLMUsageDaily.resource_id, + }[group_by] + + aggregates = [ + func.coalesce(func.sum(LLMUsageDaily.prompt_tokens), 0).label("prompt_tokens"), + func.coalesce(func.sum(LLMUsageDaily.completion_tokens), 0).label( + "completion_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.cache_read_tokens), 0).label( + "cache_read_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.cache_creation_tokens), 0).label( + "cache_creation_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.reasoning_tokens), 0).label( + "reasoning_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.call_count), 0).label("call_count"), + ] + + select_cols = [dim] + group_cols = [dim] + if group_by == "resource": + select_cols.append(LLMUsageDaily.resource_type) + group_cols.append(LLMUsageDaily.resource_type) + + query = _apply_filters( + db.query(*select_cols, *aggregates), + organization_id=organization_id, + start=start, + end=end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + resource_id=resource_id, + ).group_by(*group_cols) + + total_count = query.count() + results = ( + query.order_by(func.sum(LLMUsageDaily.call_count).desc()) + .offset(offset) + .limit(limit) + .all() + ) + + workspace_names = { + w.id: w.name + for w in db.query(Workspace) + .filter(Workspace.organization_id == organization_id) + .all() + } + resource_labels: dict = {} + if group_by == "resource": + resource_ids = [r[0] for r in results if r[0] is not None] + if resource_ids: + evals = ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.organization_id == organization_id, + CallImportEvaluation.id.in_(resource_ids), + ) + .all() + ) + for evaluation in evals: + label = (evaluation.name or "").strip() or str(evaluation.id)[:8] + resource_labels[evaluation.id] = label + + rows: List[UsageBreakdownRow] = [] + for result in results: + if group_by == "workspace": + ws_id = result[0] + metrics = result[1:] + rows.append( + UsageBreakdownRow( + workspace_id=ws_id, + workspace_name=workspace_names.get(ws_id) if ws_id else "Unknown", + prompt_tokens=int(metrics[0]), + completion_tokens=int(metrics[1]), + total_tokens=int(metrics[0]) + int(metrics[1]), + cache_read_tokens=int(metrics[2]), + cache_creation_tokens=int(metrics[3]), + reasoning_tokens=int(metrics[4]), + call_count=int(metrics[5]), + ) + ) + elif group_by == "product_section": + section = result[0] + metrics = result[1:] + rows.append( + UsageBreakdownRow( + product_section=section, + product_section_label=SECTION_LABELS.get(section or "", section), + prompt_tokens=int(metrics[0]), + completion_tokens=int(metrics[1]), + total_tokens=int(metrics[0]) + int(metrics[1]), + cache_read_tokens=int(metrics[2]), + cache_creation_tokens=int(metrics[3]), + reasoning_tokens=int(metrics[4]), + call_count=int(metrics[5]), + ) + ) + elif group_by == "model": + model_name = result[0] + metrics = result[1:] + rows.append( + UsageBreakdownRow( + model=model_name, + prompt_tokens=int(metrics[0]), + completion_tokens=int(metrics[1]), + total_tokens=int(metrics[0]) + int(metrics[1]), + cache_read_tokens=int(metrics[2]), + cache_creation_tokens=int(metrics[3]), + reasoning_tokens=int(metrics[4]), + call_count=int(metrics[5]), + ) + ) + else: + res_id, res_type = result[0], result[1] + metrics = result[2:] + rows.append( + UsageBreakdownRow( + resource_id=res_id, + resource_type=res_type, + resource_label=resource_labels.get(res_id) + if res_id + else "Unscoped", + prompt_tokens=int(metrics[0]), + completion_tokens=int(metrics[1]), + total_tokens=int(metrics[0]) + int(metrics[1]), + cache_read_tokens=int(metrics[2]), + cache_creation_tokens=int(metrics[3]), + reasoning_tokens=int(metrics[4]), + call_count=int(metrics[5]), + ) + ) + + return UsageBreakdownResponse( + start=start, + end=end, + group_by=group_by, + rows=rows, + total_count=total_count, + last_updated_at=_last_updated(db, organization_id), + ) + + +@router.get("/filters", response_model=UsageFiltersResponse) +def get_usage_filters( + start: Optional[date] = Query(None), + end: Optional[date] = Query(None), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + if start is None or end is None: + start, end = _default_range() + + flush_usage_to_catalog(db, organization_id) + + base = db.query(LLMUsageDaily).filter( + LLMUsageDaily.organization_id == organization_id, + LLMUsageDaily.usage_date >= start, + LLMUsageDaily.usage_date <= end, + ) + + workspace_ids = { + row[0] + for row in base.with_entities(LLMUsageDaily.workspace_id).distinct().all() + if row[0] is not None + } + workspaces = [ + {"id": str(w.id), "name": w.name} + for w in db.query(Workspace) + .filter(Workspace.id.in_(workspace_ids) if workspace_ids else False) + .order_by(Workspace.name) + .all() + ] + + sections = sorted( + { + row[0] + for row in base.with_entities(LLMUsageDaily.product_section).distinct().all() + if row[0] + } + ) + models = sorted( + { + row[0] + for row in base.with_entities(LLMUsageDaily.model).distinct().all() + if row[0] + } + ) + + resource_rows = ( + base.with_entities(LLMUsageDaily.resource_id, LLMUsageDaily.resource_type) + .filter(LLMUsageDaily.resource_id.isnot(None)) + .distinct() + .limit(100) + .all() + ) + resource_ids = [r[0] for r in resource_rows] + eval_names = {} + if resource_ids: + for evaluation in ( + db.query(CallImportEvaluation) + .filter(CallImportEvaluation.id.in_(resource_ids)) + .all() + ): + eval_names[evaluation.id] = (evaluation.name or "").strip() or str( + evaluation.id + )[:8] + + resources = [ + { + "id": str(rid), + "type": rtype, + "label": eval_names.get(rid, str(rid)[:8]), + } + for rid, rtype in resource_rows + if rid is not None + ] + + return UsageFiltersResponse( + workspaces=workspaces, + product_sections=[ + {"id": s, "label": SECTION_LABELS.get(s, s)} for s in sections + ], + models=models, + resources=resources, + ) diff --git a/app/app_factory.py b/app/app_factory.py index 2b6b8d61..c2af3e09 100644 --- a/app/app_factory.py +++ b/app/app_factory.py @@ -19,6 +19,7 @@ from app.core.operational_access_middleware import OperationalAccessMiddleware from app.core.rbac_middleware import ReaderReadOnlyMiddleware from app.core.security_headers_middleware import SecurityHeadersMiddleware +from app.core.usage_context_middleware import LLMUsageContextMiddleware from app.database import init_db logger = logging.getLogger(__name__) @@ -98,6 +99,7 @@ def _add_common_middleware(app: FastAPI) -> None: if _includes_http_routes(): app.add_middleware(MigrationCheckMiddleware) app.add_middleware(ReaderReadOnlyMiddleware) + app.add_middleware(LLMUsageContextMiddleware) if settings.OBSERVABILITY_ENABLED and settings.LOKI_ENABLED and settings.LOKI_MULTI_TENANT: from app.core.observability_middleware import OrgLoggingMiddleware diff --git a/app/cli.py b/app/cli.py index 3078344f..453b2a2d 100644 --- a/app/cli.py +++ b/app/cli.py @@ -692,6 +692,7 @@ def start_all( # Store worker processes for cleanup. worker_process = None worker_imports_process = None + beat_process = None telephony_process = None def _terminate(proc, label: str): @@ -711,8 +712,9 @@ def _terminate(proc, label: str): def cleanup_processes(): """Clean up spawned processes.""" - nonlocal worker_process, worker_imports_process, telephony_process + nonlocal worker_process, worker_imports_process, beat_process, telephony_process _terminate(telephony_process, "Telephony media server") + _terminate(beat_process, "Celery beat") _terminate(worker_process, "Celery worker (default)") _terminate(worker_imports_process, "Celery worker (imports)") @@ -867,6 +869,18 @@ def _stream_telephony(): ), prefix="[WORKER-IMPORTS]", ) + + beat_process = _spawn_worker( + [ + "celery", + "-A", + "app.workers.celery_app", + "beat", + f"--loglevel={worker_loglevel}", + ], + label="Celery beat (periodic flush_usage_counters)", + prefix="[BEAT]", + ) except FileNotFoundError: click.echo("❌ Celery not found. Please install it: pip install celery", err=True) sys.exit(1) diff --git a/app/core/usage_context_middleware.py b/app/core/usage_context_middleware.py new file mode 100644 index 00000000..c9531c98 --- /dev/null +++ b/app/core/usage_context_middleware.py @@ -0,0 +1,43 @@ +"""Clear LLM usage ContextVar at request boundaries to avoid thread reuse leaks.""" + +from __future__ import annotations + +from uuid import UUID + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + +from app.services.usage.context import ( + infer_product_section_from_path, + reset_usage_context, + reset_usage_hints, + set_usage_context, + set_usage_hints, +) + + +class LLMUsageContextMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + section = infer_product_section_from_path(request.url.path) + request.state.usage_product_section = section + + workspace_hint = None + raw_ws = request.headers.get("x-workspace-id") or request.query_params.get( + "workspace_id" + ) + if raw_ws: + try: + workspace_hint = UUID(raw_ws) + except (TypeError, ValueError): + workspace_hint = None + + ctx_token = set_usage_context(None) + hint_tokens = set_usage_hints( + workspace_id=workspace_hint, + product_section=section, + ) + try: + return await call_next(request) + finally: + reset_usage_hints(hint_tokens) + reset_usage_context(ctx_token) diff --git a/app/dependencies.py b/app/dependencies.py index 6277777a..c3597d75 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -193,6 +193,22 @@ def get_workspace_context( is_org_admin=org_role == RoleEnum.ADMIN, ) request.state.workspace_context = ctx + + from app.services.usage.context import ( + LLMUsageProductSection, + ensure_usage_context, + ) + + section = getattr( + request.state, "usage_product_section", LLMUsageProductSection.OTHER + ) + ensure_usage_context( + organization_id, + workspace_id=workspace.id, + product_section=section + if isinstance(section, LLMUsageProductSection) + else LLMUsageProductSection.OTHER, + ) return ctx diff --git a/app/migrations/062_llm_usage_daily.py b/app/migrations/062_llm_usage_daily.py new file mode 100644 index 00000000..e160c955 --- /dev/null +++ b/app/migrations/062_llm_usage_daily.py @@ -0,0 +1,92 @@ +"""Migration: LLM usage daily rollups for org-scoped Usage reporting.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = "Add llm_usage_daily table for LLM token/call rollups" + + +def upgrade(db: Session): + exists = db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = 'llm_usage_daily' + """ + ) + ).first() + if exists: + print("llm_usage_daily already exists, skipping 062") + db.commit() + return + + db.execute( + text( + """ + CREATE TABLE llm_usage_daily ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + workspace_id UUID REFERENCES workspaces(id) ON DELETE SET NULL, + product_section VARCHAR(64) NOT NULL, + model VARCHAR(255) NOT NULL, + resource_id UUID, + resource_type VARCHAR(64), + usage_date DATE NOT NULL, + prompt_tokens BIGINT NOT NULL DEFAULT 0, + completion_tokens BIGINT NOT NULL DEFAULT 0, + cache_read_tokens BIGINT NOT NULL DEFAULT 0, + cache_creation_tokens BIGINT NOT NULL DEFAULT 0, + reasoning_tokens BIGINT NOT NULL DEFAULT 0, + call_count BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_llm_usage_daily_org_date + ON llm_usage_daily (organization_id, usage_date) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_llm_usage_daily_org_workspace_date + ON llm_usage_daily (organization_id, workspace_id, usage_date) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_llm_usage_daily_org_resource_date + ON llm_usage_daily (organization_id, resource_id, usage_date) + """ + ) + ) + db.execute( + text( + """ + CREATE UNIQUE INDEX uq_llm_usage_daily_bucket + ON llm_usage_daily ( + organization_id, + COALESCE(workspace_id, '00000000-0000-0000-0000-000000000000'::uuid), + product_section, + model, + COALESCE(resource_id, '00000000-0000-0000-0000-000000000000'::uuid), + usage_date + ) + """ + ) + ) + db.commit() + print("Created llm_usage_daily table") + + +def downgrade(db: Session): + db.execute(text("DROP TABLE IF EXISTS llm_usage_daily")) + db.commit() diff --git a/app/models/database.py b/app/models/database.py index 68bcfebc..9f32f212 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -2777,3 +2777,40 @@ class JudgeRun(Base): created_by = Column(String, nullable=True) dataset = relationship("JudgeDataset", back_populates="runs") + + +class LLMUsageDaily(Base): + """Daily LLM usage rollups for org-scoped Usage reporting.""" + + __tablename__ = "llm_usage_daily" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column( + UUID(as_uuid=True), + ForeignKey("organizations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + workspace_id = Column( + UUID(as_uuid=True), + ForeignKey("workspaces.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + product_section = Column(String(64), nullable=False, index=True) + model = Column(String(255), nullable=False, index=True) + resource_id = Column(UUID(as_uuid=True), nullable=True, index=True) + resource_type = Column(String(64), nullable=True) + usage_date = Column(Date, nullable=False, index=True) + prompt_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") + completion_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") + cache_read_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") + cache_creation_tokens = Column( + BigInteger, nullable=False, default=0, server_default="0" + ) + reasoning_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") + call_count = Column(BigInteger, nullable=False, default=0, server_default="0") + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) diff --git a/app/services/ai/llm_gateway.py b/app/services/ai/llm_gateway.py index b0731840..750b1641 100644 --- a/app/services/ai/llm_gateway.py +++ b/app/services/ai/llm_gateway.py @@ -705,4 +705,28 @@ def litellm_completion( db=db, credential=credential, ) - return litellm.completion(**kwargs) + response = litellm.completion(**kwargs) + try: + from app.services.usage.context import ( + LLMUsageProductSection, + ensure_usage_context, + reset_usage_context, + ) + from app.services.usage.llm_usage import record_llm_usage + from app.services.usage.normalize import normalize_llm_usage + + usage_token = ensure_usage_context( + organization_id, + product_section=LLMUsageProductSection.OTHER, + ) + try: + model_name = str(kwargs.get("model") or "unknown") + if "/" in model_name: + model_name = model_name.rsplit("/", 1)[-1] + record_llm_usage(model_name, normalize_llm_usage(raw_response=response)) + finally: + if usage_token is not None: + reset_usage_context(usage_token) + except Exception as exc: + logger.debug("litellm_completion usage record skipped: {}", exc) + return response diff --git a/app/services/ai/llm_service.py b/app/services/ai/llm_service.py index fb22285d..a0b2090a 100644 --- a/app/services/ai/llm_service.py +++ b/app/services/ai/llm_service.py @@ -549,6 +549,30 @@ def generate_response( "raw_response": response, "processing_time": time.time() - start_time, } + try: + from app.services.usage.context import ( + LLMUsageProductSection, + ensure_usage_context, + reset_usage_context, + ) + from app.services.usage.normalize import normalize_llm_usage + from app.services.usage.llm_usage import record_llm_usage + + usage_token = ensure_usage_context( + organization_id, + product_section=LLMUsageProductSection.OTHER, + ) + try: + snapshot = normalize_llm_usage(raw_response=response) + result["usage"]["cache_read_tokens"] = snapshot.cache_read_tokens + result["usage"]["cache_creation_tokens"] = snapshot.cache_creation_tokens + result["usage"]["reasoning_tokens"] = snapshot.reasoning_tokens + record_llm_usage(llm_model, snapshot) + finally: + if usage_token is not None: + reset_usage_context(usage_token) + except Exception as exc: + logger.debug("llm usage record skipped: {}", exc) return result diff --git a/app/services/usage/__init__.py b/app/services/usage/__init__.py new file mode 100644 index 00000000..646e9adb --- /dev/null +++ b/app/services/usage/__init__.py @@ -0,0 +1,32 @@ +"""LLM usage tracking (tokens, calls) with Redis buffer and catalog rollups.""" + +from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + ensure_usage_context, + infer_product_section_from_path, + llm_usage_context, + reset_usage_context, + reset_usage_hints, + set_usage_context, + set_usage_hints, +) +from app.services.usage.llm_usage import flush_all_usage_to_catalog, flush_usage_to_catalog, record_llm_usage +from app.services.usage.normalize import UsageSnapshot, normalize_llm_usage + +__all__ = [ + "LLMUsageContext", + "LLMUsageProductSection", + "UsageSnapshot", + "ensure_usage_context", + "flush_all_usage_to_catalog", + "flush_usage_to_catalog", + "infer_product_section_from_path", + "llm_usage_context", + "normalize_llm_usage", + "record_llm_usage", + "reset_usage_context", + "reset_usage_hints", + "set_usage_context", + "set_usage_hints", +] diff --git a/app/services/usage/context.py b/app/services/usage/context.py new file mode 100644 index 00000000..80e84fac --- /dev/null +++ b/app/services/usage/context.py @@ -0,0 +1,177 @@ +"""Usage attribution context (org, workspace, product section, resource).""" + +from __future__ import annotations + +from contextlib import contextmanager +from contextvars import ContextVar, Token +from dataclasses import dataclass +from enum import Enum +from typing import Iterator, Optional +from uuid import UUID + + +class LLMUsageProductSection(str, Enum): + CALL_IMPORT_EVALUATIONS = "call_import_evaluations" + CALL_IMPORTS = "call_imports" + PLAYGROUND = "playground" + VOICE_PLAYGROUND = "voice_playground" + EVALUATORS = "evaluators" + METRICS = "metrics" + CHAT = "chat" + JUDGE_ALIGNMENT = "judge_alignment" + PROMPT_OPTIMIZATION = "prompt_optimization" + PERSONAS = "personas" + AGENTS = "agents" + PROMPT_PARTIALS = "prompt_partials" + CONVERSATION_EVALUATIONS = "conversation_evaluations" + TELEPHONY = "telephony" + TEST_AGENT = "test_agent" + OTHER = "other" + + +@dataclass(frozen=True) +class LLMUsageContext: + organization_id: UUID + workspace_id: Optional[UUID] = None + product_section: LLMUsageProductSection = LLMUsageProductSection.OTHER + resource_id: Optional[UUID] = None + resource_type: Optional[str] = None + + +_usage_context_var: ContextVar[Optional[LLMUsageContext]] = ContextVar( + "llm_usage_context", default=None +) +_usage_workspace_hint_var: ContextVar[Optional[UUID]] = ContextVar( + "llm_usage_workspace_hint", default=None +) +_usage_section_hint_var: ContextVar[LLMUsageProductSection] = ContextVar( + "llm_usage_section_hint", default=LLMUsageProductSection.OTHER +) + +# Path fragments under /api/v1 → product section (longest match wins). +_PATH_SECTION_RULES: tuple[tuple[str, LLMUsageProductSection], ...] = ( + ("call-import-evaluations", LLMUsageProductSection.CALL_IMPORT_EVALUATIONS), + ("call-imports", LLMUsageProductSection.CALL_IMPORTS), + ("voice-playground", LLMUsageProductSection.VOICE_PLAYGROUND), + ("playground", LLMUsageProductSection.PLAYGROUND), + ("evaluators", LLMUsageProductSection.EVALUATORS), + ("metrics", LLMUsageProductSection.METRICS), + ("chat", LLMUsageProductSection.CHAT), + ("judge-alignment", LLMUsageProductSection.JUDGE_ALIGNMENT), + ("prompt-optimization", LLMUsageProductSection.PROMPT_OPTIMIZATION), + ("personas", LLMUsageProductSection.PERSONAS), + ("agents", LLMUsageProductSection.AGENTS), + ("prompt-partials", LLMUsageProductSection.PROMPT_PARTIALS), + ("conversation-evaluations", LLMUsageProductSection.CONVERSATION_EVALUATIONS), + ("telephony", LLMUsageProductSection.TELEPHONY), + ("test-agent", LLMUsageProductSection.TEST_AGENT), +) + + +def get_usage_context() -> Optional[LLMUsageContext]: + return _usage_context_var.get() + + +def set_usage_context(ctx: Optional[LLMUsageContext]) -> Token: + return _usage_context_var.set(ctx) + + +def reset_usage_context(token: Token) -> None: + _usage_context_var.reset(token) + + +def set_usage_hints( + *, + workspace_id: Optional[UUID] = None, + product_section: Optional[LLMUsageProductSection] = None, +) -> tuple[Token, Token]: + ws_token = _usage_workspace_hint_var.set(workspace_id) + section = product_section or LLMUsageProductSection.OTHER + section_token = _usage_section_hint_var.set(section) + return ws_token, section_token + + +def reset_usage_hints(tokens: tuple[Token, Token]) -> None: + _usage_workspace_hint_var.reset(tokens[0]) + _usage_section_hint_var.reset(tokens[1]) + + +def get_usage_workspace_hint() -> Optional[UUID]: + return _usage_workspace_hint_var.get() + + +def get_usage_section_hint() -> LLMUsageProductSection: + return _usage_section_hint_var.get() + + +@contextmanager +def llm_usage_context(ctx: LLMUsageContext) -> Iterator[None]: + token = set_usage_context(ctx) + try: + yield + finally: + reset_usage_context(token) + + +def infer_product_section_from_path(path: str) -> LLMUsageProductSection: + normalized = (path or "").lower() + for fragment, section in _PATH_SECTION_RULES: + if f"/{fragment}" in normalized or normalized.endswith(fragment): + return section + return LLMUsageProductSection.OTHER + + +def ensure_usage_context( + organization_id: UUID, + *, + workspace_id: Optional[UUID] = None, + product_section: LLMUsageProductSection = LLMUsageProductSection.OTHER, + resource_id: Optional[UUID] = None, + resource_type: Optional[str] = None, +) -> Token | None: + """Set or enrich usage context. Returns token to reset, or None if unchanged.""" + resolved_workspace = workspace_id or get_usage_workspace_hint() + resolved_section = product_section + if resolved_section == LLMUsageProductSection.OTHER: + hint = get_usage_section_hint() + if hint != LLMUsageProductSection.OTHER: + resolved_section = hint + + current = get_usage_context() + if current is None: + return set_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=resolved_workspace, + product_section=resolved_section, + resource_id=resource_id, + resource_type=resource_type, + ) + ) + + upgraded_workspace = current.workspace_id or resolved_workspace + upgraded_section = ( + resolved_section + if current.product_section == LLMUsageProductSection.OTHER + and resolved_section != LLMUsageProductSection.OTHER + else current.product_section + ) + upgraded_resource_id = current.resource_id or resource_id + upgraded_resource_type = current.resource_type or resource_type + if ( + upgraded_workspace == current.workspace_id + and upgraded_section == current.product_section + and upgraded_resource_id == current.resource_id + and upgraded_resource_type == current.resource_type + ): + return None + + return set_usage_context( + LLMUsageContext( + organization_id=current.organization_id, + workspace_id=upgraded_workspace, + product_section=upgraded_section, + resource_id=upgraded_resource_id, + resource_type=upgraded_resource_type, + ) + ) diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py new file mode 100644 index 00000000..4c1fcecb --- /dev/null +++ b/app/services/usage/llm_usage.py @@ -0,0 +1,491 @@ +"""Redis-buffered LLM usage counters with catalog rollup flush.""" + +from __future__ import annotations + +import time +import uuid +from datetime import date, datetime, timezone +from typing import Any, Dict, Iterable, List, Optional, Tuple +from uuid import UUID + +import redis +from loguru import logger +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.config import settings +from app.services.usage.context import ( + LLMUsageContext, + get_usage_context, +) +from app.services.usage.normalize import UsageSnapshot + +_redis: redis.Redis | None = None + +_NONE = "__none__" +_PENDING_TTL_SECONDS = 14 * 24 * 60 * 60 +_FLUSH_LOCK_TTL_SECONDS = 45 +_FLUSH_LOCK_WAIT_SECONDS = 3.0 +_METRIC_FIELDS = ( + "prompt_tokens", + "completion_tokens", + "cache_read_tokens", + "cache_creation_tokens", + "reasoning_tokens", + "call_count", +) + +# Atomically move pending hash → claim key so only one flusher owns the deltas. +_CLAIM_LUA = """ +if redis.call('EXISTS', KEYS[1]) == 0 then + return 0 +end +redis.call('RENAME', KEYS[1], KEYS[2]) +return 1 +""" + + +def _client() -> redis.Redis: + global _redis + if _redis is None: + _redis = redis.from_url(settings.REDIS_URL, decode_responses=True) + return _redis + + +def _token(value: Optional[UUID]) -> str: + return str(value) if value else _NONE + + +def _pending_hash_key(organization_id: UUID) -> str: + return f"usage:pending:{organization_id}" + + +def _flush_lock_key(organization_id: UUID) -> str: + return f"usage:flush:lock:{organization_id}" + + +def _claim_hash_key(organization_id: UUID, claim_id: str) -> str: + return f"usage:flushing:{organization_id}:{claim_id}" + + +def _bucket_prefix( + *, + workspace_id: Optional[UUID], + product_section: str, + model: str, + resource_id: Optional[UUID], + resource_type: Optional[str], + usage_date: date, +) -> str: + return "|".join( + [ + _token(workspace_id), + product_section, + model, + _token(resource_id), + resource_type or _NONE, + usage_date.isoformat(), + ] + ) + + +def _parse_bucket_prefix(prefix: str) -> Optional[Dict[str, Any]]: + parts = prefix.split("|") + if len(parts) != 6: + return None + ws_token, section, model, resource_token, resource_type, day_str = parts + try: + usage_date = date.fromisoformat(day_str) + except ValueError: + return None + workspace_id = None if ws_token == _NONE else UUID(ws_token) + resource_id = None if resource_token == _NONE else UUID(resource_token) + resolved_resource_type = None if resource_type == _NONE else resource_type + return { + "workspace_id": workspace_id, + "product_section": section, + "model": model, + "resource_id": resource_id, + "resource_type": resolved_resource_type, + "usage_date": usage_date, + } + + +def _resolve_context(ctx: Optional[LLMUsageContext]) -> LLMUsageContext: + if ctx is not None: + return ctx + current = get_usage_context() + if current is not None: + return current + raise ValueError("LLM usage context is not set") + + +def _deltas_from_usage(usage: UsageSnapshot) -> Dict[str, int]: + return { + "prompt_tokens": usage.prompt_tokens, + "completion_tokens": usage.completion_tokens, + "cache_read_tokens": usage.cache_read_tokens, + "cache_creation_tokens": usage.cache_creation_tokens, + "reasoning_tokens": usage.reasoning_tokens, + "call_count": 1, + } + + +def record_llm_usage( + model: str, + usage: UsageSnapshot, + *, + ctx: Optional[LLMUsageContext] = None, + usage_date: Optional[date] = None, +) -> None: + """Increment Redis counters for one LLM call (best-effort, never raises).""" + if not model: + model = "unknown" + try: + context = _resolve_context(ctx) + except ValueError: + logger.debug("llm usage record skipped: missing context") + return + + deltas = _deltas_from_usage(usage) + if not any(deltas.values()): + return + + day = usage_date or datetime.now(timezone.utc).date() + prefix = _bucket_prefix( + workspace_id=context.workspace_id, + product_section=context.product_section.value, + model=model, + resource_id=context.resource_id, + resource_type=context.resource_type, + usage_date=day, + ) + hash_key = _pending_hash_key(context.organization_id) + + try: + client = _client() + pipe = client.pipeline() + for metric, delta in deltas.items(): + if delta: + pipe.hincrby(hash_key, f"{prefix}|{metric}", int(delta)) + pipe.sadd("usage:pending:orgs", str(context.organization_id)) + pipe.expire(hash_key, _PENDING_TTL_SECONDS) + pipe.execute() + except redis.RedisError as exc: + logger.warning("llm usage counter skipped: {}", exc) + + +def _parse_hash_to_buckets(raw: Dict[str, str]) -> Dict[str, Dict[str, int]]: + buckets: Dict[str, Dict[str, int]] = {} + for field, value in raw.items(): + if "|" not in field: + continue + prefix, metric = field.rsplit("|", 1) + if metric not in _METRIC_FIELDS: + continue + amount = int(value or 0) + if not amount: + continue + bucket = buckets.setdefault(prefix, {}) + bucket[metric] = bucket.get(metric, 0) + amount + return buckets + + +def _read_hash_buckets(hash_key: str) -> Dict[str, Dict[str, int]]: + try: + client = _client() + raw = client.hgetall(hash_key) + except redis.RedisError as exc: + logger.warning("llm usage read hash failed: {}", exc) + return {} + return _parse_hash_to_buckets(raw) + + +def _restore_buckets_to_pending( + organization_id: UUID, buckets: Dict[str, Dict[str, int]] +) -> None: + try: + client = _client() + pipe = client.pipeline() + hash_key = _pending_hash_key(organization_id) + for prefix, metrics in buckets.items(): + for metric, amount in metrics.items(): + if amount: + pipe.hincrby(hash_key, f"{prefix}|{metric}", amount) + pipe.sadd("usage:pending:orgs", str(organization_id)) + pipe.expire(hash_key, _PENDING_TTL_SECONDS) + pipe.execute() + except redis.RedisError as exc: + logger.warning("llm usage redis restore failed: {}", exc) + + +def _acquire_flush_lock(organization_id: UUID) -> bool: + try: + client = _client() + lock_key = _flush_lock_key(organization_id) + if client.set(lock_key, "1", nx=True, ex=_FLUSH_LOCK_TTL_SECONDS): + return True + deadline = time.monotonic() + _FLUSH_LOCK_WAIT_SECONDS + while time.monotonic() < deadline: + time.sleep(0.05) + if client.set(lock_key, "1", nx=True, ex=_FLUSH_LOCK_TTL_SECONDS): + return True + if client.get(lock_key) is None: + continue + return False + except redis.RedisError as exc: + logger.warning("llm usage flush lock failed: {}", exc) + return False + + +def _release_flush_lock(organization_id: UUID) -> None: + try: + _client().delete(_flush_lock_key(organization_id)) + except redis.RedisError: + pass + + +def _claim_pending( + organization_id: UUID, +) -> Tuple[Optional[str], Dict[str, Dict[str, int]]]: + """Rename pending → claim key. Returns (claim_key, buckets) or (None, {}).""" + claim_id = str(uuid.uuid4()) + pending_key = _pending_hash_key(organization_id) + claim_key = _claim_hash_key(organization_id, claim_id) + try: + client = _client() + claimed = int(client.eval(_CLAIM_LUA, 2, pending_key, claim_key) or 0) + if not claimed: + return None, {} + buckets = _read_hash_buckets(claim_key) + if not buckets: + client.delete(claim_key) + return None, {} + return claim_key, buckets + except redis.RedisError as exc: + logger.warning("llm usage claim failed: {}", exc) + return None, {} + + +def _ack_claim(claim_key: str, organization_id: UUID) -> None: + try: + client = _client() + client.delete(claim_key) + pending_key = _pending_hash_key(organization_id) + if not client.exists(pending_key): + client.srem("usage:pending:orgs", str(organization_id)) + except redis.RedisError: + pass + + +def _upsert_bucket( + db: Session, + organization_id: UUID, + bucket: Dict[str, Any], + deltas: Dict[str, int], + *, + resource_type: Optional[str], +) -> None: + params = { + "organization_id": str(organization_id), + "workspace_id": str(bucket["workspace_id"]) if bucket["workspace_id"] else None, + "product_section": bucket["product_section"], + "model": bucket["model"], + "resource_id": str(bucket["resource_id"]) if bucket["resource_id"] else None, + "resource_type": resource_type, + "usage_date": bucket["usage_date"].isoformat(), + "prompt_tokens": int(deltas.get("prompt_tokens", 0)), + "completion_tokens": int(deltas.get("completion_tokens", 0)), + "cache_read_tokens": int(deltas.get("cache_read_tokens", 0)), + "cache_creation_tokens": int(deltas.get("cache_creation_tokens", 0)), + "reasoning_tokens": int(deltas.get("reasoning_tokens", 0)), + "call_count": int(deltas.get("call_count", 0)), + } + result = db.execute( + text( + """ + UPDATE llm_usage_daily SET + prompt_tokens = prompt_tokens + :prompt_tokens, + completion_tokens = completion_tokens + :completion_tokens, + cache_read_tokens = cache_read_tokens + :cache_read_tokens, + cache_creation_tokens = cache_creation_tokens + :cache_creation_tokens, + reasoning_tokens = reasoning_tokens + :reasoning_tokens, + call_count = call_count + :call_count, + resource_type = COALESCE(resource_type, :resource_type), + updated_at = now() + WHERE organization_id = CAST(:organization_id AS uuid) + AND product_section = :product_section + AND model = :model + AND usage_date = CAST(:usage_date AS date) + AND workspace_id IS NOT DISTINCT FROM CAST(:workspace_id AS uuid) + AND resource_id IS NOT DISTINCT FROM CAST(:resource_id AS uuid) + """ + ), + params, + ) + if result.rowcount: + return + + db.execute( + text( + """ + INSERT INTO llm_usage_daily ( + id, organization_id, workspace_id, product_section, model, + resource_id, resource_type, usage_date, + prompt_tokens, completion_tokens, cache_read_tokens, + cache_creation_tokens, reasoning_tokens, call_count, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), CAST(:organization_id AS uuid), + CAST(:workspace_id AS uuid), :product_section, :model, + CAST(:resource_id AS uuid), :resource_type, CAST(:usage_date AS date), + :prompt_tokens, :completion_tokens, :cache_read_tokens, + :cache_creation_tokens, :reasoning_tokens, :call_count, + now(), now() + ) + """ + ), + params, + ) + + +def _is_missing_organization_fk(exc: BaseException) -> bool: + """True when insert failed because organization_id is not in organizations.""" + text_blob = " ".join( + str(part) + for part in (exc, getattr(exc, "orig", None), getattr(exc, "args", None)) + if part is not None + ).lower() + return "llm_usage_daily_organization_id_fkey" in text_blob + + +def flush_usage_to_catalog(db: Session, organization_id: UUID) -> int: + """Claim Redis deltas, commit to llm_usage_daily, then ack the claim.""" + if not _acquire_flush_lock(organization_id): + return 0 + + claim_key = None + buckets: Dict[str, Dict[str, int]] = {} + try: + claim_key, buckets = _claim_pending(organization_id) + if not claim_key or not buckets: + return 0 + + flushed = 0 + try: + for prefix, deltas in buckets.items(): + parsed = _parse_bucket_prefix(prefix) + if not parsed: + continue + _upsert_bucket( + db, + organization_id, + parsed, + deltas, + resource_type=parsed.get("resource_type"), + ) + flushed += 1 + db.commit() + except Exception as exc: + db.rollback() + if _is_missing_organization_fk(exc): + # Stale/test org ids must not be restored — that loops forever on beat. + logger.warning( + "llm usage flush dropped for unknown organization {}: {}", + organization_id, + exc, + ) + if claim_key: + _ack_claim(claim_key, organization_id) + return 0 + logger.warning("llm usage catalog flush failed, restoring redis: {}", exc) + _restore_buckets_to_pending(organization_id, buckets) + if claim_key: + try: + _client().delete(claim_key) + except redis.RedisError: + pass + return 0 + + _ack_claim(claim_key, organization_id) + return flushed + finally: + _release_flush_lock(organization_id) + + +def _recover_orphaned_claims() -> None: + """Re-queue claim hashes left behind by a crashed flusher.""" + try: + client = _client() + for claim_key in client.scan_iter(match="usage:flushing:*", count=100): + parts = claim_key.split(":") + if len(parts) < 4: + continue + try: + org_id = UUID(parts[2]) + except ValueError: + continue + if client.exists(_flush_lock_key(org_id)): + continue + buckets = _read_hash_buckets(claim_key) + if buckets: + _restore_buckets_to_pending(org_id, buckets) + client.delete(claim_key) + except redis.RedisError as exc: + logger.warning("llm usage orphan claim recovery failed: {}", exc) + + +def list_pending_organization_ids() -> List[UUID]: + try: + client = _client() + raw_ids = client.smembers("usage:pending:orgs") + result: List[UUID] = [] + stale: List[str] = [] + for value in raw_ids: + try: + org_id = UUID(value) + except ValueError: + stale.append(value) + continue + if client.exists(_pending_hash_key(org_id)): + result.append(org_id) + else: + stale.append(value) + if stale: + client.srem("usage:pending:orgs", *stale) + return result + except (redis.RedisError, ValueError): + return [] + + +def flush_all_usage_to_catalog(db_factory) -> int: + """Flush all orgs with pending usage (Celery beat).""" + _recover_orphaned_claims() + total = 0 + for org_id in list_pending_organization_ids(): + db = db_factory() + try: + total += flush_usage_to_catalog(db, org_id) + except Exception as exc: + db.rollback() + logger.warning("flush_all usage failed for {}: {}", org_id, exc) + finally: + db.close() + return total + + +def merge_usage_totals( + rows: Iterable[Any], +) -> Dict[str, int]: + totals = {field: 0 for field in _METRIC_FIELDS} + for row in rows: + totals["prompt_tokens"] += int(getattr(row, "prompt_tokens", 0) or 0) + totals["completion_tokens"] += int(getattr(row, "completion_tokens", 0) or 0) + totals["cache_read_tokens"] += int(getattr(row, "cache_read_tokens", 0) or 0) + totals["cache_creation_tokens"] += int( + getattr(row, "cache_creation_tokens", 0) or 0 + ) + totals["reasoning_tokens"] += int(getattr(row, "reasoning_tokens", 0) or 0) + totals["call_count"] += int(getattr(row, "call_count", 0) or 0) + totals["total_tokens"] = totals["prompt_tokens"] + totals["completion_tokens"] + return totals diff --git a/app/services/usage/normalize.py b/app/services/usage/normalize.py new file mode 100644 index 00000000..bfdcf5f6 --- /dev/null +++ b/app/services/usage/normalize.py @@ -0,0 +1,99 @@ +"""Normalize LiteLLM / provider usage objects into UsageSnapshot.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Mapping, Optional + + +@dataclass(frozen=True) +class UsageSnapshot: + prompt_tokens: int + completion_tokens: int + cache_read_tokens: int = 0 + cache_creation_tokens: int = 0 + reasoning_tokens: int = 0 + + @property + def total_tokens(self) -> int: + return self.prompt_tokens + self.completion_tokens + + +def _as_int(value: Any) -> int: + try: + return int(value or 0) + except (TypeError, ValueError): + return 0 + + +def _usage_mapping(raw: Any) -> Mapping[str, Any]: + if raw is None: + return {} + if isinstance(raw, Mapping): + return raw + if hasattr(raw, "model_dump"): + try: + return raw.model_dump() + except Exception: + pass + if hasattr(raw, "__dict__"): + return { + key: value + for key, value in vars(raw).items() + if not key.startswith("_") + } + return {} + + +def _details_dict(usage: Mapping[str, Any], key: str) -> Mapping[str, Any]: + details = usage.get(key) + if details is None: + return {} + if isinstance(details, Mapping): + return details + if hasattr(details, "model_dump"): + try: + return details.model_dump() + except Exception: + pass + if hasattr(details, "__dict__"): + return { + k: v for k, v in vars(details).items() if not k.startswith("_") + } + return {} + + +def normalize_llm_usage(raw_response: Any = None, *, usage: Any = None) -> UsageSnapshot: + """Extract token buckets from a LiteLLM response or raw usage object.""" + usage_obj = usage + if usage_obj is None and raw_response is not None: + usage_obj = getattr(raw_response, "usage", None) + + data = _usage_mapping(usage_obj) + prompt_tokens = _as_int(data.get("prompt_tokens") or data.get("input_tokens")) + completion_tokens = _as_int( + data.get("completion_tokens") or data.get("output_tokens") + ) + + cache_read = _as_int(data.get("cache_read_input_tokens")) + cache_creation = _as_int(data.get("cache_creation_input_tokens")) + + prompt_details = _details_dict(data, "prompt_tokens_details") + if not cache_read: + cache_read = _as_int(prompt_details.get("cached_tokens")) + if not cache_creation: + cache_creation = _as_int( + prompt_details.get("cache_write_tokens") + or prompt_details.get("cache_creation_tokens") + ) + + completion_details = _details_dict(data, "completion_tokens_details") + reasoning_tokens = _as_int(completion_details.get("reasoning_tokens")) + + return UsageSnapshot( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + cache_read_tokens=cache_read, + cache_creation_tokens=cache_creation, + reasoning_tokens=reasoning_tokens, + ) diff --git a/app/services/usage/voice_usage_processor.py b/app/services/usage/voice_usage_processor.py new file mode 100644 index 00000000..56d65f57 --- /dev/null +++ b/app/services/usage/voice_usage_processor.py @@ -0,0 +1,80 @@ +"""Voice pipeline processor that records LLM token usage from MetricsFrames.""" + +from __future__ import annotations + +from typing import Optional +from uuid import UUID + +from loguru import logger + + +def create_llm_usage_recorder( + *, + organization_id: UUID | str | None, + workspace_id: UUID | str | None = None, + product_section: str = "playground", + resource_id: UUID | str | None = None, + resource_type: Optional[str] = None, +): + """Build a FrameProcessor that records LLM usage from MetricsFrames. + + Returns None when organization_id is missing or efficientai is unavailable. + """ + if not organization_id: + return None + + try: + from efficientai.frames.frames import Frame, MetricsFrame + from efficientai.metrics.metrics import LLMUsageMetricsData + from efficientai.processors.frame_processor import FrameDirection, FrameProcessor + + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + set_usage_context, + ) + from app.services.usage.llm_usage import record_llm_usage + from app.services.usage.normalize import UsageSnapshot + except Exception as exc: + logger.debug("voice usage recorder unavailable: {}", exc) + return None + + try: + section = LLMUsageProductSection(product_section) + except ValueError: + section = LLMUsageProductSection.OTHER + + org_uuid = UUID(str(organization_id)) + ws_uuid = UUID(str(workspace_id)) if workspace_id else None + res_uuid = UUID(str(resource_id)) if resource_id else None + + set_usage_context( + LLMUsageContext( + organization_id=org_uuid, + workspace_id=ws_uuid, + product_section=section, + resource_id=res_uuid, + resource_type=resource_type, + ) + ) + + class LLMUsageRecorderProcessor(FrameProcessor): + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + if isinstance(frame, MetricsFrame): + for item in frame.data or []: + if isinstance(item, LLMUsageMetricsData) and item.value is not None: + tokens = item.value + snapshot = UsageSnapshot( + prompt_tokens=int(tokens.prompt_tokens or 0), + completion_tokens=int(tokens.completion_tokens or 0), + cache_read_tokens=int(tokens.cache_read_input_tokens or 0), + cache_creation_tokens=int( + tokens.cache_creation_input_tokens or 0 + ), + reasoning_tokens=int(tokens.reasoning_tokens or 0), + ) + record_llm_usage(item.model or "unknown", snapshot) + await self.push_frame(frame, direction) + + return LLMUsageRecorderProcessor() diff --git a/app/services/voice_agent/bot_fast_api.py b/app/services/voice_agent/bot_fast_api.py index 38fa2733..3e2d25e4 100644 --- a/app/services/voice_agent/bot_fast_api.py +++ b/app/services/voice_agent/bot_fast_api.py @@ -98,7 +98,7 @@ def _get_imports(): """ -async def run_bot(websocket_client, google_api_key: str, system_instruction: str = None, organization_id: str = None, agent_id: str = None, persona_id: str = None, scenario_id: str = None, evaluator_id: str = None, result_id: str = None, model_name: str = None, serializer=None, telephony_mode: bool = False, call_short_id: str = None, silence_hangup_secs: float | None = None): +async def run_bot(websocket_client, google_api_key: str, system_instruction: str = None, organization_id: str = None, agent_id: str = None, persona_id: str = None, scenario_id: str = None, evaluator_id: str = None, result_id: str = None, model_name: str = None, serializer=None, telephony_mode: bool = False, call_short_id: str = None, silence_hangup_secs: float | None = None, workspace_id: str = None): """ Run the voice agent bot with the provided Google API key. @@ -246,6 +246,15 @@ async def on_silence_hangup(): if user_transcript_processor: pipeline_processors.append(user_transcript_processor) pipeline_processors.append(llm) + from app.services.usage.voice_usage_processor import create_llm_usage_recorder + + usage_recorder = create_llm_usage_recorder( + organization_id=organization_id, + workspace_id=workspace_id, + product_section="telephony" if telephony_mode else "playground", + ) + if usage_recorder: + pipeline_processors.append(usage_recorder) if agent_transcript_processor: pipeline_processors.append(agent_transcript_processor) pipeline_processors.extend([ @@ -278,18 +287,30 @@ async def on_client_disconnected(transport, client): # RTVI events for efficientai client UI rtvi = imports["RTVIProcessor"](config=imports["RTVIConfig"](config=[])) - pipeline = imports["Pipeline"]( + from app.services.usage.voice_usage_processor import create_llm_usage_recorder + + usage_recorder = create_llm_usage_recorder( + organization_id=organization_id, + workspace_id=workspace_id, + product_section="playground", + ) + pipeline_steps = [ + ws_transport.input(), + user_recorder, + context_aggregator.user(), + rtvi, + llm, + ] + if usage_recorder: + pipeline_steps.append(usage_recorder) + pipeline_steps.extend( [ - ws_transport.input(), - user_recorder, - context_aggregator.user(), - rtvi, - llm, bot_recorder, ws_transport.output(), context_aggregator.assistant(), ] ) + pipeline = imports["Pipeline"](pipeline_steps) task = imports["PipelineTask"]( pipeline, diff --git a/app/workers/config.py b/app/workers/config.py index dc1cb711..3448429d 100644 --- a/app/workers/config.py +++ b/app/workers/config.py @@ -137,4 +137,13 @@ "generate_evaluation_prompt_improvements": {"queue": "evaluations"}, "generate_agent_flowchart": {"queue": "celery"}, "map_agent_flowchart_prompt_sections": {"queue": "celery"}, + "flush_usage_counters": {"queue": "celery"}, +} + +# Periodic flush of Redis LLM usage counters into catalog rollups. +celery_app.conf.beat_schedule = { + "flush-llm-usage-counters": { + "task": "flush_usage_counters", + "schedule": 120.0, + }, } diff --git a/app/workers/tasks/__init__.py b/app/workers/tasks/__init__.py index e4862d8e..04eb0380 100644 --- a/app/workers/tasks/__init__.py +++ b/app/workers/tasks/__init__.py @@ -22,6 +22,7 @@ from . import initiate_vobiz_outbound from . import finalize_telephony_recording from . import call_import_bulk_ops +from . import flush_usage_counters from app.workers.concurrency import eval_dispatch from app.workers.concurrency import fair_dispatch from app.workers.concurrency import fair_diarization_dispatch @@ -115,3 +116,4 @@ materialize_call_import_evaluation_task = ( call_import_bulk_ops.materialize_call_import_evaluation_task ) +flush_usage_counters_task = flush_usage_counters.flush_usage_counters_task diff --git a/app/workers/tasks/evaluate_call_import_row.py b/app/workers/tasks/evaluate_call_import_row.py index 29247d24..e87b3285 100644 --- a/app/workers/tasks/evaluate_call_import_row.py +++ b/app/workers/tasks/evaluate_call_import_row.py @@ -332,6 +332,7 @@ def evaluate_call_import_row_task( slot_task_id = _eval_slot_task_id or self.request.id scoring_inputs: dict[str, Any] | None = None restricted_metric_uuids: list[UUID] | None = None + usage_ctx_token = None try: from app.db_sharding.row_ops import ( close_row_sessions, @@ -363,6 +364,22 @@ def evaluate_call_import_row_task( row_db.commit() return {"status": "failed", "reason": "evaluation_missing"} + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + set_usage_context, + ) + + usage_ctx_token = set_usage_context( + LLMUsageContext( + organization_id=evaluation.organization_id, + workspace_id=evaluation.workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, + resource_id=evaluation.id, + resource_type="call_import_evaluation", + ) + ) + previous_row_status = eval_row.status eval_row.status = "running" @@ -761,6 +778,10 @@ def evaluate_call_import_row_task( if row_db is not None: close_row_sessions(row_db, catalog_db) finally: + if usage_ctx_token is not None: + from app.services.usage.context import reset_usage_context + + reset_usage_context(usage_ctx_token) from app.workers.concurrency.fair_dispatch import ( finish_eval_work_and_redispatch, ) diff --git a/app/workers/tasks/flush_usage_counters.py b/app/workers/tasks/flush_usage_counters.py new file mode 100644 index 00000000..3dc158d0 --- /dev/null +++ b/app/workers/tasks/flush_usage_counters.py @@ -0,0 +1,18 @@ +"""Celery task: flush Redis LLM usage counters into catalog rollups.""" + +from __future__ import annotations + +from loguru import logger + +from app.database import SessionLocal +from app.workers.config import celery_app + + +@celery_app.task(name="flush_usage_counters") +def flush_usage_counters_task() -> dict: + from app.services.usage.llm_usage import flush_all_usage_to_catalog + + flushed = flush_all_usage_to_catalog(SessionLocal) + if flushed: + logger.info("Flushed {} LLM usage rollup buckets", flushed) + return {"flushed_buckets": flushed} diff --git a/app/workers/tasks/generate_evaluation_tldr_insights.py b/app/workers/tasks/generate_evaluation_tldr_insights.py index c1e5de73..b236d0f7 100644 --- a/app/workers/tasks/generate_evaluation_tldr_insights.py +++ b/app/workers/tasks/generate_evaluation_tldr_insights.py @@ -40,14 +40,29 @@ def generate_evaluation_tldr_insights_task( if evaluation is None: return {"error": "evaluation_not_found", "status_code": 404} + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) + try: - summary = _generate_and_persist_tldr_summary( - db, - evaluation, - organization_id=UUID(organization_id), - provider=provider, - model=model, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=evaluation.organization_id, + workspace_id=evaluation.workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, + resource_id=evaluation.id, + resource_type="call_import_evaluation", + ) + ): + summary = _generate_and_persist_tldr_summary( + db, + evaluation, + organization_id=UUID(organization_id), + provider=provider, + model=model, + ) except HTTPException as exc: return {"error": exc.detail, "status_code": exc.status_code} diff --git a/app/workers/tasks/process_evaluator_result.py b/app/workers/tasks/process_evaluator_result.py index d338f294..dc0e1b64 100644 --- a/app/workers/tasks/process_evaluator_result.py +++ b/app/workers/tasks/process_evaluator_result.py @@ -488,6 +488,23 @@ def process_evaluator_result_task(self, result_id: str): logger.error(f"[EvaluatorResult {result_id}] Job not found in database") return {"error": "Evaluator result not found"} + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + set_usage_context, + reset_usage_context, + ) + + usage_token = set_usage_context( + LLMUsageContext( + organization_id=result.organization_id, + workspace_id=result.workspace_id, + product_section=LLMUsageProductSection.EVALUATORS, + resource_id=result.id, + resource_type="evaluator_result", + ) + ) + logger.info(f"[EvaluatorResult {result.result_id}] Starting processing task") result.celery_task_id = self.request.id @@ -746,4 +763,9 @@ def process_evaluator_result_task(self, result_id: str): except Exception as exc: raise self.retry(exc=exc, countdown=60) finally: + try: + if "usage_token" in locals() and usage_token is not None: + reset_usage_context(usage_token) + except Exception: + pass db.close() diff --git a/app/workers/tasks/transcribe_call_import_row.py b/app/workers/tasks/transcribe_call_import_row.py index c6e89198..0f342ab3 100644 --- a/app/workers/tasks/transcribe_call_import_row.py +++ b/app/workers/tasks/transcribe_call_import_row.py @@ -391,6 +391,11 @@ def _persist_diarization_failure( def _run_diarization_pipeline(ctx: dict[str, Any]) -> dict[str, Any]: """STT / S3 / LLM diarisation without a long-lived DB session.""" from app.models.enums import ModelProvider + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) from app.workers.tasks.helpers.llm_diarisation import ( LLMDiarisationError, diarize_audio_with_llm, @@ -406,6 +411,48 @@ def _run_diarization_pipeline(ctx: dict[str, Any]) -> dict[str, Any]: llm_credential_uuid = ctx["llm_credential_uuid"] effective_prompt = ctx["effective_prompt"] + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=ctx.get("workspace_id"), + product_section=LLMUsageProductSection.CALL_IMPORTS, + resource_id=ctx.get("call_import_id"), + resource_type="call_import", + ) + ): + return _run_diarization_pipeline_inner( + ctx, + row_id=row_id, + normalised_mode=normalised_mode, + recording_key=recording_key, + organization_id=organization_id, + llm_provider_value=llm_provider_value, + llm_model_value=llm_model_value, + llm_credential_uuid=llm_credential_uuid, + effective_prompt=effective_prompt, + ModelProvider=ModelProvider, + LLMDiarisationError=LLMDiarisationError, + diarize_audio_with_llm=diarize_audio_with_llm, + diarize_transcript_with_llm=diarize_transcript_with_llm, + ) + + +def _run_diarization_pipeline_inner( + ctx: dict[str, Any], + *, + row_id, + normalised_mode, + recording_key, + organization_id, + llm_provider_value, + llm_model_value, + llm_credential_uuid, + effective_prompt, + ModelProvider, + LLMDiarisationError, + diarize_audio_with_llm, + diarize_transcript_with_llm, +) -> dict[str, Any]: plain_text: Optional[str] = None raw_turns: Optional[List[Dict[str, Any]]] = None @@ -911,6 +958,8 @@ def transcribe_call_import_row_task( "normalised_mode": normalised_mode, "recording_key": recording_key, "organization_id": row.organization_id, + "workspace_id": getattr(row, "workspace_id", None), + "call_import_id": row.call_import_id, "stt_provider": provider_enum.value if provider_enum else None, "stt_model": stt_model, "credential_uuid": credential_uuid, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d745a02..d18118b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,9 @@ import SelectOrganization from './pages/auth/SelectOrganization' // Dashboard import Dashboard from './pages/dashboard/Dashboard' +// Usage +import Usage from './pages/usage/Usage' + // Prompt Partials import PromptPartials from './pages/promptPartials/PromptPartials' @@ -182,6 +185,7 @@ function App() { } /> } /> } /> + } /> } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index c621a485..bfe7c0ad 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -25,6 +25,7 @@ import { Mic, Bot, Activity, + PieChart, Bell, History, Key, @@ -99,6 +100,13 @@ const navigationSections: NavSection[] = [ { name: 'Calls', href: '/observability/calls', icon: Phone }, ], }, + { + title: 'Usage', + icon: PieChart, + items: [ + { name: 'Overview', href: '/usage', icon: PieChart }, + ], + }, { title: 'Alerting', icon: Bell, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 1ac0029d..f8701723 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2991,6 +2991,88 @@ class ApiClient { return response.data } + async getOrgUsageSummary(params: { + start?: string + end?: string + workspace_id?: string + product_section?: string + model?: string + resource_id?: string + }): Promise<{ + start: string + end: string + totals: { + prompt_tokens: number + completion_tokens: number + total_tokens: number + cache_read_tokens: number + cache_creation_tokens: number + reasoning_tokens: number + call_count: number + } + last_updated_at?: string | null + }> { + const response = await this.client.get('/api/v1/organizations/usage/summary', { + params, + }) + return response.data + } + + async getOrgUsageBreakdown(params: { + start?: string + end?: string + group_by?: 'workspace' | 'product_section' | 'model' | 'resource' + workspace_id?: string + product_section?: string + model?: string + resource_id?: string + limit?: number + offset?: number + }): Promise<{ + start: string + end: string + group_by: string + rows: Array<{ + workspace_id?: string | null + workspace_name?: string | null + product_section?: string | null + product_section_label?: string | null + model?: string | null + resource_id?: string | null + resource_type?: string | null + resource_label?: string | null + prompt_tokens: number + completion_tokens: number + total_tokens: number + cache_read_tokens: number + cache_creation_tokens: number + reasoning_tokens: number + call_count: number + }> + total_count: number + last_updated_at?: string | null + }> { + const response = await this.client.get('/api/v1/organizations/usage/breakdown', { + params, + }) + return response.data + } + + async getOrgUsageFilters(params?: { + start?: string + end?: string + }): Promise<{ + workspaces: Array<{ id: string; name: string }> + product_sections: Array<{ id: string; label: string }> + models: string[] + resources: Array<{ id: string; type?: string; label: string }> + }> { + const response = await this.client.get('/api/v1/organizations/usage/filters', { + params, + }) + return response.data + } + async getAllModels(): Promise> { const response = await this.client.get('/api/v1/model-config/models') return response.data diff --git a/frontend/src/pages/usage/Usage.tsx b/frontend/src/pages/usage/Usage.tsx new file mode 100644 index 00000000..0f7cdcb9 --- /dev/null +++ b/frontend/src/pages/usage/Usage.tsx @@ -0,0 +1,294 @@ +import { useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import { useSearchParams } from 'react-router-dom' +import { Card, CardBody, Spinner } from '@heroui/react' +import { Activity } from 'lucide-react' +import { apiClient } from '../../lib/api' + +type GroupBy = 'workspace' | 'product_section' | 'model' | 'resource' + +function formatNumber(value: number): string { + return new Intl.NumberFormat().format(value || 0) +} + +function toDateInput(d: Date): string { + return d.toISOString().slice(0, 10) +} + +function FilterField({ + label, + children, +}: { + label: string + children: React.ReactNode +}) { + return ( + + ) +} + +const fieldClassName = + 'h-9 rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-900 outline-none focus:border-indigo-400 focus:ring-1 focus:ring-indigo-200' + +export default function Usage() { + const [searchParams, setSearchParams] = useSearchParams() + const today = useMemo(() => new Date(), []) + const defaultStart = useMemo(() => { + const d = new Date() + d.setDate(d.getDate() - 29) + return toDateInput(d) + }, []) + + const start = searchParams.get('start') || defaultStart + const end = searchParams.get('end') || toDateInput(today) + const groupBy = (searchParams.get('group_by') as GroupBy) || 'workspace' + const workspaceId = searchParams.get('workspace_id') || '' + const productSection = searchParams.get('product_section') || '' + const model = searchParams.get('model') || '' + const resourceId = searchParams.get('resource_id') || '' + + const setParam = (key: string, value: string) => { + const next = new URLSearchParams(searchParams) + if (!value) next.delete(key) + else next.set(key, value) + setSearchParams(next) + } + + const filterParams = { + start, + end, + workspace_id: workspaceId || undefined, + product_section: productSection || undefined, + model: model || undefined, + resource_id: resourceId || undefined, + } + + const { data: summary, isLoading: summaryLoading } = useQuery({ + queryKey: ['org-usage', 'summary', filterParams], + queryFn: () => apiClient.getOrgUsageSummary(filterParams), + }) + + const { data: breakdown, isLoading: breakdownLoading } = useQuery({ + queryKey: ['org-usage', 'breakdown', groupBy, filterParams], + queryFn: () => + apiClient.getOrgUsageBreakdown({ + ...filterParams, + group_by: groupBy, + limit: 100, + }), + }) + + const { data: filters } = useQuery({ + queryKey: ['org-usage', 'filters', start, end], + queryFn: () => apiClient.getOrgUsageFilters({ start, end }), + }) + + const totals = summary?.totals + const rows = breakdown?.rows || [] + + const dimensionLabel = (row: (typeof rows)[number]): string => { + if (groupBy === 'workspace') return row.workspace_name || 'Unknown' + if (groupBy === 'product_section') + return row.product_section_label || row.product_section || '—' + if (groupBy === 'model') return row.model || '—' + return row.resource_label || row.resource_id || 'Unscoped' + } + + return ( +
+
+
+

+ + Usage +

+

+ Organization-wide LLM tokens and calls. Filter by workspace, product + section, model, or evaluation. +

+
+ {summary?.last_updated_at && ( +

+ Updated {new Date(summary.last_updated_at).toLocaleString()} +

+ )} +
+ +
+ + + + +
+ + {(totals?.cache_read_tokens || totals?.cache_creation_tokens || totals?.reasoning_tokens) ? ( +
+ + + +
+ ) : null} + + + +
+ + setParam('start', e.target.value)} + /> + + + setParam('end', e.target.value)} + /> + + + + + + + + + + + + + +
+
+
+ + + + {breakdownLoading ? ( +
+ +
+ ) : rows.length === 0 ? ( +
+ No usage in this period. Run evaluations or playground calls, then + refresh in a minute. +
+ ) : ( + + + + + + + + + + + + + {rows.map((row, idx) => ( + + + + + + + + + ))} + +
+ {groupBy === 'workspace' + ? 'Workspace' + : groupBy === 'product_section' + ? 'Section' + : groupBy === 'model' + ? 'Model' + : 'Resource'} + CallsInputOutputTotalCache read
{dimensionLabel(row)} + {formatNumber(row.call_count)} + + {formatNumber(row.prompt_tokens)} + + {formatNumber(row.completion_tokens)} + + {formatNumber(row.total_tokens)} + + {formatNumber(row.cache_read_tokens)} +
+ )} +
+
+
+ ) +} + +function StatCard({ + label, + value, + loading, +}: { + label: string + value?: number + loading?: boolean +}) { + return ( + + +

+ {label} +

+

+ {loading ? '—' : formatNumber(value || 0)} +

+
+
+ ) +} diff --git a/tests/test_services/test_usage/__init__.py b/tests/test_services/test_usage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_services/test_usage/test_llm_usage.py b/tests/test_services/test_usage/test_llm_usage.py new file mode 100644 index 00000000..0fdd77bf --- /dev/null +++ b/tests/test_services/test_usage/test_llm_usage.py @@ -0,0 +1,387 @@ +"""Tests for Redis-buffered LLM usage counters and flush durability.""" + +from __future__ import annotations + +from datetime import date +from typing import Any, Dict, List, Optional +from unittest.mock import MagicMock +from uuid import uuid4 + +import pytest + +from app.services.usage import llm_usage as usage_mod +from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + ensure_usage_context, + get_usage_context, + infer_product_section_from_path, + llm_usage_context, + reset_usage_context, + set_usage_context, +) +from app.services.usage.normalize import UsageSnapshot + + +class _FakePipeline: + def __init__(self, client: "_FakeRedis"): + self._client = client + self._ops: List[tuple] = [] + + def hincrby(self, key: str, field: str, amount: int): + self._ops.append(("hincrby", key, field, amount)) + return self + + def sadd(self, key: str, *members: str): + self._ops.append(("sadd", key, members)) + return self + + def expire(self, key: str, ttl: int): + self._ops.append(("expire", key, ttl)) + return self + + def execute(self): + results = [] + for op in self._ops: + kind = op[0] + if kind == "hincrby": + results.append(self._client.hincrby(op[1], op[2], op[3])) + elif kind == "sadd": + results.append(self._client.sadd(op[1], *op[2])) + elif kind == "expire": + results.append(self._client.expire(op[1], op[2])) + self._ops.clear() + return results + + +class _FakeRedis: + """Minimal Redis stand-in for usage counter tests.""" + + def __init__(self): + self.hashes: Dict[str, Dict[str, int]] = {} + self.sets: Dict[str, set] = {} + self.kv: Dict[str, str] = {} + + def pipeline(self): + return _FakePipeline(self) + + def hincrby(self, key: str, field: str, amount: int) -> int: + bucket = self.hashes.setdefault(key, {}) + bucket[field] = int(bucket.get(field, 0)) + int(amount) + return bucket[field] + + def hgetall(self, key: str) -> Dict[str, str]: + return {k: str(v) for k, v in self.hashes.get(key, {}).items()} + + def hdel(self, key: str, *fields: str) -> int: + bucket = self.hashes.get(key, {}) + deleted = 0 + for field in fields: + if field in bucket: + del bucket[field] + deleted += 1 + if key in self.hashes and not self.hashes[key]: + del self.hashes[key] + return deleted + + def hlen(self, key: str) -> int: + return len(self.hashes.get(key, {})) + + def sadd(self, key: str, *members: str) -> int: + s = self.sets.setdefault(key, set()) + before = len(s) + s.update(members) + return len(s) - before + + def srem(self, key: str, *members: str) -> int: + s = self.sets.get(key, set()) + removed = 0 + for member in members: + if member in s: + s.remove(member) + removed += 1 + return removed + + def smembers(self, key: str) -> set: + return set(self.sets.get(key, set())) + + def exists(self, key: str) -> int: + return int(key in self.hashes or key in self.sets or key in self.kv) + + def delete(self, *keys: str) -> int: + deleted = 0 + for key in keys: + if key in self.hashes: + del self.hashes[key] + deleted += 1 + if key in self.sets: + del self.sets[key] + deleted += 1 + if key in self.kv: + del self.kv[key] + deleted += 1 + return deleted + + def expire(self, key: str, ttl: int) -> bool: + return self.exists(key) == 1 + + def set(self, key: str, value: str, nx: bool = False, ex: Optional[int] = None) -> Optional[bool]: + if nx and key in self.kv: + return None + self.kv[key] = value + return True + + def get(self, key: str) -> Optional[str]: + return self.kv.get(key) + + def rename(self, src: str, dst: str) -> bool: + if src not in self.hashes: + raise KeyError(src) + self.hashes[dst] = self.hashes.pop(src) + return True + + def eval(self, script: str, numkeys: int, *keys_and_args: Any) -> int: + pending, claim = keys_and_args[0], keys_and_args[1] + if pending not in self.hashes: + return 0 + self.rename(pending, claim) + return 1 + + def scan_iter(self, match: str = "*", count: int = 100): + prefix = match.rstrip("*") + for key in list(self.hashes.keys()): + if key.startswith(prefix): + yield key + + +@pytest.fixture(autouse=True) +def fake_redis(monkeypatch): + """Always use in-memory Redis for this module — never touch real REDIS_URL.""" + client = _FakeRedis() + usage_mod._redis = client + + def _forbid_real_redis(*_args, **_kwargs): + raise AssertionError( + "usage tests must not open real Redis; fake_redis fixture failed to isolate" + ) + + monkeypatch.setattr(usage_mod.redis, "from_url", _forbid_real_redis) + yield client + usage_mod._redis = None + + +@pytest.fixture +def org_ctx(): + org_id = uuid4() + workspace_id = uuid4() + ctx = LLMUsageContext( + organization_id=org_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, + resource_id=uuid4(), + resource_type="call_import_evaluation", + ) + return org_id, workspace_id, ctx + + +def test_record_increments_pending_and_counts_zero_token_calls(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=10, completion_tokens=5), + usage_date=date(2026, 8, 11), + ) + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=0, completion_tokens=0), + usage_date=date(2026, 8, 11), + ) + + pending_key = usage_mod._pending_hash_key(org_id) + fields = fake_redis.hgetall(pending_key) + assert str(org_id) in fake_redis.smembers("usage:pending:orgs") + + prompt = sum(int(v) for k, v in fields.items() if k.endswith("|prompt_tokens")) + completion = sum( + int(v) for k, v in fields.items() if k.endswith("|completion_tokens") + ) + calls = sum(int(v) for k, v in fields.items() if k.endswith("|call_count")) + assert prompt == 10 + assert completion == 5 + assert calls == 2 + + +def test_record_skipped_without_context(fake_redis): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=10, completion_tokens=5), + ) + assert fake_redis.hashes == {} + assert fake_redis.sets == {} + + +def test_flush_commits_and_acks_claim(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=4, completion_tokens=2), + usage_date=date(2026, 8, 11), + ) + + db = MagicMock() + db.execute.return_value = MagicMock(rowcount=1) + + flushed = usage_mod.flush_usage_to_catalog(db, org_id) + assert flushed == 1 + db.commit.assert_called_once() + assert usage_mod._pending_hash_key(org_id) not in fake_redis.hashes + assert not any(k.startswith("usage:flushing:") for k in fake_redis.hashes) + assert str(org_id) not in fake_redis.smembers("usage:pending:orgs") + assert fake_redis.get(usage_mod._flush_lock_key(org_id)) is None + + +def test_flush_restores_redis_when_db_fails(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=7, completion_tokens=3), + usage_date=date(2026, 8, 11), + ) + + db = MagicMock() + db.execute.side_effect = RuntimeError("db down") + + flushed = usage_mod.flush_usage_to_catalog(db, org_id) + assert flushed == 0 + db.rollback.assert_called() + db.commit.assert_not_called() + + pending = fake_redis.hgetall(usage_mod._pending_hash_key(org_id)) + prompt = sum(int(v) for k, v in pending.items() if k.endswith("|prompt_tokens")) + completion = sum( + int(v) for k, v in pending.items() if k.endswith("|completion_tokens") + ) + assert prompt == 7 + assert completion == 3 + assert str(org_id) in fake_redis.smembers("usage:pending:orgs") + assert not any(k.startswith("usage:flushing:") for k in fake_redis.hashes) + + +def test_flush_drops_pending_when_organization_missing(fake_redis, org_ctx): + """Unknown org FK must not restore Redis (avoids infinite beat retries).""" + from sqlalchemy.exc import IntegrityError + + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=3, completion_tokens=1), + usage_date=date(2026, 8, 11), + ) + + db = MagicMock() + db.execute.side_effect = IntegrityError( + "INSERT", + {}, + Exception( + 'insert or update on table "llm_usage_daily" violates foreign key ' + 'constraint "llm_usage_daily_organization_id_fkey"' + ), + ) + + flushed = usage_mod.flush_usage_to_catalog(db, org_id) + assert flushed == 0 + db.rollback.assert_called() + assert usage_mod._pending_hash_key(org_id) not in fake_redis.hashes + assert str(org_id) not in fake_redis.smembers("usage:pending:orgs") + assert not any(k.startswith("usage:flushing:") for k in fake_redis.hashes) + + +def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=9, completion_tokens=1), + usage_date=date(2026, 8, 11), + ) + + db_a = MagicMock() + db_a.execute.return_value = MagicMock(rowcount=0) # force INSERT path + db_b = MagicMock() + db_b.execute.return_value = MagicMock(rowcount=1) + + first = usage_mod.flush_usage_to_catalog(db_a, org_id) + second = usage_mod.flush_usage_to_catalog(db_b, org_id) + + assert first == 1 + assert second == 0 + assert db_a.commit.call_count == 1 + assert db_b.commit.call_count == 0 + # INSERT once for the claimed bucket; second flusher never upserts. + assert db_a.execute.call_count == 2 # UPDATE miss + INSERT + assert db_b.execute.call_count == 0 + + +def test_ensure_usage_context_and_path_inference(): + assert ( + infer_product_section_from_path("/api/v1/call-imports/abc") + == LLMUsageProductSection.CALL_IMPORTS + ) + assert ( + infer_product_section_from_path("/api/v1/chat/completion") + == LLMUsageProductSection.CHAT + ) + assert ( + infer_product_section_from_path("/api/v1/unknown") + == LLMUsageProductSection.OTHER + ) + + org_id = uuid4() + workspace_id = uuid4() + token = set_usage_context(None) + try: + assert get_usage_context() is None + created = ensure_usage_context( + org_id, + product_section=LLMUsageProductSection.PLAYGROUND, + ) + assert created is not None + assert get_usage_context().organization_id == org_id + assert get_usage_context().product_section == LLMUsageProductSection.PLAYGROUND + + # Enrich missing workspace / upgrade OTHER section. + upgraded = ensure_usage_context( + org_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.CHAT, + ) + assert upgraded is not None + assert get_usage_context().workspace_id == workspace_id + # Existing non-OTHER section is preserved. + assert get_usage_context().product_section == LLMUsageProductSection.PLAYGROUND + finally: + reset_usage_context(token) + + +def test_ensure_uses_workspace_and_section_hints(): + from app.services.usage.context import reset_usage_hints, set_usage_hints + + org_id = uuid4() + workspace_id = uuid4() + ctx_token = set_usage_context(None) + hint_tokens = set_usage_hints( + workspace_id=workspace_id, + product_section=LLMUsageProductSection.METRICS, + ) + try: + created = ensure_usage_context(org_id) + assert created is not None + assert get_usage_context().workspace_id == workspace_id + assert get_usage_context().product_section == LLMUsageProductSection.METRICS + finally: + reset_usage_hints(hint_tokens) + reset_usage_context(ctx_token) From f0ebac9382e17d504a46b6f277d526676f391cc4 Mon Sep 17 00:00:00 2001 From: M Sami Date: Thu, 13 Aug 2026 03:17:12 +0530 Subject: [PATCH 16/55] feat: enhance LLM/STT usage tracking with additional context and metrics across services --- app/api/v1/routes/agents.py | 34 +- app/api/v1/routes/call_import_evaluations.py | 30 +- app/api/v1/routes/org_usage.py | 1242 ++++++++++++++-- app/api/v1/routes/playground.py | 291 +--- app/api/v1/routes/vobiz_telephony.py | 1 + app/api/v1/routes/voice_agent.py | 1 + .../063_usage_kind_stt_and_buffer.py | 233 +++ .../064_usage_kind_tts_characters.py | 74 + app/migrations/065_usage_context_jsonb.py | 216 +++ ...ll_llm_usage_workspace_from_call_import.py | 279 ++++ .../067_fix_llm_usage_bucket_unique_index.py | 264 ++++ app/models/database.py | 10 +- app/services/ai/llm_gateway.py | 56 +- app/services/ai/llm_service.py | 4 +- app/services/ai/stt_clients/google.py | 24 + app/services/ai/transcription_service.py | 45 + app/services/ai/tts_service.py | 37 + .../evaluators/call_data_transcript.py | 195 +++ app/services/judge_alignment/gepa_bridge.py | 51 +- app/services/optimization/gepa_service.py | 26 +- .../testing/test_agent_bridge_service.py | 2 + app/services/usage/__init__.py | 22 +- app/services/usage/bucket_context.py | 114 ++ app/services/usage/call_import_context.py | 200 +++ app/services/usage/context.py | 111 +- app/services/usage/dates.py | 38 + app/services/usage/llm_usage.py | 779 ++++++++-- app/services/usage/usage_labels.py | 424 ++++++ app/services/usage/voice_usage_processor.py | 56 +- app/services/voice_agent/bot_fast_api.py | 8 +- app/services/voice_agent/voice_bundle.py | 32 + .../webrtc_bridge/test_agent_processor.py | 88 +- app/workers/tasks/agent_flowchart_jobs.py | 40 +- app/workers/tasks/evaluate_call_import_row.py | 16 +- .../generate_evaluation_metric_clusters.py | 30 +- ...generate_evaluation_prompt_improvements.py | 42 +- .../generate_evaluation_tldr_insights.py | 14 +- .../generate_evaluation_user_insights.py | 34 +- app/workers/tasks/process_evaluator_result.py | 671 +++++---- app/workers/tasks/run_judge_alignment.py | 8 +- app/workers/tasks/run_prompt_optimization.py | 27 +- .../tasks/transcribe_call_import_row.py | 32 +- app/workers/tasks/tts_comparison.py | 200 +-- frontend/src/lib/api.ts | 42 +- .../agents/components/AgentTalkSidebar.tsx | 29 +- frontend/src/pages/usage/SearchableSelect.tsx | 125 ++ frontend/src/pages/usage/Usage.tsx | 1305 ++++++++++++++--- .../src/pages/usage/UsageDateRangePicker.tsx | 252 ++++ frontend/src/pages/usage/UsageDrillPath.tsx | 40 + frontend/src/pages/usage/UsageFiltersBar.tsx | 353 +++++ frontend/src/pages/usage/usageProductHints.ts | 47 + frontend/src/pages/usage/usageTheme.ts | 18 + frontend/src/pages/usage/usageTimezone.ts | 11 + .../test_call_data_transcript.py | 34 + .../test_usage/test_bucket_context.py | 40 + .../test_usage/test_call_import_context.py | 120 ++ .../test_usage/test_llm_usage.py | 210 ++- .../test_usage/test_usage_dates.py | 32 + .../test_usage/test_usage_labels.py | 287 ++++ .../test_process_evaluator_result_helpers.py | 41 +- 60 files changed, 7935 insertions(+), 1152 deletions(-) create mode 100644 app/migrations/063_usage_kind_stt_and_buffer.py create mode 100644 app/migrations/064_usage_kind_tts_characters.py create mode 100644 app/migrations/065_usage_context_jsonb.py create mode 100644 app/migrations/066_backfill_llm_usage_workspace_from_call_import.py create mode 100644 app/migrations/067_fix_llm_usage_bucket_unique_index.py create mode 100644 app/services/evaluators/call_data_transcript.py create mode 100644 app/services/usage/bucket_context.py create mode 100644 app/services/usage/call_import_context.py create mode 100644 app/services/usage/dates.py create mode 100644 app/services/usage/usage_labels.py create mode 100644 frontend/src/pages/usage/SearchableSelect.tsx create mode 100644 frontend/src/pages/usage/UsageDateRangePicker.tsx create mode 100644 frontend/src/pages/usage/UsageDrillPath.tsx create mode 100644 frontend/src/pages/usage/UsageFiltersBar.tsx create mode 100644 frontend/src/pages/usage/usageProductHints.ts create mode 100644 frontend/src/pages/usage/usageTheme.ts create mode 100644 frontend/src/pages/usage/usageTimezone.ts create mode 100644 tests/test_services/test_evaluators/test_call_data_transcript.py create mode 100644 tests/test_services/test_usage/test_bucket_context.py create mode 100644 tests/test_services/test_usage/test_call_import_context.py create mode 100644 tests/test_services/test_usage/test_usage_dates.py create mode 100644 tests/test_services/test_usage/test_usage_labels.py diff --git a/app/api/v1/routes/agents.py b/app/api/v1/routes/agents.py index 9d1d77b0..4c4b295e 100644 --- a/app/api/v1/routes/agents.py +++ b/app/api/v1/routes/agents.py @@ -149,7 +149,10 @@ async def generate_agent_description( db: Session = Depends(get_db), ): """Generate an agent description using AI from a brief description.""" + from contextlib import nullcontext + from app.services.ai.llm_service import llm_service + from app.services.usage.context import llm_usage_context, usage_context_for_agent from app.services.testing.test_agent_simulation_prompt import ( format_scenarios_for_generation_context, format_scenarios_reference_appendix, @@ -202,15 +205,28 @@ async def generate_agent_description( ] try: - result = llm_service.generate_response( - messages=messages, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - temperature=0.7, - max_tokens=4000, - ) + usage_ctx = nullcontext() + if data.agent_id: + agent_for_usage = db.query(Agent).filter( + Agent.id == data.agent_id, + Agent.organization_id == organization_id, + Agent.workspace_id == workspace_id, + ).first() + if agent_for_usage: + usage_ctx = llm_usage_context( + usage_context_for_agent(agent_for_usage, workspace_id=workspace_id) + ) + + with usage_ctx: + result = llm_service.generate_response( + messages=messages, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + temperature=0.7, + max_tokens=4000, + ) content = result["text"] if data.append_scenarios_to_output and linked_scenarios: appendix = format_scenarios_reference_appendix(linked_scenarios) diff --git a/app/api/v1/routes/call_import_evaluations.py b/app/api/v1/routes/call_import_evaluations.py index 030c98ef..880393c7 100644 --- a/app/api/v1/routes/call_import_evaluations.py +++ b/app/api/v1/routes/call_import_evaluations.py @@ -3702,20 +3702,18 @@ async def generate_call_import_evaluation_pdf_report( ) return _pdf_report_response_from_row(cached_pdf_report, cache_hit=True) - from app.services.usage.context import ( - LLMUsageContext, - LLMUsageProductSection, - llm_usage_context, + from app.services.usage.call_import_context import ( + call_import_evaluation_usage_context, ) + from app.services.usage.context import llm_usage_context with llm_usage_context( - LLMUsageContext( + call_import_evaluation_usage_context( organization_id=organization_id, workspace_id=getattr(call_import, "workspace_id", None) or evaluation.workspace_id, - product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, - resource_id=evaluation.id, - resource_type="call_import_evaluation", + evaluation_id=evaluation.id, + call_import_id=evaluation.call_import_id, ) ): narrative = _generate_report_narrative( @@ -5353,11 +5351,10 @@ def _generate_and_persist_tldr_summary( from app.services.ai.llm_resolver import get_llm_provider_and_model from app.services.ai.llm_service import llm_service - from app.services.usage.context import ( - LLMUsageContext, - LLMUsageProductSection, - llm_usage_context, + from app.services.usage.call_import_context import ( + call_import_evaluation_usage_context, ) + from app.services.usage.context import llm_usage_context provider_enum, model_str = get_llm_provider_and_model( organization_id, db, provider, model @@ -5365,12 +5362,11 @@ def _generate_and_persist_tldr_summary( try: with llm_usage_context( - LLMUsageContext( + call_import_evaluation_usage_context( organization_id=organization_id, workspace_id=evaluation.workspace_id, - product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, - resource_id=evaluation.id, - resource_type="call_import_evaluation", + evaluation_id=evaluation.id, + call_import_id=evaluation.call_import_id, ) ): llm_result = llm_service.generate_response( @@ -5388,8 +5384,8 @@ def _generate_and_persist_tldr_summary( status_code=502, detail=f"LLM call failed: {e}" ) from e - summary = _parse_insights_response(llm_result.get("text", "")) total = int(evaluation.total_rows or 0) + summary = _parse_insights_response(llm_result.get("text", "")) ui_completed = min(int(evaluation.completed_rows or 0), total) if total else int( evaluation.completed_rows or 0 ) diff --git a/app/api/v1/routes/org_usage.py b/app/api/v1/routes/org_usage.py index 05f984a0..4c702138 100644 --- a/app/api/v1/routes/org_usage.py +++ b/app/api/v1/routes/org_usage.py @@ -2,23 +2,49 @@ from __future__ import annotations -from datetime import date, datetime, timedelta, timezone +from datetime import date, datetime, timezone +from app.services.usage.dates import usage_date_filter_bounds, usage_local_today from typing import List, Literal, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field -from sqlalchemy import func +from sqlalchemy import and_, case, cast, func, or_, select, String +from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.orm import Session from app.database import get_db from app.dependencies import get_organization_id -from app.models.database import CallImportEvaluation, LLMUsageDaily, Workspace -from app.services.usage.llm_usage import flush_usage_to_catalog, merge_usage_totals +from app.models.database import ( + CallImport, + CallImportEvaluation, + CallImportEvaluationRow, + CallImportRow, + CallImportTag, + CallImportTagAssignment, + Agent, + LLMUsageDaily, + Workspace, +) +from app.services.usage.llm_usage import flush_usage_to_catalog +from app.services.usage.usage_labels import ( + labels_for_call_import_ids, + labels_for_resource_buckets, + usage_kind_label, + UsageNameResolver, + parse_uuid, +) router = APIRouter(prefix="/organizations/usage", tags=["Usage"]) -GroupBy = Literal["workspace", "product_section", "model", "resource"] +GroupBy = Literal[ + "workspace", + "product_section", + "model", + "resource", + "usage_kind", + "call_import", +] SECTION_LABELS = { "call_import_evaluations": "Call Import Evaluations", @@ -39,6 +65,27 @@ "other": "Other", } +_LABEL_ROW_LIMIT = 5000 + + +def _usage_row_weight(): + return ( + LLMUsageDaily.prompt_tokens + + LLMUsageDaily.completion_tokens + + LLMUsageDaily.cache_read_tokens + + LLMUsageDaily.cache_creation_tokens + + LLMUsageDaily.reasoning_tokens + + LLMUsageDaily.audio_seconds + + LLMUsageDaily.tts_characters + ) + + +def _label_row_order(): + return ( + _usage_row_weight().desc(), + LLMUsageDaily.call_count.desc(), + ) + class UsageTotals(BaseModel): prompt_tokens: int = 0 @@ -47,6 +94,8 @@ class UsageTotals(BaseModel): cache_read_tokens: int = 0 cache_creation_tokens: int = 0 reasoning_tokens: int = 0 + audio_seconds: int = 0 + tts_characters: int = 0 call_count: int = 0 @@ -66,12 +115,17 @@ class UsageBreakdownRow(BaseModel): resource_id: Optional[UUID] = None resource_type: Optional[str] = None resource_label: Optional[str] = None + call_import_id: Optional[UUID] = None + call_import_label: Optional[str] = None + usage_kind: Optional[str] = None prompt_tokens: int = 0 completion_tokens: int = 0 total_tokens: int = 0 cache_read_tokens: int = 0 cache_creation_tokens: int = 0 reasoning_tokens: int = 0 + audio_seconds: int = 0 + tts_characters: int = 0 call_count: int = 0 @@ -81,20 +135,249 @@ class UsageBreakdownResponse(BaseModel): group_by: GroupBy rows: List[UsageBreakdownRow] total_count: int + truncated_at_limit: bool = False last_updated_at: Optional[datetime] = None class UsageFiltersResponse(BaseModel): workspaces: List[dict] = Field(default_factory=list) product_sections: List[dict] = Field(default_factory=list) + call_imports: List[dict] = Field(default_factory=list) + evaluations: List[dict] = Field(default_factory=list) models: List[str] = Field(default_factory=list) resources: List[dict] = Field(default_factory=list) + usage_kinds: List[dict] = Field(default_factory=list) + datasets: List[str] = Field(default_factory=list) + tags: List[dict] = Field(default_factory=list) -def _default_range() -> tuple[date, date]: - end = datetime.now(timezone.utc).date() - start = end - timedelta(days=29) - return start, end +def _parse_usage_range( + start: Optional[date], + end: Optional[date], + tz: Optional[str], +) -> tuple[date, date, date, date]: + """Return display_start, display_end, filter_start, filter_end.""" + today = usage_local_today(tz) + display_start = start or today + display_end = end or today + filter_start, filter_end = usage_date_filter_bounds( + display_start, display_end, tz + ) + return display_start, display_end, filter_start, filter_end + + +def _evaluation_id_expr(): + return func.coalesce( + LLMUsageDaily.context["evaluation_id"].astext, + case( + ( + LLMUsageDaily.context["resource_type"].astext + == "call_import_evaluation", + LLMUsageDaily.context["resource_id"].astext, + ), + else_=None, + ), + ) + + +def _resource_id_expr(): + """Resource rollup key: explicit resource_id or agent_id fallback.""" + return func.coalesce( + LLMUsageDaily.context["resource_id"].astext, + LLMUsageDaily.context["agent_id"].astext, + ) + + +def _resource_type_expr(): + rid_expr = _resource_id_expr() + return func.coalesce( + LLMUsageDaily.context["resource_type"].astext, + case( + (LLMUsageDaily.context["agent_id"].astext.isnot(None), "agent"), + ( + and_( + LLMUsageDaily.product_section == "agents", + rid_expr.isnot(None), + ), + "agent", + ), + else_=None, + ), + ) + + +def _call_import_group_expr(): + """Resolve call import id from context, resource row, evaluation, or import row.""" + eval_call_import = ( + select(CallImportEvaluation.call_import_id) + .where( + CallImportEvaluation.id == cast(_evaluation_id_expr(), PG_UUID) + ) + .correlate(LLMUsageDaily) + .scalar_subquery() + ) + row_call_import = ( + select(CallImportRow.call_import_id) + .where( + CallImportRow.id + == cast(LLMUsageDaily.context["call_import_row_id"].astext, PG_UUID) + ) + .correlate(LLMUsageDaily) + .scalar_subquery() + ) + eval_row_call_import = ( + select(CallImportEvaluation.call_import_id) + .select_from(CallImportEvaluationRow) + .join( + CallImportEvaluation, + CallImportEvaluation.id == CallImportEvaluationRow.evaluation_id, + ) + .where( + CallImportEvaluationRow.id + == cast(LLMUsageDaily.context["evaluation_row_id"].astext, PG_UUID) + ) + .correlate(LLMUsageDaily) + .scalar_subquery() + ) + return cast( + func.coalesce( + LLMUsageDaily.context["call_import_id"].astext, + case( + ( + LLMUsageDaily.context["resource_type"].astext == "call_import", + LLMUsageDaily.context["resource_id"].astext, + ), + else_=None, + ), + cast(eval_call_import, String), + cast(row_call_import, String), + cast(eval_row_call_import, String), + ), + String, + ) + + +def _resource_scope_filter(resource_id: UUID): + """Match usage attributed to a product resource (agent, simulation, etc.).""" + rid = str(resource_id) + return or_( + LLMUsageDaily.context["resource_id"].astext == rid, + LLMUsageDaily.context["agent_id"].astext == rid, + ) + + +def _evaluation_scope_filter( + evaluation_id: UUID, + organization_id: UUID, + db: Session, +): + eid = str(evaluation_id) + row_ids = [ + str(row[0]) + for row in db.query(CallImportEvaluationRow.id) + .filter( + CallImportEvaluationRow.evaluation_id == evaluation_id, + ) + .all() + ] + clauses = [ + LLMUsageDaily.context["evaluation_id"].astext == eid, + and_( + LLMUsageDaily.context["resource_id"].astext == eid, + LLMUsageDaily.context["resource_type"].astext == "call_import_evaluation", + ), + ] + if row_ids: + clauses.append(LLMUsageDaily.context["evaluation_row_id"].astext.in_(row_ids)) + return or_(*clauses) + + +def _call_import_scope_filter( + call_import_id: UUID, + organization_id: UUID, + db: Session, +): + """Match usage tied to a call import (direct context, resource row, or eval runs).""" + cid = str(call_import_id) + eval_ids = [ + str(row[0]) + for row in db.query(CallImportEvaluation.id) + .filter( + CallImportEvaluation.organization_id == organization_id, + CallImportEvaluation.call_import_id == call_import_id, + ) + .all() + ] + clauses = [ + LLMUsageDaily.context["call_import_id"].astext == cid, + and_( + LLMUsageDaily.context["resource_id"].astext == cid, + LLMUsageDaily.context["resource_type"].astext == "call_import", + ), + ] + if eval_ids: + clauses.append(LLMUsageDaily.context["evaluation_id"].astext.in_(eval_ids)) + clauses.append( + and_( + LLMUsageDaily.context["resource_id"].astext.in_(eval_ids), + LLMUsageDaily.context["resource_type"].astext == "call_import_evaluation", + ) + ) + return or_(*clauses) + + +def _call_import_ids_for_filters( + db: Session, + *, + organization_id: UUID, + workspace_id: Optional[UUID] = None, + dataset: Optional[str] = None, + tag_id: Optional[UUID] = None, +) -> List[UUID]: + query = db.query(CallImport.id).filter( + CallImport.organization_id == organization_id, + ) + if workspace_id is not None: + query = query.filter(CallImport.workspace_id == workspace_id) + if dataset: + query = query.filter(CallImport.dataset == dataset) + if tag_id is not None: + query = query.filter( + CallImport.id.in_( + db.query(CallImportTagAssignment.call_import_id).filter( + CallImportTagAssignment.tag_id == tag_id, + ) + ) + ) + return [row[0] for row in query.all()] + + +def _call_import_ids_scope_filter( + allowed_import_ids: List[UUID], +): + if not allowed_import_ids: + return LLMUsageDaily.id.is_(None) + allowed = [str(uid) for uid in allowed_import_ids] + return cast(_call_import_group_expr(), String).in_(allowed) + + +def _workspace_scope_filter( + workspace_id: UUID, + organization_id: UUID, +): + """Match workspace-scoped rows plus legacy call-import usage with null workspace_id.""" + ws_call_import_ids = ( + select(cast(CallImport.id, String)) + .where( + CallImport.organization_id == organization_id, + CallImport.workspace_id == workspace_id, + ) + ) + legacy_call_import = and_( + LLMUsageDaily.workspace_id.is_(None), + _call_import_group_expr().in_(ws_call_import_ids), + ) + return or_(LLMUsageDaily.workspace_id == workspace_id, legacy_call_import) def _apply_filters( @@ -107,6 +390,13 @@ def _apply_filters( product_section: Optional[str], model: Optional[str], resource_id: Optional[UUID], + usage_kind: Optional[str] = None, + call_import_id: Optional[UUID] = None, + evaluation_id: Optional[UUID] = None, + evaluation_row_id: Optional[UUID] = None, + dataset: Optional[str] = None, + tag_id: Optional[UUID] = None, + db: Optional[Session] = None, ): query = query.filter( LLMUsageDaily.organization_id == organization_id, @@ -114,16 +404,449 @@ def _apply_filters( LLMUsageDaily.usage_date <= end, ) if workspace_id is not None: - query = query.filter(LLMUsageDaily.workspace_id == workspace_id) + query = query.filter( + _workspace_scope_filter(workspace_id, organization_id) + ) if product_section: query = query.filter(LLMUsageDaily.product_section == product_section) if model: query = query.filter(LLMUsageDaily.model == model) if resource_id is not None: - query = query.filter(LLMUsageDaily.resource_id == resource_id) + rid_filter = _resource_scope_filter(resource_id) + if db is not None: + query = query.filter( + or_( + _evaluation_scope_filter(resource_id, organization_id, db), + rid_filter, + ) + ) + else: + query = query.filter( + or_( + LLMUsageDaily.context["resource_id"].astext == str(resource_id), + LLMUsageDaily.context["agent_id"].astext == str(resource_id), + LLMUsageDaily.context["evaluation_id"].astext == str(resource_id), + LLMUsageDaily.context["evaluation_row_id"].astext == str(resource_id), + ) + ) + if call_import_id is not None: + if db is not None: + query = query.filter( + _call_import_scope_filter(call_import_id, organization_id, db) + ) + else: + query = query.filter( + LLMUsageDaily.context["call_import_id"].astext == str(call_import_id) + ) + if evaluation_id is not None: + if db is not None: + query = query.filter( + _evaluation_scope_filter(evaluation_id, organization_id, db) + ) + else: + query = query.filter( + LLMUsageDaily.context["evaluation_id"].astext == str(evaluation_id) + ) + if evaluation_row_id is not None: + query = query.filter( + LLMUsageDaily.context["evaluation_row_id"].astext + == str(evaluation_row_id) + ) + if usage_kind: + query = query.filter(LLMUsageDaily.usage_kind == usage_kind) + if dataset or tag_id is not None: + if db is None: + raise ValueError("db required for dataset/tag filters") + allowed = _call_import_ids_for_filters( + db, + organization_id=organization_id, + workspace_id=workspace_id, + dataset=dataset, + tag_id=tag_id, + ) + query = query.filter(_call_import_ids_scope_filter(allowed)) return query +def _filtered_query( + db: Session, + *, + organization_id: UUID, + start: date, + end: date, + workspace_id: Optional[UUID] = None, + product_section: Optional[str] = None, + model: Optional[str] = None, + resource_id: Optional[UUID] = None, + usage_kind: Optional[str] = None, + call_import_id: Optional[UUID] = None, + evaluation_id: Optional[UUID] = None, + evaluation_row_id: Optional[UUID] = None, + dataset: Optional[str] = None, + tag_id: Optional[UUID] = None, +): + return _apply_filters( + db.query(LLMUsageDaily), + organization_id=organization_id, + start=start, + end=end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + resource_id=resource_id, + usage_kind=usage_kind, + call_import_id=call_import_id, + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, + dataset=dataset, + tag_id=tag_id, + db=db, + ) + + +def _infer_agent_types_for_label_buckets( + db: Session, + organization_id: UUID, + grouped: dict[str, tuple[Optional[str], list]], +) -> None: + """When context JSON omits resource_type, infer agent rows from Agent.id.""" + candidate_ids: list[UUID] = [] + for rid, (rtype, _) in grouped.items(): + if rtype: + continue + uid = parse_uuid(rid) + if uid: + candidate_ids.append(uid) + if not candidate_ids: + return + known_agent_ids = { + row.id + for row in db.query(Agent.id).filter( + Agent.organization_id == organization_id, + Agent.id.in_(candidate_ids), + ).all() + } + for rid in grouped: + uid = parse_uuid(rid) + if uid and uid in known_agent_ids: + rtype, contexts = grouped[rid] + if not rtype: + grouped[rid] = ("agent", contexts) + + +def _resource_label_map( + db: Session, + organization_id: UUID, + query, +) -> dict[str, str]: + """Build resource_id -> hierarchical label from usage rows in query.""" + rows = ( + query.with_entities( + _resource_id_expr(), + _resource_type_expr(), + LLMUsageDaily.context, + ) + .filter( + or_( + LLMUsageDaily.context["resource_id"].astext.isnot(None), + LLMUsageDaily.context["agent_id"].astext.isnot(None), + ) + ) + .order_by(*_label_row_order()) + .limit(_LABEL_ROW_LIMIT) + .all() + ) + grouped: dict[str, tuple[Optional[str], list]] = {} + for raw_id, rtype, ctx in rows: + if not raw_id: + continue + key = str(raw_id) + if key not in grouped: + grouped[key] = (rtype, []) + grouped[key][1].append(ctx) + + _infer_agent_types_for_label_buckets(db, organization_id, grouped) + + buckets = [(rid, rtype, contexts) for rid, (rtype, contexts) in grouped.items()] + resolver = UsageNameResolver(db, organization_id) + contexts_for_preload = [] + for rid, rtype, contexts in buckets: + for ctx in contexts: + merged = dict(ctx or {}) + if rid: + merged.setdefault("resource_id", rid) + if rtype: + merged.setdefault("resource_type", rtype) + contexts_for_preload.append(merged) + resolver.preload(contexts_for_preload) + return labels_for_resource_buckets(buckets, resolver) + + +def _resource_filter_meta_map( + db: Session, + organization_id: UUID, + query, +) -> dict[str, dict[str, Optional[str]]]: + """Map resource id -> {type, product_section} for filter dropdowns.""" + rows = ( + query.with_entities( + _resource_id_expr(), + _resource_type_expr(), + LLMUsageDaily.product_section, + ) + .filter( + or_( + LLMUsageDaily.context["resource_id"].astext.isnot(None), + LLMUsageDaily.context["agent_id"].astext.isnot(None), + ) + ) + .distinct() + .limit(_LABEL_ROW_LIMIT) + .all() + ) + meta: dict[str, dict[str, Optional[str]]] = {} + for raw_id, rtype, section in rows: + if not raw_id: + continue + key = str(raw_id) + entry = meta.setdefault(key, {"type": None, "product_section": None}) + if rtype: + entry["type"] = str(rtype) + if section: + entry["product_section"] = str(section) + if not entry["type"] and section == "agents": + entry["type"] = "agent" + return meta + + +def _breakdown_resource_label( + raw_res_id: Optional[str], + res_type: Optional[str], + section: Optional[str], + resource_labels: dict[str, str], + db: Session, + organization_id: UUID, +) -> str: + if not raw_res_id: + return "Unscoped" + key = str(raw_res_id) + label = resource_labels.get(key) + if label and label != "Unscoped": + return label + effective_type = res_type or ("agent" if section == "agents" else None) + if effective_type == "agent": + resolver = UsageNameResolver(db, organization_id) + resolver.preload([{"resource_id": key, "resource_type": "agent"}]) + return resolver.agent_name(key) + return label or "Unscoped" + + +def _collect_call_import_ids_from_usage_rows( + db: Session, + organization_id: UUID, + rows: list, +) -> set[UUID]: + """Extract call import ids referenced in usage row context tuples.""" + import_ids: set[UUID] = set() + eval_ids: set[UUID] = set() + row_ids: set[UUID] = set() + eval_row_ids: set[UUID] = set() + + for row in rows: + cid_raw = row[0] if len(row) > 0 else None + res_raw = row[1] if len(row) > 1 else None + rtype = row[2] if len(row) > 2 else None + eval_raw = row[3] if len(row) > 3 else None + row_id_raw = row[4] if len(row) > 4 else None + eval_row_raw = row[5] if len(row) > 5 else None + + if cid_raw: + uid = parse_uuid(cid_raw) + if uid: + import_ids.add(uid) + if res_raw and rtype == "call_import": + uid = parse_uuid(res_raw) + if uid: + import_ids.add(uid) + if eval_raw: + uid = parse_uuid(eval_raw) + if uid: + eval_ids.add(uid) + if res_raw and rtype == "call_import_evaluation": + uid = parse_uuid(res_raw) + if uid: + eval_ids.add(uid) + if row_id_raw: + uid = parse_uuid(row_id_raw) + if uid: + row_ids.add(uid) + if eval_row_raw: + uid = parse_uuid(eval_row_raw) + if uid: + eval_row_ids.add(uid) + + if eval_ids: + for ev in ( + db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.organization_id == organization_id, + CallImportEvaluation.id.in_(eval_ids), + ) + .all() + ): + import_ids.add(ev.call_import_id) + + if row_ids: + for cid in ( + db.query(CallImportRow.call_import_id) + .filter(CallImportRow.id.in_(row_ids)) + .distinct() + .all() + ): + import_ids.add(cid[0]) + + if eval_row_ids: + for cid in ( + db.query(CallImportEvaluation.call_import_id) + .join( + CallImportEvaluationRow, + CallImportEvaluationRow.evaluation_id == CallImportEvaluation.id, + ) + .filter(CallImportEvaluationRow.id.in_(eval_row_ids)) + .distinct() + .all() + ): + import_ids.add(cid[0]) + + return import_ids + + +def _call_import_label_map( + db: Session, + organization_id: UUID, + query, +) -> dict[str, str]: + rows = ( + query.with_entities( + LLMUsageDaily.context["call_import_id"].astext, + LLMUsageDaily.context["resource_id"].astext, + LLMUsageDaily.context["resource_type"].astext, + LLMUsageDaily.context["evaluation_id"].astext, + LLMUsageDaily.context["call_import_row_id"].astext, + LLMUsageDaily.context["evaluation_row_id"].astext, + ) + .order_by(*_label_row_order()) + .limit(_LABEL_ROW_LIMIT) + .all() + ) + import_ids = _collect_call_import_ids_from_usage_rows( + db, organization_id, rows + ) + + if not import_ids: + return {} + resolver = UsageNameResolver(db, organization_id) + resolver.preload( + [{"call_import_id": str(uid)} for uid in import_ids] + ) + return labels_for_call_import_ids(list(import_ids), resolver) + + +def _call_import_filter_labels( + db: Session, + organization_id: UUID, + query, + workspace_id: Optional[UUID] = None, + dataset: Optional[str] = None, + tag_id: Optional[UUID] = None, +) -> dict[str, str]: + """Call imports for filters: usage in range plus workspace imports when scoped.""" + import_ids = _collect_call_import_ids_from_usage_rows( + db, + organization_id, + query.with_entities( + LLMUsageDaily.context["call_import_id"].astext, + LLMUsageDaily.context["resource_id"].astext, + LLMUsageDaily.context["resource_type"].astext, + LLMUsageDaily.context["evaluation_id"].astext, + LLMUsageDaily.context["call_import_row_id"].astext, + LLMUsageDaily.context["evaluation_row_id"].astext, + ).order_by(*_label_row_order()).limit(_LABEL_ROW_LIMIT).all(), + ) + + if workspace_id is not None: + scoped_ids = _call_import_ids_for_filters( + db, + organization_id=organization_id, + workspace_id=workspace_id, + dataset=dataset, + tag_id=tag_id, + ) + import_ids.update(scoped_ids) + + if not import_ids: + return {} + resolver = UsageNameResolver(db, organization_id) + resolver.preload([{"call_import_id": str(uid)} for uid in import_ids]) + return labels_for_call_import_ids(list(import_ids), resolver) + + +def _evaluation_label_map( + db: Session, + organization_id: UUID, + query, +) -> dict[str, str]: + """Evaluation id -> short label (name + id suffix) for filter dropdowns.""" + rows = ( + query.with_entities( + LLMUsageDaily.context["evaluation_id"].astext, + LLMUsageDaily.context["resource_id"].astext, + LLMUsageDaily.context["resource_type"].astext, + LLMUsageDaily.context, + ) + .filter( + or_( + LLMUsageDaily.context["evaluation_id"].astext.isnot(None), + LLMUsageDaily.context["resource_type"].astext == "call_import_evaluation", + ) + ) + .order_by(*_label_row_order()) + .limit(_LABEL_ROW_LIMIT) + .all() + ) + grouped: dict[str, tuple[Optional[str], list]] = {} + for eval_raw, res_raw, rtype, ctx in rows: + key_raw = eval_raw + if not key_raw and rtype == "call_import_evaluation" and res_raw: + key_raw = res_raw + if not key_raw: + continue + key = str(key_raw) + if key not in grouped: + grouped[key] = (rtype, []) + grouped[key][1].append(ctx) + + buckets = [(rid, rtype, contexts) for rid, (rtype, contexts) in grouped.items()] + resolver = UsageNameResolver(db, organization_id) + contexts_for_preload = [] + for _, rtype, contexts in buckets: + for ctx in contexts: + merged = dict(ctx or {}) + if rtype and "resource_type" not in merged: + merged["resource_type"] = rtype + contexts_for_preload.append(merged) + resolver.preload(contexts_for_preload) + + labels: dict[str, str] = {} + for raw_id, rtype, contexts in buckets: + ctx = max([dict(c or {}) for c in contexts], key=len, default={}) + if rtype and "resource_type" not in ctx: + ctx["resource_type"] = rtype + eval_key = ctx.get("evaluation_id") or raw_id + labels[str(raw_id)] = resolver.evaluation_name(str(eval_key)) + return labels + + def _last_updated(db: Session, organization_id: UUID) -> Optional[datetime]: return ( db.query(func.max(LLMUsageDaily.updated_at)) @@ -132,39 +855,120 @@ def _last_updated(db: Session, organization_id: UUID) -> Optional[datetime]: ) +def _summary_aggregate_query( + db: Session, + *, + organization_id: UUID, + start: date, + end: date, + workspace_id: Optional[UUID], + product_section: Optional[str], + model: Optional[str], + resource_id: Optional[UUID], + usage_kind: Optional[str], + call_import_id: Optional[UUID], + evaluation_id: Optional[UUID], + evaluation_row_id: Optional[UUID], + dataset: Optional[str] = None, + tag_id: Optional[UUID] = None, +): + return _apply_filters( + db.query( + func.coalesce(func.sum(LLMUsageDaily.prompt_tokens), 0).label("prompt_tokens"), + func.coalesce(func.sum(LLMUsageDaily.completion_tokens), 0).label( + "completion_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.cache_read_tokens), 0).label( + "cache_read_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.cache_creation_tokens), 0).label( + "cache_creation_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.reasoning_tokens), 0).label( + "reasoning_tokens" + ), + func.coalesce(func.sum(LLMUsageDaily.audio_seconds), 0).label("audio_seconds"), + func.coalesce(func.sum(LLMUsageDaily.tts_characters), 0).label("tts_characters"), + func.coalesce(func.sum(LLMUsageDaily.call_count), 0).label("call_count"), + ), + organization_id=organization_id, + start=start, + end=end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + resource_id=resource_id, + usage_kind=usage_kind, + call_import_id=call_import_id, + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, + dataset=dataset, + tag_id=tag_id, + db=db, + ) + + @router.get("/summary", response_model=UsageSummaryResponse) def get_usage_summary( start: Optional[date] = Query(None), end: Optional[date] = Query(None), + tz: Optional[str] = Query( + None, + description="IANA timezone for interpreting start/end calendar dates", + ), workspace_id: Optional[UUID] = Query(None), product_section: Optional[str] = Query(None), model: Optional[str] = Query(None), resource_id: Optional[UUID] = Query(None), + usage_kind: Optional[str] = Query(None), + call_import_id: Optional[UUID] = Query(None), + evaluation_id: Optional[UUID] = Query(None), + evaluation_row_id: Optional[UUID] = Query(None), + dataset: Optional[str] = Query(None), + tag_id: Optional[UUID] = Query(None), organization_id: UUID = Depends(get_organization_id), db: Session = Depends(get_db), ): - if start is None or end is None: - start, end = _default_range() - if end < start: + display_start, display_end, filter_start, filter_end = _parse_usage_range( + start, end, tz + ) + if display_end < display_start: raise HTTPException(status_code=400, detail="end must be >= start") flush_usage_to_catalog(db, organization_id) - query = _apply_filters( - db.query(LLMUsageDaily), + row = _summary_aggregate_query( + db, organization_id=organization_id, - start=start, - end=end, + start=filter_start, + end=filter_end, workspace_id=workspace_id, product_section=product_section, model=model, resource_id=resource_id, - ) - rows = query.all() - totals = merge_usage_totals(rows) + usage_kind=usage_kind, + call_import_id=call_import_id, + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, + dataset=dataset, + tag_id=tag_id, + ).one() + prompt = int(row.prompt_tokens) + completion = int(row.completion_tokens) + totals = { + "prompt_tokens": prompt, + "completion_tokens": completion, + "total_tokens": prompt + completion, + "cache_read_tokens": int(row.cache_read_tokens), + "cache_creation_tokens": int(row.cache_creation_tokens), + "reasoning_tokens": int(row.reasoning_tokens), + "audio_seconds": int(row.audio_seconds), + "tts_characters": int(row.tts_characters), + "call_count": int(row.call_count), + } return UsageSummaryResponse( - start=start, - end=end, + start=display_start, + end=display_end, totals=UsageTotals(**totals), last_updated_at=_last_updated(db, organization_id), ) @@ -174,19 +978,30 @@ def get_usage_summary( def get_usage_breakdown( start: Optional[date] = Query(None), end: Optional[date] = Query(None), + tz: Optional[str] = Query( + None, + description="IANA timezone for interpreting start/end calendar dates", + ), group_by: GroupBy = Query("workspace"), workspace_id: Optional[UUID] = Query(None), product_section: Optional[str] = Query(None), model: Optional[str] = Query(None), resource_id: Optional[UUID] = Query(None), + usage_kind: Optional[str] = Query(None), + call_import_id: Optional[UUID] = Query(None), + evaluation_id: Optional[UUID] = Query(None), + evaluation_row_id: Optional[UUID] = Query(None), + dataset: Optional[str] = Query(None), + tag_id: Optional[UUID] = Query(None), limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), organization_id: UUID = Depends(get_organization_id), db: Session = Depends(get_db), ): - if start is None or end is None: - start, end = _default_range() - if end < start: + display_start, display_end, filter_start, filter_end = _parse_usage_range( + start, end, tz + ) + if display_end < display_start: raise HTTPException(status_code=400, detail="end must be >= start") flush_usage_to_catalog(db, organization_id) @@ -195,7 +1010,9 @@ def get_usage_breakdown( "workspace": LLMUsageDaily.workspace_id, "product_section": LLMUsageDaily.product_section, "model": LLMUsageDaily.model, - "resource": LLMUsageDaily.resource_id, + "resource": cast(_resource_id_expr(), String), + "usage_kind": LLMUsageDaily.usage_kind, + "call_import": _call_import_group_expr(), }[group_by] aggregates = [ @@ -212,27 +1029,37 @@ def get_usage_breakdown( func.coalesce(func.sum(LLMUsageDaily.reasoning_tokens), 0).label( "reasoning_tokens" ), + func.coalesce(func.sum(LLMUsageDaily.audio_seconds), 0).label("audio_seconds"), + func.coalesce(func.sum(LLMUsageDaily.tts_characters), 0).label("tts_characters"), func.coalesce(func.sum(LLMUsageDaily.call_count), 0).label("call_count"), ] select_cols = [dim] group_cols = [dim] if group_by == "resource": - select_cols.append(LLMUsageDaily.resource_type) - group_cols.append(LLMUsageDaily.resource_type) + select_cols.append(cast(_resource_type_expr(), String)) + group_cols.append(cast(_resource_type_expr(), String)) + select_cols.append(LLMUsageDaily.product_section) + group_cols.append(LLMUsageDaily.product_section) query = _apply_filters( db.query(*select_cols, *aggregates), organization_id=organization_id, - start=start, - end=end, + start=filter_start, + end=filter_end, workspace_id=workspace_id, product_section=product_section, model=model, resource_id=resource_id, + usage_kind=usage_kind, + call_import_id=call_import_id, + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, + dataset=dataset, + tag_id=tag_id, + db=db, ).group_by(*group_cols) - total_count = query.count() results = ( query.order_by(func.sum(LLMUsageDaily.call_count).desc()) .offset(offset) @@ -240,27 +1067,53 @@ def get_usage_breakdown( .all() ) - workspace_names = { - w.id: w.name - for w in db.query(Workspace) - .filter(Workspace.organization_id == organization_id) - .all() - } - resource_labels: dict = {} - if group_by == "resource": - resource_ids = [r[0] for r in results if r[0] is not None] - if resource_ids: - evals = ( - db.query(CallImportEvaluation) + workspace_names: dict = {} + if group_by == "workspace": + ws_ids = {r[0] for r in results if r[0] is not None} + if ws_ids: + workspace_names = { + w.id: w.name + for w in db.query(Workspace) .filter( - CallImportEvaluation.organization_id == organization_id, - CallImportEvaluation.id.in_(resource_ids), + Workspace.organization_id == organization_id, + Workspace.id.in_(ws_ids), ) .all() - ) - for evaluation in evals: - label = (evaluation.name or "").strip() or str(evaluation.id)[:8] - resource_labels[evaluation.id] = label + } + resource_labels: dict[str, str] = {} + call_import_labels: dict[str, str] = {} + if group_by == "resource": + label_query = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + resource_id=resource_id, + usage_kind=usage_kind, + call_import_id=call_import_id, + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, + ) + resource_labels = _resource_label_map(db, organization_id, label_query) + elif group_by == "call_import": + label_query = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + resource_id=resource_id, + usage_kind=usage_kind, + call_import_id=call_import_id, + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, + ) + call_import_labels = _call_import_label_map(db, organization_id, label_query) rows: List[UsageBreakdownRow] = [] for result in results: @@ -277,7 +1130,9 @@ def get_usage_breakdown( cache_read_tokens=int(metrics[2]), cache_creation_tokens=int(metrics[3]), reasoning_tokens=int(metrics[4]), - call_count=int(metrics[5]), + audio_seconds=int(metrics[5]), + tts_characters=int(metrics[6]), + call_count=int(metrics[7]), ) ) elif group_by == "product_section": @@ -293,7 +1148,9 @@ def get_usage_breakdown( cache_read_tokens=int(metrics[2]), cache_creation_tokens=int(metrics[3]), reasoning_tokens=int(metrics[4]), - call_count=int(metrics[5]), + audio_seconds=int(metrics[5]), + tts_characters=int(metrics[6]), + call_count=int(metrics[7]), ) ) elif group_by == "model": @@ -308,35 +1165,101 @@ def get_usage_breakdown( cache_read_tokens=int(metrics[2]), cache_creation_tokens=int(metrics[3]), reasoning_tokens=int(metrics[4]), - call_count=int(metrics[5]), + audio_seconds=int(metrics[5]), + tts_characters=int(metrics[6]), + call_count=int(metrics[7]), + ) + ) + elif group_by == "usage_kind": + kind = result[0] + metrics = result[1:] + rows.append( + UsageBreakdownRow( + usage_kind=kind, + prompt_tokens=int(metrics[0]), + completion_tokens=int(metrics[1]), + total_tokens=int(metrics[0]) + int(metrics[1]), + cache_read_tokens=int(metrics[2]), + cache_creation_tokens=int(metrics[3]), + reasoning_tokens=int(metrics[4]), + audio_seconds=int(metrics[5]), + tts_characters=int(metrics[6]), + call_count=int(metrics[7]), + ) + ) + elif group_by == "call_import": + raw_cid = result[0] + metrics = result[1:] + cid = None + if raw_cid: + try: + cid = UUID(str(raw_cid)) + except (ValueError, TypeError): + cid = None + label = ( + call_import_labels.get(str(raw_cid), "Unscoped") + if raw_cid + else "Unscoped" + ) + rows.append( + UsageBreakdownRow( + call_import_id=cid, + call_import_label=label, + prompt_tokens=int(metrics[0]), + completion_tokens=int(metrics[1]), + total_tokens=int(metrics[0]) + int(metrics[1]), + cache_read_tokens=int(metrics[2]), + cache_creation_tokens=int(metrics[3]), + reasoning_tokens=int(metrics[4]), + audio_seconds=int(metrics[5]), + tts_characters=int(metrics[6]), + call_count=int(metrics[7]), ) ) else: - res_id, res_type = result[0], result[1] - metrics = result[2:] + raw_res_id, res_type, section = result[0], result[1], result[2] + metrics = result[3:] + res_id = None + if raw_res_id: + try: + res_id = UUID(str(raw_res_id)) + except (ValueError, TypeError): + res_id = None rows.append( UsageBreakdownRow( resource_id=res_id, - resource_type=res_type, - resource_label=resource_labels.get(res_id) - if res_id - else "Unscoped", + resource_type=res_type or ( + "agent" if section == "agents" and raw_res_id else None + ), + resource_label=_breakdown_resource_label( + raw_res_id, + res_type, + section, + resource_labels, + db, + organization_id, + ), + product_section=section, + product_section_label=SECTION_LABELS.get(section or "", section), prompt_tokens=int(metrics[0]), completion_tokens=int(metrics[1]), total_tokens=int(metrics[0]) + int(metrics[1]), cache_read_tokens=int(metrics[2]), cache_creation_tokens=int(metrics[3]), reasoning_tokens=int(metrics[4]), - call_count=int(metrics[5]), + audio_seconds=int(metrics[5]), + tts_characters=int(metrics[6]), + call_count=int(metrics[7]), ) ) return UsageBreakdownResponse( - start=start, - end=end, + start=display_start, + end=display_end, group_by=group_by, rows=rows, - total_count=total_count, + total_count=len(rows), + truncated_at_limit=len(rows) >= limit, last_updated_at=_last_updated(db, organization_id), ) @@ -345,23 +1268,93 @@ def get_usage_breakdown( def get_usage_filters( start: Optional[date] = Query(None), end: Optional[date] = Query(None), + tz: Optional[str] = Query( + None, + description="IANA timezone for interpreting start/end calendar dates", + ), + workspace_id: Optional[UUID] = Query(None), + product_section: Optional[str] = Query(None), + model: Optional[str] = Query(None), + resource_id: Optional[UUID] = Query(None), + usage_kind: Optional[str] = Query(None), + call_import_id: Optional[UUID] = Query(None), + dataset: Optional[str] = Query(None), + tag_id: Optional[UUID] = Query(None), + q: Optional[str] = Query(None, description="Optional resource label search"), organization_id: UUID = Depends(get_organization_id), db: Session = Depends(get_db), ): - if start is None or end is None: - start, end = _default_range() + _, _, filter_start, filter_end = _parse_usage_range(start, end, tz) flush_usage_to_catalog(db, organization_id) - base = db.query(LLMUsageDaily).filter( - LLMUsageDaily.organization_id == organization_id, - LLMUsageDaily.usage_date >= start, - LLMUsageDaily.usage_date <= end, + workspace_base = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + dataset=dataset, + tag_id=tag_id, + ) + section_base = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + dataset=dataset, + tag_id=tag_id, + ) + kind_base = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + product_section=product_section, + call_import_id=call_import_id, + dataset=dataset, + tag_id=tag_id, + ) + model_base = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + product_section=product_section, + usage_kind=usage_kind, + call_import_id=call_import_id, + dataset=dataset, + tag_id=tag_id, + ) + call_import_base = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + product_section=product_section, + dataset=dataset, + tag_id=tag_id, + ) + resource_base = _filtered_query( + db, + organization_id=organization_id, + start=filter_start, + end=filter_end, + workspace_id=workspace_id, + product_section=product_section, + model=model, + usage_kind=usage_kind, + call_import_id=call_import_id, + dataset=dataset, + tag_id=tag_id, ) workspace_ids = { row[0] - for row in base.with_entities(LLMUsageDaily.workspace_id).distinct().all() + for row in workspace_base.with_entities(LLMUsageDaily.workspace_id).distinct().all() if row[0] is not None } workspaces = [ @@ -375,45 +1368,94 @@ def get_usage_filters( sections = sorted( { row[0] - for row in base.with_entities(LLMUsageDaily.product_section).distinct().all() + for row in section_base.with_entities(LLMUsageDaily.product_section).distinct().all() if row[0] } ) models = sorted( { row[0] - for row in base.with_entities(LLMUsageDaily.model).distinct().all() + for row in model_base.with_entities(LLMUsageDaily.model).distinct().all() + if row[0] + } + ) + kinds = sorted( + { + row[0] + for row in kind_base.with_entities(LLMUsageDaily.usage_kind).distinct().all() if row[0] } ) - resource_rows = ( - base.with_entities(LLMUsageDaily.resource_id, LLMUsageDaily.resource_type) - .filter(LLMUsageDaily.resource_id.isnot(None)) - .distinct() - .limit(100) - .all() + call_import_labels = _call_import_filter_labels( + db, + organization_id, + call_import_base, + workspace_id=workspace_id, + dataset=dataset, + tag_id=tag_id, ) - resource_ids = [r[0] for r in resource_rows] - eval_names = {} - if resource_ids: - for evaluation in ( - db.query(CallImportEvaluation) - .filter(CallImportEvaluation.id.in_(resource_ids)) - .all() - ): - eval_names[evaluation.id] = (evaluation.name or "").strip() or str( - evaluation.id - )[:8] + call_imports = [ + {"id": cid, "label": label} + for cid, label in sorted(call_import_labels.items(), key=lambda x: x[1].lower()) + ] - resources = [ - { - "id": str(rid), - "type": rtype, - "label": eval_names.get(rid, str(rid)[:8]), - } - for rid, rtype in resource_rows - if rid is not None + evaluation_labels = _evaluation_label_map(db, organization_id, resource_base) + needle = (q or "").strip().lower() + evaluations = [] + for rid, label in sorted(evaluation_labels.items(), key=lambda x: x[1].lower()): + if needle and needle not in label.lower(): + continue + evaluations.append({"id": rid, "label": label}) + + resource_labels = _resource_label_map(db, organization_id, resource_base) + resource_meta = _resource_filter_meta_map(db, organization_id, resource_base) + resources = [] + for rid, label in sorted(resource_labels.items(), key=lambda x: x[1].lower()): + if needle and needle not in label.lower(): + continue + info = resource_meta.get(rid, {}) + rtype = info.get("type") + if rtype == "call_import_evaluation": + continue + resources.append( + { + "id": rid, + "label": label, + "type": rtype, + "product_section": info.get("product_section"), + } + ) + + dataset_query = db.query(CallImport.dataset).filter( + CallImport.organization_id == organization_id, + CallImport.dataset.isnot(None), + CallImport.dataset != "", + ) + if workspace_id is not None: + dataset_query = dataset_query.filter(CallImport.workspace_id == workspace_id) + if dataset or tag_id is not None: + scoped_import_ids = _call_import_ids_for_filters( + db, + organization_id=organization_id, + workspace_id=workspace_id, + dataset=dataset, + tag_id=tag_id, + ) + if scoped_import_ids: + dataset_query = dataset_query.filter(CallImport.id.in_(scoped_import_ids)) + else: + dataset_query = dataset_query.filter(CallImport.id.is_(None)) + datasets = sorted({row[0] for row in dataset_query.distinct().all() if row[0]}) + + tags = [ + {"id": str(tag.id), "label": tag.name} + for tag in ( + db.query(CallImportTag) + .filter(CallImportTag.organization_id == organization_id) + .order_by(CallImportTag.name) + .all() + ) ] return UsageFiltersResponse( @@ -421,6 +1463,12 @@ def get_usage_filters( product_sections=[ {"id": s, "label": SECTION_LABELS.get(s, s)} for s in sections ], + call_imports=call_imports, + evaluations=evaluations, models=models, resources=resources, + usage_kinds=[{"id": k, "label": usage_kind_label(k)} for k in kinds], + datasets=datasets, + tags=tags, ) + diff --git a/app/api/v1/routes/playground.py b/app/api/v1/routes/playground.py index 19e97a65..4c4d93dd 100644 --- a/app/api/v1/routes/playground.py +++ b/app/api/v1/routes/playground.py @@ -38,215 +38,9 @@ from app.services.evaluators.evaluator_result_call_data import slim_call_data_for_evaluator_result from app.utils.call_recordings import generate_unique_call_short_id -router = APIRouter(prefix="/playground", tags=["playground"]) - - -def extract_transcript_from_call_data(call_data: Dict[str, Any], provider_platform: str) -> tuple: - """ - Extract transcript and speaker segments from provider call_data. - - Args: - call_data: Full call data from voice provider - provider_platform: The provider platform ("vapi", "retell", "elevenlabs", "smallest") - - Returns: - Tuple of (transcript_text, speaker_segments) - - transcript_text: Plain text transcript - - speaker_segments: List of segments with speaker labels - """ - transcript_text = "" - speaker_segments = [] - - if not call_data: - return transcript_text, speaker_segments - - provider_platform_lower = provider_platform.lower() if provider_platform else "" - - if provider_platform_lower == "vapi": - # Vapi: keep provider payload raw and derive transcript from transcript/messages. - transcript_text = call_data.get("transcript", "") - - # Get structured messages for speaker segments - transcript_object = call_data.get("transcript_object", []) - if not transcript_object: - # Try messages array - artifact = call_data.get("artifact", {}) if isinstance(call_data, dict) else {} - messages = call_data.get("messages", []) or artifact.get("messages", []) - for msg in messages: - role = msg.get("role", "unknown") - content = msg.get("message", "") or msg.get("content", "") - - if not content or role == "system": - continue - - # Map roles - if role in ["bot", "assistant"]: - normalized_role = "agent" - elif role == "user": - normalized_role = "user" - else: - continue - - speaker_segments.append({ - "speaker": "Agent" if normalized_role == "agent" else "User", - "text": content, - "start": msg.get("secondsFromStart", 0), - "end": msg.get("secondsFromStart", 0) + (msg.get("duration", 0) / 1000), - }) - else: - for entry in transcript_object: - role = entry.get("role", "unknown") - content = entry.get("content", "") - - if not content: - continue - - speaker_segments.append({ - "speaker": "Agent" if role == "agent" else "User", - "text": content, - "start": entry.get("seconds_from_start", 0), - "end": entry.get("seconds_from_start", 0) + (entry.get("duration_ms", 0) / 1000), - }) - - # Build transcript text from segments if not available - if not transcript_text and speaker_segments: - transcript_text = "\n".join([ - f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments - ]) - - elif provider_platform_lower == "elevenlabs": - raw_transcript = call_data.get("transcript") - transcript_obj = call_data.get("transcript_object", []) - - # retrieve_call_metrics already processes the transcript into a - # formatted string + speaker_segments list, so handle both the - # pre-processed shape and the raw ElevenLabs API shape. - if isinstance(raw_transcript, str) and raw_transcript: - transcript_text = raw_transcript - if isinstance(transcript_obj, list): - for seg in transcript_obj: - speaker_segments.append({ - "speaker": seg.get("speaker", "Unknown"), - "text": seg.get("text", ""), - "start": seg.get("start", 0), - "end": seg.get("end", 0), - }) - elif isinstance(raw_transcript, list): - for entry in raw_transcript: - role = entry.get("role", "unknown") - content = entry.get("message", "") or entry.get("text", "") - if not content: - continue - speaker = "Agent" if role in ("agent", "assistant", "ai") else "User" - speaker_segments.append({ - "speaker": speaker, - "text": content, - "start": entry.get("time_in_call_secs", 0) or entry.get("start", 0), - "end": entry.get("time_in_call_secs", 0) or entry.get("end", 0), - }) - transcript_text = "\n".join( - f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments - ) - - elif provider_platform_lower == "smallest": - transcript_raw = call_data.get("transcript") - transcript_object = call_data.get("transcript_object", []) - if isinstance(transcript_object, list) and transcript_object: - for entry in transcript_object: - if not isinstance(entry, dict): - continue - text = entry.get("text", "") - if not text: - continue - speaker = entry.get("speaker", "Unknown") - speaker_segments.append( - { - "speaker": speaker, - "text": text, - "start": entry.get("start", 0), - "end": entry.get("end", entry.get("start", 0)), - } - ) - if not transcript_text: - transcript_text = "\n".join( - f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments - ) - elif isinstance(transcript_raw, list): - for entry in transcript_raw: - if not isinstance(entry, dict): - continue - role = str(entry.get("speaker") or entry.get("role") or "").lower() - speaker = "Agent" if role in ("agent", "assistant", "ai", "bot") else "User" - text = entry.get("text", "") or entry.get("message", "") or entry.get("content", "") - if not text: - continue - ts = entry.get("timeInCallSecs", 0) or entry.get("start", 0) or entry.get("timestamp", 0) - speaker_segments.append( - { - "speaker": speaker, - "text": text, - "start": ts, - "end": entry.get("end", ts), - } - ) - transcript_text = "\n".join( - f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments - ) - elif isinstance(transcript_raw, str): - transcript_text = transcript_raw - - elif provider_platform_lower == "retell": - # Retell: transcript can be a string or list of objects - transcript_raw = call_data.get("transcript", "") - - if isinstance(transcript_raw, str): - transcript_text = transcript_raw - # Parse transcript text into speaker segments if it has pattern like "Agent: text\nUser: text" - lines = transcript_raw.split("\n") if transcript_raw else [] - for line in lines: - line = line.strip() - if not line: - continue - if line.startswith("Agent:") or line.startswith("agent:"): - speaker_segments.append({ - "speaker": "Agent", - "text": line.split(":", 1)[1].strip() if ":" in line else line, - "start": 0, - "end": 0, - }) - elif line.startswith("User:") or line.startswith("user:"): - speaker_segments.append({ - "speaker": "User", - "text": line.split(":", 1)[1].strip() if ":" in line else line, - "start": 0, - "end": 0, - }) - elif isinstance(transcript_raw, list): - # Retell sometimes returns transcript as array of objects - for item in transcript_raw: - if isinstance(item, dict): - role = item.get("role", "") - content = item.get("content", "") or item.get("text", "") - - if not content: - continue - - speaker = "Agent" if role in ["agent", "assistant", "bot"] else "User" - speaker_segments.append({ - "speaker": speaker, - "text": content, - "start": item.get("start_time", 0) or item.get("timestamp", 0), - "end": item.get("end_time", 0), - }) - - # Build transcript text from segments - transcript_text = "\n".join([ - f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments - ]) - - return transcript_text, speaker_segments - +from app.services.evaluators.call_data_transcript import extract_transcript_from_call_data +router = APIRouter(prefix="/playground", tags=["playground"]) def generate_unique_result_id(db: Session) -> str: """Generate a unique 6-digit result ID for EvaluatorResult.""" max_attempts = 100 @@ -411,7 +205,8 @@ def poll_call_metrics( or recording_urls.get("stereo_url") ) if audio_url: - resp = _http.get(audio_url, timeout=120) + vapi_headers = {"Authorization": f"Bearer {integration_api_key}"} + resp = _http.get(audio_url, headers=vapi_headers, timeout=120) if resp.status_code == 200: audio_bytes = resp.content @@ -656,13 +451,32 @@ async def create_web_call( # For now, we'll skip it for Retell. Other providers can handle it in their implementation. if integration.platform != "retell" and web_call_data.custom_sip_headers: call_params["custom_sip_headers"] = web_call_data.custom_sip_headers - - web_call_response = provider.create_web_call(**call_params) - - # Store call recording in database + + platform_value = ( + integration.platform.value + if hasattr(integration.platform, "value") + else integration.platform + ) + plat_lower = str(platform_value).lower() + + # Vapi Web SDK creates the call in the browser; server-side /call/web + # would spawn a second call that never receives the user's microphone. + if plat_lower == "vapi": + from app.services.voice_providers.vapi import VAPI_SAMPLE_RATE + + web_call_response = { + "call_type": "web_call", + "agent_id": agent.voice_ai_agent_id, + "metadata": web_call_data.metadata or {}, + "sample_rate": VAPI_SAMPLE_RATE, + "client_sdk_creates_call": True, + } + provider_call_id = None + else: + web_call_response = provider.create_web_call(**call_params) + provider_call_id = web_call_response.get("call_id") + call_short_id = generate_unique_call_short_id(db) - provider_call_id = web_call_response.get("call_id") - call_recording = CallRecording( organization_id=organization_id, workspace_id=workspace_id, @@ -702,17 +516,13 @@ async def create_web_call( # Add call_short_id to response for frontend response = web_call_response.copy() response["call_short_id"] = call_short_id - - platform_value = integration.platform.value if hasattr(integration.platform, 'value') else integration.platform - - # For Vapi, include the public key in the response (needed for frontend SDK) - if platform_value.lower() == "vapi" and integration.public_key: + + if plat_lower == "vapi" and integration.public_key: response["public_key"] = integration.public_key - - # For ElevenLabs, pass through the signed_url (frontend SDK connects directly) - if platform_value.lower() == "elevenlabs": + + if plat_lower == "elevenlabs": response["signed_url"] = web_call_response.get("signed_url") - + return response except Exception as e: raise HTTPException( @@ -1850,7 +1660,10 @@ async def summarize_transcript( 1. The voice bundle of ``agent_id`` (or the agent on ``call_short_id``). 2. Any configured AIProvider matching the fallback preference list. """ + from contextlib import nullcontext + from app.services.ai.llm_service import llm_service + from app.services.usage.context import llm_usage_context, usage_context_for_agent transcript_text = (payload.transcript or "").strip() if not transcript_text and payload.entries: @@ -1947,15 +1760,27 @@ async def summarize_transcript( ] try: - result = llm_service.generate_response( - messages=messages, - llm_provider=llm_provider, - llm_model=llm_model, - organization_id=organization_id, - db=db, - temperature=0.3, - max_tokens=400, - ) + usage_ctx = nullcontext() + if agent_uuid: + agent_row = db.query(Agent).filter( + Agent.id == agent_uuid, + Agent.organization_id == organization_id, + ).first() + if agent_row: + usage_ctx = llm_usage_context( + usage_context_for_agent(agent_row, workspace_id=workspace_id) + ) + + with usage_ctx: + result = llm_service.generate_response( + messages=messages, + llm_provider=llm_provider, + llm_model=llm_model, + organization_id=organization_id, + db=db, + temperature=0.3, + max_tokens=400, + ) except Exception as e: logger.error(f"[summarize-transcript] LLM call failed: {e}") raise HTTPException( diff --git a/app/api/v1/routes/vobiz_telephony.py b/app/api/v1/routes/vobiz_telephony.py index 084d03b7..052d03b9 100644 --- a/app/api/v1/routes/vobiz_telephony.py +++ b/app/api/v1/routes/vobiz_telephony.py @@ -698,6 +698,7 @@ async def vobiz_media_websocket(websocket: WebSocket): websocket, context.system_instruction, str(context.organization_id), + str(context.workspace_id) if context.workspace_id else None, agent_id, persona_id, scenario_id, diff --git a/app/api/v1/routes/voice_agent.py b/app/api/v1/routes/voice_agent.py index 47f7350e..e6d553b5 100644 --- a/app/api/v1/routes/voice_agent.py +++ b/app/api/v1/routes/voice_agent.py @@ -517,6 +517,7 @@ def resolve_azure_endpoint_for_provider(provider: ModelProvider) -> str | None: websocket, system_instruction, str(organization_id), + str(workspace_id) if workspace_id else None, agent_id, persona_id, scenario_id, diff --git a/app/migrations/063_usage_kind_stt_and_buffer.py b/app/migrations/063_usage_kind_stt_and_buffer.py new file mode 100644 index 00000000..1cbb51d9 --- /dev/null +++ b/app/migrations/063_usage_kind_stt_and_buffer.py @@ -0,0 +1,233 @@ +"""Migration: STT usage_kind/audio_seconds + Redis-fallback pending buffer.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Add usage_kind/audio_seconds to llm_usage_daily and usage_pending_buffer " + "for durable STT + LLM usage when Redis is unavailable" +) + +_ZERO_UUID = "00000000-0000-0000-0000-000000000000" + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def _dedupe_llm_usage_daily(db: Session) -> int: + """Merge duplicate bucket rows so the unique index can be created. + + Keeps the oldest row (MIN id), sums metrics into it, deletes extras. + """ + db.execute( + text( + f""" + WITH dupes AS ( + SELECT + organization_id, + COALESCE( + workspace_id, + '{_ZERO_UUID}'::uuid + ) AS ws_key, + product_section, + model, + COALESCE( + resource_id, + '{_ZERO_UUID}'::uuid + ) AS rid_key, + usage_date, + COALESCE(usage_kind, 'llm') AS kind_key, + MIN(id::text)::uuid AS keep_id, + SUM(prompt_tokens)::bigint AS prompt_tokens, + SUM(completion_tokens)::bigint AS completion_tokens, + SUM(cache_read_tokens)::bigint AS cache_read_tokens, + SUM(cache_creation_tokens)::bigint AS cache_creation_tokens, + SUM(reasoning_tokens)::bigint AS reasoning_tokens, + SUM(COALESCE(audio_seconds, 0))::bigint AS audio_seconds, + SUM(call_count)::bigint AS call_count + FROM llm_usage_daily + GROUP BY 1, 2, 3, 4, 5, 6, 7 + HAVING COUNT(*) > 1 + ) + UPDATE llm_usage_daily AS u SET + prompt_tokens = d.prompt_tokens, + completion_tokens = d.completion_tokens, + cache_read_tokens = d.cache_read_tokens, + cache_creation_tokens = d.cache_creation_tokens, + reasoning_tokens = d.reasoning_tokens, + audio_seconds = d.audio_seconds, + call_count = d.call_count, + updated_at = now() + FROM dupes AS d + WHERE u.id = d.keep_id + """ + ) + ) + + result = db.execute( + text( + f""" + WITH keepers AS ( + SELECT MIN(id::text)::uuid AS keep_id + FROM llm_usage_daily + GROUP BY + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + COALESCE(resource_id, '{_ZERO_UUID}'::uuid), + usage_date, + COALESCE(usage_kind, 'llm') + HAVING COUNT(*) > 1 + ) + DELETE FROM llm_usage_daily AS u + USING keepers AS k, + llm_usage_daily AS peer + WHERE peer.id = k.keep_id + AND u.id <> k.keep_id + AND u.organization_id = peer.organization_id + AND u.product_section = peer.product_section + AND u.model = peer.model + AND u.usage_date = peer.usage_date + AND COALESCE(u.usage_kind, 'llm') = COALESCE(peer.usage_kind, 'llm') + AND u.workspace_id IS NOT DISTINCT FROM peer.workspace_id + AND u.resource_id IS NOT DISTINCT FROM peer.resource_id + """ + ) + ) + return int(result.rowcount or 0) + + +def _ensure_unique_bucket_index(db: Session) -> None: + """Drop legacy/broken unique index and recreate with usage_kind.""" + db.execute(text("DROP INDEX IF EXISTS uq_llm_usage_daily_bucket")) + db.execute( + text( + f""" + CREATE UNIQUE INDEX uq_llm_usage_daily_bucket + ON llm_usage_daily ( + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + COALESCE(resource_id, '{_ZERO_UUID}'::uuid), + usage_date, + usage_kind + ) + """ + ) + ) + + +def upgrade(db: Session): + if not _table_exists(db, "llm_usage_daily"): + print("llm_usage_daily missing; run 062 first — skipping 063") + db.commit() + return + + if not _column_exists(db, "llm_usage_daily", "usage_kind"): + db.execute( + text( + """ + ALTER TABLE llm_usage_daily + ADD COLUMN usage_kind VARCHAR(16) NOT NULL DEFAULT 'llm' + """ + ) + ) + + if not _column_exists(db, "llm_usage_daily", "audio_seconds"): + db.execute( + text( + """ + ALTER TABLE llm_usage_daily + ADD COLUMN audio_seconds BIGINT NOT NULL DEFAULT 0 + """ + ) + ) + + removed = _dedupe_llm_usage_daily(db) + if removed: + print(f"Merged/removed {removed} duplicate llm_usage_daily row(s)") + + _ensure_unique_bucket_index(db) + + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_llm_usage_daily_org_kind_date + ON llm_usage_daily (organization_id, usage_kind, usage_date) + """ + ) + ) + print("Ensured usage_kind + audio_seconds + unique bucket index") + + if not _table_exists(db, "usage_pending_buffer"): + db.execute( + text( + """ + CREATE TABLE usage_pending_buffer ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL, + workspace_id UUID, + product_section VARCHAR(64) NOT NULL, + model VARCHAR(255) NOT NULL, + context JSONB NOT NULL DEFAULT '{}'::jsonb, + usage_date DATE NOT NULL, + usage_kind VARCHAR(16) NOT NULL DEFAULT 'llm', + prompt_tokens BIGINT NOT NULL DEFAULT 0, + completion_tokens BIGINT NOT NULL DEFAULT 0, + cache_read_tokens BIGINT NOT NULL DEFAULT 0, + cache_creation_tokens BIGINT NOT NULL DEFAULT 0, + reasoning_tokens BIGINT NOT NULL DEFAULT 0, + audio_seconds BIGINT NOT NULL DEFAULT 0, + call_count BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_usage_pending_buffer_org_created + ON usage_pending_buffer (organization_id, created_at) + """ + ) + ) + print("Created usage_pending_buffer") + + db.commit() + + +def downgrade(db: Session): + db.execute(text("DROP TABLE IF EXISTS usage_pending_buffer")) + db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_org_kind_date")) + # Keep usage_kind/audio_seconds columns on downgrade to avoid data loss. + db.commit() diff --git a/app/migrations/064_usage_kind_tts_characters.py b/app/migrations/064_usage_kind_tts_characters.py new file mode 100644 index 00000000..57d42da0 --- /dev/null +++ b/app/migrations/064_usage_kind_tts_characters.py @@ -0,0 +1,74 @@ +"""Migration: TTS usage_kind + tts_characters metric.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = "Add tts_characters to llm_usage_daily and usage_pending_buffer for TTS usage" + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def upgrade(db: Session): + if _table_exists(db, "llm_usage_daily") and not _column_exists( + db, "llm_usage_daily", "tts_characters" + ): + db.execute( + text( + """ + ALTER TABLE llm_usage_daily + ADD COLUMN tts_characters BIGINT NOT NULL DEFAULT 0 + """ + ) + ) + print("Added tts_characters to llm_usage_daily") + + if _table_exists(db, "usage_pending_buffer") and not _column_exists( + db, "usage_pending_buffer", "tts_characters" + ): + db.execute( + text( + """ + ALTER TABLE usage_pending_buffer + ADD COLUMN tts_characters BIGINT NOT NULL DEFAULT 0 + """ + ) + ) + print("Added tts_characters to usage_pending_buffer") + + db.commit() + + +def downgrade(db: Session): + if _column_exists(db, "llm_usage_daily", "tts_characters"): + db.execute(text("ALTER TABLE llm_usage_daily DROP COLUMN tts_characters")) + if _column_exists(db, "usage_pending_buffer", "tts_characters"): + db.execute(text("ALTER TABLE usage_pending_buffer DROP COLUMN tts_characters")) + db.commit() diff --git a/app/migrations/065_usage_context_jsonb.py b/app/migrations/065_usage_context_jsonb.py new file mode 100644 index 00000000..4dd62378 --- /dev/null +++ b/app/migrations/065_usage_context_jsonb.py @@ -0,0 +1,216 @@ +"""Migration: JSONB context column for usage attribution metadata.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Move resource_id/resource_type into llm_usage_daily.context JSONB and " + "recreate bucket uniqueness on context keys" +) + +_ZERO_UUID = "00000000-0000-0000-0000-000000000000" + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def _ensure_unique_bucket_index(db: Session) -> None: + db.execute(text("DROP INDEX IF EXISTS uq_llm_usage_daily_bucket")) + db.execute( + text( + f""" + CREATE UNIQUE INDEX uq_llm_usage_daily_bucket + ON llm_usage_daily ( + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + usage_date, + usage_kind, + context + ) + """ + ) + ) + + +def _migrate_table_context(db: Session, table: str) -> None: + if not _table_exists(db, table): + return + if not _column_exists(db, table, "context"): + db.execute( + text( + f""" + ALTER TABLE {table} + ADD COLUMN context JSONB NOT NULL DEFAULT '{{}}'::jsonb + """ + ) + ) + + if _column_exists(db, table, "resource_id") or _column_exists(db, table, "resource_type"): + db.execute( + text( + f""" + UPDATE {table} + SET context = COALESCE(context, '{{}}'::jsonb) + || CASE + WHEN resource_id IS NOT NULL THEN + jsonb_build_object('resource_id', resource_id::text) + ELSE '{{}}'::jsonb + END + || CASE + WHEN resource_type IS NOT NULL AND resource_type <> '' THEN + jsonb_build_object('resource_type', resource_type) + ELSE '{{}}'::jsonb + END + WHERE resource_id IS NOT NULL + OR (resource_type IS NOT NULL AND resource_type <> '') + """ + ) + ) + if _column_exists(db, table, "resource_id"): + db.execute(text(f"DROP INDEX IF EXISTS ix_llm_usage_daily_org_resource_date")) + db.execute(text(f"ALTER TABLE {table} DROP COLUMN resource_id")) + if _column_exists(db, table, "resource_type"): + db.execute(text(f"ALTER TABLE {table} DROP COLUMN resource_type")) + + +def upgrade(db: Session): + if not _table_exists(db, "llm_usage_daily"): + print("llm_usage_daily missing; run 062 first — skipping 065") + db.commit() + return + + _migrate_table_context(db, "llm_usage_daily") + _migrate_table_context(db, "usage_pending_buffer") + + _ensure_unique_bucket_index(db) + + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_llm_usage_daily_context_gin + ON llm_usage_daily USING gin (context) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_llm_usage_daily_context_resource_id + ON llm_usage_daily ((context->>'resource_id')) + WHERE context ? 'resource_id' + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_llm_usage_daily_context_call_import_id + ON llm_usage_daily ((context->>'call_import_id')) + WHERE context ? 'call_import_id' + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_llm_usage_daily_context_evaluation_id + ON llm_usage_daily ((context->>'evaluation_id')) + WHERE context ? 'evaluation_id' + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_llm_usage_daily_context_evaluation_row_id + ON llm_usage_daily ((context->>'evaluation_row_id')) + WHERE context ? 'evaluation_row_id' + """ + ) + ) + print("Added context JSONB + migrated resource attribution") + db.commit() + + +def downgrade(db: Session): + if not _table_exists(db, "llm_usage_daily"): + db.commit() + return + + if not _column_exists(db, "llm_usage_daily", "resource_id"): + db.execute( + text( + """ + ALTER TABLE llm_usage_daily + ADD COLUMN resource_id UUID, + ADD COLUMN resource_type VARCHAR(64) + """ + ) + ) + db.execute( + text( + """ + UPDATE llm_usage_daily + SET resource_id = NULLIF(context->>'resource_id', '')::uuid, + resource_type = NULLIF(context->>'resource_type', '') + WHERE context IS NOT NULL + """ + ) + ) + + db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_context_evaluation_row_id")) + db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_context_evaluation_id")) + db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_context_call_import_id")) + db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_context_resource_id")) + db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_context_gin")) + db.execute(text("DROP INDEX IF EXISTS uq_llm_usage_daily_bucket")) + + if _column_exists(db, "llm_usage_daily", "context"): + db.execute(text("ALTER TABLE llm_usage_daily DROP COLUMN context")) + + db.execute( + text( + f""" + CREATE UNIQUE INDEX uq_llm_usage_daily_bucket + ON llm_usage_daily ( + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + COALESCE(resource_id, '{_ZERO_UUID}'::uuid), + usage_date, + usage_kind + ) + """ + ) + ) + db.commit() diff --git a/app/migrations/066_backfill_llm_usage_workspace_from_call_import.py b/app/migrations/066_backfill_llm_usage_workspace_from_call_import.py new file mode 100644 index 00000000..478afd4f --- /dev/null +++ b/app/migrations/066_backfill_llm_usage_workspace_from_call_import.py @@ -0,0 +1,279 @@ +"""Migration: Backfill llm_usage_daily.workspace_id from call-import attribution.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Backfill workspace_id on llm_usage_daily and usage_pending_buffer rows " + "that have call-import context but were recorded with NULL workspace_id" +) + +_ZERO_UUID = "00000000-0000-0000-0000-000000000000" +_UUID_RE = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def _dedupe_llm_usage_daily(db: Session) -> int: + """Merge duplicate buckets after workspace backfill.""" + tts_col = ( + "SUM(COALESCE(tts_characters, 0))::bigint AS tts_characters," + if _column_exists(db, "llm_usage_daily", "tts_characters") + else "" + ) + tts_set = ( + "tts_characters = d.tts_characters," + if _column_exists(db, "llm_usage_daily", "tts_characters") + else "" + ) + + db.execute( + text( + f""" + WITH dupes AS ( + SELECT + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid) AS ws_key, + product_section, + model, + usage_date, + COALESCE(usage_kind, 'llm') AS kind_key, + context, + MIN(id::text)::uuid AS keep_id, + SUM(prompt_tokens)::bigint AS prompt_tokens, + SUM(completion_tokens)::bigint AS completion_tokens, + SUM(cache_read_tokens)::bigint AS cache_read_tokens, + SUM(cache_creation_tokens)::bigint AS cache_creation_tokens, + SUM(reasoning_tokens)::bigint AS reasoning_tokens, + SUM(COALESCE(audio_seconds, 0))::bigint AS audio_seconds, + {tts_col} + SUM(call_count)::bigint AS call_count + FROM llm_usage_daily + GROUP BY 1, 2, 3, 4, 5, 6, 7 + HAVING COUNT(*) > 1 + ) + UPDATE llm_usage_daily AS u SET + prompt_tokens = d.prompt_tokens, + completion_tokens = d.completion_tokens, + cache_read_tokens = d.cache_read_tokens, + cache_creation_tokens = d.cache_creation_tokens, + reasoning_tokens = d.reasoning_tokens, + audio_seconds = d.audio_seconds, + {tts_set} + call_count = d.call_count, + updated_at = now() + FROM dupes AS d + WHERE u.id = d.keep_id + """ + ) + ) + + result = db.execute( + text( + f""" + WITH keepers AS ( + SELECT MIN(id::text)::uuid AS keep_id + FROM llm_usage_daily + GROUP BY + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + usage_date, + COALESCE(usage_kind, 'llm'), + context + HAVING COUNT(*) > 1 + ) + DELETE FROM llm_usage_daily AS u + USING keepers AS k, + llm_usage_daily AS peer + WHERE peer.id = k.keep_id + AND u.id <> k.keep_id + AND u.organization_id = peer.organization_id + AND u.product_section = peer.product_section + AND u.model = peer.model + AND u.usage_date = peer.usage_date + AND COALESCE(u.usage_kind, 'llm') = COALESCE(peer.usage_kind, 'llm') + AND u.workspace_id IS NOT DISTINCT FROM peer.workspace_id + AND u.context IS NOT DISTINCT FROM peer.context + """ + ) + ) + return int(result.rowcount or 0) + + +def _backfill_table_workspace(db: Session, table: str) -> int: + if not _table_exists(db, table): + return 0 + if not _column_exists(db, table, "workspace_id"): + return 0 + if not _column_exists(db, table, "context"): + return 0 + + total = 0 + + # context.call_import_id + result = db.execute( + text( + f""" + UPDATE {table} AS u + SET workspace_id = ci.workspace_id + FROM call_imports AS ci + WHERE u.workspace_id IS NULL + AND u.context ? 'call_import_id' + AND u.context->>'call_import_id' ~ :uuid_re + AND ci.id = (u.context->>'call_import_id')::uuid + AND ci.organization_id = u.organization_id + """ + ), + {"uuid_re": _UUID_RE}, + ) + total += int(result.rowcount or 0) + + # resource_type = call_import + result = db.execute( + text( + f""" + UPDATE {table} AS u + SET workspace_id = ci.workspace_id + FROM call_imports AS ci + WHERE u.workspace_id IS NULL + AND u.context->>'resource_type' = 'call_import' + AND u.context->>'resource_id' ~ :uuid_re + AND ci.id = (u.context->>'resource_id')::uuid + AND ci.organization_id = u.organization_id + """ + ), + {"uuid_re": _UUID_RE}, + ) + total += int(result.rowcount or 0) + + # context.evaluation_id + result = db.execute( + text( + f""" + UPDATE {table} AS u + SET workspace_id = e.workspace_id + FROM call_import_evaluations AS e + WHERE u.workspace_id IS NULL + AND u.context ? 'evaluation_id' + AND u.context->>'evaluation_id' ~ :uuid_re + AND e.id = (u.context->>'evaluation_id')::uuid + AND e.organization_id = u.organization_id + """ + ), + {"uuid_re": _UUID_RE}, + ) + total += int(result.rowcount or 0) + + # resource_type = call_import_evaluation + result = db.execute( + text( + f""" + UPDATE {table} AS u + SET workspace_id = e.workspace_id + FROM call_import_evaluations AS e + WHERE u.workspace_id IS NULL + AND u.context->>'resource_type' = 'call_import_evaluation' + AND u.context->>'resource_id' ~ :uuid_re + AND e.id = (u.context->>'resource_id')::uuid + AND e.organization_id = u.organization_id + """ + ), + {"uuid_re": _UUID_RE}, + ) + total += int(result.rowcount or 0) + + # context.call_import_row_id + result = db.execute( + text( + f""" + UPDATE {table} AS u + SET workspace_id = cir.workspace_id + FROM call_import_rows AS cir + WHERE u.workspace_id IS NULL + AND u.context ? 'call_import_row_id' + AND u.context->>'call_import_row_id' ~ :uuid_re + AND cir.id = (u.context->>'call_import_row_id')::uuid + AND cir.organization_id = u.organization_id + """ + ), + {"uuid_re": _UUID_RE}, + ) + total += int(result.rowcount or 0) + + # context.evaluation_row_id + result = db.execute( + text( + f""" + UPDATE {table} AS u + SET workspace_id = e.workspace_id + FROM call_import_evaluation_rows AS er + JOIN call_import_evaluations AS e ON e.id = er.evaluation_id + WHERE u.workspace_id IS NULL + AND u.context ? 'evaluation_row_id' + AND u.context->>'evaluation_row_id' ~ :uuid_re + AND er.id = (u.context->>'evaluation_row_id')::uuid + AND e.organization_id = u.organization_id + """ + ), + {"uuid_re": _UUID_RE}, + ) + total += int(result.rowcount or 0) + + return total + + +def upgrade(db: Session): + if not _table_exists(db, "llm_usage_daily"): + print("llm_usage_daily missing; skipping 066") + db.commit() + return + + daily_updated = _backfill_table_workspace(db, "llm_usage_daily") + print(f"Backfilled workspace_id on {daily_updated} llm_usage_daily row(s)") + + removed = _dedupe_llm_usage_daily(db) + if removed: + print(f"Removed {removed} duplicate llm_usage_daily row(s) after merge") + + buffer_updated = _backfill_table_workspace(db, "usage_pending_buffer") + if buffer_updated: + print( + f"Backfilled workspace_id on {buffer_updated} usage_pending_buffer row(s)" + ) + + db.commit() + + +def downgrade(db: Session): + # No-op: cannot distinguish backfilled workspace_id from originally recorded values. + db.commit() diff --git a/app/migrations/067_fix_llm_usage_bucket_unique_index.py b/app/migrations/067_fix_llm_usage_bucket_unique_index.py new file mode 100644 index 00000000..98a78e5d --- /dev/null +++ b/app/migrations/067_fix_llm_usage_bucket_unique_index.py @@ -0,0 +1,264 @@ +"""Migration: Reconcile llm_usage_daily bucket unique index with full JSONB context.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Merge legacy resource-scoped usage buckets and recreate uq_llm_usage_daily_bucket " + "on full context JSONB (per-row attribution)" +) + +_ZERO_UUID = "00000000-0000-0000-0000-000000000000" + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _dedupe_by_context_resource_keys(db: Session) -> int: + """Merge rows that share context resource_id/resource_type (legacy unique index).""" + tts_col = ( + "SUM(COALESCE(tts_characters, 0))::bigint AS tts_characters," + if _column_exists(db, "llm_usage_daily", "tts_characters") + else "" + ) + tts_set = ( + "tts_characters = d.tts_characters," + if _column_exists(db, "llm_usage_daily", "tts_characters") + else "" + ) + db.execute( + text( + f""" + WITH dupes AS ( + SELECT + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid) AS ws_key, + product_section, + model, + usage_date, + COALESCE(usage_kind, 'llm') AS kind_key, + COALESCE(context->>'resource_id', '') AS res_id_key, + COALESCE(context->>'resource_type', '') AS res_type_key, + MIN(id::text)::uuid AS keep_id, + SUM(prompt_tokens)::bigint AS prompt_tokens, + SUM(completion_tokens)::bigint AS completion_tokens, + SUM(cache_read_tokens)::bigint AS cache_read_tokens, + SUM(cache_creation_tokens)::bigint AS cache_creation_tokens, + SUM(reasoning_tokens)::bigint AS reasoning_tokens, + SUM(COALESCE(audio_seconds, 0))::bigint AS audio_seconds, + {tts_col} + SUM(call_count)::bigint AS call_count + FROM llm_usage_daily + GROUP BY 1, 2, 3, 4, 5, 6, 7, 8 + HAVING COUNT(*) > 1 + ) + UPDATE llm_usage_daily AS u SET + prompt_tokens = d.prompt_tokens, + completion_tokens = d.completion_tokens, + cache_read_tokens = d.cache_read_tokens, + cache_creation_tokens = d.cache_creation_tokens, + reasoning_tokens = d.reasoning_tokens, + audio_seconds = d.audio_seconds, + {tts_set} + call_count = d.call_count, + updated_at = now() + FROM dupes AS d + WHERE u.id = d.keep_id + """ + ) + ) + result = db.execute( + text( + f""" + WITH keepers AS ( + SELECT MIN(id::text)::uuid AS keep_id + FROM llm_usage_daily + GROUP BY + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + usage_date, + COALESCE(usage_kind, 'llm'), + COALESCE(context->>'resource_id', ''), + COALESCE(context->>'resource_type', '') + HAVING COUNT(*) > 1 + ) + DELETE FROM llm_usage_daily AS u + USING keepers AS k, + llm_usage_daily AS peer + WHERE peer.id = k.keep_id + AND u.id <> k.keep_id + AND u.organization_id = peer.organization_id + AND u.product_section = peer.product_section + AND u.model = peer.model + AND u.usage_date = peer.usage_date + AND COALESCE(u.usage_kind, 'llm') = COALESCE(peer.usage_kind, 'llm') + AND u.workspace_id IS NOT DISTINCT FROM peer.workspace_id + AND COALESCE(u.context->>'resource_id', '') = + COALESCE(peer.context->>'resource_id', '') + AND COALESCE(u.context->>'resource_type', '') = + COALESCE(peer.context->>'resource_type', '') + """ + ) + ) + return int(result.rowcount or 0) + + +def _dedupe_by_full_context(db: Session) -> int: + tts_col = ( + "SUM(COALESCE(tts_characters, 0))::bigint AS tts_characters," + if _column_exists(db, "llm_usage_daily", "tts_characters") + else "" + ) + tts_set = ( + "tts_characters = d.tts_characters," + if _column_exists(db, "llm_usage_daily", "tts_characters") + else "" + ) + db.execute( + text( + f""" + WITH dupes AS ( + SELECT + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid) AS ws_key, + product_section, + model, + usage_date, + COALESCE(usage_kind, 'llm') AS kind_key, + context, + MIN(id::text)::uuid AS keep_id, + SUM(prompt_tokens)::bigint AS prompt_tokens, + SUM(completion_tokens)::bigint AS completion_tokens, + SUM(cache_read_tokens)::bigint AS cache_read_tokens, + SUM(cache_creation_tokens)::bigint AS cache_creation_tokens, + SUM(reasoning_tokens)::bigint AS reasoning_tokens, + SUM(COALESCE(audio_seconds, 0))::bigint AS audio_seconds, + {tts_col} + SUM(call_count)::bigint AS call_count + FROM llm_usage_daily + GROUP BY 1, 2, 3, 4, 5, 6, 7 + HAVING COUNT(*) > 1 + ) + UPDATE llm_usage_daily AS u SET + prompt_tokens = d.prompt_tokens, + completion_tokens = d.completion_tokens, + cache_read_tokens = d.cache_read_tokens, + cache_creation_tokens = d.cache_creation_tokens, + reasoning_tokens = d.reasoning_tokens, + audio_seconds = d.audio_seconds, + {tts_set} + call_count = d.call_count, + updated_at = now() + FROM dupes AS d + WHERE u.id = d.keep_id + """ + ) + ) + result = db.execute( + text( + f""" + WITH keepers AS ( + SELECT MIN(id::text)::uuid AS keep_id + FROM llm_usage_daily + GROUP BY + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + usage_date, + COALESCE(usage_kind, 'llm'), + context + HAVING COUNT(*) > 1 + ) + DELETE FROM llm_usage_daily AS u + USING keepers AS k, + llm_usage_daily AS peer + WHERE peer.id = k.keep_id + AND u.id <> k.keep_id + AND u.organization_id = peer.organization_id + AND u.product_section = peer.product_section + AND u.model = peer.model + AND u.usage_date = peer.usage_date + AND COALESCE(u.usage_kind, 'llm') = COALESCE(peer.usage_kind, 'llm') + AND u.workspace_id IS NOT DISTINCT FROM peer.workspace_id + AND u.context IS NOT DISTINCT FROM peer.context + """ + ) + ) + return int(result.rowcount or 0) + + +def _ensure_context_bucket_index(db: Session) -> None: + db.execute(text("DROP INDEX IF EXISTS uq_llm_usage_daily_bucket")) + db.execute( + text( + f""" + CREATE UNIQUE INDEX uq_llm_usage_daily_bucket + ON llm_usage_daily ( + organization_id, + COALESCE(workspace_id, '{_ZERO_UUID}'::uuid), + product_section, + model, + usage_date, + usage_kind, + context + ) + """ + ) + ) + + +def upgrade(db: Session): + if not _table_exists(db, "llm_usage_daily"): + print("llm_usage_daily missing; skipping 067") + db.commit() + return + if not _column_exists(db, "llm_usage_daily", "context"): + print("llm_usage_daily.context missing; run 065 first — skipping 067") + db.commit() + return + + legacy_removed = _dedupe_by_context_resource_keys(db) + if legacy_removed: + print(f"Merged {legacy_removed} legacy resource-scoped duplicate row(s)") + + context_removed = _dedupe_by_full_context(db) + if context_removed: + print(f"Merged {context_removed} duplicate full-context row(s)") + + _ensure_context_bucket_index(db) + print("Recreated uq_llm_usage_daily_bucket on full context JSONB") + db.commit() + + +def downgrade(db: Session): + db.commit() diff --git a/app/models/database.py b/app/models/database.py index 9f32f212..00b99fdb 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -19,7 +19,7 @@ select, text, ) -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import relationship from sqlalchemy.sql import func import uuid @@ -2780,7 +2780,7 @@ class JudgeRun(Base): class LLMUsageDaily(Base): - """Daily LLM usage rollups for org-scoped Usage reporting.""" + """Daily LLM/STT usage rollups for org-scoped Usage reporting.""" __tablename__ = "llm_usage_daily" @@ -2799,9 +2799,9 @@ class LLMUsageDaily(Base): ) product_section = Column(String(64), nullable=False, index=True) model = Column(String(255), nullable=False, index=True) - resource_id = Column(UUID(as_uuid=True), nullable=True, index=True) - resource_type = Column(String(64), nullable=True) + context = Column(JSONB, nullable=False, server_default="{}", default=dict) usage_date = Column(Date, nullable=False, index=True) + usage_kind = Column(String(16), nullable=False, default="llm", server_default="llm") prompt_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") completion_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") cache_read_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") @@ -2809,6 +2809,8 @@ class LLMUsageDaily(Base): BigInteger, nullable=False, default=0, server_default="0" ) reasoning_tokens = Column(BigInteger, nullable=False, default=0, server_default="0") + audio_seconds = Column(BigInteger, nullable=False, default=0, server_default="0") + tts_characters = Column(BigInteger, nullable=False, default=0, server_default="0") call_count = Column(BigInteger, nullable=False, default=0, server_default="0") created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column( diff --git a/app/services/ai/llm_gateway.py b/app/services/ai/llm_gateway.py index 750b1641..4b1a08dc 100644 --- a/app/services/ai/llm_gateway.py +++ b/app/services/ai/llm_gateway.py @@ -10,6 +10,7 @@ from __future__ import annotations import os +from contextlib import contextmanager from dataclasses import dataclass from typing import Any, Dict, Literal, Optional, Tuple from urllib.parse import urlparse @@ -723,10 +724,63 @@ def litellm_completion( model_name = str(kwargs.get("model") or "unknown") if "/" in model_name: model_name = model_name.rsplit("/", 1)[-1] - record_llm_usage(model_name, normalize_llm_usage(raw_response=response)) + record_llm_usage( + model_name, + normalize_llm_usage(raw_response=response), + organization_id=organization_id, + ) finally: if usage_token is not None: reset_usage_context(usage_token) except Exception as exc: logger.debug("litellm_completion usage record skipped: {}", exc) return response + + +@contextmanager +def litellm_batch_completion_recording( + *, + organization_id: UUID, + db: Session, + model: Optional[str] = None, + credential: Optional[CredentialRoutingContext] = None, +): + """Temporarily wrap litellm.batch_completion to record usage for each response.""" + import litellm + + from app.services.usage.llm_usage import record_llm_usage + from app.services.usage.normalize import normalize_llm_usage + + original = litellm.batch_completion + + def _recording_batch(**kwargs: Any): + kwargs = apply_llm_gateway( + kwargs, + organization_id=organization_id, + db=db, + model=model or kwargs.get("model"), + credential=credential, + ) + responses = original(**kwargs) + model_name = str(kwargs.get("model") or model or "unknown") + if "/" in model_name: + model_name = model_name.rsplit("/", 1)[-1] + items = responses if isinstance(responses, list) else [responses] + for resp in items: + if resp is None: + continue + try: + record_llm_usage( + model_name, + normalize_llm_usage(raw_response=resp), + organization_id=organization_id, + ) + except Exception as exc: + logger.debug("litellm batch usage record skipped: {}", exc) + return responses + + litellm.batch_completion = _recording_batch + try: + yield + finally: + litellm.batch_completion = original diff --git a/app/services/ai/llm_service.py b/app/services/ai/llm_service.py index a0b2090a..4bf5b61f 100644 --- a/app/services/ai/llm_service.py +++ b/app/services/ai/llm_service.py @@ -567,7 +567,9 @@ def generate_response( result["usage"]["cache_read_tokens"] = snapshot.cache_read_tokens result["usage"]["cache_creation_tokens"] = snapshot.cache_creation_tokens result["usage"]["reasoning_tokens"] = snapshot.reasoning_tokens - record_llm_usage(llm_model, snapshot) + record_llm_usage( + llm_model, snapshot, organization_id=organization_id + ) finally: if usage_token is not None: reset_usage_context(usage_token) diff --git a/app/services/ai/stt_clients/google.py b/app/services/ai/stt_clients/google.py index 6d86ec30..2bc100a1 100644 --- a/app/services/ai/stt_clients/google.py +++ b/app/services/ai/stt_clients/google.py @@ -152,6 +152,30 @@ def transcribe_google( f"Gemini transcription failed for {litellm_model}: {e}" ) + if organization_id is not None: + try: + from app.services.usage.llm_usage import record_llm_usage + from app.services.usage.normalize import normalize_llm_usage + + gemini_model = _strip_stt_suffix(model) + record_llm_usage( + gemini_model, + normalize_llm_usage(raw_response=response), + organization_id=organization_id, + ) + from app.services.usage.llm_usage import probe_audio_seconds, record_stt_usage + + audio_seconds = probe_audio_seconds(audio_file_path) + if audio_seconds > 0: + record_stt_usage( + gemini_model, + audio_seconds=audio_seconds, + organization_id=organization_id, + count_call=False, + ) + except Exception as exc: + logger.debug("[transcribe_google] llm usage record skipped: %s", exc) + text = "" try: text = (response.choices[0].message.content or "").strip() diff --git a/app/services/ai/transcription_service.py b/app/services/ai/transcription_service.py index fbe87c7c..9620e1ca 100644 --- a/app/services/ai/transcription_service.py +++ b/app/services/ai/transcription_service.py @@ -579,6 +579,24 @@ def transcribe_text_only( return None text = (result.get("text") or "").strip() + try: + from app.services.usage.llm_usage import ( + probe_audio_seconds, + record_stt_usage, + ) + + audio_seconds = probe_audio_seconds(audio_file_path) + if stt_provider != ModelProvider.GOOGLE: + record_stt_usage( + stt_model or "unknown", + audio_seconds=audio_seconds, + organization_id=organization_id, + ) + except Exception as exc: + logger.debug( + "[TranscriptionService] text-only stt usage record skipped: %s", + exc, + ) return text or None except Exception as e: logger.error(f"[TranscriptionService] text-only transcription failed ({stt_provider}/{stt_model}): {e}") @@ -661,6 +679,33 @@ def transcribe( else: result = self._transcribe_with_whisper_local(temp_file_path, "base") + try: + from app.services.usage.llm_usage import ( + probe_audio_seconds, + record_stt_usage, + ) + + audio_seconds = 0 + if isinstance(result, dict): + raw_duration = result.get("duration") + if raw_duration is not None: + try: + audio_seconds = int(float(raw_duration)) + except (TypeError, ValueError): + audio_seconds = 0 + if audio_seconds <= 0 and temp_file_path: + audio_seconds = probe_audio_seconds(temp_file_path) + if stt_provider != ModelProvider.GOOGLE: + record_stt_usage( + stt_model or "unknown", + audio_seconds=audio_seconds, + organization_id=organization_id, + ) + except Exception as exc: + logger.debug( + "[TranscriptionService] stt usage record skipped: %s", exc + ) + # Apply speaker diarization if enabled speaker_segments = None if enable_speaker_diarization: diff --git a/app/services/ai/tts_service.py b/app/services/ai/tts_service.py index 640c682c..4d1162f4 100644 --- a/app/services/ai/tts_service.py +++ b/app/services/ai/tts_service.py @@ -262,6 +262,11 @@ def synthesize( api_key = self._get_api_key_for_provider(tts_provider, db, organization_id) handler = self._get_tts_handler(tts_provider) audio_bytes, _ttfb_ms = handler(text, tts_model, api_key, voice, config) + self._record_tts_usage( + text=text, + tts_model=tts_model, + organization_id=organization_id, + ) return audio_bytes def synthesize_timed( @@ -280,8 +285,40 @@ def synthesize_timed( start = time.time() audio_bytes, ttfb_ms = handler(text, tts_model, api_key, voice, config) total_latency_ms = (time.time() - start) * 1000 + self._record_tts_usage( + text=text, + tts_model=tts_model, + organization_id=organization_id, + ) return audio_bytes, total_latency_ms, ttfb_ms + def _record_tts_usage( + self, + *, + text: str, + tts_model: str, + organization_id: UUID, + ) -> None: + try: + from app.services.usage.context import ( + ensure_usage_context, + reset_usage_context, + ) + from app.services.usage.llm_usage import record_tts_usage + + usage_token = ensure_usage_context(organization_id) + try: + record_tts_usage( + tts_model, + characters=len(text or ""), + organization_id=organization_id, + ) + finally: + if usage_token is not None: + reset_usage_context(usage_token) + except Exception: + pass + def synthesize_and_upload( self, text: str, diff --git a/app/services/evaluators/call_data_transcript.py b/app/services/evaluators/call_data_transcript.py new file mode 100644 index 00000000..4c7afe2e --- /dev/null +++ b/app/services/evaluators/call_data_transcript.py @@ -0,0 +1,195 @@ +"""Extract transcript text from voice-provider call payloads.""" + +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + + +def extract_transcript_from_call_data( + call_data: Dict[str, Any], + provider_platform: str, +) -> Tuple[str, List[dict]]: + """Return plain-text transcript and speaker segments from provider call_data.""" + transcript_text = "" + speaker_segments: List[dict] = [] + + if not call_data: + return transcript_text, speaker_segments + + provider_platform_lower = provider_platform.lower() if provider_platform else "" + + if provider_platform_lower == "vapi": + transcript_text = call_data.get("transcript", "") or "" + transcript_object = call_data.get("transcript_object", []) + if not transcript_object: + artifact = call_data.get("artifact", {}) if isinstance(call_data, dict) else {} + messages = call_data.get("messages", []) or artifact.get("messages", []) + for msg in messages: + role = msg.get("role", "unknown") + content = msg.get("message", "") or msg.get("content", "") + if not content or role == "system": + continue + if role in ("bot", "assistant"): + normalized_role = "agent" + elif role == "user": + normalized_role = "user" + else: + continue + speaker_segments.append( + { + "speaker": "Agent" if normalized_role == "agent" else "User", + "text": content, + "start": msg.get("secondsFromStart", 0), + "end": msg.get("secondsFromStart", 0) + + (msg.get("duration", 0) / 1000), + } + ) + else: + for entry in transcript_object: + role = entry.get("role", "unknown") + content = entry.get("content", "") + if not content: + continue + speaker_segments.append( + { + "speaker": "Agent" if role == "agent" else "User", + "text": content, + "start": entry.get("seconds_from_start", 0), + "end": entry.get("seconds_from_start", 0) + + (entry.get("duration_ms", 0) / 1000), + } + ) + if not transcript_text and speaker_segments: + transcript_text = "\n".join( + f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments + ) + + elif provider_platform_lower == "elevenlabs": + raw_transcript = call_data.get("transcript") + transcript_obj = call_data.get("transcript_object", []) + if isinstance(raw_transcript, str) and raw_transcript: + transcript_text = raw_transcript + if isinstance(transcript_obj, list): + for seg in transcript_obj: + speaker_segments.append( + { + "speaker": seg.get("speaker", "Unknown"), + "text": seg.get("text", ""), + "start": seg.get("start", 0), + "end": seg.get("end", 0), + } + ) + elif isinstance(raw_transcript, list): + for entry in raw_transcript: + role = entry.get("role", "unknown") + content = entry.get("message", "") or entry.get("text", "") + if not content: + continue + speaker = "Agent" if role in ("agent", "assistant", "ai") else "User" + speaker_segments.append( + { + "speaker": speaker, + "text": content, + "start": entry.get("time_in_call_secs", 0) or entry.get("start", 0), + "end": entry.get("time_in_call_secs", 0) or entry.get("end", 0), + } + ) + transcript_text = "\n".join( + f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments + ) + + elif provider_platform_lower == "smallest": + transcript_raw = call_data.get("transcript") + transcript_object = call_data.get("transcript_object", []) + if isinstance(transcript_object, list) and transcript_object: + for entry in transcript_object: + if not isinstance(entry, dict): + continue + text = entry.get("text", "") + if not text: + continue + speaker_segments.append( + { + "speaker": entry.get("speaker", "Unknown"), + "text": text, + "start": entry.get("start", 0), + "end": entry.get("end", entry.get("start", 0)), + } + ) + if not transcript_text: + transcript_text = "\n".join( + f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments + ) + elif isinstance(transcript_raw, list): + for entry in transcript_raw: + if not isinstance(entry, dict): + continue + role = str(entry.get("speaker") or entry.get("role") or "").lower() + speaker = "Agent" if role in ("agent", "assistant", "ai", "bot") else "User" + text = entry.get("text", "") or entry.get("message", "") or entry.get("content", "") + if not text: + continue + ts = entry.get("timeInCallSecs", 0) or entry.get("start", 0) or entry.get("timestamp", 0) + speaker_segments.append( + { + "speaker": speaker, + "text": text, + "start": ts, + "end": entry.get("end", ts), + } + ) + transcript_text = "\n".join( + f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments + ) + elif isinstance(transcript_raw, str): + transcript_text = transcript_raw + + elif provider_platform_lower == "retell": + transcript_raw = call_data.get("transcript", "") + if isinstance(transcript_raw, str): + transcript_text = transcript_raw + lines = transcript_raw.split("\n") if transcript_raw else [] + for line in lines: + line = line.strip() + if not line: + continue + if line.startswith("Agent:") or line.startswith("agent:"): + speaker_segments.append( + { + "speaker": "Agent", + "text": line.split(":", 1)[1].strip() if ":" in line else line, + "start": 0, + "end": 0, + } + ) + elif line.startswith("User:") or line.startswith("user:"): + speaker_segments.append( + { + "speaker": "User", + "text": line.split(":", 1)[1].strip() if ":" in line else line, + "start": 0, + "end": 0, + } + ) + elif isinstance(transcript_raw, list): + for item in transcript_raw: + if not isinstance(item, dict): + continue + role = item.get("role", "") + content = item.get("content", "") or item.get("text", "") + if not content: + continue + speaker = "Agent" if role in ["agent", "assistant", "bot"] else "User" + speaker_segments.append( + { + "speaker": speaker, + "text": content, + "start": item.get("start_time", 0) or item.get("timestamp", 0), + "end": item.get("end_time", 0), + } + ) + transcript_text = "\n".join( + f"{seg['speaker']}: {seg['text']}" for seg in speaker_segments + ) + + return transcript_text, speaker_segments diff --git a/app/services/judge_alignment/gepa_bridge.py b/app/services/judge_alignment/gepa_bridge.py index 37926fa1..6a12506b 100644 --- a/app/services/judge_alignment/gepa_bridge.py +++ b/app/services/judge_alignment/gepa_bridge.py @@ -219,6 +219,31 @@ def execute_judge_gepa(run_id: str, db: Session) -> Dict[str, Any]: if not evaluator: raise RuntimeError("Evaluator vanished between dispatch and execution") + from app.services.usage.context import ( + reset_usage_context, + set_usage_context, + usage_context_for_prompt_optimization_run, + ) + + usage_token = set_usage_context(usage_context_for_prompt_optimization_run(run)) + try: + return _execute_judge_gepa_with_context( + run=run, + cfg=cfg, + evaluator=evaluator, + db=db, + ) + finally: + reset_usage_context(usage_token) + + +def _execute_judge_gepa_with_context( + *, + run: PromptOptimizationRun, + cfg: Dict[str, Any], + evaluator: Evaluator, + db: Session, +) -> Dict[str, Any]: dev_ids: List[str] = cfg.get("dev_sample_ids", []) if not dev_ids: raise RuntimeError("Optimisation run has no dev_sample_ids in config") @@ -370,19 +395,27 @@ def reflection_lm(prompt: str) -> str: resp = litellm_completion(**reflection_kwargs, credential=credential_ctx) return resp.choices[0].message.content + from app.services.ai.llm_gateway import litellm_batch_completion_recording + run.status = PromptOptimizationStatus.RUNNING.value db.commit() try: - result = gepa_optimize( - seed_candidate={"system_prompt": evaluator.custom_prompt}, - trainset=trainset, - adapter=adapter, - reflection_lm=reflection_lm, - max_metric_calls=int(cfg.get("max_metric_calls", 20)), - reflection_minibatch_size=1, - candidate_selection_strategy="pareto", - ) + with litellm_batch_completion_recording( + organization_id=run.organization_id, + db=db, + model=lm_identifier, + credential=credential_ctx, + ): + result = gepa_optimize( + seed_candidate={"system_prompt": evaluator.custom_prompt}, + trainset=trainset, + adapter=adapter, + reflection_lm=reflection_lm, + max_metric_calls=int(cfg.get("max_metric_calls", 20)), + reflection_minibatch_size=1, + candidate_selection_strategy="pareto", + ) except Exception as exc: run.status = PromptOptimizationStatus.FAILED.value run.error_message = str(exc) diff --git a/app/services/optimization/gepa_service.py b/app/services/optimization/gepa_service.py index 5328dba9..d92895ac 100644 --- a/app/services/optimization/gepa_service.py +++ b/app/services/optimization/gepa_service.py @@ -149,15 +149,23 @@ def reflection_lm(prompt: str) -> str: resp = litellm_completion(**reflection_kwargs, credential=credential_ctx) return resp.choices[0].message.content - result = gepa_optimize( - seed_candidate={"system_prompt": seed_prompt}, - trainset=trainset, - adapter=adapter, - reflection_lm=reflection_lm, - max_metric_calls=max_metric_calls, - reflection_minibatch_size=min(minibatch_size, len(trainset)), - candidate_selection_strategy="pareto", - ) + from app.services.ai.llm_gateway import litellm_batch_completion_recording + + with litellm_batch_completion_recording( + organization_id=organization_id, + db=db, + model=lm_identifier, + credential=credential_ctx, + ): + result = gepa_optimize( + seed_candidate={"system_prompt": seed_prompt}, + trainset=trainset, + adapter=adapter, + reflection_lm=reflection_lm, + max_metric_calls=max_metric_calls, + reflection_minibatch_size=min(minibatch_size, len(trainset)), + candidate_selection_strategy="pareto", + ) return _format_result(result, seed_prompt) diff --git a/app/services/testing/test_agent_bridge_service.py b/app/services/testing/test_agent_bridge_service.py index f8e8d41a..4a05cdb0 100644 --- a/app/services/testing/test_agent_bridge_service.py +++ b/app/services/testing/test_agent_bridge_service.py @@ -613,6 +613,8 @@ def resolve_api_key_for_provider( ) test_agent_config = TestAgentConfig( + organization_id=organization_id, + workspace_id=getattr(agent, "workspace_id", None), agent_name=agent.name or "Voice AI Agent", agent_description=agent.description or "A voice AI assistant", test_agent_simulation_prompt=simulation_prompt, diff --git a/app/services/usage/__init__.py b/app/services/usage/__init__.py index 646e9adb..4ebc75ca 100644 --- a/app/services/usage/__init__.py +++ b/app/services/usage/__init__.py @@ -1,4 +1,4 @@ -"""LLM usage tracking (tokens, calls) with Redis buffer and catalog rollups.""" +"""LLM/STT/TTS usage tracking with Redis buffer, PG fallback, and catalog rollups.""" from app.services.usage.context import ( LLMUsageContext, @@ -10,13 +10,27 @@ reset_usage_hints, set_usage_context, set_usage_hints, + usage_context_for_judge_run, + usage_context_for_prompt_optimization_run, + usage_context_for_prompt_partial, +) +from app.services.usage.llm_usage import ( + flush_all_usage_to_catalog, + flush_usage_to_catalog, + probe_audio_seconds, + record_call_usage, + record_llm_usage, + record_stt_usage, + record_tts_usage, ) -from app.services.usage.llm_usage import flush_all_usage_to_catalog, flush_usage_to_catalog, record_llm_usage from app.services.usage.normalize import UsageSnapshot, normalize_llm_usage __all__ = [ "LLMUsageContext", "LLMUsageProductSection", + "usage_context_for_judge_run", + "usage_context_for_prompt_optimization_run", + "usage_context_for_prompt_partial", "UsageSnapshot", "ensure_usage_context", "flush_all_usage_to_catalog", @@ -24,7 +38,11 @@ "infer_product_section_from_path", "llm_usage_context", "normalize_llm_usage", + "probe_audio_seconds", + "record_call_usage", "record_llm_usage", + "record_stt_usage", + "record_tts_usage", "reset_usage_context", "reset_usage_hints", "set_usage_context", diff --git a/app/services/usage/bucket_context.py b/app/services/usage/bucket_context.py new file mode 100644 index 00000000..ad6202d9 --- /dev/null +++ b/app/services/usage/bucket_context.py @@ -0,0 +1,114 @@ +"""Canonical usage bucket context (JSONB) helpers.""" + +from __future__ import annotations + +import json +from typing import Any, Dict, Optional +from uuid import UUID + +_NONE = "__none__" + +# Stable keys stored in llm_usage_daily.context (extend without new columns). +KNOWN_CONTEXT_KEYS = frozenset( + { + "resource_id", + "resource_type", + "call_import_id", + "evaluation_id", + "evaluation_row_id", + "call_import_row_id", + "credential_id", + "agent_id", + "job_id", + "user_id", + "trace_id", + } +) + +# Never store roll-up counters in JSONB — they stay as BIGINT columns for SUM(). +_FORBIDDEN_CONTEXT_KEYS = frozenset( + { + "prompt_tokens", + "completion_tokens", + "cache_read_tokens", + "cache_creation_tokens", + "reasoning_tokens", + "audio_seconds", + "tts_characters", + "call_count", + "total_tokens", + } +) + + +def build_bucket_context( + *, + resource_id: Optional[UUID] = None, + resource_type: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, +) -> Dict[str, str]: + """Build a normalized string-keyed context dict for rollup buckets.""" + ctx: Dict[str, str] = {} + if resource_id is not None: + ctx["resource_id"] = str(resource_id) + if resource_type: + ctx["resource_type"] = resource_type + if extra: + for key, value in extra.items(): + if value is None or key in ctx: + continue + if key in _FORBIDDEN_CONTEXT_KEYS: + continue + ctx[key] = str(value) + return ctx + + +def context_bucket_token(context: Optional[Dict[str, Any]]) -> str: + """Stable Redis bucket token for a context dict.""" + if not context: + return _NONE + normalized = {k: str(v) for k, v in sorted(context.items()) if v is not None} + if not normalized: + return _NONE + return json.dumps(normalized, sort_keys=True, separators=(",", ":")) + + +def parse_context_bucket_token(token: str) -> Dict[str, str]: + if not token or token == _NONE: + return {} + try: + parsed = json.loads(token) + except json.JSONDecodeError: + return {} + if not isinstance(parsed, dict): + return {} + return {str(k): str(v) for k, v in parsed.items() if v is not None} + + +def legacy_resource_context( + resource_id: Optional[UUID], + resource_type: Optional[str], +) -> Dict[str, str]: + return build_bucket_context( + resource_id=resource_id, + resource_type=resource_type, + ) + + +def resource_id_from_context(context: Optional[Dict[str, Any]]) -> Optional[UUID]: + if not context: + return None + raw = context.get("resource_id") + if not raw: + return None + try: + return UUID(str(raw)) + except (ValueError, TypeError): + return None + + +def resource_type_from_context(context: Optional[Dict[str, Any]]) -> Optional[str]: + if not context: + return None + raw = context.get("resource_type") + return str(raw) if raw else None diff --git a/app/services/usage/call_import_context.py b/app/services/usage/call_import_context.py new file mode 100644 index 00000000..e23f46cf --- /dev/null +++ b/app/services/usage/call_import_context.py @@ -0,0 +1,200 @@ +"""Usage context builders for call-import evaluation pipelines.""" + +from __future__ import annotations + +from typing import Any, Optional +from uuid import UUID + +from app.services.usage.context import LLMUsageContext, LLMUsageProductSection + + +def _parse_uuid(raw: Any) -> Optional[UUID]: + try: + return UUID(str(raw)) + except (TypeError, ValueError): + return None + + +def call_import_ids_from_usage_context(ctx: LLMUsageContext) -> dict[str, Optional[UUID]]: + """Extract call-import linkage ids from a usage context (no DB).""" + extra = ctx.extra or {} + ids: dict[str, Optional[UUID]] = { + "call_import_id": _parse_uuid(extra.get("call_import_id")), + "evaluation_id": _parse_uuid(extra.get("evaluation_id")), + "call_import_row_id": _parse_uuid(extra.get("call_import_row_id")), + "evaluation_row_id": _parse_uuid(extra.get("evaluation_row_id")), + } + if ctx.resource_type == "call_import" and ctx.resource_id: + ids["call_import_id"] = ids["call_import_id"] or ctx.resource_id + if ctx.resource_type == "call_import_evaluation" and ctx.resource_id: + ids["evaluation_id"] = ids["evaluation_id"] or ctx.resource_id + return ids + + +def resolve_workspace_id_for_usage_context(ctx: LLMUsageContext) -> Optional[UUID]: + """Resolve workspace from call-import entities when context omitted workspace_id.""" + if ctx.workspace_id is not None: + return ctx.workspace_id + + ids = call_import_ids_from_usage_context(ctx) + if not any(ids.values()): + return None + + from app.database import SessionLocal + from app.models.database import ( + CallImport, + CallImportEvaluation, + CallImportEvaluationRow, + CallImportRow, + ) + + org_id = ctx.organization_id + db = SessionLocal() + try: + if ids["call_import_id"]: + ws = ( + db.query(CallImport.workspace_id) + .filter( + CallImport.id == ids["call_import_id"], + CallImport.organization_id == org_id, + ) + .scalar() + ) + if ws: + return ws + + if ids["evaluation_id"]: + ws = ( + db.query(CallImportEvaluation.workspace_id) + .filter( + CallImportEvaluation.id == ids["evaluation_id"], + CallImportEvaluation.organization_id == org_id, + ) + .scalar() + ) + if ws: + return ws + + if ids["call_import_row_id"]: + ws = ( + db.query(CallImportRow.workspace_id) + .filter( + CallImportRow.id == ids["call_import_row_id"], + CallImportRow.organization_id == org_id, + ) + .scalar() + ) + if ws: + return ws + + if ids["evaluation_row_id"]: + ws = ( + db.query(CallImportEvaluation.workspace_id) + .join( + CallImportEvaluationRow, + CallImportEvaluationRow.evaluation_id == CallImportEvaluation.id, + ) + .filter( + CallImportEvaluationRow.id == ids["evaluation_row_id"], + CallImportEvaluation.organization_id == org_id, + ) + .scalar() + ) + if ws: + return ws + + return None + finally: + db.close() + + +def enrich_usage_context_workspace(ctx: LLMUsageContext) -> LLMUsageContext: + """Fill workspace_id from call-import linkage when missing at record time.""" + if ctx.workspace_id is not None: + return ctx + try: + resolved = resolve_workspace_id_for_usage_context(ctx) + except Exception: + return ctx + if resolved is None: + return ctx + return LLMUsageContext( + organization_id=ctx.organization_id, + workspace_id=resolved, + product_section=ctx.product_section, + resource_id=ctx.resource_id, + resource_type=ctx.resource_type, + extra=ctx.extra, + ) + + +def call_import_evaluation_usage_context( + *, + organization_id: UUID, + workspace_id: Optional[UUID], + evaluation_id: UUID, + call_import_id: UUID, + evaluation_row_id: Optional[UUID] = None, + call_import_row_id: Optional[UUID] = None, +) -> LLMUsageContext: + """Full attribution for eval-run LLM/STT/TTS inside a call import evaluation.""" + extra: dict[str, str] = { + "call_import_id": str(call_import_id), + "evaluation_id": str(evaluation_id), + } + if evaluation_row_id is not None: + extra["evaluation_row_id"] = str(evaluation_row_id) + if call_import_row_id is not None: + extra["call_import_row_id"] = str(call_import_row_id) + return LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, + resource_id=evaluation_id, + resource_type="call_import_evaluation", + extra=extra, + ) + + +def call_import_row_usage_context( + *, + organization_id: UUID, + workspace_id: Optional[UUID], + call_import_id: UUID, + call_import_row_id: UUID, + evaluation_id: Optional[UUID] = None, + evaluation_row_id: Optional[UUID] = None, +) -> LLMUsageContext: + """Attribution for diarisation / STT on a single call-import row.""" + if evaluation_id is not None: + return call_import_evaluation_usage_context( + organization_id=organization_id, + workspace_id=workspace_id, + evaluation_id=evaluation_id, + call_import_id=call_import_id, + evaluation_row_id=evaluation_row_id, + call_import_row_id=call_import_row_id, + ) + return LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.CALL_IMPORTS, + resource_id=call_import_id, + resource_type="call_import", + extra={ + "call_import_id": str(call_import_id), + "call_import_row_id": str(call_import_row_id), + }, + ) + + +def usage_context_for_evaluation( + evaluation: Any, +) -> LLMUsageContext: + """Usage context from a loaded CallImportEvaluation row.""" + return call_import_evaluation_usage_context( + organization_id=evaluation.organization_id, + workspace_id=evaluation.workspace_id, + evaluation_id=evaluation.id, + call_import_id=evaluation.call_import_id, + ) diff --git a/app/services/usage/context.py b/app/services/usage/context.py index 80e84fac..8bd895e1 100644 --- a/app/services/usage/context.py +++ b/app/services/usage/context.py @@ -6,7 +6,7 @@ from contextvars import ContextVar, Token from dataclasses import dataclass from enum import Enum -from typing import Iterator, Optional +from typing import Iterator, Optional, Any from uuid import UUID @@ -36,6 +36,7 @@ class LLMUsageContext: product_section: LLMUsageProductSection = LLMUsageProductSection.OTHER resource_id: Optional[UUID] = None resource_type: Optional[str] = None + extra: Optional[dict[str, str]] = None _usage_context_var: ContextVar[Optional[LLMUsageContext]] = ContextVar( @@ -128,6 +129,7 @@ def ensure_usage_context( product_section: LLMUsageProductSection = LLMUsageProductSection.OTHER, resource_id: Optional[UUID] = None, resource_type: Optional[str] = None, + extra: Optional[dict[str, str]] = None, ) -> Token | None: """Set or enrich usage context. Returns token to reset, or None if unchanged.""" resolved_workspace = workspace_id or get_usage_workspace_hint() @@ -146,6 +148,7 @@ def ensure_usage_context( product_section=resolved_section, resource_id=resource_id, resource_type=resource_type, + extra=extra, ) ) @@ -158,11 +161,13 @@ def ensure_usage_context( ) upgraded_resource_id = current.resource_id or resource_id upgraded_resource_type = current.resource_type or resource_type + upgraded_extra = {**(current.extra or {}), **(extra or {})} or None if ( upgraded_workspace == current.workspace_id and upgraded_section == current.product_section and upgraded_resource_id == current.resource_id and upgraded_resource_type == current.resource_type + and upgraded_extra == current.extra ): return None @@ -173,5 +178,109 @@ def ensure_usage_context( product_section=upgraded_section, resource_id=upgraded_resource_id, resource_type=upgraded_resource_type, + extra=upgraded_extra, ) ) + + +def usage_context_for_agent( + agent: Any, + *, + workspace_id: Optional[UUID] = None, + extra: Optional[dict[str, str]] = None, +) -> LLMUsageContext: + """Usage context for agent-scoped LLM work (simulations, setup, summaries).""" + merged: dict[str, str] = dict(extra or {}) + merged.setdefault("agent_id", str(agent.id)) + short = getattr(agent, "agent_id", None) + if short: + merged.setdefault("agent_short_id", str(short)) + return LLMUsageContext( + organization_id=agent.organization_id, + workspace_id=workspace_id or agent.workspace_id, + product_section=LLMUsageProductSection.AGENTS, + resource_id=agent.id, + resource_type="agent", + extra=merged, + ) + + +def usage_context_for_evaluator_result(result: Any) -> LLMUsageContext: + """Usage context for processing an evaluator result (Vapi / playground runs).""" + if result.agent_id: + return LLMUsageContext( + organization_id=result.organization_id, + workspace_id=result.workspace_id, + product_section=LLMUsageProductSection.AGENTS, + resource_id=result.agent_id, + resource_type="agent", + extra={"agent_id": str(result.agent_id)}, + ) + extra: dict[str, str] = {"evaluator_result_id": str(result.id)} + if getattr(result, "result_id", None): + extra["result_short_id"] = str(result.result_id) + if result.evaluator_id: + extra["evaluator_id"] = str(result.evaluator_id) + return LLMUsageContext( + organization_id=result.organization_id, + workspace_id=result.workspace_id, + product_section=LLMUsageProductSection.EVALUATORS, + resource_id=result.id, + resource_type="evaluator_result", + extra=extra, + ) + + +def usage_context_for_prompt_optimization_run(run: Any) -> LLMUsageContext: + """Usage context for a GEPA prompt optimization run.""" + cfg = run.config if isinstance(run.config, dict) else {} + is_judge = cfg.get("source") == "judge_alignment" + extra: dict[str, str] = { + "optimization_run_id": str(run.id), + "agent_id": str(run.agent_id), + } + if run.evaluator_id: + extra["evaluator_id"] = str(run.evaluator_id) + if is_judge: + extra["source"] = "judge_alignment" + if cfg.get("judge_dataset_id"): + extra["judge_dataset_id"] = str(cfg["judge_dataset_id"]) + return LLMUsageContext( + organization_id=run.organization_id, + workspace_id=run.workspace_id, + product_section=( + LLMUsageProductSection.JUDGE_ALIGNMENT + if is_judge + else LLMUsageProductSection.PROMPT_OPTIMIZATION + ), + resource_id=run.agent_id, + resource_type="agent", + extra=extra, + ) + + +def usage_context_for_judge_run(run: Any) -> LLMUsageContext: + """Usage context for a judge alignment scoring run.""" + return LLMUsageContext( + organization_id=run.organization_id, + workspace_id=run.workspace_id, + product_section=LLMUsageProductSection.JUDGE_ALIGNMENT, + resource_id=run.evaluator_id, + resource_type="evaluator", + extra={ + "judge_run_id": str(run.id), + "judge_dataset_id": str(run.dataset_id), + }, + ) + + +def usage_context_for_prompt_partial(partial: Any) -> LLMUsageContext: + """Usage context for prompt partial / agent flowchart LLM work.""" + return LLMUsageContext( + organization_id=partial.organization_id, + workspace_id=partial.workspace_id, + product_section=LLMUsageProductSection.PROMPT_PARTIALS, + resource_id=partial.id, + resource_type="prompt_partial", + extra={"prompt_partial_id": str(partial.id)}, + ) diff --git a/app/services/usage/dates.py b/app/services/usage/dates.py new file mode 100644 index 00000000..340ba9eb --- /dev/null +++ b/app/services/usage/dates.py @@ -0,0 +1,38 @@ +"""Local calendar dates vs UTC usage_date bucket bounds.""" + +from __future__ import annotations + +from datetime import date, datetime, time, timedelta, timezone +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + + +def resolve_usage_timezone(tz: str | None) -> ZoneInfo: + if not tz: + return ZoneInfo("UTC") + try: + return ZoneInfo(tz) + except ZoneInfoNotFoundError: + return ZoneInfo("UTC") + + +def usage_local_today(tz: str | None) -> date: + return datetime.now(resolve_usage_timezone(tz)).date() + + +def usage_date_filter_bounds( + start: date, + end: date, + tz: str | None, +) -> tuple[date, date]: + """Map inclusive local calendar days to usage_date (UTC-day) filter bounds.""" + if not tz: + return start, end + + zone = resolve_usage_timezone(tz) + start_local = datetime.combine(start, time.min, tzinfo=zone) + end_exclusive = datetime.combine(end + timedelta(days=1), time.min, tzinfo=zone) + filter_start = start_local.astimezone(timezone.utc).date() + filter_end = ( + end_exclusive - timedelta(seconds=1) + ).astimezone(timezone.utc).date() + return filter_start, filter_end diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py index 4c1fcecb..33fda254 100644 --- a/app/services/usage/llm_usage.py +++ b/app/services/usage/llm_usage.py @@ -1,9 +1,11 @@ -"""Redis-buffered LLM usage counters with catalog rollup flush.""" +"""Redis-buffered LLM/STT usage counters with catalog rollup flush.""" from __future__ import annotations +import math import time import uuid +import json from datetime import date, datetime, timezone from typing import Any, Dict, Iterable, List, Optional, Tuple from uuid import UUID @@ -11,11 +13,19 @@ import redis from loguru import logger from sqlalchemy import text +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from app.config import settings +from app.services.usage.bucket_context import ( + build_bucket_context, + context_bucket_token, + legacy_resource_context, + parse_context_bucket_token, +) from app.services.usage.context import ( LLMUsageContext, + LLMUsageProductSection, get_usage_context, ) from app.services.usage.normalize import UsageSnapshot @@ -26,16 +36,20 @@ _PENDING_TTL_SECONDS = 14 * 24 * 60 * 60 _FLUSH_LOCK_TTL_SECONDS = 45 _FLUSH_LOCK_WAIT_SECONDS = 3.0 +USAGE_KIND_LLM = "llm" +USAGE_KIND_STT = "stt" +USAGE_KIND_TTS = "tts" _METRIC_FIELDS = ( "prompt_tokens", "completion_tokens", "cache_read_tokens", "cache_creation_tokens", "reasoning_tokens", + "audio_seconds", + "tts_characters", "call_count", ) -# Atomically move pending hash → claim key so only one flusher owns the deltas. _CLAIM_LUA = """ if redis.call('EXISTS', KEYS[1]) == 0 then return 0 @@ -73,51 +87,106 @@ def _bucket_prefix( workspace_id: Optional[UUID], product_section: str, model: str, - resource_id: Optional[UUID], - resource_type: Optional[str], + context: Optional[Dict[str, Any]], usage_date: date, + usage_kind: str, ) -> str: return "|".join( [ _token(workspace_id), product_section, model, - _token(resource_id), - resource_type or _NONE, + context_bucket_token(context), usage_date.isoformat(), + usage_kind or USAGE_KIND_LLM, ] ) def _parse_bucket_prefix(prefix: str) -> Optional[Dict[str, Any]]: parts = prefix.split("|") - if len(parts) != 6: + # New: 6 parts with JSON context token + usage_kind. + if len(parts) == 6: + ws_token, section, model, context_token, day_str, usage_kind = parts + context = parse_context_bucket_token(context_token) + elif len(parts) == 7: + # Legacy Redis keys: resource_id + resource_type before date/kind. + ( + ws_token, + section, + model, + resource_token, + resource_type, + day_str, + usage_kind, + ) = parts + resource_id = None if resource_token == _NONE else UUID(resource_token) + resolved_resource_type = None if resource_type == _NONE else resource_type + context = legacy_resource_context(resource_id, resolved_resource_type) + elif len(parts) == 5: + ws_token, section, model, context_token, day_str = parts + usage_kind = USAGE_KIND_LLM + context = parse_context_bucket_token(context_token) + else: return None - ws_token, section, model, resource_token, resource_type, day_str = parts try: usage_date = date.fromisoformat(day_str) except ValueError: return None workspace_id = None if ws_token == _NONE else UUID(ws_token) - resource_id = None if resource_token == _NONE else UUID(resource_token) - resolved_resource_type = None if resource_type == _NONE else resource_type return { "workspace_id": workspace_id, "product_section": section, "model": model, - "resource_id": resource_id, - "resource_type": resolved_resource_type, + "context": context, "usage_date": usage_date, + "usage_kind": usage_kind or USAGE_KIND_LLM, } -def _resolve_context(ctx: Optional[LLMUsageContext]) -> LLMUsageContext: +def _bucket_from_context( + context: LLMUsageContext, + *, + model: str, + usage_date: date, + usage_kind: str, +) -> Dict[str, Any]: + return { + "workspace_id": context.workspace_id, + "product_section": context.product_section.value, + "model": model, + "context": build_bucket_context( + resource_id=context.resource_id, + resource_type=context.resource_type, + extra=context.extra, + ), + "usage_date": usage_date, + "usage_kind": usage_kind, + } + + +def _resolve_context( + *, + organization_id: Optional[UUID] = None, + ctx: Optional[LLMUsageContext] = None, +) -> Optional[LLMUsageContext]: if ctx is not None: return ctx current = get_usage_context() if current is not None: return current - raise ValueError("LLM usage context is not set") + if organization_id is not None: + from app.services.usage.context import ( + get_usage_section_hint, + get_usage_workspace_hint, + ) + + return LLMUsageContext( + organization_id=organization_id, + workspace_id=get_usage_workspace_hint(), + product_section=get_usage_section_hint(), + ) + return None def _deltas_from_usage(usage: UsageSnapshot) -> Dict[str, int]: @@ -127,24 +196,147 @@ def _deltas_from_usage(usage: UsageSnapshot) -> Dict[str, int]: "cache_read_tokens": usage.cache_read_tokens, "cache_creation_tokens": usage.cache_creation_tokens, "reasoning_tokens": usage.reasoning_tokens, + "audio_seconds": 0, + "tts_characters": 0, + "call_count": 1, + } + + +def _deltas_from_stt(audio_seconds: int, *, count_call: bool = True) -> Dict[str, int]: + return { + "prompt_tokens": 0, + "completion_tokens": 0, + "cache_read_tokens": 0, + "cache_creation_tokens": 0, + "reasoning_tokens": 0, + "audio_seconds": max(0, int(audio_seconds)), + "tts_characters": 0, + "call_count": 1 if count_call else 0, + } + + +def _deltas_from_tts(characters: int) -> Dict[str, int]: + return { + "prompt_tokens": 0, + "completion_tokens": 0, + "cache_read_tokens": 0, + "cache_creation_tokens": 0, + "reasoning_tokens": 0, + "audio_seconds": 0, + "tts_characters": max(0, int(characters)), "call_count": 1, } +def _buffer_to_postgres( + organization_id: UUID, + bucket: Dict[str, Any], + deltas: Dict[str, int], +) -> None: + """Durable fallback when Redis is unavailable.""" + try: + from app.database import SessionLocal + except Exception as exc: + logger.warning("usage postgres fallback unavailable: {}", exc) + return + + params = { + "organization_id": str(organization_id), + "workspace_id": str(bucket["workspace_id"]) if bucket.get("workspace_id") else None, + "product_section": bucket["product_section"], + "model": bucket["model"], + "context": json.dumps(bucket.get("context") or {}), + "usage_date": bucket["usage_date"].isoformat(), + "usage_kind": bucket.get("usage_kind") or USAGE_KIND_LLM, + "prompt_tokens": int(deltas.get("prompt_tokens", 0)), + "completion_tokens": int(deltas.get("completion_tokens", 0)), + "cache_read_tokens": int(deltas.get("cache_read_tokens", 0)), + "cache_creation_tokens": int(deltas.get("cache_creation_tokens", 0)), + "reasoning_tokens": int(deltas.get("reasoning_tokens", 0)), + "audio_seconds": int(deltas.get("audio_seconds", 0)), + "tts_characters": int(deltas.get("tts_characters", 0)), + "call_count": int(deltas.get("call_count", 0)), + } + db = SessionLocal() + try: + db.execute( + text( + """ + INSERT INTO usage_pending_buffer ( + id, organization_id, workspace_id, product_section, model, + context, usage_date, usage_kind, + prompt_tokens, completion_tokens, cache_read_tokens, + cache_creation_tokens, reasoning_tokens, audio_seconds, + tts_characters, call_count, created_at + ) VALUES ( + gen_random_uuid(), CAST(:organization_id AS uuid), + CAST(:workspace_id AS uuid), :product_section, :model, + CAST(:context AS jsonb), + CAST(:usage_date AS date), :usage_kind, + :prompt_tokens, :completion_tokens, :cache_read_tokens, + :cache_creation_tokens, :reasoning_tokens, :audio_seconds, + :tts_characters, :call_count, now() + ) + """ + ), + params, + ) + db.commit() + except Exception as exc: + db.rollback() + logger.warning("usage postgres fallback insert failed: {}", exc) + finally: + db.close() + + +def _incr_pending( + organization_id: UUID, + prefix: str, + deltas: Dict[str, int], + bucket: Dict[str, Any], +) -> None: + hash_key = _pending_hash_key(organization_id) + try: + client = _client() + pipe = client.pipeline() + for metric, delta in deltas.items(): + if delta: + pipe.hincrby(hash_key, f"{prefix}|{metric}", int(delta)) + pipe.sadd("usage:pending:orgs", str(organization_id)) + pipe.expire(hash_key, _PENDING_TTL_SECONDS) + pipe.execute() + except redis.RedisError as exc: + logger.warning("usage redis counter failed, buffering to postgres: {}", exc) + _buffer_to_postgres(organization_id, bucket, deltas) + + +def _context_for_record( + *, + organization_id: Optional[UUID] = None, + ctx: Optional[LLMUsageContext] = None, +) -> Optional[LLMUsageContext]: + context = _resolve_context(organization_id=organization_id, ctx=ctx) + if context is None: + return None + from app.services.usage.call_import_context import enrich_usage_context_workspace + + return enrich_usage_context_workspace(context) + + def record_llm_usage( model: str, usage: UsageSnapshot, *, + organization_id: Optional[UUID] = None, ctx: Optional[LLMUsageContext] = None, usage_date: Optional[date] = None, ) -> None: - """Increment Redis counters for one LLM call (best-effort, never raises).""" + """Increment counters for one LLM call (best-effort, never raises).""" if not model: model = "unknown" - try: - context = _resolve_context(ctx) - except ValueError: - logger.debug("llm usage record skipped: missing context") + context = _context_for_record(organization_id=organization_id, ctx=ctx) + if context is None: + logger.warning("llm usage record skipped: missing organization_id") return deltas = _deltas_from_usage(usage) @@ -152,27 +344,133 @@ def record_llm_usage( return day = usage_date or datetime.now(timezone.utc).date() - prefix = _bucket_prefix( - workspace_id=context.workspace_id, - product_section=context.product_section.value, + bucket = _bucket_from_context( + context, model=model, - resource_id=context.resource_id, - resource_type=context.resource_type, usage_date=day, + usage_kind=USAGE_KIND_LLM, ) - hash_key = _pending_hash_key(context.organization_id) + prefix = _bucket_prefix(**bucket) + _incr_pending(context.organization_id, prefix, deltas, bucket) + + +def record_stt_usage( + model: str, + *, + audio_seconds: float | int = 0, + organization_id: Optional[UUID] = None, + ctx: Optional[LLMUsageContext] = None, + usage_date: Optional[date] = None, + count_call: bool = True, +) -> None: + """Increment counters for one STT call (audio seconds + optional call_count).""" + if not model: + model = "unknown" + context = _context_for_record(organization_id=organization_id, ctx=ctx) + if context is None: + logger.warning("stt usage record skipped: missing organization_id") + return + seconds = int(max(0, math.ceil(float(audio_seconds or 0)))) + deltas = _deltas_from_stt(seconds, count_call=count_call) + day = usage_date or datetime.now(timezone.utc).date() + bucket = _bucket_from_context( + context, + model=model, + usage_date=day, + usage_kind=USAGE_KIND_STT, + ) + prefix = _bucket_prefix(**bucket) + _incr_pending(context.organization_id, prefix, deltas, bucket) + + +def record_call_usage( + model: str = "voice-call", + *, + organization_id: Optional[UUID] = None, + ctx: Optional[LLMUsageContext] = None, + usage_date: Optional[date] = None, + audio_seconds: int = 0, +) -> None: + """Record one completed call session (call_count + optional duration). + + Best-effort, never raises. Uses the same Redis-buffered path as other + usage counters (one pipelined HINCRBY batch per call). + """ + if not model: + model = "voice-call" + context = _context_for_record(organization_id=organization_id, ctx=ctx) + if context is None: + logger.warning("call usage record skipped: missing organization_id") + return + + seconds = max(0, int(audio_seconds or 0)) + deltas = { + "prompt_tokens": 0, + "completion_tokens": 0, + "cache_read_tokens": 0, + "cache_creation_tokens": 0, + "reasoning_tokens": 0, + "audio_seconds": seconds, + "tts_characters": 0, + "call_count": 1, + } + day = usage_date or datetime.now(timezone.utc).date() + bucket = _bucket_from_context( + context, + model=model, + usage_date=day, + usage_kind=USAGE_KIND_LLM, + ) + prefix = _bucket_prefix(**bucket) + _incr_pending(context.organization_id, prefix, deltas, bucket) + + +def record_tts_usage( + model: str, + *, + characters: int = 0, + organization_id: Optional[UUID] = None, + ctx: Optional[LLMUsageContext] = None, + usage_date: Optional[date] = None, +) -> None: + """Increment counters for one TTS call (characters + call_count).""" + if not model: + model = "unknown" + context = _context_for_record(organization_id=organization_id, ctx=ctx) + if context is None: + logger.warning("tts usage record skipped: missing organization_id") + return + + chars = max(0, int(characters or 0)) + deltas = _deltas_from_tts(chars) + day = usage_date or datetime.now(timezone.utc).date() + bucket = _bucket_from_context( + context, + model=model, + usage_date=day, + usage_kind=USAGE_KIND_TTS, + ) + prefix = _bucket_prefix(**bucket) + _incr_pending(context.organization_id, prefix, deltas, bucket) + + +def probe_audio_seconds(audio_file_path: str) -> int: + """Best-effort audio duration in whole seconds.""" + if not audio_file_path: + return 0 try: - client = _client() - pipe = client.pipeline() - for metric, delta in deltas.items(): - if delta: - pipe.hincrby(hash_key, f"{prefix}|{metric}", int(delta)) - pipe.sadd("usage:pending:orgs", str(context.organization_id)) - pipe.expire(hash_key, _PENDING_TTL_SECONDS) - pipe.execute() - except redis.RedisError as exc: - logger.warning("llm usage counter skipped: {}", exc) + from pydub import AudioSegment + + return max(0, int(math.ceil(AudioSegment.from_file(audio_file_path).duration_seconds))) + except Exception: + pass + try: + import librosa + + return max(0, int(math.ceil(librosa.get_duration(path=audio_file_path)))) + except Exception: + return 0 def _parse_hash_to_buckets(raw: Dict[str, str]) -> Dict[str, Dict[str, int]]: @@ -216,7 +514,42 @@ def _restore_buckets_to_pending( pipe.expire(hash_key, _PENDING_TTL_SECONDS) pipe.execute() except redis.RedisError as exc: - logger.warning("llm usage redis restore failed: {}", exc) + logger.warning("llm usage redis restore failed, buffering: {}", exc) + for prefix, metrics in buckets.items(): + parsed = _parse_bucket_prefix(prefix) + if parsed: + _buffer_to_postgres(organization_id, parsed, metrics) + + +_CLAIM_COMMITTED_TTL_SECONDS = 24 * 60 * 60 + + +def _claim_committed_key(claim_key: str) -> str: + return f"usage:claim_done:{claim_key}" + + +def _mark_claim_committed(claim_key: str) -> None: + try: + _client().set(_claim_committed_key(claim_key), "1", ex=_CLAIM_COMMITTED_TTL_SECONDS) + except redis.RedisError: + pass + + +def _is_claim_committed(claim_key: str) -> bool: + try: + return bool(_client().exists(_claim_committed_key(claim_key))) + except redis.RedisError: + return False + + +def _has_pending_usage(organization_id: UUID) -> bool: + try: + client = _client() + if client.exists(_pending_hash_key(organization_id)): + return True + return bool(client.sismember("usage:pending:orgs", str(organization_id))) + except redis.RedisError: + return False def _acquire_flush_lock(organization_id: UUID) -> bool: @@ -248,7 +581,6 @@ def _release_flush_lock(organization_id: UUID) -> None: def _claim_pending( organization_id: UUID, ) -> Tuple[Optional[str], Dict[str, Dict[str, int]]]: - """Rename pending → claim key. Returns (claim_key, buckets) or (None, {}).""" claim_id = str(uuid.uuid4()) pending_key = _pending_hash_key(organization_id) claim_key = _claim_hash_key(organization_id, claim_id) @@ -283,74 +615,137 @@ def _upsert_bucket( organization_id: UUID, bucket: Dict[str, Any], deltas: Dict[str, int], - *, - resource_type: Optional[str], ) -> None: + context = bucket.get("context") or {} params = { "organization_id": str(organization_id), "workspace_id": str(bucket["workspace_id"]) if bucket["workspace_id"] else None, "product_section": bucket["product_section"], "model": bucket["model"], - "resource_id": str(bucket["resource_id"]) if bucket["resource_id"] else None, - "resource_type": resource_type, + "context": json.dumps(context), + "context_resource_id": str(context.get("resource_id") or ""), + "context_resource_type": str(context.get("resource_type") or ""), "usage_date": bucket["usage_date"].isoformat(), + "usage_kind": bucket.get("usage_kind") or USAGE_KIND_LLM, "prompt_tokens": int(deltas.get("prompt_tokens", 0)), "completion_tokens": int(deltas.get("completion_tokens", 0)), "cache_read_tokens": int(deltas.get("cache_read_tokens", 0)), "cache_creation_tokens": int(deltas.get("cache_creation_tokens", 0)), "reasoning_tokens": int(deltas.get("reasoning_tokens", 0)), + "audio_seconds": int(deltas.get("audio_seconds", 0)), + "tts_characters": int(deltas.get("tts_characters", 0)), "call_count": int(deltas.get("call_count", 0)), } - result = db.execute( - text( - """ - UPDATE llm_usage_daily SET + update_set = """ prompt_tokens = prompt_tokens + :prompt_tokens, completion_tokens = completion_tokens + :completion_tokens, cache_read_tokens = cache_read_tokens + :cache_read_tokens, cache_creation_tokens = cache_creation_tokens + :cache_creation_tokens, reasoning_tokens = reasoning_tokens + :reasoning_tokens, + audio_seconds = audio_seconds + :audio_seconds, + tts_characters = tts_characters + :tts_characters, call_count = call_count + :call_count, - resource_type = COALESCE(resource_type, :resource_type), + context = CASE + WHEN context = '{}'::jsonb THEN CAST(:context AS jsonb) + ELSE context || CAST(:context AS jsonb) + END, updated_at = now() + """ + where_base = """ WHERE organization_id = CAST(:organization_id AS uuid) AND product_section = :product_section AND model = :model AND usage_date = CAST(:usage_date AS date) + AND usage_kind = :usage_kind AND workspace_id IS NOT DISTINCT FROM CAST(:workspace_id AS uuid) - AND resource_id IS NOT DISTINCT FROM CAST(:resource_id AS uuid) - """ - ), + """ + exact_context_where = where_base + " AND context = CAST(:context AS jsonb)" + legacy_context_where = ( + where_base + + """ + AND COALESCE(context->>'resource_id', '') = :context_resource_id + AND COALESCE(context->>'resource_type', '') = :context_resource_type + """ + ) + + result = db.execute( + text(f"UPDATE llm_usage_daily SET {update_set} {exact_context_where}"), params, ) if result.rowcount: return - db.execute( - text( - """ + result = db.execute( + text(f"UPDATE llm_usage_daily SET {update_set} {legacy_context_where}"), + params, + ) + if result.rowcount: + return + + db.execute(text("SAVEPOINT llm_usage_bucket_insert")) + try: + db.execute( + text( + """ INSERT INTO llm_usage_daily ( id, organization_id, workspace_id, product_section, model, - resource_id, resource_type, usage_date, + context, usage_date, usage_kind, prompt_tokens, completion_tokens, cache_read_tokens, - cache_creation_tokens, reasoning_tokens, call_count, + cache_creation_tokens, reasoning_tokens, audio_seconds, + tts_characters, call_count, created_at, updated_at ) VALUES ( gen_random_uuid(), CAST(:organization_id AS uuid), CAST(:workspace_id AS uuid), :product_section, :model, - CAST(:resource_id AS uuid), :resource_type, CAST(:usage_date AS date), + CAST(:context AS jsonb), CAST(:usage_date AS date), + :usage_kind, :prompt_tokens, :completion_tokens, :cache_read_tokens, - :cache_creation_tokens, :reasoning_tokens, :call_count, + :cache_creation_tokens, :reasoning_tokens, :audio_seconds, + :tts_characters, :call_count, now(), now() ) """ - ), - params, - ) + ), + params, + ) + db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) + except IntegrityError as exc: + if not _is_unique_violation(exc): + db.execute(text("ROLLBACK TO SAVEPOINT llm_usage_bucket_insert")) + db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) + raise + db.execute(text("ROLLBACK TO SAVEPOINT llm_usage_bucket_insert")) + result = db.execute( + text(f"UPDATE llm_usage_daily SET {update_set} {legacy_context_where}"), + params, + ) + if not result.rowcount: + result = db.execute( + text(f"UPDATE llm_usage_daily SET {update_set} {exact_context_where}"), + params, + ) + db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) + if not result.rowcount: + logger.warning( + "llm usage upsert unique conflict but no matching bucket for org {}", + organization_id, + ) + + +def _is_unique_violation(exc: BaseException) -> bool: + text_blob = " ".join( + str(part) + for part in ( + exc, + getattr(exc, "orig", None), + getattr(getattr(exc, "orig", None), "pgcode", None), + ) + if part is not None + ).lower() + return "uniqueviolation" in text_blob or "duplicate key" in text_blob def _is_missing_organization_fk(exc: BaseException) -> bool: - """True when insert failed because organization_id is not in organizations.""" text_blob = " ".join( str(part) for part in (exc, getattr(exc, "orig", None), getattr(exc, "args", None)) @@ -359,62 +754,193 @@ def _is_missing_organization_fk(exc: BaseException) -> bool: return "llm_usage_daily_organization_id_fkey" in text_blob -def flush_usage_to_catalog(db: Session, organization_id: UUID) -> int: - """Claim Redis deltas, commit to llm_usage_daily, then ack the claim.""" - if not _acquire_flush_lock(organization_id): +def _flush_pending_buffer(db: Session, organization_id: UUID) -> int: + """Drain Postgres write-ahead rows into llm_usage_daily.""" + try: + rows = db.execute( + text( + """ + SELECT id, workspace_id, product_section, model, context, + usage_date, usage_kind, + prompt_tokens, completion_tokens, cache_read_tokens, + cache_creation_tokens, reasoning_tokens, audio_seconds, + tts_characters, call_count + FROM usage_pending_buffer + WHERE organization_id = CAST(:organization_id AS uuid) + ORDER BY created_at ASC + LIMIT 2000 + """ + ), + {"organization_id": str(organization_id)}, + ).mappings().all() + except Exception as exc: + db.rollback() + logger.debug("usage buffer read skipped: {}", exc) + return 0 + if not rows: return 0 - claim_key = None - buckets: Dict[str, Dict[str, int]] = {} + flushed = 0 + ids: List[str] = [] try: - claim_key, buckets = _claim_pending(organization_id) - if not claim_key or not buckets: - return 0 + for row in rows: + row_context = row["context"] or {} + if isinstance(row_context, str): + row_context = json.loads(row_context) + bucket = { + "workspace_id": row["workspace_id"], + "product_section": row["product_section"], + "model": row["model"], + "context": row_context, + "usage_date": row["usage_date"], + "usage_kind": row["usage_kind"] or USAGE_KIND_LLM, + } + deltas = { + "prompt_tokens": int(row["prompt_tokens"] or 0), + "completion_tokens": int(row["completion_tokens"] or 0), + "cache_read_tokens": int(row["cache_read_tokens"] or 0), + "cache_creation_tokens": int(row["cache_creation_tokens"] or 0), + "reasoning_tokens": int(row["reasoning_tokens"] or 0), + "audio_seconds": int(row["audio_seconds"] or 0), + "tts_characters": int(row.get("tts_characters") or 0), + "call_count": int(row["call_count"] or 0), + } + _upsert_bucket( + db, + organization_id, + bucket, + deltas, + ) + ids.append(str(row["id"])) + flushed += 1 + if ids: + from sqlalchemy import bindparam - flushed = 0 - try: - for prefix, deltas in buckets.items(): - parsed = _parse_bucket_prefix(prefix) - if not parsed: - continue - _upsert_bucket( - db, - organization_id, - parsed, - deltas, - resource_type=parsed.get("resource_type"), - ) - flushed += 1 - db.commit() - except Exception as exc: - db.rollback() - if _is_missing_organization_fk(exc): - # Stale/test org ids must not be restored — that loops forever on beat. - logger.warning( - "llm usage flush dropped for unknown organization {}: {}", - organization_id, - exc, - ) - if claim_key: - _ack_claim(claim_key, organization_id) - return 0 - logger.warning("llm usage catalog flush failed, restoring redis: {}", exc) - _restore_buckets_to_pending(organization_id, buckets) - if claim_key: + db.execute( + text( + "DELETE FROM usage_pending_buffer WHERE id IN :ids" + ).bindparams(bindparam("ids", expanding=True)), + {"ids": ids}, + ) + db.commit() + return flushed + except Exception as exc: + db.rollback() + if _is_missing_organization_fk(exc): + logger.warning( + "usage buffer dropped for unknown organization {}: {}", + organization_id, + exc, + ) + if ids: try: - _client().delete(claim_key) - except redis.RedisError: - pass + db.execute( + text( + """ + DELETE FROM usage_pending_buffer + WHERE organization_id = CAST(:organization_id AS uuid) + """ + ), + {"organization_id": str(organization_id)}, + ) + db.commit() + except Exception: + db.rollback() return 0 + logger.warning("usage buffer flush failed: {}", exc) + return 0 - _ack_claim(claim_key, organization_id) - return flushed - finally: - _release_flush_lock(organization_id) + +_CATALOG_FLUSH_COOLDOWN_SEC = 20 + + +def _catalog_flush_recently(organization_id: UUID) -> bool: + """Skip flush if another request flushed this org within the cooldown window.""" + try: + client = _client() + key = f"usage:catalog_flush:{organization_id}" + return not client.set(key, "1", nx=True, ex=_CATALOG_FLUSH_COOLDOWN_SEC) + except redis.RedisError: + return False + + +def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = False) -> int: + """Claim Redis deltas + drain PG buffer into llm_usage_daily.""" + _recover_orphaned_claims() + skip_redis_flush = not force and _catalog_flush_recently(organization_id) + if skip_redis_flush and _has_pending_usage(organization_id): + skip_redis_flush = False + flushed = 0 + if not skip_redis_flush: + redis_locked = _acquire_flush_lock(organization_id) + claim_key = None + buckets: Dict[str, Dict[str, int]] = {} + try: + if redis_locked: + claim_key, buckets = _claim_pending(organization_id) + if claim_key and buckets: + skipped: Dict[str, Dict[str, int]] = {} + try: + for prefix, deltas in buckets.items(): + parsed = _parse_bucket_prefix(prefix) + if not parsed: + skipped[prefix] = deltas + logger.warning( + "llm usage skipped unparseable bucket prefix for org {}", + organization_id, + ) + continue + _upsert_bucket( + db, + organization_id, + parsed, + deltas, + ) + flushed += 1 + db.commit() + except Exception as exc: + db.rollback() + if _is_missing_organization_fk(exc): + logger.warning( + "llm usage flush dropped for unknown organization {}: {}", + organization_id, + exc, + ) + if claim_key: + _mark_claim_committed(claim_key) + _ack_claim(claim_key, organization_id) + claim_key = None + buckets = {} + else: + logger.warning( + "llm usage catalog flush failed, restoring redis: {}", exc + ) + restore_buckets = dict(buckets) + if skipped: + restore_buckets.update(skipped) + _restore_buckets_to_pending(organization_id, restore_buckets) + if claim_key: + try: + _client().delete(claim_key) + except redis.RedisError: + pass + claim_key = None + return flushed + _flush_pending_buffer(db, organization_id) + if skipped: + _restore_buckets_to_pending(organization_id, skipped) + if claim_key: + _mark_claim_committed(claim_key) + _ack_claim(claim_key, organization_id) + claim_key = None + finally: + if redis_locked: + _release_flush_lock(organization_id) + + flushed += _flush_pending_buffer(db, organization_id) + return flushed def _recover_orphaned_claims() -> None: - """Re-queue claim hashes left behind by a crashed flusher.""" try: client = _client() for claim_key in client.scan_iter(match="usage:flushing:*", count=100): @@ -427,6 +953,9 @@ def _recover_orphaned_claims() -> None: continue if client.exists(_flush_lock_key(org_id)): continue + if _is_claim_committed(claim_key): + client.delete(claim_key) + continue buckets = _read_hash_buckets(claim_key) if buckets: _restore_buckets_to_pending(org_id, buckets) @@ -436,10 +965,11 @@ def _recover_orphaned_claims() -> None: def list_pending_organization_ids() -> List[UUID]: + result: List[UUID] = [] + seen: set[UUID] = set() try: client = _client() raw_ids = client.smembers("usage:pending:orgs") - result: List[UUID] = [] stale: List[str] = [] for value in raw_ids: try: @@ -449,17 +979,44 @@ def list_pending_organization_ids() -> List[UUID]: continue if client.exists(_pending_hash_key(org_id)): result.append(org_id) + seen.add(org_id) else: stale.append(value) if stale: client.srem("usage:pending:orgs", *stale) - return result except (redis.RedisError, ValueError): - return [] + pass + + try: + from app.database import SessionLocal + + db = SessionLocal() + try: + rows = db.execute( + text( + """ + SELECT DISTINCT organization_id + FROM usage_pending_buffer + LIMIT 5000 + """ + ) + ).all() + for row in rows: + org_id = row[0] if not isinstance(row, dict) else row["organization_id"] + if isinstance(org_id, str): + org_id = UUID(org_id) + if org_id not in seen: + result.append(org_id) + seen.add(org_id) + finally: + db.close() + except Exception as exc: + logger.debug("usage buffer org scan skipped: {}", exc) + + return result def flush_all_usage_to_catalog(db_factory) -> int: - """Flush all orgs with pending usage (Celery beat).""" _recover_orphaned_claims() total = 0 for org_id in list_pending_organization_ids(): @@ -486,6 +1043,8 @@ def merge_usage_totals( getattr(row, "cache_creation_tokens", 0) or 0 ) totals["reasoning_tokens"] += int(getattr(row, "reasoning_tokens", 0) or 0) + totals["audio_seconds"] += int(getattr(row, "audio_seconds", 0) or 0) + totals["tts_characters"] += int(getattr(row, "tts_characters", 0) or 0) totals["call_count"] += int(getattr(row, "call_count", 0) or 0) totals["total_tokens"] = totals["prompt_tokens"] + totals["completion_tokens"] return totals diff --git a/app/services/usage/usage_labels.py b/app/services/usage/usage_labels.py new file mode 100644 index 00000000..4cee5167 --- /dev/null +++ b/app/services/usage/usage_labels.py @@ -0,0 +1,424 @@ +"""Human-readable labels for usage attribution context (no raw UUIDs in UI).""" + +from __future__ import annotations + +from typing import Any, Dict, Optional, Set +from uuid import UUID + +from sqlalchemy.orm import Session + +from app.models.database import ( + Agent, + CallImport, + CallImportEvaluation, + CallImportRow, + CallImportTag, + CallImportTagAssignment, + EvaluatorResult, + TTSComparison, +) + +RESOURCE_TYPE_LABELS = { + "call_import_evaluation": "Evaluation", + "call_import": "Import", + "tts_comparison": "Simulation", + "evaluator_result": "Evaluator result", + "agent": "Agent", + "metric": "Metric", +} + +_USAGE_KIND_LABELS = { + "llm": "LLM", + "stt": "STT", + "tts": "TTS", +} + + +def usage_kind_label(kind: Optional[str]) -> str: + if not kind: + return "—" + return _USAGE_KIND_LABELS.get(kind, kind) + + +def short_entity_id(uid: UUID) -> str: + return str(uid)[:8] + + +def format_entity_label( + custom_name: Optional[str], + uid: UUID, + default_prefix: str, +) -> str: + """Human label: name-shortId (e.g. unauthenticated sheet.xlsx-3111d376).""" + short = short_entity_id(uid) + text = (custom_name or "").strip() + if text: + return f"{text}-{short}" + return f"{default_prefix}-{short}" + + +class UsageNameResolver: + """Batch-resolve entity names for usage context JSONB keys.""" + + def __init__(self, db: Session, organization_id: UUID) -> None: + self._db = db + self._organization_id = organization_id + self._evaluations: Dict[UUID, str] = {} + self._call_imports: Dict[UUID, str] = {} + self._call_import_rows: Dict[UUID, str] = {} + self._tts_comparisons: Dict[UUID, str] = {} + self._agents: Dict[UUID, str] = {} + + def preload(self, contexts: list[Dict[str, Any]]) -> None: + eval_ids: Set[UUID] = set() + import_ids: Set[UUID] = set() + row_ids: Set[UUID] = set() + comparison_ids: Set[UUID] = set() + agent_ids: Set[UUID] = set() + evaluator_result_ids: Set[UUID] = set() + + for ctx in contexts: + if not ctx: + continue + norm = _normalize_context(ctx) + rtype = norm.get("resource_type") + resource_id = norm.get("resource_id") + if resource_id: + uid = parse_uuid(resource_id) + if uid: + if rtype == "call_import": + import_ids.add(uid) + elif rtype == "call_import_evaluation": + eval_ids.add(uid) + elif rtype == "tts_comparison": + comparison_ids.add(uid) + elif rtype == "agent": + agent_ids.add(uid) + elif rtype == "evaluator_result": + eval_ids.add(uid) + else: + eval_ids.add(uid) + import_ids.add(uid) + for key, bucket in ( + ("evaluation_id", eval_ids), + ("call_import_id", import_ids), + ("call_import_row_id", row_ids), + ("agent_id", agent_ids), + ("evaluator_result_id", evaluator_result_ids), + ): + raw = norm.get(key) + if not raw: + continue + uid = parse_uuid(raw) + if uid: + bucket.add(uid) + + if eval_ids: + for row in ( + self._db.query(CallImportEvaluation) + .filter( + CallImportEvaluation.organization_id == self._organization_id, + CallImportEvaluation.id.in_(eval_ids), + ) + .all() + ): + self._evaluations[row.id] = format_entity_label( + row.name, row.id, "Evaluation" + ) + + tag_names_by_import: Dict[UUID, list[str]] = {} + if import_ids: + for cid, tag_name in ( + self._db.query( + CallImportTagAssignment.call_import_id, + CallImportTag.name, + ) + .join( + CallImportTag, + CallImportTag.id == CallImportTagAssignment.tag_id, + ) + .filter(CallImportTagAssignment.call_import_id.in_(import_ids)) + .all() + ): + tag_names_by_import.setdefault(cid, []).append(tag_name) + + if import_ids: + for row in ( + self._db.query(CallImport) + .filter( + CallImport.organization_id == self._organization_id, + CallImport.id.in_(import_ids), + ) + .all() + ): + display_name, prefix = _call_import_label_parts( + row, + sorted(tag_names_by_import.get(row.id, [])), + ) + self._call_imports[row.id] = format_entity_label( + display_name, row.id, prefix + ) + + if comparison_ids: + for row in ( + self._db.query(TTSComparison) + .filter( + TTSComparison.organization_id == self._organization_id, + TTSComparison.id.in_(comparison_ids), + ) + .all() + ): + self._tts_comparisons[row.id] = _tts_comparison_display_name(row) + + if evaluator_result_ids: + for row in ( + self._db.query(EvaluatorResult) + .filter( + EvaluatorResult.organization_id == self._organization_id, + EvaluatorResult.id.in_(evaluator_result_ids), + ) + .all() + ): + if row.agent_id: + agent_ids.add(row.agent_id) + + missing_agent_ids = [uid for uid in agent_ids if uid not in self._agents] + if missing_agent_ids: + for row in ( + self._db.query(Agent) + .filter( + Agent.organization_id == self._organization_id, + Agent.id.in_(missing_agent_ids), + ) + .all() + ): + self._agents[row.id] = _agent_display_name(row) + + if row_ids: + for row in ( + self._db.query(CallImportRow) + .filter( + CallImportRow.organization_id == self._organization_id, + CallImportRow.id.in_(row_ids), + ) + .all() + ): + self._call_import_rows[row.id] = _clean_name( + row.conversation_id, "Conversation" + ) + + def evaluation_name(self, raw_id: str) -> str: + uid = parse_uuid(raw_id) + if uid and uid in self._evaluations: + return self._evaluations[uid] + if uid: + return format_entity_label(None, uid, "Evaluation") + return "Evaluation" + + def call_import_name(self, raw_id: str) -> str: + uid = parse_uuid(raw_id) + if uid and uid in self._call_imports: + return self._call_imports[uid] + if uid: + return format_entity_label(None, uid, "Import") + return "Import" + + def tts_comparison_name(self, raw_id: str) -> str: + uid = parse_uuid(raw_id) + if uid and uid in self._tts_comparisons: + return self._tts_comparisons[uid] + if uid: + return format_entity_label(None, uid, "Simulation") + return "Simulation" + + def agent_name(self, raw_id: str) -> str: + uid = parse_uuid(raw_id) + if uid and uid in self._agents: + return self._agents[uid] + if uid: + return format_entity_label(None, uid, "Agent") + return "Agent" + + def resource_name(self, raw_id: str, resource_type: Optional[str]) -> str: + if resource_type == "call_import_evaluation": + return self.evaluation_name(raw_id) + if resource_type == "call_import": + return self.call_import_name(raw_id) + if resource_type == "tts_comparison": + return self.tts_comparison_name(raw_id) + if resource_type == "agent": + return self.agent_name(raw_id) + uid = parse_uuid(raw_id) + if uid: + prefix = RESOURCE_TYPE_LABELS.get(resource_type or "", "Resource") + return format_entity_label(None, uid, prefix) + return RESOURCE_TYPE_LABELS.get(resource_type or "", "Unscoped") + + def call_import_row_name(self, raw_id: str) -> str: + uid = parse_uuid(raw_id) + if uid and uid in self._call_import_rows: + return self._call_import_rows[uid] + return "Conversation" + + +def parse_uuid(raw: Any) -> Optional[UUID]: + try: + return UUID(str(raw)) + except (ValueError, TypeError): + return None + + +def _clean_name(value: Optional[str], fallback: str) -> str: + text = (value or "").strip() + return text or fallback + + +def _call_import_title(row: CallImport) -> Optional[str]: + filename = (row.original_filename or "").strip() + if filename: + return filename + dataset = (row.dataset or "").strip() + if dataset: + return dataset + return None + + +def _call_import_meta_suffix(row: CallImport, tag_names: list[str]) -> str: + parts: list[str] = [] + dataset = (row.dataset or "").strip() + filename = (row.original_filename or "").strip() + if dataset and dataset != filename: + parts.append(dataset) + for name in tag_names: + if name and name not in parts: + parts.append(name) + if not parts: + return "" + return f" ({' · '.join(parts)})" + + +def _call_import_label_parts( + row: CallImport, + tag_names: list[str], +) -> tuple[Optional[str], str]: + base = _call_import_title(row) + suffix = _call_import_meta_suffix(row, tag_names) + if base: + return f"{base}{suffix}", "Import" + if suffix: + return suffix.strip(" ()"), "Import" + return None, "Import" + + +def _tts_comparison_display_name(row: TTSComparison) -> str: + name = (row.name or "").strip() or "Simulation" + sim = (row.simulation_id or "").strip() + if sim: + return f"{name} #{sim}" + return format_entity_label(name, row.id, "Simulation") + + +def _agent_display_name(row: Agent) -> str: + name = (row.name or "").strip() or "Agent" + short = (row.agent_id or "").strip() + if short: + return f"{name} #{short}" + return format_entity_label(name, row.id, "Agent") + + +def _normalize_context(raw: Any) -> Dict[str, str]: + if not raw or not isinstance(raw, dict): + return {} + return {str(k): str(v) for k, v in raw.items() if v is not None} + + +def _richest_context(contexts: list[Dict[str, str]]) -> Dict[str, str]: + if not contexts: + return {} + return max(contexts, key=lambda c: len(c)) + + +def build_usage_resource_label( + context: Optional[Dict[str, Any]], + resource_type: Optional[str], + resolver: UsageNameResolver, +) -> str: + """Hierarchical label: call import · evaluation · conversation.""" + ctx = _normalize_context(context) + parts: list[str] = [] + + call_import_id = ctx.get("call_import_id") + if call_import_id: + parts.append(resolver.call_import_name(call_import_id)) + elif resource_type == "call_import" and ctx.get("resource_id"): + parts.append(resolver.call_import_name(ctx["resource_id"])) + + evaluation_id = ctx.get("evaluation_id") + agent_id = ctx.get("agent_id") + resource_id = ctx.get("resource_id") + if evaluation_id: + parts.append(resolver.evaluation_name(evaluation_id)) + elif resource_type == "call_import_evaluation" and resource_id: + parts.append(resolver.evaluation_name(resource_id)) + elif resource_type == "tts_comparison" and resource_id: + parts.append(resolver.tts_comparison_name(resource_id)) + elif resource_type == "agent" and resource_id: + parts.append(resolver.agent_name(resource_id)) + elif agent_id: + parts.append(resolver.agent_name(agent_id)) + elif resource_type == "evaluator_result" and resource_id: + parts.append(resolver.resource_name(resource_id, resource_type)) + elif resource_type and resource_id: + parts.append(resolver.resource_name(resource_id, resource_type)) + + row_id = ctx.get("call_import_row_id") + if row_id: + parts.append(resolver.call_import_row_name(row_id)) + + if not parts: + rid = resource_id or agent_id + if rid and (resource_type == "agent" or agent_id): + return resolver.agent_name(rid) + if resource_type: + prefix = RESOURCE_TYPE_LABELS.get(resource_type, resource_type) + uid = parse_uuid(rid) + if uid: + return format_entity_label(None, uid, prefix) + return prefix + return "Unscoped" + + return " / ".join(parts) + + +def labels_for_resource_buckets( + buckets: list[tuple[Optional[str], Optional[str], list[Dict[str, Any]]]], + resolver: UsageNameResolver, +) -> Dict[str, str]: + """Map resource_id string -> label; buckets are (resource_id, resource_type, contexts).""" + labels: Dict[str, str] = {} + for raw_id, resource_type, contexts in buckets: + if not raw_id: + continue + ctx = _richest_context([_normalize_context(c) for c in contexts]) + merged = dict(ctx) + merged.setdefault("resource_id", raw_id) + if resource_type: + merged.setdefault("resource_type", resource_type) + labels[str(raw_id)] = build_usage_resource_label( + merged, resource_type, resolver + ) + return labels + + +def labels_for_call_import_ids( + import_ids: list[UUID], + resolver: UsageNameResolver, +) -> Dict[str, str]: + labels: Dict[str, str] = {} + for uid in import_ids: + labels[str(uid)] = resolver.call_import_name(str(uid)) + return labels + + +def collect_contexts_from_rows(rows: list[Any]) -> list[Dict[str, Any]]: + return [_normalize_context(r[0]) for r in rows if r and r[0]] diff --git a/app/services/usage/voice_usage_processor.py b/app/services/usage/voice_usage_processor.py index 56d65f57..d1529507 100644 --- a/app/services/usage/voice_usage_processor.py +++ b/app/services/usage/voice_usage_processor.py @@ -1,7 +1,8 @@ -"""Voice pipeline processor that records LLM token usage from MetricsFrames.""" +"""Voice pipeline processor that records LLM/TTS usage from MetricsFrames.""" from __future__ import annotations +import math from typing import Optional from uuid import UUID @@ -16,7 +17,7 @@ def create_llm_usage_recorder( resource_id: UUID | str | None = None, resource_type: Optional[str] = None, ): - """Build a FrameProcessor that records LLM usage from MetricsFrames. + """Build a FrameProcessor that records LLM/TTS usage from MetricsFrames. Returns None when organization_id is missing or efficientai is unavailable. """ @@ -25,15 +26,22 @@ def create_llm_usage_recorder( try: from efficientai.frames.frames import Frame, MetricsFrame - from efficientai.metrics.metrics import LLMUsageMetricsData + from efficientai.metrics.metrics import ( + LLMUsageMetricsData, + ProcessingMetricsData, + TTSUsageMetricsData, + ) from efficientai.processors.frame_processor import FrameDirection, FrameProcessor from app.services.usage.context import ( LLMUsageContext, LLMUsageProductSection, - set_usage_context, ) - from app.services.usage.llm_usage import record_llm_usage + from app.services.usage.llm_usage import ( + record_llm_usage, + record_stt_usage, + record_tts_usage, + ) from app.services.usage.normalize import UsageSnapshot except Exception as exc: logger.debug("voice usage recorder unavailable: {}", exc) @@ -48,14 +56,12 @@ def create_llm_usage_recorder( ws_uuid = UUID(str(workspace_id)) if workspace_id else None res_uuid = UUID(str(resource_id)) if resource_id else None - set_usage_context( - LLMUsageContext( - organization_id=org_uuid, - workspace_id=ws_uuid, - product_section=section, - resource_id=res_uuid, - resource_type=resource_type, - ) + usage_ctx = LLMUsageContext( + organization_id=org_uuid, + workspace_id=ws_uuid, + product_section=section, + resource_id=res_uuid, + resource_type=resource_type, ) class LLMUsageRecorderProcessor(FrameProcessor): @@ -74,7 +80,29 @@ async def process_frame(self, frame: Frame, direction: FrameDirection): ), reasoning_tokens=int(tokens.reasoning_tokens or 0), ) - record_llm_usage(item.model or "unknown", snapshot) + record_llm_usage( + item.model or "unknown", + snapshot, + organization_id=org_uuid, + ctx=usage_ctx, + ) + elif isinstance(item, TTSUsageMetricsData): + record_tts_usage( + item.model or "unknown", + characters=int(item.value or 0), + organization_id=org_uuid, + ctx=usage_ctx, + ) + elif isinstance(item, ProcessingMetricsData): + processor_name = (item.processor or "").lower() + if "stt" in processor_name and item.value is not None: + seconds = max(1, int(math.ceil(float(item.value)))) + record_stt_usage( + item.model or "unknown", + audio_seconds=seconds, + organization_id=org_uuid, + ctx=usage_ctx, + ) await self.push_frame(frame, direction) return LLMUsageRecorderProcessor() diff --git a/app/services/voice_agent/bot_fast_api.py b/app/services/voice_agent/bot_fast_api.py index 3e2d25e4..3bcd92a7 100644 --- a/app/services/voice_agent/bot_fast_api.py +++ b/app/services/voice_agent/bot_fast_api.py @@ -251,7 +251,9 @@ async def on_silence_hangup(): usage_recorder = create_llm_usage_recorder( organization_id=organization_id, workspace_id=workspace_id, - product_section="telephony" if telephony_mode else "playground", + product_section="agents" if agent_id else ("telephony" if telephony_mode else "playground"), + resource_id=agent_id, + resource_type="agent" if agent_id else None, ) if usage_recorder: pipeline_processors.append(usage_recorder) @@ -292,7 +294,9 @@ async def on_client_disconnected(transport, client): usage_recorder = create_llm_usage_recorder( organization_id=organization_id, workspace_id=workspace_id, - product_section="playground", + product_section="agents" if agent_id else "playground", + resource_id=agent_id, + resource_type="agent" if agent_id else None, ) pipeline_steps = [ ws_transport.input(), diff --git a/app/services/voice_agent/voice_bundle.py b/app/services/voice_agent/voice_bundle.py index 296bf032..edebc4d0 100644 --- a/app/services/voice_agent/voice_bundle.py +++ b/app/services/voice_agent/voice_bundle.py @@ -510,6 +510,7 @@ async def run_voice_bundle_fastapi( websocket_client, system_instruction: str | None = None, organization_id: str | None = None, + workspace_id: str | None = None, agent_id: str | None = None, persona_id: str | None = None, scenario_id: str | None = None, @@ -835,8 +836,27 @@ async def on_silence_hangup(): if telephony_mode and call_short_id and agent_transcript_processor: pipeline_processors.append(agent_transcript_processor) + from app.services.usage.voice_usage_processor import create_llm_usage_recorder + + agent_usage_section = ( + "agents" + if agent_id + else ("telephony" if telephony_mode else "voice_playground") + ) + usage_recorder = create_llm_usage_recorder( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=agent_usage_section, + resource_id=agent_id, + resource_type="agent" if agent_id else None, + ) + pipeline_processors.extend([ tts, + ]) + if usage_recorder: + pipeline_processors.append(usage_recorder) + pipeline_processors.extend([ bot_recorder if use_aligned_recorders else audio_buffer_output, ws_transport.output(), context_aggregator.assistant(), @@ -865,6 +885,7 @@ async def on_client_disconnected(transport, client): await task.cancel() else: rtvi = imports["RTVIProcessor"](config=imports["RTVIConfig"](config=[])) + rtvi_processors = [ws_transport.input()] if silence_hangup_processor: rtvi_processors.append(silence_hangup_processor) @@ -875,6 +896,17 @@ async def on_client_disconnected(transport, client): rtvi, llm, tts, + ]) + usage_recorder = create_llm_usage_recorder( + organization_id=organization_id, + workspace_id=workspace_id, + product_section="agents" if agent_id else "voice_playground", + resource_id=agent_id, + resource_type="agent" if agent_id else None, + ) + if usage_recorder: + rtvi_processors.append(usage_recorder) + rtvi_processors.extend([ audio_buffer_output, ws_transport.output(), context_aggregator.assistant(), diff --git a/app/services/webrtc_bridge/test_agent_processor.py b/app/services/webrtc_bridge/test_agent_processor.py index 6d5ea658..990fa6bd 100644 --- a/app/services/webrtc_bridge/test_agent_processor.py +++ b/app/services/webrtc_bridge/test_agent_processor.py @@ -9,8 +9,9 @@ import asyncio import io import os -from typing import Optional, Callable, Awaitable, List, Dict, Any +from typing import Optional, Callable, Awaitable, List, Dict, Any, Union from dataclasses import dataclass, field +from uuid import UUID from loguru import logger # TTS service imports @@ -90,6 +91,9 @@ class TestAgentConfig: response_delay_ms: int = 500 # Delay before responding (more natural) allow_interruptions: bool = False + organization_id: Optional[Union[UUID, str]] = None + workspace_id: Optional[Union[UUID, str]] = None + class TestAgentProcessor: """ @@ -366,7 +370,9 @@ async def _generate_llm_response(self) -> Optional[str]: max_tokens=self.config.llm_max_tokens if self.config.llm_max_tokens is not None else 150, temperature=self.config.llm_temperature if self.config.llm_temperature is not None else 0.7, ) - + + self._record_llm_usage(response=response) + return response.choices[0].message.content.strip() except Exception as e: @@ -392,6 +398,8 @@ async def _text_to_speech(self, text: str) -> Optional[bytes]: else: audio = await self._tts_cartesia(text) + if audio: + self._record_tts_usage(text=text) return audio except Exception as e: logger.error(f"[TestAgent] TTS ({provider}) error: {e}") @@ -400,6 +408,82 @@ async def _text_to_speech(self, text: str) -> Optional[bytes]: def _tts_settings(self) -> Dict[str, Any]: return dict(self.config.tts_config or {}) + def _record_tts_usage(self, *, text: str) -> None: + if not self.config.organization_id: + return + try: + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) + from app.services.usage.llm_usage import record_tts_usage + + model = self.config.tts_model or TTS_DEFAULT_MODELS.get( + self.config.tts_provider.lower(), "unknown" + ) + org_id = UUID(str(self.config.organization_id)) + ws_id = ( + UUID(str(self.config.workspace_id)) + if self.config.workspace_id + else None + ) + with llm_usage_context( + LLMUsageContext( + organization_id=org_id, + workspace_id=ws_id, + product_section=LLMUsageProductSection.TEST_AGENT, + ) + ): + record_tts_usage( + model, + characters=len(text or ""), + organization_id=org_id, + ) + except Exception as exc: + logger.debug("test agent tts usage record skipped: {}", exc) + + def _record_llm_usage(self, *, response: Any) -> None: + if not self.config.organization_id: + return + try: + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) + from app.services.usage.llm_usage import record_llm_usage + from app.services.usage.normalize import UsageSnapshot + + usage = getattr(response, "usage", None) + if usage is None: + return + org_id = UUID(str(self.config.organization_id)) + ws_id = ( + UUID(str(self.config.workspace_id)) + if self.config.workspace_id + else None + ) + model = self.config.llm_model or "unknown" + snapshot = UsageSnapshot( + prompt_tokens=int(getattr(usage, "prompt_tokens", 0) or 0), + completion_tokens=int(getattr(usage, "completion_tokens", 0) or 0), + ) + with llm_usage_context( + LLMUsageContext( + organization_id=org_id, + workspace_id=ws_id, + product_section=LLMUsageProductSection.TEST_AGENT, + ) + ): + record_llm_usage( + model, + snapshot, + organization_id=org_id, + ) + except Exception as exc: + logger.debug("test agent llm usage record skipped: {}", exc) + async def _tts_cartesia(self, text: str) -> Optional[bytes]: """Synthesize speech via Cartesia.""" import httpx diff --git a/app/workers/tasks/agent_flowchart_jobs.py b/app/workers/tasks/agent_flowchart_jobs.py index 8704fd6c..02431794 100644 --- a/app/workers/tasks/agent_flowchart_jobs.py +++ b/app/workers/tasks/agent_flowchart_jobs.py @@ -52,13 +52,19 @@ def generate_agent_flowchart_task( ) return - graph, provider_enum, model_str = generate_agent_flowchart( - prompt_text=partial.content, - organization_id=partial.organization_id, - db=db, - provider=provider, - model=model, + from app.services.usage.context import ( + llm_usage_context, + usage_context_for_prompt_partial, ) + + with llm_usage_context(usage_context_for_prompt_partial(partial)): + graph, provider_enum, model_str = generate_agent_flowchart( + prompt_text=partial.content, + organization_id=partial.organization_id, + db=db, + provider=provider, + model=model, + ) partial.agent_flowchart = graph.model_dump(mode="json") if isinstance(partial.agent_flowchart, dict): generated_at = graph.generated_at @@ -130,18 +136,24 @@ def map_agent_flowchart_prompt_sections_task( ): raise ValueError("Generate a flowchart before mapping prompt sections") + from app.services.usage.context import ( + llm_usage_context, + usage_context_for_prompt_partial, + ) + graph = apply_prompt_hash_staleness( AgentFlowGraph.model_validate(partial.agent_flowchart), partial.content, ) - mapped_graph = map_all_flow_nodes_to_prompt( - prompt_text=partial.content, - graph=graph, - organization_id=partial.organization_id, - db=db, - provider=provider, - model=model, - ) + with llm_usage_context(usage_context_for_prompt_partial(partial)): + mapped_graph = map_all_flow_nodes_to_prompt( + prompt_text=partial.content, + graph=graph, + organization_id=partial.organization_id, + db=db, + provider=provider, + model=model, + ) partial.agent_flowchart = mapped_graph.model_dump(mode="json") if isinstance(partial.agent_flowchart, dict): if mapped_graph.generated_at is not None: diff --git a/app/workers/tasks/evaluate_call_import_row.py b/app/workers/tasks/evaluate_call_import_row.py index e87b3285..dafba330 100644 --- a/app/workers/tasks/evaluate_call_import_row.py +++ b/app/workers/tasks/evaluate_call_import_row.py @@ -364,19 +364,19 @@ def evaluate_call_import_row_task( row_db.commit() return {"status": "failed", "reason": "evaluation_missing"} - from app.services.usage.context import ( - LLMUsageContext, - LLMUsageProductSection, - set_usage_context, + from app.services.usage.call_import_context import ( + call_import_evaluation_usage_context, ) + from app.services.usage.context import set_usage_context usage_ctx_token = set_usage_context( - LLMUsageContext( + call_import_evaluation_usage_context( organization_id=evaluation.organization_id, workspace_id=evaluation.workspace_id, - product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, - resource_id=evaluation.id, - resource_type="call_import_evaluation", + evaluation_id=evaluation.id, + call_import_id=evaluation.call_import_id, + evaluation_row_id=eval_row.id, + call_import_row_id=source_row.id, ) ) diff --git a/app/workers/tasks/generate_evaluation_metric_clusters.py b/app/workers/tasks/generate_evaluation_metric_clusters.py index e91dc108..8cb59775 100644 --- a/app/workers/tasks/generate_evaluation_metric_clusters.py +++ b/app/workers/tasks/generate_evaluation_metric_clusters.py @@ -120,19 +120,23 @@ def on_progress(completed: int, total: int) -> None: flag_modified(evaluation, "metric_clusters") db.commit() - state = generate_metric_clusters( - db, - evaluation, - evaluation.organization_id, - provider_enum, - model_str, - completed_row_pairs=completed_pairs, - metrics=metrics, - policies=policies, - on_progress=on_progress, - max_llm_calls=max_llm_calls, - is_cancelled=_reload_cancelled, - ) + from app.services.usage.call_import_context import usage_context_for_evaluation + from app.services.usage.context import llm_usage_context + + with llm_usage_context(usage_context_for_evaluation(evaluation)): + state = generate_metric_clusters( + db, + evaluation, + evaluation.organization_id, + provider_enum, + model_str, + completed_row_pairs=completed_pairs, + metrics=metrics, + policies=policies, + on_progress=on_progress, + max_llm_calls=max_llm_calls, + is_cancelled=_reload_cancelled, + ) if _reload_cancelled(): return diff --git a/app/workers/tasks/generate_evaluation_prompt_improvements.py b/app/workers/tasks/generate_evaluation_prompt_improvements.py index 0ba282a2..fd22b1fb 100644 --- a/app/workers/tasks/generate_evaluation_prompt_improvements.py +++ b/app/workers/tasks/generate_evaluation_prompt_improvements.py @@ -107,25 +107,29 @@ def generate_evaluation_prompt_improvements_task( eval_rows, ) - state = generate_prompt_improvements( - evaluation=evaluation, - imported_agent=imported_agent, - clusters_state=clusters_state, - organization_id=evaluation.organization_id, - db=db, - provider=provider, - model=model, - credential_id=UUID(credential_id) if credential_id else None, - period_deltas=period_deltas, - ) - evaluation.prompt_improvements = prompt_improvements_state_to_db(state) - flag_modified(evaluation, "prompt_improvements") - db.commit() - logger.info( - "Prompt improvements completed for evaluation {} ({} suggestions)", - evaluation_id, - len(state.suggestions), - ) + from app.services.usage.call_import_context import usage_context_for_evaluation + from app.services.usage.context import llm_usage_context + + with llm_usage_context(usage_context_for_evaluation(evaluation)): + state = generate_prompt_improvements( + evaluation=evaluation, + imported_agent=imported_agent, + clusters_state=clusters_state, + organization_id=evaluation.organization_id, + db=db, + provider=provider, + model=model, + credential_id=UUID(credential_id) if credential_id else None, + period_deltas=period_deltas, + ) + evaluation.prompt_improvements = prompt_improvements_state_to_db(state) + flag_modified(evaluation, "prompt_improvements") + db.commit() + logger.info( + "Prompt improvements completed for evaluation {} ({} suggestions)", + evaluation_id, + len(state.suggestions), + ) except Exception as exc: logger.exception( "Prompt improvements failed for evaluation {}: {}", diff --git a/app/workers/tasks/generate_evaluation_tldr_insights.py b/app/workers/tasks/generate_evaluation_tldr_insights.py index b236d0f7..6f9e96a1 100644 --- a/app/workers/tasks/generate_evaluation_tldr_insights.py +++ b/app/workers/tasks/generate_evaluation_tldr_insights.py @@ -40,20 +40,18 @@ def generate_evaluation_tldr_insights_task( if evaluation is None: return {"error": "evaluation_not_found", "status_code": 404} - from app.services.usage.context import ( - LLMUsageContext, - LLMUsageProductSection, - llm_usage_context, + from app.services.usage.call_import_context import ( + call_import_evaluation_usage_context, ) + from app.services.usage.context import llm_usage_context try: with llm_usage_context( - LLMUsageContext( + call_import_evaluation_usage_context( organization_id=evaluation.organization_id, workspace_id=evaluation.workspace_id, - product_section=LLMUsageProductSection.CALL_IMPORT_EVALUATIONS, - resource_id=evaluation.id, - resource_type="call_import_evaluation", + evaluation_id=evaluation.id, + call_import_id=evaluation.call_import_id, ) ): summary = _generate_and_persist_tldr_summary( diff --git a/app/workers/tasks/generate_evaluation_user_insights.py b/app/workers/tasks/generate_evaluation_user_insights.py index 6029ce4a..71f666df 100644 --- a/app/workers/tasks/generate_evaluation_user_insights.py +++ b/app/workers/tasks/generate_evaluation_user_insights.py @@ -81,21 +81,25 @@ def on_progress(completed: int, total: int) -> None: flag_modified(evaluation, "user_insights") db.commit() - state = generate_user_insights( - db, - evaluation, - evaluation.organization_id, - provider_enum, - model_str, - completed_row_pairs=completed_pairs, - metrics=metrics, - aggregate=aggregate, - on_progress=on_progress, - max_llm_calls=max_llm_calls, - ) - evaluation.user_insights = user_insights_state_to_db(state) - flag_modified(evaluation, "user_insights") - db.commit() + from app.services.usage.call_import_context import usage_context_for_evaluation + from app.services.usage.context import llm_usage_context + + with llm_usage_context(usage_context_for_evaluation(evaluation)): + state = generate_user_insights( + db, + evaluation, + evaluation.organization_id, + provider_enum, + model_str, + completed_row_pairs=completed_pairs, + metrics=metrics, + aggregate=aggregate, + on_progress=on_progress, + max_llm_calls=max_llm_calls, + ) + evaluation.user_insights = user_insights_state_to_db(state) + flag_modified(evaluation, "user_insights") + db.commit() except Exception as exc: # noqa: BLE001 logger.exception( "User insights generation failed for evaluation {}: {}", diff --git a/app/workers/tasks/process_evaluator_result.py b/app/workers/tasks/process_evaluator_result.py index dc0e1b64..a86a4503 100644 --- a/app/workers/tasks/process_evaluator_result.py +++ b/app/workers/tasks/process_evaluator_result.py @@ -1,5 +1,6 @@ """Celery task: process evaluator result (transcribe and evaluate metrics).""" +import math import time import uuid as _uuid from uuid import UUID @@ -24,6 +25,11 @@ handle_llm_evaluation_error, ) from app.services.evaluators.evaluator_result_call_data import slim_call_data_for_evaluator_result +from app.services.evaluators.call_data_transcript import extract_transcript_from_call_data + + +class EvaluatorInputUnavailableError(ValueError): + """Permanent failure: no audio or transcript available for evaluation.""" def _commit_evaluator_result(db, result) -> None: @@ -288,6 +294,98 @@ def _run_bucket(bucket, parent_metric=None): return metric_scores, evaluation_time +_PERMANENT_VAPI_ENDED_REASONS = frozenset( + { + "call.in-progress.error-assistant-did-not-receive-customer-audio", + "call.in-progress.error-assistant-did-not-receive-customer-media", + "customer-did-not-give-microphone-permission", + "customer-did-not-answer", + } +) + + +def _call_data_for_transcript_extraction(result, db) -> tuple[dict, str]: + """Resolve provider payload and platform for transcript extraction.""" + platform = _normalize_platform(result.provider_platform) + recording = _playground_call_recording(db, result) if db is not None else None + if recording: + if not result.provider_call_id and recording.provider_call_id: + result.provider_call_id = recording.provider_call_id + if not result.provider_platform and recording.provider_platform: + result.provider_platform = recording.provider_platform + platform = _normalize_platform(result.provider_platform or recording.provider_platform) + + result_data = result.call_data if isinstance(result.call_data, dict) else {} + recording_data = ( + recording.call_data + if recording and isinstance(recording.call_data, dict) + else {} + ) + + if not result_data and not recording_data: + return {}, platform + + merged = dict(recording_data) + merged.update(result_data) + for key in ("endedReason", "messages", "transcript", "transcript_object"): + if not merged.get(key) and recording_data.get(key): + merged[key] = recording_data[key] + return merged, platform + + +def _hydrate_transcript_from_call_data(result, db) -> bool: + """Copy transcript from provider call_data onto the result row when missing.""" + if (result.transcription or "").strip(): + return True + + call_data, platform = _call_data_for_transcript_extraction(result, db) + if not call_data or not platform: + return False + + transcript_text, speaker_segments = extract_transcript_from_call_data( + call_data, platform + ) + if not (transcript_text or "").strip(): + return False + + result.transcription = transcript_text.strip() + if speaker_segments: + result.speaker_segments = speaker_segments + + if isinstance(result.call_data, dict) and result.call_data: + result.call_data = slim_call_data_for_evaluator_result(result.call_data) + + _commit_evaluator_result(db, result) + logger.info( + f"[EvaluatorResult {result.result_id}] Hydrated transcript from provider call_data" + ) + return True + + +def _permanent_input_failure_message(result, db) -> str: + """Return a user-facing error when retrying cannot succeed.""" + call_data, platform = _call_data_for_transcript_extraction(result, db) + if not isinstance(call_data, dict): + call_data = {} + + ended_reason = str( + call_data.get("endedReason") or call_data.get("ended_reason") or "" + ).strip() + if not platform and ended_reason.startswith("call."): + platform = "vapi" + + if platform == "vapi": + if ended_reason in _PERMANENT_VAPI_ENDED_REASONS: + return ( + "Call ended before customer audio was available " + f"({ended_reason}). Check microphone permissions and try again." + ) + if ended_reason: + return f"Call ended without usable audio or transcript ({ended_reason})." + + return "No audio or transcript available for evaluation." + + def _normalize_platform(platform: object) -> str: """Normalize provider platform enum/string into lowercase string.""" if not platform: @@ -394,7 +492,11 @@ def _recover_missing_audio_for_result(result, db, refresh_call_data: bool = True ) return False - headers = {"xi-api-key": decrypted_key} if platform == "elevenlabs" and decrypted_key else None + headers = None + if platform == "elevenlabs" and decrypted_key: + headers = {"xi-api-key": decrypted_key} + elif platform == "vapi" and decrypted_key: + headers = {"Authorization": f"Bearer {decrypted_key}"} try: response = _http.get(audio_url, headers=headers, timeout=120) except Exception as download_err: @@ -458,6 +560,71 @@ def _playground_call_recording(db, result): ) +_EXTERNAL_VOICE_PROVIDER_PLATFORMS = frozenset( + {"vapi", "retell", "elevenlabs", "smallest"} +) + + +def _should_record_external_agent_call_usage(result) -> bool: + """Only bill completed calls that ran on an external voice provider. + + Internal voice-bundle / WebSocket calls already emit LLM/STT/TTS usage + from the live pipeline; recording again here would double-count. + """ + if not result.agent_id: + return False + platform = (getattr(result, "provider_platform", None) or "").strip().lower() + return platform in _EXTERNAL_VOICE_PROVIDER_PLATFORMS + + +def _resolve_call_duration_seconds(result) -> int: + if result.duration_seconds: + try: + return max(0, int(math.ceil(float(result.duration_seconds)))) + except (TypeError, ValueError): + pass + call_data = result.call_data if isinstance(result.call_data, dict) else {} + for key in ("duration_seconds", "duration"): + raw = call_data.get(key) + if raw is not None: + try: + return max(0, int(math.ceil(float(raw)))) + except (TypeError, ValueError): + continue + started_at = call_data.get("startedAt") or call_data.get("start_timestamp") + ended_at = call_data.get("endedAt") or call_data.get("end_timestamp") + if started_at and ended_at: + try: + from datetime import datetime + + start = datetime.fromisoformat(str(started_at).replace("Z", "+00:00")) + end = datetime.fromisoformat(str(ended_at).replace("Z", "+00:00")) + return max(0, int(math.ceil((end - start).total_seconds()))) + except Exception: + return 0 + return 0 + + +def _record_agent_call_usage(result, *, usage_ctx) -> None: + if not _should_record_external_agent_call_usage(result): + return + try: + from app.services.usage.llm_usage import record_call_usage + + record_call_usage( + "voice-agent-call", + organization_id=result.organization_id, + ctx=usage_ctx, + audio_seconds=_resolve_call_duration_seconds(result), + ) + except Exception as exc: + logger.debug( + "[EvaluatorResult {}] agent call usage record skipped: {}", + result.result_id, + exc, + ) + + @celery_app.task(name="process_evaluator_result", bind=True, max_retries=3) def process_evaluator_result_task(self, result_id: str): """ @@ -489,283 +656,295 @@ def process_evaluator_result_task(self, result_id: str): return {"error": "Evaluator result not found"} from app.services.usage.context import ( - LLMUsageContext, - LLMUsageProductSection, - set_usage_context, - reset_usage_context, - ) - - usage_token = set_usage_context( - LLMUsageContext( - organization_id=result.organization_id, - workspace_id=result.workspace_id, - product_section=LLMUsageProductSection.EVALUATORS, - resource_id=result.id, - resource_type="evaluator_result", - ) + llm_usage_context, + usage_context_for_evaluator_result, ) - logger.info(f"[EvaluatorResult {result.result_id}] Starting processing task") + usage_ctx = usage_context_for_evaluator_result(result) + with llm_usage_context(usage_ctx): + logger.info(f"[EvaluatorResult {result.result_id}] Starting processing task") - result.celery_task_id = self.request.id - db.commit() + result.celery_task_id = self.request.id + db.commit() - try: - if not result.audio_s3_key: - _recover_missing_audio_for_result(result, db, refresh_call_data=True) - - has_existing_transcript = bool(result.transcription) - if not result.audio_s3_key and not has_existing_transcript: - raise ValueError("No audio S3 key or existing transcript found") + try: + if not result.audio_s3_key: + _recover_missing_audio_for_result(result, db, refresh_call_data=True) - evaluator, agent, persona, scenario = _load_related_entities(db, result) - is_custom_evaluator = evaluator and ( - bool(evaluator.custom_prompt) - or evaluator.agent_id is None - ) + _hydrate_transcript_from_call_data(result, db) - if not is_custom_evaluator and not agent: - raise ValueError("Agent not found and no custom prompt available") + has_existing_transcript = bool((result.transcription or "").strip()) + if not result.audio_s3_key and not has_existing_transcript: + raise EvaluatorInputUnavailableError( + _permanent_input_failure_message(result, db) + ) - ai_providers = db.query(AIProvider).filter( - AIProvider.organization_id == result.organization_id, - AIProvider.is_active == True, - ).all() + evaluator, agent, persona, scenario = _load_related_entities(db, result) + is_custom_evaluator = evaluator and ( + bool(evaluator.custom_prompt) + or evaluator.agent_id is None + ) - # Step 1: Transcription - if has_existing_transcript: - transcription = result.transcription - speaker_segments = result.speaker_segments or [] - transcription_time = 0.0 - else: - result.status = EvaluatorResultStatus.TRANSCRIBING.value - db.commit() + if not is_custom_evaluator and not agent: + raise ValueError("Agent not found and no custom prompt available") + + ai_providers = db.query(AIProvider).filter( + AIProvider.organization_id == result.organization_id, + AIProvider.is_active == True, + ).all() + + # Step 1: Transcription + if has_existing_transcript: + transcription = result.transcription + speaker_segments = result.speaker_segments or [] + transcription_time = 0.0 + else: + result.status = EvaluatorResultStatus.TRANSCRIBING.value + db.commit() - transcription, speaker_segments, transcription_time = _transcribe_audio( - result, ai_providers, db - ) - result.transcription = transcription - # Avoid duplicating transcript structure when provider call_data already carries it. - if not result.call_data: - result.speaker_segments = speaker_segments if speaker_segments else None - db.commit() + transcription, speaker_segments, transcription_time = _transcribe_audio( + result, ai_providers, db + ) + result.transcription = transcription + # Avoid duplicating transcript structure when provider call_data already carries it. + if not result.call_data: + result.speaker_segments = speaker_segments if speaker_segments else None + db.commit() - # Step 2: Load and categorize metrics - # Include metrics that have "agent" in their enabled_surfaces so users can - # restrict metrics to specific surfaces from the metrics page. Legacy rows - # (created before the surfaces column existed, or via fixtures that don't - # set the field) keep the original behavior: an enabled=True metric with - # an empty enabled_surfaces list is treated as agent-enabled. - enabled_metrics = db.query(Metric).filter( - Metric.organization_id == result.organization_id, - Metric.enabled == True, - ).all() - enabled_metrics = [ - m for m in enabled_metrics - if (m.name or "").strip().lower() not in REMOVED_EVALUATION_METRIC_NAMES - and ( - "agent" in (m.enabled_surfaces or []) - or not (m.enabled_surfaces or []) # legacy/unset → default to agent - ) - ] - - # Explicit metric selection on the evaluator/suite. Categorization - # parents are stored as a single ID and expanded to child labels here. - from app.services.evaluators.evaluator_helpers import expand_metric_ids_for_evaluation - - selected_metric_ids = { - str(mid) for mid in (getattr(evaluator, "metric_ids", None) or []) - } - if selected_metric_ids: - expanded_ids = expand_metric_ids_for_evaluation( - db, - result.organization_id, - list(selected_metric_ids), - ) or set() + # Step 2: Load and categorize metrics + # Include metrics that have "agent" in their enabled_surfaces so users can + # restrict metrics to specific surfaces from the metrics page. Legacy rows + # (created before the surfaces column existed, or via fixtures that don't + # set the field) keep the original behavior: an enabled=True metric with + # an empty enabled_surfaces list is treated as agent-enabled. + enabled_metrics = db.query(Metric).filter( + Metric.organization_id == result.organization_id, + Metric.enabled == True, + ).all() enabled_metrics = [ - m for m in enabled_metrics if str(m.id) in expanded_ids + m for m in enabled_metrics + if (m.name or "").strip().lower() not in REMOVED_EVALUATION_METRIC_NAMES + and ( + "agent" in (m.enabled_surfaces or []) + or not (m.enabled_surfaces or []) # legacy/unset → default to agent + ) ] - has_audio = bool(result.audio_s3_key) - llm_metrics, audio_metrics, metric_scores = _categorize_metrics(enabled_metrics, has_audio) - selected_metric_count = len(llm_metrics) + len(audio_metrics) + # Explicit metric selection on the evaluator/suite. Categorization + # parents are stored as a single ID and expanded to child labels here. + from app.services.evaluators.evaluator_helpers import expand_metric_ids_for_evaluation - call_recording = _playground_call_recording(db, result) - if call_recording: - from app.services.billing.flexprice_service import ( - record_playground_call_evaluated, - ) + selected_metric_ids = { + str(mid) for mid in (getattr(evaluator, "metric_ids", None) or []) + } + if selected_metric_ids: + expanded_ids = expand_metric_ids_for_evaluation( + db, + result.organization_id, + list(selected_metric_ids), + ) or set() + enabled_metrics = [ + m for m in enabled_metrics if str(m.id) in expanded_ids + ] + + has_audio = bool(result.audio_s3_key) + llm_metrics, audio_metrics, metric_scores = _categorize_metrics(enabled_metrics, has_audio) + selected_metric_count = len(llm_metrics) + len(audio_metrics) + + call_recording = _playground_call_recording(db, result) + if call_recording: + from app.services.billing.flexprice_service import ( + record_playground_call_evaluated, + ) - evaluation_attempt_id = f"{result.id}:{self.request.id}" - record_playground_call_evaluated( - result.organization_id, - evaluation_attempt_id, - evaluator_result_id=result.id, - workspace_id=result.workspace_id, - call_short_id=call_recording.call_short_id, - metric_count=selected_metric_count, - ) + evaluation_attempt_id = f"{result.id}:{self.request.id}" + record_playground_call_evaluated( + result.organization_id, + evaluation_attempt_id, + evaluator_result_id=result.id, + workspace_id=result.workspace_id, + call_short_id=call_recording.call_short_id, + metric_count=selected_metric_count, + ) - evaluation_time = None + evaluation_time = None - # Step 3: Audio metrics evaluation - if audio_metrics and has_audio: - try: - audio_scores = evaluate_audio_metrics( - audio_s3_key=result.audio_s3_key, - audio_metrics=audio_metrics, - result_id=result.result_id, - ) + # Step 3: Audio metrics evaluation + if audio_metrics and has_audio: + try: + audio_scores = evaluate_audio_metrics( + audio_s3_key=result.audio_s3_key, + audio_metrics=audio_metrics, + result_id=result.result_id, + ) - if _all_audio_scores_download_failed(audio_scores): + if _all_audio_scores_download_failed(audio_scores): + logger.warning( + f"[EvaluatorResult {result.result_id}] Existing S3 audio unavailable; " + "attempting provider audio recovery" + ) + recovered = _recover_missing_audio_for_result(result, db, refresh_call_data=True) + if recovered and result.audio_s3_key: + audio_scores = evaluate_audio_metrics( + audio_s3_key=result.audio_s3_key, + audio_metrics=audio_metrics, + result_id=result.result_id, + ) + + metric_scores.update(audio_scores) + except Exception as audio_err: + logger.error( + f"[EvaluatorResult {result.result_id}] Audio analysis failed: {audio_err}", + exc_info=True, + ) + metric_scores.update(handle_audio_evaluation_error(audio_metrics, audio_err)) + + # Step 4: LLM metrics evaluation + if llm_metrics and transcription: + result.status = EvaluatorResultStatus.EVALUATING.value + db.commit() + + try: + llm_scores, evaluation_time = _evaluate_llm_metrics_grouped( + transcription=transcription, + llm_metrics=llm_metrics, + ai_providers=ai_providers, + organization_id=result.organization_id, + result_id=result.result_id, + db=db, + evaluator=evaluator, + agent=agent, + persona=persona, + scenario=scenario, + ) + metric_scores.update(llm_scores) + except Exception as llm_err: + error_msg = str(llm_err).replace("{", "{{").replace("}", "}}") + logger.error( + f"[EvaluatorResult {result.result_id}] ✗ LLM evaluation failed: {error_msg}", + exc_info=True, + ) + metric_scores.update(handle_llm_evaluation_error(llm_metrics, llm_err)) + else: + if not llm_metrics: logger.warning( - f"[EvaluatorResult {result.result_id}] Existing S3 audio unavailable; " - "attempting provider audio recovery" + f"[EvaluatorResult {result.result_id}] No LLM-evaluable metrics found " + "(audio-only metrics were skipped), skipping evaluation" + ) + if not transcription: + logger.warning( + f"[EvaluatorResult {result.result_id}] No transcription available, " + "skipping evaluation" ) - recovered = _recover_missing_audio_for_result(result, db, refresh_call_data=True) - if recovered and result.audio_s3_key: - audio_scores = evaluate_audio_metrics( - audio_s3_key=result.audio_s3_key, - audio_metrics=audio_metrics, - result_id=result.result_id, - ) - metric_scores.update(audio_scores) - except Exception as audio_err: - logger.error( - f"[EvaluatorResult {result.result_id}] Audio analysis failed: {audio_err}", - exc_info=True, - ) - metric_scores.update(handle_audio_evaluation_error(audio_metrics, audio_err)) + # Step 5: Call Analysis + if transcription and not (result.call_data and result.call_data.get("call_analysis")): + try: + call_analysis = _generate_call_analysis( + transcription=transcription, + ai_providers=ai_providers, + organization_id=result.organization_id, + result_id=result.result_id, + db=db, + agent=agent, + scenario=scenario, + ) + if call_analysis: + existing_call_data = dict(result.call_data) if isinstance(result.call_data, dict) else {} + existing_call_data["call_analysis"] = call_analysis + result.call_data = slim_call_data_for_evaluator_result(existing_call_data) + except Exception as analysis_err: + logger.warning( + f"[EvaluatorResult {result.result_id}] Call analysis failed (non-fatal): {analysis_err}" + ) - # Step 4: LLM metrics evaluation - if llm_metrics and transcription: - result.status = EvaluatorResultStatus.EVALUATING.value - db.commit() + # Step 6: Complete + from sqlalchemy.orm.attributes import flag_modified + + result.metric_scores = _make_json_serializable(metric_scores) + flag_modified(result, "metric_scores") + if isinstance(result.call_data, dict): + result.call_data = slim_call_data_for_evaluator_result(result.call_data) + if isinstance(result.call_data, (dict, list)): + result.call_data = _make_json_serializable(result.call_data) + flag_modified(result, "call_data") + result.status = EvaluatorResultStatus.COMPLETED.value + result.error_message = None + _record_agent_call_usage(result, usage_ctx=usage_ctx) + _commit_evaluator_result(db, result) - try: - llm_scores, evaluation_time = _evaluate_llm_metrics_grouped( - transcription=transcription, - llm_metrics=llm_metrics, - ai_providers=ai_providers, - organization_id=result.organization_id, - result_id=result.result_id, - db=db, - evaluator=evaluator, - agent=agent, - persona=persona, - scenario=scenario, - ) - metric_scores.update(llm_scores) - except Exception as llm_err: - error_msg = str(llm_err).replace("{", "{{").replace("}", "}}") - logger.error( - f"[EvaluatorResult {result.result_id}] ✗ LLM evaluation failed: {error_msg}", - exc_info=True, - ) - metric_scores.update(handle_llm_evaluation_error(llm_metrics, llm_err)) - else: - if not llm_metrics: - logger.warning( - f"[EvaluatorResult {result.result_id}] No LLM-evaluable metrics found " - "(audio-only metrics were skipped), skipping evaluation" - ) - if not transcription: - logger.warning( - f"[EvaluatorResult {result.result_id}] No transcription available, " - "skipping evaluation" - ) + from app.services.billing.flexprice_service import ( + record_playground_evaluation_completed, + ) - # Step 5: Call Analysis - if transcription and not (result.call_data and result.call_data.get("call_analysis")): - try: - call_analysis = _generate_call_analysis( - transcription=transcription, - ai_providers=ai_providers, - organization_id=result.organization_id, - result_id=result.result_id, - db=db, - agent=agent, - scenario=scenario, - ) - if call_analysis: - existing_call_data = dict(result.call_data) if isinstance(result.call_data, dict) else {} - existing_call_data["call_analysis"] = call_analysis - result.call_data = slim_call_data_for_evaluator_result(existing_call_data) - except Exception as analysis_err: - logger.warning( - f"[EvaluatorResult {result.result_id}] Call analysis failed (non-fatal): {analysis_err}" + call_recording = _playground_call_recording(db, result) + if call_recording: + record_playground_evaluation_completed( + result.organization_id, + f"{result.id}:{self.request.id}", + evaluator_result_id=result.id, + workspace_id=result.workspace_id, + call_short_id=call_recording.call_short_id, + duration_seconds=result.duration_seconds, + metric_count=len(metric_scores) or selected_metric_count, ) - # Step 6: Complete - from sqlalchemy.orm.attributes import flag_modified - - result.metric_scores = _make_json_serializable(metric_scores) - flag_modified(result, "metric_scores") - if isinstance(result.call_data, dict): - result.call_data = slim_call_data_for_evaluator_result(result.call_data) - if isinstance(result.call_data, (dict, list)): - result.call_data = _make_json_serializable(result.call_data) - flag_modified(result, "call_data") - result.status = EvaluatorResultStatus.COMPLETED.value - result.error_message = None - _commit_evaluator_result(db, result) - - from app.services.billing.flexprice_service import ( - record_playground_evaluation_completed, - ) - - call_recording = _playground_call_recording(db, result) - if call_recording: - record_playground_evaluation_completed( - result.organization_id, - f"{result.id}:{self.request.id}", - evaluator_result_id=result.id, - workspace_id=result.workspace_id, - call_short_id=call_recording.call_short_id, - duration_seconds=result.duration_seconds, - metric_count=len(metric_scores) or selected_metric_count, + total_time = time.time() - task_start_time + logger.info( + f"[EvaluatorResult {result.result_id}] Completed in {total_time:.2f}s, " + f"{len(metric_scores)} metrics evaluated" ) - total_time = time.time() - task_start_time - logger.info( - f"[EvaluatorResult {result.result_id}] Completed in {total_time:.2f}s, " - f"{len(metric_scores)} metrics evaluated" - ) + return { + "result_id": result_id, + "status": "completed", + "transcription": transcription, + "metrics_evaluated": len(metric_scores), + "processing_time": total_time, + "transcription_time": transcription_time, + "evaluation_time": evaluation_time, + } - return { - "result_id": result_id, - "status": "completed", - "transcription": transcription, - "metrics_evaluated": len(metric_scores), - "processing_time": total_time, - "transcription_time": transcription_time, - "evaluation_time": evaluation_time, - } - - except Exception as e: - db.rollback() - logger.error(f"[EvaluatorResult {result_id}] Processing failed: {e}", exc_info=True) - try: - failed_result = db.query(EvaluatorResult).filter(EvaluatorResult.id == result_uuid).first() - if failed_result: - failed_result.status = EvaluatorResultStatus.FAILED.value - failed_result.error_message = str(e) - db.commit() - except Exception as persist_err: + except EvaluatorInputUnavailableError: db.rollback() - logger.error( - f"[EvaluatorResult {result_id}] Failed to persist FAILED status: {persist_err}", - exc_info=True, - ) - raise + raise + except Exception as e: + db.rollback() + logger.error(f"[EvaluatorResult {result_id}] Processing failed: {e}", exc_info=True) + try: + failed_result = db.query(EvaluatorResult).filter(EvaluatorResult.id == result_uuid).first() + if failed_result: + failed_result.status = EvaluatorResultStatus.FAILED.value + failed_result.error_message = str(e) + db.commit() + except Exception as persist_err: + db.rollback() + logger.error( + f"[EvaluatorResult {result_id}] Failed to persist FAILED status: {persist_err}", + exc_info=True, + ) + raise + except EvaluatorInputUnavailableError as exc: + logger.warning(f"[EvaluatorResult {result_id}] Input unavailable: {exc}") + try: + from app.models.database import EvaluatorResult, EvaluatorResultStatus + + failed_result = db.query(EvaluatorResult).filter( + EvaluatorResult.id == result_uuid + ).first() + if failed_result: + failed_result.status = EvaluatorResultStatus.FAILED.value + failed_result.error_message = str(exc) + db.commit() + except Exception as persist_err: + db.rollback() + logger.error( + f"[EvaluatorResult {result_id}] Failed to persist FAILED status: {persist_err}", + exc_info=True, + ) + return {"error": str(exc), "status": "failed"} except Exception as exc: raise self.retry(exc=exc, countdown=60) finally: - try: - if "usage_token" in locals() and usage_token is not None: - reset_usage_context(usage_token) - except Exception: - pass db.close() diff --git a/app/workers/tasks/run_judge_alignment.py b/app/workers/tasks/run_judge_alignment.py index 6ffbb655..053546cc 100644 --- a/app/workers/tasks/run_judge_alignment.py +++ b/app/workers/tasks/run_judge_alignment.py @@ -79,7 +79,13 @@ def run_judge_alignment_task( return {"error": "No samples"} try: - metrics = run_judge(run, dataset, evaluator, samples, db) + from app.services.usage.context import ( + llm_usage_context, + usage_context_for_judge_run, + ) + + with llm_usage_context(usage_context_for_judge_run(run)): + metrics = run_judge(run, dataset, evaluator, samples, db) except Exception as exc: logger.error( f"[JudgeAlignment] Run {judge_run_id} crashed: {exc}", diff --git a/app/workers/tasks/run_prompt_optimization.py b/app/workers/tasks/run_prompt_optimization.py index dbd9c869..b7efb247 100644 --- a/app/workers/tasks/run_prompt_optimization.py +++ b/app/workers/tasks/run_prompt_optimization.py @@ -106,19 +106,24 @@ def run_prompt_optimization_task(self, optimization_run_id: str): ) from app.services.optimization import run_optimization - - result = run_optimization( - agent=agent, - evaluator=evaluator, - voice_bundle=voice_bundle, - training_data=training_data, - metrics=enabled_metrics, - ai_providers=ai_providers, - organization_id=run.organization_id, - db=db, - config=run.config, + from app.services.usage.context import ( + llm_usage_context, + usage_context_for_prompt_optimization_run, ) + with llm_usage_context(usage_context_for_prompt_optimization_run(run)): + result = run_optimization( + agent=agent, + evaluator=evaluator, + voice_bundle=voice_bundle, + training_data=training_data, + metrics=enabled_metrics, + ai_providers=ai_providers, + organization_id=run.organization_id, + db=db, + config=run.config, + ) + run.best_prompt = result["best_candidate"] run.best_score = result["best_score"] run.metric_history = result["metric_history"] diff --git a/app/workers/tasks/transcribe_call_import_row.py b/app/workers/tasks/transcribe_call_import_row.py index 0f342ab3..bd09bf25 100644 --- a/app/workers/tasks/transcribe_call_import_row.py +++ b/app/workers/tasks/transcribe_call_import_row.py @@ -390,12 +390,11 @@ def _persist_diarization_failure( def _run_diarization_pipeline(ctx: dict[str, Any]) -> dict[str, Any]: """STT / S3 / LLM diarisation without a long-lived DB session.""" + from uuid import UUID + from app.models.enums import ModelProvider - from app.services.usage.context import ( - LLMUsageContext, - LLMUsageProductSection, - llm_usage_context, - ) + from app.services.usage.call_import_context import call_import_row_usage_context + from app.services.usage.context import llm_usage_context from app.workers.tasks.helpers.llm_diarisation import ( LLMDiarisationError, diarize_audio_with_llm, @@ -411,13 +410,20 @@ def _run_diarization_pipeline(ctx: dict[str, Any]) -> dict[str, Any]: llm_credential_uuid = ctx["llm_credential_uuid"] effective_prompt = ctx["effective_prompt"] + evaluation_id = ctx.get("evaluation_id") + evaluation_row_id = ctx.get("evaluation_row_id") + call_import_id = ctx.get("call_import_id") + if not call_import_id: + raise ValueError("call_import_id missing from transcribe pipeline context") + with llm_usage_context( - LLMUsageContext( + call_import_row_usage_context( organization_id=organization_id, workspace_id=ctx.get("workspace_id"), - product_section=LLMUsageProductSection.CALL_IMPORTS, - resource_id=ctx.get("call_import_id"), - resource_type="call_import", + call_import_id=call_import_id, + call_import_row_id=UUID(str(row_id)), + evaluation_id=evaluation_id, + evaluation_row_id=evaluation_row_id, ) ): return _run_diarization_pipeline_inner( @@ -960,6 +966,14 @@ def transcribe_call_import_row_task( "organization_id": row.organization_id, "workspace_id": getattr(row, "workspace_id", None), "call_import_id": row.call_import_id, + "evaluation_id": ( + UUID(evaluation_id_for_dispatch) + if evaluation_id_for_dispatch + else None + ), + "evaluation_row_id": ( + UUID(run_eval_row_id) if run_eval_row_id else None + ), "stt_provider": provider_enum.value if provider_enum else None, "stt_model": stt_model, "credential_uuid": credential_uuid, diff --git a/app/workers/tasks/tts_comparison.py b/app/workers/tasks/tts_comparison.py index 8ae42ea9..2c8968ac 100644 --- a/app/workers/tasks/tts_comparison.py +++ b/app/workers/tasks/tts_comparison.py @@ -180,6 +180,7 @@ def generate_tts_comparison_task(self, comparison_id: str): ) from app.services.ai.tts_service import tts_service, get_audio_file_extension from app.services.storage.s3_service import s3_service + from app.services.usage.context import LLMUsageContext, LLMUsageProductSection, llm_usage_context db = SessionLocal() try: @@ -258,15 +259,24 @@ def _resolve_voice_meta(sample_obj): f"provider={sample.provider} voice={sample.voice_id} " f"sample_rate_hz={sample_rate_hz} config={tts_config}" ) - audio_bytes, latency_ms, ttfb_ms = tts_service.synthesize_timed( - text=sample.text, - tts_provider=provider_enum, - tts_model=sample.model, - organization_id=comp.organization_id, - db=db, - voice=sample.voice_id, - config=tts_config or None, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=comp.organization_id, + workspace_id=comp.workspace_id, + product_section=LLMUsageProductSection.VOICE_PLAYGROUND, + resource_id=comp.id, + resource_type="tts_comparison", + ) + ): + audio_bytes, latency_ms, ttfb_ms = tts_service.synthesize_timed( + text=sample.text, + tts_provider=provider_enum, + tts_model=sample.model, + organization_id=comp.organization_id, + db=db, + voice=sample.voice_id, + config=tts_config or None, + ) audio_ext = get_audio_file_extension( sample.provider, int(sample_rate_hz) if sample_rate_hz else None @@ -514,6 +524,20 @@ def evaluate_tts_comparison_task(self, comparison_id: str): db.commit() return {"evaluated": 0} + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) + + usage_ctx = LLMUsageContext( + organization_id=comp.organization_id, + workspace_id=comp.workspace_id, + product_section=LLMUsageProductSection.VOICE_PLAYGROUND, + resource_id=comp.id, + resource_type="tts_comparison", + ) + stt_provider_str, stt_model = _resolve_stt_config(comp, db) stt_available = bool(stt_provider_str and stt_model) if stt_available: @@ -535,86 +559,86 @@ def evaluate_tts_comparison_task(self, comparison_id: str): ) evaluated = 0 - for sample in samples: - tmp_path = None - try: - audio_bytes = s3_service.download_file_by_key(sample.audio_s3_key) - if not audio_bytes: - continue - - ext = ".mp3" - if sample.audio_s3_key: - key_ext = os.path.splitext(sample.audio_s3_key)[1].lower() - if key_ext in {".wav", ".mp3", ".flac", ".ogg", ".m4a"}: - ext = key_ext - tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext) - os.close(tmp_fd) - with open(tmp_path, "wb") as f: - f.write(audio_bytes) - - if any_qualitative_enabled: - metrics = qualitative_voice_service.calculate_all_metrics(tmp_path) - metrics = _filter_qualitative_metrics(metrics, enabled_voice_metric_names) - else: - metrics = {} - - if stt_available and sample.text: - selected_stt_provider = ModelProvider(stt_provider_str) - selected_language = None - if selected_stt_provider == ModelProvider.SARVAM: - # Sarvam saarika models accept language_code; saaras models do not. - if "saarika" in (stt_model or "").lower(): - selected_language = "hi-IN" - asr_transcript = transcription_service.transcribe_text_only( - audio_file_path=tmp_path, - stt_provider=selected_stt_provider, - stt_model=stt_model, - organization_id=comp.organization_id, - db=db, - language=selected_language, - ) - if asr_transcript: - score_bundle = _compute_wer_cer(sample.text, asr_transcript) - metrics["WER Raw"] = score_bundle.get("raw_wer") - metrics["CER Raw"] = score_bundle.get("raw_cer") - metrics["WER Normalized"] = score_bundle.get("normalized_wer") - metrics["CER Normalized"] = score_bundle.get("normalized_cer") - metrics["WER"] = ( - score_bundle.get("normalized_wer") - if score_bundle.get("normalized_wer") is not None - else score_bundle.get("raw_wer") - ) - metrics["CER"] = ( - score_bundle.get("normalized_cer") - if score_bundle.get("normalized_cer") is not None - else score_bundle.get("raw_cer") - ) - metrics["ASR Transcript"] = asr_transcript + with llm_usage_context(usage_ctx): + for sample in samples: + tmp_path = None + try: + audio_bytes = s3_service.download_file_by_key(sample.audio_s3_key) + if not audio_bytes: + continue + + ext = ".mp3" + if sample.audio_s3_key: + key_ext = os.path.splitext(sample.audio_s3_key)[1].lower() + if key_ext in {".wav", ".mp3", ".flac", ".ogg", ".m4a"}: + ext = key_ext + tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext) + os.close(tmp_fd) + with open(tmp_path, "wb") as f: + f.write(audio_bytes) + + if any_qualitative_enabled: + metrics = qualitative_voice_service.calculate_all_metrics(tmp_path) + metrics = _filter_qualitative_metrics(metrics, enabled_voice_metric_names) else: - metrics["WER"] = None - metrics["CER"] = None - metrics["WER Raw"] = None - metrics["CER Raw"] = None - metrics["WER Normalized"] = None - metrics["CER Normalized"] = None - metrics["ASR Transcript"] = None - - _evaluate_custom_voice_metrics(sample, metrics, comp, db) - - sample.evaluation_metrics = _sanitize_metrics(metrics) - db.commit() - evaluated += 1 - - logger.info(f"[TTS Eval] Sample {sample.id} metrics: {metrics}") - - except Exception as e: - logger.warning("[TTS Eval] Sample {} eval failed: {}", sample.id, e) - finally: - if tmp_path and os.path.exists(tmp_path): - try: - os.unlink(tmp_path) - except Exception: - pass + metrics = {} + + if stt_available and sample.text: + selected_stt_provider = ModelProvider(stt_provider_str) + selected_language = None + if selected_stt_provider == ModelProvider.SARVAM: + if "saarika" in (stt_model or "").lower(): + selected_language = "hi-IN" + asr_transcript = transcription_service.transcribe_text_only( + audio_file_path=tmp_path, + stt_provider=selected_stt_provider, + stt_model=stt_model, + organization_id=comp.organization_id, + db=db, + language=selected_language, + ) + if asr_transcript: + score_bundle = _compute_wer_cer(sample.text, asr_transcript) + metrics["WER Raw"] = score_bundle.get("raw_wer") + metrics["CER Raw"] = score_bundle.get("raw_cer") + metrics["WER Normalized"] = score_bundle.get("normalized_wer") + metrics["CER Normalized"] = score_bundle.get("normalized_cer") + metrics["WER"] = ( + score_bundle.get("normalized_wer") + if score_bundle.get("normalized_wer") is not None + else score_bundle.get("raw_wer") + ) + metrics["CER"] = ( + score_bundle.get("normalized_cer") + if score_bundle.get("normalized_cer") is not None + else score_bundle.get("raw_cer") + ) + metrics["ASR Transcript"] = asr_transcript + else: + metrics["WER"] = None + metrics["CER"] = None + metrics["WER Raw"] = None + metrics["CER Raw"] = None + metrics["WER Normalized"] = None + metrics["CER Normalized"] = None + metrics["ASR Transcript"] = None + + _evaluate_custom_voice_metrics(sample, metrics, comp, db) + + sample.evaluation_metrics = _sanitize_metrics(metrics) + db.commit() + evaluated += 1 + + logger.info(f"[TTS Eval] Sample {sample.id} metrics: {metrics}") + + except Exception as e: + logger.warning("[TTS Eval] Sample {} eval failed: {}", sample.id, e) + finally: + if tmp_path and os.path.exists(tmp_path): + try: + os.unlink(tmp_path) + except Exception: + pass from app.api.v1.routes.voice_playground import _recompute_summary diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f8701723..7e2c4369 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -2994,10 +2994,16 @@ class ApiClient { async getOrgUsageSummary(params: { start?: string end?: string + tz?: string workspace_id?: string product_section?: string model?: string resource_id?: string + usage_kind?: string + call_import_id?: string + evaluation_id?: string + dataset?: string + tag_id?: string }): Promise<{ start: string end: string @@ -3008,6 +3014,8 @@ class ApiClient { cache_read_tokens: number cache_creation_tokens: number reasoning_tokens: number + audio_seconds: number + tts_characters: number call_count: number } last_updated_at?: string | null @@ -3021,11 +3029,17 @@ class ApiClient { async getOrgUsageBreakdown(params: { start?: string end?: string - group_by?: 'workspace' | 'product_section' | 'model' | 'resource' + tz?: string + group_by?: 'workspace' | 'product_section' | 'model' | 'resource' | 'usage_kind' | 'call_import' workspace_id?: string product_section?: string model?: string resource_id?: string + usage_kind?: string + call_import_id?: string + evaluation_id?: string + dataset?: string + tag_id?: string limit?: number offset?: number }): Promise<{ @@ -3041,15 +3055,21 @@ class ApiClient { resource_id?: string | null resource_type?: string | null resource_label?: string | null + call_import_id?: string | null + call_import_label?: string | null + usage_kind?: string | null prompt_tokens: number completion_tokens: number total_tokens: number cache_read_tokens: number cache_creation_tokens: number reasoning_tokens: number + audio_seconds: number + tts_characters: number call_count: number }> total_count: number + truncated_at_limit?: boolean last_updated_at?: string | null }> { const response = await this.client.get('/api/v1/organizations/usage/breakdown', { @@ -3061,11 +3081,27 @@ class ApiClient { async getOrgUsageFilters(params?: { start?: string end?: string + tz?: string + workspace_id?: string + product_section?: string + model?: string + resource_id?: string + evaluation_id?: string + usage_kind?: string + call_import_id?: string + q?: string + dataset?: string + tag_id?: string }): Promise<{ workspaces: Array<{ id: string; name: string }> product_sections: Array<{ id: string; label: string }> + call_imports: Array<{ id: string; label: string }> + evaluations: Array<{ id: string; label: string }> models: string[] - resources: Array<{ id: string; type?: string; label: string }> + resources: Array<{ id: string; type?: string; label: string; product_section?: string }> + usage_kinds: Array<{ id: string; label: string }> + datasets: string[] + tags: Array<{ id: string; label: string }> }> { const response = await this.client.get('/api/v1/organizations/usage/filters', { params, @@ -3237,7 +3273,7 @@ class ApiClient { }): Promise<{ call_type: string access_token?: string - call_id: string + call_id?: string agent_id: string agent_version?: number call_status?: string diff --git a/frontend/src/pages/agents/components/AgentTalkSidebar.tsx b/frontend/src/pages/agents/components/AgentTalkSidebar.tsx index be2231e7..1afd845d 100644 --- a/frontend/src/pages/agents/components/AgentTalkSidebar.tsx +++ b/frontend/src/pages/agents/components/AgentTalkSidebar.tsx @@ -59,6 +59,7 @@ export default function AgentTalkSidebar({ const userInitiatedDisconnectRef = useRef(false) const wasOpenRef = useRef(false) const userSpeakingTimeoutRef = useRef | null>(null) + const callShortIdRef = useRef(null) const pulseUserSpeaking = (durationMs = 1200) => { setActiveSpeaker('user') @@ -205,9 +206,16 @@ export default function AgentTalkSidebar({ }) } else if (isVapi) { const client = vapiClientRef.current! - client.on('call-start', () => { + client.on('call-start', async (call: any) => { setIsConnected(true) setIsConnecting(false) + if (callShortIdRef.current && call?.id) { + try { + await apiClient.updateCallRecording(callShortIdRef.current, call.id) + } catch (err) { + console.error('Failed to update Vapi call recording', err) + } + } }) client.on('speech-start', () => setActiveSpeaker('agent')) client.on('speech-end', () => setActiveSpeaker((prev) => (prev === 'agent' ? null : prev))) @@ -224,13 +232,26 @@ export default function AgentTalkSidebar({ } } }) - client.on('call-end', () => { + client.on('call-end', async () => { setIsConnected(false) setIsConnecting(false) setActiveSpeaker(null) + if (callShortIdRef.current) { + apiClient.refreshCallRecording(callShortIdRef.current).catch((err) => { + console.error('Failed to refresh Vapi call recording', err) + }) + } }) - await apiClient.createWebCall({ agent_id: agent.id, metadata: {} }) - await client.start(agent.voice_ai_agent_id!) + const webCall = await apiClient.createWebCall({ agent_id: agent.id, metadata: {} }) + callShortIdRef.current = webCall.call_short_id ?? null + const vapiCall = await client.start(agent.voice_ai_agent_id!) + if (callShortIdRef.current && vapiCall?.id) { + try { + await apiClient.updateCallRecording(callShortIdRef.current, vapiCall.id) + } catch (err) { + console.error('Failed to update Vapi call recording from start()', err) + } + } } else if (isElevenLabs) { const webCall = await apiClient.createWebCall({ agent_id: agent.id, metadata: {} }) if (!webCall.signed_url) throw new Error('No signed URL') diff --git a/frontend/src/pages/usage/SearchableSelect.tsx b/frontend/src/pages/usage/SearchableSelect.tsx new file mode 100644 index 00000000..66f625dd --- /dev/null +++ b/frontend/src/pages/usage/SearchableSelect.tsx @@ -0,0 +1,125 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { ChevronDown, Search, X } from 'lucide-react' +import { usageTheme } from './usageTheme' + +export type SearchableOption = { id: string; label: string } + +type SearchableSelectProps = { + label: string + placeholder: string + value: string + options: SearchableOption[] + onChange: (id: string) => void + disabled?: boolean + emptyMessage?: string +} + +export default function SearchableSelect({ + label, + placeholder, + value, + options, + onChange, + disabled, + emptyMessage = 'No matches', +}: SearchableSelectProps) { + const [open, setOpen] = useState(false) + const [search, setSearch] = useState('') + const rootRef = useRef(null) + + const selected = options.find((o) => o.id === value) + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase() + if (!q) return options + return options.filter((o) => o.label.toLowerCase().includes(q)) + }, [options, search]) + + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (!rootRef.current?.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', onDoc) + return () => document.removeEventListener('mousedown', onDoc) + }, []) + + return ( +
+ {label} + + + {open && !disabled ? ( +
+
+ + setSearch(e.target.value)} + placeholder="Search…" + className="flex-1 text-sm outline-none" + autoFocus + /> +
+
    + {filtered.length === 0 ? ( +
  • {emptyMessage}
  • + ) : ( + filtered.map((opt) => ( +
  • + +
  • + )) + )} +
+
+ ) : null} +
+ ) +} diff --git a/frontend/src/pages/usage/Usage.tsx b/frontend/src/pages/usage/Usage.tsx index 0f7cdcb9..c97b6f7c 100644 --- a/frontend/src/pages/usage/Usage.tsx +++ b/frontend/src/pages/usage/Usage.tsx @@ -1,100 +1,974 @@ -import { useMemo } from 'react' -import { useQuery } from '@tanstack/react-query' +import { useEffect, useMemo } from 'react' +import { keepPreviousData, useQuery } from '@tanstack/react-query' import { useSearchParams } from 'react-router-dom' import { Card, CardBody, Spinner } from '@heroui/react' -import { Activity } from 'lucide-react' +import { Activity, ChevronRight } from 'lucide-react' import { apiClient } from '../../lib/api' +import UsageFiltersBar from './UsageFiltersBar' +import UsageDrillPath from './UsageDrillPath' +import { defaultUsageDateRange } from './UsageDateRangePicker' +import { getUsageTimezone } from './usageTimezone' +import { + CALL_IMPORT_BATCH_HEADLINE, + CALL_IMPORT_HINT, + CALL_IMPORT_PRODUCT_SECTIONS, + PRODUCT_SECTION_HEADLINES, + PRODUCT_SECTION_HINTS, +} from './usageProductHints' -type GroupBy = 'workspace' | 'product_section' | 'model' | 'resource' +type DrillGroupBy = + | 'workspace' + | 'call_import' + | 'resource' + | 'model' + | 'usage_kind' + | 'product_section' +type Kind = '' | 'llm' | 'stt' | 'tts' + +type FilterOptions = { + workspaces: Array<{ id: string; name: string }> + call_imports: Array<{ id: string; label: string }> + evaluations: Array<{ id: string; label: string }> + resources?: Array<{ id: string; label: string; type?: string; product_section?: string }> + models: string[] + usage_kinds: Array<{ id: string; label: string }> + product_sections?: Array<{ id: string; label: string }> + datasets?: string[] + tags?: Array<{ id: string; label: string }> +} + +type BreakdownRow = { + workspace_id?: string | null + workspace_name?: string | null + call_import_id?: string | null + call_import_label?: string | null + resource_id?: string | null + resource_type?: string | null + resource_label?: string | null + model?: string | null + usage_kind?: string | null + product_section?: string | null + product_section_label?: string | null + prompt_tokens: number + completion_tokens: number + total_tokens: number + cache_read_tokens: number + cache_creation_tokens: number + reasoning_tokens: number + audio_seconds: number + tts_characters: number + call_count: number +} + +type WorkspaceSourceRow = BreakdownRow & { + rowKind: 'call_import' | 'workspace_resource' + hint: string +} + +const NON_COMPOSITE_RESOURCE_TYPES = new Set([ + 'call_import', + 'call_import_evaluation', +]) + +type TableRow = BreakdownRow | WorkspaceSourceRow + +const EMPTY_METRICS = { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + cache_read_tokens: 0, + cache_creation_tokens: 0, + reasoning_tokens: 0, + audio_seconds: 0, + tts_characters: 0, + call_count: 0, +} + +function compositeRowHeadline(row: WorkspaceSourceRow): string { + if (row.rowKind === 'call_import') return CALL_IMPORT_BATCH_HEADLINE + const section = row.product_section || '' + return ( + PRODUCT_SECTION_HEADLINES[section] || + row.product_section_label || + section || + 'Other' + ) +} + +function usableResourceLabel(label: string | null | undefined): string | undefined { + if (!label || label === 'Unscoped') return undefined + return label +} + +function compositeRowTitle(row: WorkspaceSourceRow, options?: FilterOptions): string { + if (row.rowKind === 'call_import') { + return row.call_import_label || 'Call import batch' + } + const fromFilters = row.resource_id + ? options?.resources?.find((r) => idKey(r.id) === idKey(row.resource_id))?.label + : undefined + return ( + usableResourceLabel(row.resource_label) || + fromFilters || + row.product_section_label || + 'Unscoped usage' + ) +} + +function sortRows(rows: BreakdownRow[]): BreakdownRow[] { + return [...rows].sort((a, b) => b.total_tokens - a.total_tokens) +} function formatNumber(value: number): string { return new Intl.NumberFormat().format(value || 0) } -function toDateInput(d: Date): string { - return d.toISOString().slice(0, 10) +function formatAudio(seconds: number): string { + const total = Math.max(0, Math.floor(seconds || 0)) + if (total < 60) return `${total}s` + const mins = Math.floor(total / 60) + const secs = total % 60 + if (mins < 60) return secs ? `${mins}m ${secs}s` : `${mins}m` + const hours = Math.floor(mins / 60) + const remMins = mins % 60 + return remMins ? `${hours}h ${remMins}m` : `${hours}h` } -function FilterField({ - label, - children, -}: { - label: string - children: React.ReactNode -}) { +function rowHasUsageForKind(row: BreakdownRow, kind: Kind): boolean { + if (!kind) return true + if (kind === 'llm') { + return (row.total_tokens ?? 0) > 0 || (row.call_count ?? 0) > 0 + } + if (kind === 'stt') { + return (row.audio_seconds ?? 0) > 0 || (row.call_count ?? 0) > 0 + } + if (kind === 'tts') { + return (row.tts_characters ?? 0) > 0 || (row.call_count ?? 0) > 0 + } + return true +} + +function filterRowsForUsageKind(rows: BreakdownRow[], kind: Kind): BreakdownRow[] { + if (!kind) return rows + return rows.filter((row) => rowHasUsageForKind(row, kind)) +} + +function rowHasAnyUsage(row: BreakdownRow): boolean { return ( - + (row.total_tokens ?? 0) > 0 || + (row.call_count ?? 0) > 0 || + (row.audio_seconds ?? 0) > 0 || + (row.tts_characters ?? 0) > 0 ) } -const fieldClassName = - 'h-9 rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-900 outline-none focus:border-indigo-400 focus:ring-1 focus:ring-indigo-200' +function isUsageScopeActive( + workspaceId: string, + callImportId: string, + evaluationId: string, + dataset: string, + tagId: string, + usageKind: Kind, + model: string, + productSection: string, +): boolean { + return Boolean( + workspaceId || + callImportId || + evaluationId || + dataset || + tagId || + usageKind || + model || + productSection, + ) +} + +/** Drop zero rows when any filter is active; kind filter uses kind-specific metrics. */ +function filterTableRows( + rows: BreakdownRow[], + usageKind: Kind, + scopeActive: boolean, +): BreakdownRow[] { + if (usageKind) return filterRowsForUsageKind(rows, usageKind) + if (scopeActive) return rows.filter(rowHasAnyUsage) + return rows +} + +function drillGroupBy( + workspaceId: string, + callImportId: string, + evaluationId: string, + model: string, + productSection: string, +): DrillGroupBy { + if (productSection && model) return 'usage_kind' + if (productSection) return 'model' + if (evaluationId && model) return 'usage_kind' + if (evaluationId) return 'model' + if (callImportId) return 'resource' + if (workspaceId) return 'call_import' + return 'workspace' +} + +function enrichCallImportRows( + rawRows: BreakdownRow[], + options?: FilterOptions, +): BreakdownRow[] { + const labelById = new Map( + (options?.call_imports ?? []).map((c) => [idKey(c.id), c.label]), + ) + return sortRows( + rawRows + .filter((r) => r.call_import_id) + .map((row) => { + const key = idKey(row.call_import_id) + return { + ...row, + call_import_label: + labelById.get(key) || row.call_import_label || 'Call import', + } + }), + ) +} + +function buildWorkspaceCompositeRows( + callImportRaw: BreakdownRow[], + resourceRaw: BreakdownRow[], + options?: FilterOptions, + padMissingRows = true, +): WorkspaceSourceRow[] { + const resourceLabelById = new Map( + (options?.resources ?? []).map((r) => [idKey(r.id), r.label]), + ) + const importRows = enrichCallImportRows(callImportRaw, options) + const rows: WorkspaceSourceRow[] = [] + const shownImportIds = new Set() + + for (const row of importRows) { + if (!row.call_import_id) continue + shownImportIds.add(idKey(row.call_import_id)) + rows.push({ + ...row, + rowKind: 'call_import', + hint: CALL_IMPORT_HINT, + }) + } + + if (padMissingRows) { + for (const ci of options?.call_imports ?? []) { + const key = idKey(ci.id) + if (shownImportIds.has(key)) continue + shownImportIds.add(key) + rows.push({ + call_import_id: ci.id, + call_import_label: ci.label, + ...EMPTY_METRICS, + rowKind: 'call_import', + hint: CALL_IMPORT_HINT, + }) + } + } + + for (const row of resourceRaw) { + const section = row.product_section + if (!section || CALL_IMPORT_PRODUCT_SECTIONS.has(section)) continue + if (NON_COMPOSITE_RESOURCE_TYPES.has(row.resource_type || '')) continue + if ( + row.total_tokens === 0 && + row.call_count === 0 && + !row.audio_seconds && + !row.tts_characters + ) { + continue + } + if (!rowHasAnyUsage(row)) { + continue + } + rows.push({ + ...row, + rowKind: 'workspace_resource', + product_section: section, + product_section_label: row.product_section_label || section, + resource_label: + resourceLabelById.get(idKey(row.resource_id)) || + usableResourceLabel(row.resource_label), + hint: PRODUCT_SECTION_HINTS[section] || 'Product usage', + }) + } + + return sortRows(rows) as WorkspaceSourceRow[] +} + +function drillColumnLabel(groupBy: DrillGroupBy, composite = false): string { + if (composite) return 'Source' + if (groupBy === 'workspace') return 'Workspace' + if (groupBy === 'call_import') return 'Call import' + if (groupBy === 'resource') return 'Evaluation run' + if (groupBy === 'product_section') return 'Product area' + if (groupBy === 'model') return 'Model' + return 'Kind' +} + +function rowLabel(groupBy: DrillGroupBy, row: BreakdownRow, options?: FilterOptions): string { + if (groupBy === 'workspace') return row.workspace_name || 'Unknown' + if (groupBy === 'call_import') return row.call_import_label || 'Call import' + if (groupBy === 'resource') { + const fromFilters = row.resource_id + ? options?.resources?.find((r) => idKey(r.id) === idKey(row.resource_id))?.label + : undefined + return ( + usableResourceLabel(row.resource_label) || fromFilters || 'Unscoped' + ) + } + if (groupBy === 'model') return row.model || '—' + if (groupBy === 'product_section') + return row.product_section_label || row.product_section || '—' + if (row.usage_kind === 'stt') return 'STT' + if (row.usage_kind === 'llm') return 'LLM' + if (row.usage_kind === 'tts') return 'TTS' + return row.usage_kind || '—' +} + +function tableRowLabel( + groupBy: DrillGroupBy, + row: TableRow, + options?: FilterOptions, +): string { + if ('rowKind' in row) { + if (row.rowKind === 'call_import') return row.call_import_label || 'Call import batch' + return compositeRowTitle(row, options) + } + return rowLabel(groupBy, row, options) +} + +function idKey(id: string | null | undefined): string { + return id ? String(id).toLowerCase() : '' +} + +function mergeDrillRows( + groupBy: DrillGroupBy, + rawRows: BreakdownRow[], + options?: FilterOptions, + padMissingRows = true, +): BreakdownRow[] { + if (groupBy === 'workspace' && options?.workspaces?.length && padMissingRows) { + const byId = new Map( + rawRows + .filter((r) => r.workspace_id) + .map((r) => [idKey(r.workspace_id), r]), + ) + const merged: BreakdownRow[] = options.workspaces.map((ws) => ({ + workspace_id: ws.id, + workspace_name: ws.name, + ...(byId.get(idKey(ws.id)) ?? EMPTY_METRICS), + })) + for (const row of rawRows) { + if (!row.workspace_id) { + merged.push({ + ...row, + workspace_name: row.workspace_name || 'No workspace', + }) + } + } + return sortRows(merged) + } + + if (groupBy === 'call_import') { + if (rawRows.length === 0) return [] + + const labelById = new Map( + (options?.call_imports ?? []).map((c) => [idKey(c.id), c.label]), + ) + const shown = new Set() + const merged: BreakdownRow[] = [] + + for (const row of rawRows) { + if (row.call_import_id) { + const key = idKey(row.call_import_id) + shown.add(key) + merged.push({ + ...row, + call_import_label: + labelById.get(key) || row.call_import_label || 'Call import', + }) + } + } + + if (padMissingRows) { + for (const ci of options?.call_imports ?? []) { + const key = idKey(ci.id) + if (!shown.has(key)) { + merged.push({ + call_import_id: ci.id, + call_import_label: ci.label, + ...EMPTY_METRICS, + }) + } + } + } + + return sortRows(merged) + } + + if (groupBy === 'resource') { + if (rawRows.length === 0) return [] + + const evalLabelById = new Map( + (options?.evaluations ?? []).map((e) => [idKey(e.id), e.label]), + ) + const resourceLabelById = new Map( + (options?.resources ?? []).map((r) => [idKey(r.id), r.label]), + ) + const shown = new Set() + const merged: BreakdownRow[] = [] + + for (const row of rawRows) { + if (row.resource_id) { + const key = idKey(row.resource_id) + shown.add(key) + merged.push({ + ...row, + resource_label: + resourceLabelById.get(key) || + evalLabelById.get(key) || + usableResourceLabel(row.resource_label) || + 'Evaluation', + }) + } + } + + if (padMissingRows) { + for (const ev of options?.evaluations ?? []) { + const key = idKey(ev.id) + if (!shown.has(key)) { + merged.push({ + resource_id: ev.id, + resource_label: ev.label, + ...EMPTY_METRICS, + }) + } + } + } + + return sortRows(merged) + } + + if (groupBy === 'model' && options?.models?.length && padMissingRows) { + if (rawRows.length === 0) return [] + const byName = new Map( + rawRows.filter((r) => r.model).map((r) => [r.model!, r]), + ) + const merged = options.models.map((name) => ({ + model: name, + ...(byName.get(name) ?? EMPTY_METRICS), + })) + return sortRows(merged) + } + + if (groupBy === 'usage_kind') { + return sortRows( + rawRows.filter((row) => rowHasUsageForKind(row, row.usage_kind as Kind)), + ) + } + + return sortRows(rawRows) +} export default function Usage() { const [searchParams, setSearchParams] = useSearchParams() - const today = useMemo(() => new Date(), []) - const defaultStart = useMemo(() => { - const d = new Date() - d.setDate(d.getDate() - 29) - return toDateInput(d) - }, []) - - const start = searchParams.get('start') || defaultStart - const end = searchParams.get('end') || toDateInput(today) - const groupBy = (searchParams.get('group_by') as GroupBy) || 'workspace' + const defaultRange = useMemo(() => defaultUsageDateRange(), []) + const usageTimezone = useMemo(() => getUsageTimezone(), []) + + const start = searchParams.get('start') || defaultRange.start + const end = searchParams.get('end') || defaultRange.end const workspaceId = searchParams.get('workspace_id') || '' - const productSection = searchParams.get('product_section') || '' + const callImportId = searchParams.get('call_import_id') || '' + const dataset = searchParams.get('dataset') || '' + const tagId = searchParams.get('tag_id') || '' + const evaluationId = searchParams.get('resource_id') || '' const model = searchParams.get('model') || '' - const resourceId = searchParams.get('resource_id') || '' + const usageKind = (searchParams.get('usage_kind') as Kind) || '' + const productSection = searchParams.get('product_section') || '' + + const showWorkspaceComposite = + Boolean(workspaceId) && + !callImportId && + !evaluationId && + !productSection + + const groupBy = drillGroupBy( + workspaceId, + callImportId, + evaluationId, + model, + productSection, + ) - const setParam = (key: string, value: string) => { + const setParams = (updates: Record) => { const next = new URLSearchParams(searchParams) - if (!value) next.delete(key) - else next.set(key, value) + for (const [key, value] of Object.entries(updates)) { + if (!value) next.delete(key) + else next.set(key, value) + } setSearchParams(next) } - const filterParams = { + const scopeParams = { start, end, + tz: usageTimezone, workspace_id: workspaceId || undefined, + call_import_id: callImportId || undefined, + dataset: dataset || undefined, + tag_id: tagId || undefined, product_section: productSection || undefined, + usage_kind: usageKind || undefined, model: model || undefined, - resource_id: resourceId || undefined, + resource_id: evaluationId || undefined, + evaluation_id: evaluationId || undefined, } - const { data: summary, isLoading: summaryLoading } = useQuery({ - queryKey: ['org-usage', 'summary', filterParams], - queryFn: () => apiClient.getOrgUsageSummary(filterParams), + const dataParams = { + ...scopeParams, + } + + const usageQueryDefaults = { + staleTime: 60 * 1000, + } + + const { data: summary, isLoading: summaryLoading, isFetching: summaryFetching } = useQuery({ + queryKey: ['org-usage', 'summary', dataParams], + queryFn: () => apiClient.getOrgUsageSummary(dataParams), + ...usageQueryDefaults, + placeholderData: keepPreviousData, }) - const { data: breakdown, isLoading: breakdownLoading } = useQuery({ - queryKey: ['org-usage', 'breakdown', groupBy, filterParams], + const { + data: breakdown, + isLoading: breakdownLoading, + isFetching: breakdownFetching, + } = useQuery({ + queryKey: ['org-usage', 'breakdown', groupBy, dataParams], queryFn: () => apiClient.getOrgUsageBreakdown({ - ...filterParams, + ...dataParams, group_by: groupBy, limit: 100, }), + enabled: !showWorkspaceComposite, + ...usageQueryDefaults, + placeholderData: (previousData, previousQuery) => { + if (!previousQuery || previousQuery.queryKey[2] !== groupBy) return undefined + return previousData + }, }) - const { data: filters } = useQuery({ - queryKey: ['org-usage', 'filters', start, end], - queryFn: () => apiClient.getOrgUsageFilters({ start, end }), + const { + data: importBreakdown, + isLoading: importBreakdownLoading, + isFetching: importBreakdownFetching, + } = useQuery({ + queryKey: ['org-usage', 'breakdown', 'call_import', dataParams], + queryFn: () => + apiClient.getOrgUsageBreakdown({ + ...dataParams, + group_by: 'call_import', + limit: 100, + }), + enabled: showWorkspaceComposite, + ...usageQueryDefaults, }) + const { + data: resourceBreakdown, + isLoading: resourceBreakdownLoading, + isFetching: resourceBreakdownFetching, + } = useQuery({ + queryKey: ['org-usage', 'breakdown', 'resource', dataParams], + queryFn: () => + apiClient.getOrgUsageBreakdown({ + ...dataParams, + group_by: 'resource', + limit: 100, + }), + enabled: showWorkspaceComposite, + ...usageQueryDefaults, + }) + + const { data: filterOptions, isFetching: filtersLoading } = useQuery({ + queryKey: ['org-usage', 'filters', scopeParams], + queryFn: () => apiClient.getOrgUsageFilters(scopeParams), + staleTime: 60 * 1000, + placeholderData: (previousData, previousQuery) => { + if (!previousQuery) return undefined + const prevScope = previousQuery.queryKey[2] as typeof scopeParams + if (JSON.stringify(prevScope) !== JSON.stringify(scopeParams)) return undefined + return previousData + }, + }) + + useEffect(() => { + if (!filterOptions) return + const updates: Record = {} + if ( + workspaceId && + !filterOptions.workspaces?.some((w) => w.id === workspaceId) + ) { + updates.workspace_id = null + updates.call_import_id = null + updates.resource_id = null + } + if ( + callImportId && + !filterOptions.call_imports?.some((c) => c.id === callImportId) + ) { + updates.call_import_id = null + updates.resource_id = null + } + if ( + evaluationId && + !filterOptions.evaluations?.some((e) => e.id === evaluationId) + ) { + updates.resource_id = null + } + if (model && !filterOptions.models?.includes(model)) { + updates.model = null + } + if ( + usageKind && + !filterOptions.usage_kinds?.some((k) => k.id === usageKind) + ) { + updates.usage_kind = null + } + if ( + productSection && + !filterOptions.product_sections?.some((s) => s.id === productSection) + ) { + updates.product_section = null + } + if (dataset && !filterOptions.datasets?.includes(dataset)) { + updates.dataset = null + } + if (tagId && !filterOptions.tags?.some((t) => t.id === tagId)) { + updates.tag_id = null + } + if (Object.keys(updates).length > 0) setParams(updates) + }, [ + filterOptions, + workspaceId, + callImportId, + dataset, + tagId, + evaluationId, + model, + usageKind, + productSection, + ]) + + const breakdownMatchesLevel = breakdown?.group_by === groupBy + const rawRows = useMemo((): BreakdownRow[] => { + if (!breakdown || !breakdownMatchesLevel) return [] + return breakdown.rows as BreakdownRow[] + }, [breakdown, breakdownMatchesLevel]) + + const mergeOptions = filterOptions + const scopeActive = isUsageScopeActive( + workspaceId, + callImportId, + evaluationId, + dataset, + tagId, + usageKind, + model, + productSection, + ) + const padMissingRows = !scopeActive + const filteredRawRows = useMemo( + () => filterTableRows(rawRows, usageKind, scopeActive), + [rawRows, usageKind, scopeActive], + ) + const rows: TableRow[] = useMemo(() => { + if (showWorkspaceComposite) { + const importReady = importBreakdown?.group_by === 'call_import' + const resourceReady = resourceBreakdown?.group_by === 'resource' + if (!importReady && !resourceReady) return [] + return buildWorkspaceCompositeRows( + importReady + ? filterTableRows(importBreakdown.rows as BreakdownRow[], usageKind, scopeActive) + : [], + resourceReady + ? filterTableRows(resourceBreakdown.rows as BreakdownRow[], usageKind, scopeActive) + : [], + mergeOptions, + padMissingRows, + ) + } + return mergeDrillRows(groupBy, filteredRawRows, mergeOptions, padMissingRows) + }, [ + showWorkspaceComposite, + importBreakdown, + resourceBreakdown, + groupBy, + filteredRawRows, + mergeOptions, + padMissingRows, + usageKind, + scopeActive, + ]) + + const breakdownStale = + !showWorkspaceComposite && breakdownFetching && !breakdownMatchesLevel + const tableLoading = showWorkspaceComposite + ? (importBreakdownLoading || resourceBreakdownLoading) && + !importBreakdown && + !resourceBreakdown + : breakdownLoading && !breakdown + const tableFetching = showWorkspaceComposite + ? importBreakdownFetching || resourceBreakdownFetching + : breakdownFetching const totals = summary?.totals - const rows = breakdown?.rows || [] - - const dimensionLabel = (row: (typeof rows)[number]): string => { - if (groupBy === 'workspace') return row.workspace_name || 'Unknown' - if (groupBy === 'product_section') - return row.product_section_label || row.product_section || '—' - if (groupBy === 'model') return row.model || '—' - return row.resource_label || row.resource_id || 'Unscoped' + const showAudio = Boolean(totals?.audio_seconds) + const showTts = + Boolean(totals?.tts_characters) || + rows.some((r) => Boolean(r.tts_characters)) + + const showTruncation = + (!showWorkspaceComposite && + breakdownMatchesLevel && + Boolean(breakdown?.truncated_at_limit)) || + (showWorkspaceComposite && + Boolean( + importBreakdown?.truncated_at_limit || resourceBreakdown?.truncated_at_limit, + )) + + const workspaceLabel = + filterOptions?.workspaces?.find((w) => w.id === workspaceId)?.name + const callImportLabel = + filterOptions?.call_imports?.find((c) => c.id === callImportId)?.label + const evaluationLabel = + filterOptions?.resources?.find((r) => r.id === evaluationId)?.label || + filterOptions?.evaluations?.find((e) => e.id === evaluationId)?.label + + const productSectionLabel = + filterOptions?.product_sections?.find((s) => s.id === productSection)?.label + + const scopeSubtitle = model + ? model + : evaluationId + ? evaluationLabel || 'Evaluation' + : callImportId + ? callImportLabel || 'Call import' + : productSection + ? productSectionLabel || 'Product area' + : workspaceId + ? workspaceLabel || 'Workspace' + : 'Organization' + + const levelHint = (() => { + if (showWorkspaceComposite) { + return 'Call import batches and other product usage — click a row to drill down' + } + if (groupBy === 'workspace') return 'Click a workspace to drill down' + if (groupBy === 'call_import') return 'Click a call import to see evaluation runs' + if (groupBy === 'product_section') return 'Click a product area to see models used' + if (groupBy === 'resource') return 'Click an evaluation to see models used' + if (groupBy === 'model') return 'Click a model to see usage by kind' + return 'Token totals by LLM / STT / TTS' + })() + + const drillCrumbs = [ + { + label: 'Organization', + onClick: + workspaceId || + callImportId || + evaluationId || + model || + productSection + ? () => + setParams({ + workspace_id: null, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + : undefined, + }, + ...(workspaceId + ? [ + { + label: workspaceLabel || 'Workspace', + onClick: + callImportId || evaluationId || model || productSection + ? () => + setParams({ + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + : undefined, + }, + ] + : []), + ...(productSection + ? [ + { + label: productSectionLabel || 'Product area', + onClick: + model + ? () => setParams({ model: null, usage_kind: null }) + : undefined, + }, + ] + : []), + ...(callImportId + ? [ + { + label: callImportLabel || 'Call import', + onClick: + evaluationId || model + ? () => + setParams({ + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + : undefined, + }, + ] + : []), + ...(evaluationId + ? [ + { + label: evaluationLabel || 'Evaluation', + onClick: model + ? () => setParams({ model: null, usage_kind: null }) + : undefined, + }, + ] + : []), + ...(model ? [{ label: model }] : []), + ] + + const handleWorkspaceChange = (id: string) => { + setParams({ + workspace_id: id || null, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + } + + const handleCallImportChange = (id: string) => { + setParams({ + call_import_id: id || null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + } + + const handleClearAll = () => { + setParams({ + workspace_id: null, + call_import_id: null, + dataset: null, + tag_id: null, + resource_id: null, + usage_kind: null, + model: null, + product_section: null, + }) + } + + const handleRowDrill = (row: TableRow) => { + if ('rowKind' in row) { + if (row.rowKind === 'call_import' && row.call_import_id) { + setParams({ + call_import_id: row.call_import_id, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (row.rowKind === 'workspace_resource' && row.product_section) { + setParams({ + product_section: row.product_section, + call_import_id: null, + resource_id: row.resource_id || null, + model: null, + usage_kind: null, + }) + return + } + } + if (groupBy === 'workspace' && row.workspace_id) { + setParams({ + workspace_id: row.workspace_id, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (groupBy === 'call_import' && row.call_import_id) { + setParams({ + call_import_id: row.call_import_id, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (groupBy === 'product_section' && row.product_section) { + setParams({ + product_section: row.product_section, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + }) + return + } + if (groupBy === 'resource' && row.resource_id) { + setParams({ + resource_id: row.resource_id, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (groupBy === 'model' && row.model) { + setParams({ model: row.model, usage_kind: null }) + } + } + + const isRowDrillable = (row: TableRow): boolean => { + if ('rowKind' in row) { + if (row.rowKind === 'call_import') return Boolean(row.call_import_id) + return Boolean(row.product_section) + } + if (groupBy === 'workspace') return Boolean(row.workspace_id) + if (groupBy === 'call_import') return Boolean(row.call_import_id) + if (groupBy === 'product_section') return Boolean(row.product_section) + if (groupBy === 'resource') return Boolean(row.resource_id) + if (groupBy === 'model') return Boolean(row.model) + return false } return ( @@ -102,167 +976,234 @@ export default function Usage() {

- + Usage

- Organization-wide LLM tokens and calls. Filter by workspace, product - section, model, or evaluation. + Cards show usage for {scopeSubtitle}. + Drill down: workspaces → call imports or product areas → evaluations / models.

- {summary?.last_updated_at && ( + {summary?.last_updated_at ? (

Updated {new Date(summary.last_updated_at).toLocaleString()}

- )} + ) : null}
-
+
- {(totals?.cache_read_tokens || totals?.cache_creation_tokens || totals?.reasoning_tokens) ? ( -
- - - + {(showAudio || + showTts || + totals?.cache_read_tokens || + totals?.cache_creation_tokens || + totals?.reasoning_tokens) ? ( +
+ {showAudio ? ( + + ) : null} + {showTts ? ( + + ) : null} + {(totals?.cache_read_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.cache_creation_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.reasoning_tokens || 0) > 0 ? ( + + ) : null}
) : null} - - -
- - setParam('start', e.target.value)} - /> - - - setParam('end', e.target.value)} - /> - - - - - - - - - - - - - -
-
-
+ setParams({ start: s, end: e })} + onWorkspaceChange={handleWorkspaceChange} + onCallImportChange={handleCallImportChange} + onDatasetChange={(v) => + setParams({ + dataset: v || null, + call_import_id: null, + resource_id: null, + }) + } + onTagChange={(v) => + setParams({ + tag_id: v || null, + call_import_id: null, + resource_id: null, + }) + } + onEvaluationChange={(id) => { + const resource = filterOptions?.resources?.find((r) => r.id === id) + setParams({ + resource_id: id || null, + product_section: id ? resource?.product_section || null : null, + model: null, + usage_kind: null, + }) + }} + onUsageKindChange={(k) => setParams({ usage_kind: k || null })} + onModelChange={(v) => setParams({ model: v || null })} + onClearAll={handleClearAll} + /> - - - {breakdownLoading ? ( + +
+ + {showTruncation ? ( +

+ Showing the first 100 rows for this level. Narrow the date range or drill + further for complete detail. +

+ ) : null} +
+ + {tableLoading || breakdownStale ? (
) : rows.length === 0 ? (
- No usage in this period. Run evaluations or playground calls, then - refresh in a minute. + No usage in this period for the current scope. Try 7d or 30d, or go back up a + level.
) : ( +
+ {tableFetching ? ( +
+ +
+ ) : null} - + - - - - + + + + + + - {rows.map((row, idx) => ( - - - - - - - - - ))} + {rows.map((row, idx) => { + const drillable = isRowDrillable(row) + const compositeHeadline = + 'rowKind' in row ? compositeRowHeadline(row) : null + const rowTitle = tableRowLabel(groupBy, row, filterOptions) + const showRowTitle = + 'rowKind' in row + ? row.rowKind === 'workspace_resource' || + (compositeHeadline && + rowTitle.toLowerCase() !== compositeHeadline.toLowerCase()) + : true + return ( + drillable && handleRowDrill(row)} + > + + + + + + + + + + ) + })}
- {groupBy === 'workspace' - ? 'Workspace' - : groupBy === 'product_section' - ? 'Section' - : groupBy === 'model' - ? 'Model' - : 'Resource'} + {drillColumnLabel(groupBy, showWorkspaceComposite)} CallsInputOutputTotalLLM callsInput tokensOutput tokensTotal tokensSTT audioTTS chars Cache read
{dimensionLabel(row)} - {formatNumber(row.call_count)} - - {formatNumber(row.prompt_tokens)} - - {formatNumber(row.completion_tokens)} - - {formatNumber(row.total_tokens)} - - {formatNumber(row.cache_read_tokens)} -
+ + + {'rowKind' in row ? ( + + {compositeHeadline} + + ) : null} + {showRowTitle ? ( + + {rowTitle} + + ) : !('rowKind' in row) ? ( + {rowTitle} + ) : null} + {'hint' in row && row.hint ? ( + + {row.hint} + + ) : null} + + {drillable ? ( + + ) : null} + + + {formatNumber(row.call_count)} + + {formatNumber(row.prompt_tokens)} + + {formatNumber(row.completion_tokens)} + + {formatNumber(row.total_tokens)} + + {row.audio_seconds ? formatAudio(row.audio_seconds) : '—'} + + {row.tts_characters ? formatNumber(row.tts_characters) : '—'} + + {formatNumber(row.cache_read_tokens)} +
+
)}
@@ -273,20 +1214,22 @@ export default function Usage() { function StatCard({ label, value, + valueLabel, loading, }: { label: string value?: number + valueLabel?: string loading?: boolean }) { return ( - - + +

{label}

-

- {loading ? '—' : formatNumber(value || 0)} +

+ {loading ? '—' : valueLabel ?? formatNumber(value || 0)}

diff --git a/frontend/src/pages/usage/UsageDateRangePicker.tsx b/frontend/src/pages/usage/UsageDateRangePicker.tsx new file mode 100644 index 00000000..3ffeaeec --- /dev/null +++ b/frontend/src/pages/usage/UsageDateRangePicker.tsx @@ -0,0 +1,252 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { Calendar } from 'lucide-react' +import { usageTheme } from './usageTheme' +import { getUsageTimezone, formatUsageTimezoneLabel } from './usageTimezone' + +type Mode = 'relative' | 'absolute' + +type UsageDateRangePickerProps = { + start: string + end: string + onApply: (start: string, end: string) => void +} + +function toDateInput(d: Date): string { + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +function formatDisplay(start: string, end: string): string { + if (start === end) return start + return `${start} → ${end}` +} + +function rangeForDays(days: number): { start: string; end: string } { + const end = new Date() + const start = new Date() + start.setDate(start.getDate() - (days - 1)) + return { start: toDateInput(start), end: toDateInput(end) } +} + +const QUICK_RANGES = [ + { label: '1d', days: 1 }, + { label: '7d', days: 7 }, + { label: '30d', days: 30 }, + { label: '90d', days: 90 }, +] as const + +const RELATIVE_DAYS = [1, 2, 3, 4, 5, 6] +const RELATIVE_WEEKS = [1, 2, 3, 4] + +export default function UsageDateRangePicker({ + start, + end, + onApply, +}: UsageDateRangePickerProps) { + const [open, setOpen] = useState(false) + const [mode, setMode] = useState('relative') + const [draftStart, setDraftStart] = useState(start) + const [draftEnd, setDraftEnd] = useState(end) + const [relDays, setRelDays] = useState(1) + const rootRef = useRef(null) + + const activeQuick = useMemo(() => { + for (const q of QUICK_RANGES) { + const r = rangeForDays(q.days) + if (r.start === start && r.end === end) return q.label + } + return null + }, [start, end]) + + useEffect(() => { + if (open) { + setDraftStart(start) + setDraftEnd(end) + } + }, [open, start, end]) + + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (!rootRef.current?.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', onDoc) + return () => document.removeEventListener('mousedown', onDoc) + }, []) + + const applyRelativeDays = (days: number) => { + const r = rangeForDays(days) + onApply(r.start, r.end) + setOpen(false) + } + + const applyAbsolute = () => { + if (!draftStart || !draftEnd) return + if (draftEnd < draftStart) return + onApply(draftStart, draftEnd) + setOpen(false) + } + + const pillClass = (active: boolean) => + `rounded-md px-2.5 py-1 text-xs font-medium transition-colors border ${ + active ? usageTheme.pillActive : usageTheme.pillInactive + }` + + return ( +
+
+ {QUICK_RANGES.map((q) => ( + + ))} + +
+ + + {formatDisplay(start, end)} + + {' '} + · {formatUsageTimezoneLabel(getUsageTimezone())} + + + + {open ? ( +
+
+ + +
+ + {mode === 'relative' ? ( +
+

+ Dates use your local timezone ({formatUsageTimezoneLabel(getUsageTimezone())}). + Usage is stored by UTC day; filters include activity that happened on each + selected local day. +

+
+

Days

+
+ {RELATIVE_DAYS.map((d) => ( + + ))} +
+
+
+

Weeks

+
+ {RELATIVE_WEEKS.map((w) => ( + + ))} +
+
+
+ + +
+
+ ) : ( +
+
+ + +
+
+ + +
+
+ )} +
+ ) : null} +
+ ) +} + +export function defaultUsageDateRange(): { start: string; end: string } { + const today = toDateInput(new Date()) + return { start: today, end: today } +} diff --git a/frontend/src/pages/usage/UsageDrillPath.tsx b/frontend/src/pages/usage/UsageDrillPath.tsx new file mode 100644 index 00000000..0e25d673 --- /dev/null +++ b/frontend/src/pages/usage/UsageDrillPath.tsx @@ -0,0 +1,40 @@ +import { ChevronRight } from 'lucide-react' +import { usageTheme } from './usageTheme' + +type DrillCrumb = { + label: string + onClick?: () => void +} + +type UsageDrillPathProps = { + crumbs: DrillCrumb[] + levelLabel: string +} + +export default function UsageDrillPath({ crumbs, levelLabel }: UsageDrillPathProps) { + return ( +
+ +

{levelLabel}

+
+ ) +} diff --git a/frontend/src/pages/usage/UsageFiltersBar.tsx b/frontend/src/pages/usage/UsageFiltersBar.tsx new file mode 100644 index 00000000..747fc2ed --- /dev/null +++ b/frontend/src/pages/usage/UsageFiltersBar.tsx @@ -0,0 +1,353 @@ +import { useMemo, useState } from 'react' +import { Chip } from '@heroui/react' +import { ChevronDown, Filter, SlidersHorizontal } from 'lucide-react' +import SearchableSelect from './SearchableSelect' +import UsageDateRangePicker from './UsageDateRangePicker' +import { usageTheme } from './usageTheme' + +type Kind = '' | 'llm' | 'stt' | 'tts' + +type FilterOptions = { + workspaces: Array<{ id: string; name: string }> + call_imports: Array<{ id: string; label: string }> + evaluations: Array<{ id: string; label: string }> + resources?: Array<{ id: string; label: string; type?: string; product_section?: string }> + models: string[] + usage_kinds: Array<{ id: string; label: string }> + datasets?: string[] + tags?: Array<{ id: string; label: string }> +} + +type ActiveChip = { key: string; label: string; onClear: () => void } + +type UsageFiltersBarProps = { + start: string + end: string + workspaceId: string + callImportId: string + evaluationId: string + dataset: string + tagId: string + usageKind: Kind + model: string + options?: FilterOptions + filtersLoading?: boolean + onDateApply: (start: string, end: string) => void + onWorkspaceChange: (id: string) => void + onCallImportChange: (id: string) => void + onDatasetChange: (value: string) => void + onTagChange: (value: string) => void + onEvaluationChange: (id: string) => void + onUsageKindChange: (v: Kind) => void + onModelChange: (v: string) => void + onClearAll: () => void +} + +const KIND_OPTIONS: Array<{ id: Kind; label: string }> = [ + { id: '', label: 'All' }, + { id: 'llm', label: 'LLM' }, + { id: 'stt', label: 'STT' }, + { id: 'tts', label: 'TTS' }, +] + +export default function UsageFiltersBar({ + start, + end, + workspaceId, + callImportId, + evaluationId, + dataset, + tagId, + usageKind, + model, + options, + filtersLoading, + onDateApply, + onWorkspaceChange, + onCallImportChange, + onDatasetChange, + onTagChange, + onEvaluationChange, + onUsageKindChange, + onModelChange, + onClearAll, +}: UsageFiltersBarProps) { + const [expanded, setExpanded] = useState(false) + + const workspaces = options?.workspaces ?? [] + const callImports = options?.call_imports ?? [] + const evaluations = options?.evaluations ?? [] + const resources = options?.resources ?? [] + const datasets = options?.datasets ?? [] + const tags = options?.tags ?? [] + const models = options?.models ?? [] + const availableKinds = useMemo( + () => new Set(options?.usage_kinds?.map((k) => k.id) ?? []), + [options?.usage_kinds], + ) + + const sourceOptions = callImportId ? evaluations : resources + + const activeChips = useMemo((): ActiveChip[] => { + const chips: ActiveChip[] = [] + const wsLabel = workspaces.find((w) => w.id === workspaceId)?.name + if (workspaceId && wsLabel) { + chips.push({ + key: 'workspace', + label: wsLabel, + onClear: () => onWorkspaceChange(''), + }) + } + const importLabel = callImports.find((c) => c.id === callImportId)?.label + if (callImportId && importLabel) { + chips.push({ + key: 'call_import', + label: importLabel, + onClear: () => onCallImportChange(''), + }) + } + if (dataset) { + chips.push({ + key: 'dataset', + label: dataset, + onClear: () => onDatasetChange(''), + }) + } + if (tagId) { + const tagLabel = tags.find((t) => t.id === tagId)?.label + if (tagLabel) { + chips.push({ + key: 'tag', + label: tagLabel, + onClear: () => onTagChange(''), + }) + } + } + const sourceLabel = + sourceOptions.find((e) => e.id === evaluationId)?.label || + evaluations.find((e) => e.id === evaluationId)?.label || + resources.find((r) => r.id === evaluationId)?.label + if (evaluationId && sourceLabel) { + chips.push({ + key: 'evaluation', + label: sourceLabel, + onClear: () => onEvaluationChange(''), + }) + } + if (usageKind) { + chips.push({ + key: 'kind', + label: usageKind.toUpperCase(), + onClear: () => onUsageKindChange(''), + }) + } + if (model) { + chips.push({ + key: 'model', + label: model, + onClear: () => onModelChange(''), + }) + } + return chips + }, [ + workspaceId, + workspaces, + callImportId, + callImports, + dataset, + tagId, + tags, + evaluationId, + sourceOptions, + evaluations, + resources, + usageKind, + model, + onWorkspaceChange, + onCallImportChange, + onDatasetChange, + onTagChange, + onEvaluationChange, + onUsageKindChange, + onModelChange, + ]) + + const hasScopeFilters = activeChips.length > 0 + + const kindHint = (kind: Kind): string | undefined => { + if (!kind) return undefined + if (availableKinds.has(kind)) return undefined + return `No ${kind.toUpperCase()} usage in this date range` + } + + return ( +
+
+ + +
+ + + + {filtersLoading ? ( + Updating options… + ) : null} + + {hasScopeFilters ? ( + + ) : null} +
+ + {!expanded && hasScopeFilters ? ( +
+ {activeChips.map((chip) => ( + + {chip.label} + + ))} +
+ ) : null} + + {expanded ? ( +
+

+ + Jump to a level or click rows in the table to drill down. +

+ +
+ ({ id: w.id, label: w.name }))} + onChange={onWorkspaceChange} + emptyMessage="No workspace usage in this date range" + /> + ({ id: d, label: d }))} + onChange={onDatasetChange} + emptyMessage="No datasets in this workspace" + disabled={datasets.length === 0} + /> + +
+ +
+ + +
+ +
+
+ Kind + {KIND_OPTIONS.map((k) => { + const disabled = k.id !== '' && !availableKinds.has(k.id) + const hint = kindHint(k.id) + return ( + + ) + })} + {!availableKinds.has('tts') ? ( + No TTS + ) : null} + {!availableKinds.has('stt') ? ( + No STT + ) : null} +
+ +
+ + {models.length === 0 ? ( + No models in this range + ) : null} +
+
+
+ ) : null} +
+ ) +} diff --git a/frontend/src/pages/usage/usageProductHints.ts b/frontend/src/pages/usage/usageProductHints.ts new file mode 100644 index 00000000..4a057b1c --- /dev/null +++ b/frontend/src/pages/usage/usageProductHints.ts @@ -0,0 +1,47 @@ +/** Product areas shown separately from call-import drill-down. */ +export const CALL_IMPORT_PRODUCT_SECTIONS = new Set([ + 'call_imports', + 'call_import_evaluations', +]) + +/** Short table headline per product section (workspace composite rows). */ +export const PRODUCT_SECTION_HEADLINES: Record = { + voice_playground: 'Voice playground', + playground: 'Playground', + chat: 'Chat', + telephony: 'Telephony', + evaluators: 'Evaluators', + metrics: 'Metrics', + judge_alignment: 'Judge alignment', + prompt_optimization: 'Prompt optimization', + personas: 'Personas', + agents: 'Agents', + prompt_partials: 'Prompt partials', + conversation_evaluations: 'Conversation evaluations', + test_agent: 'Test agent', + call_import_evaluations: 'Call import evaluations', + call_imports: 'Call imports', + other: 'Other', +} + +export const CALL_IMPORT_BATCH_HEADLINE = 'Call import batch' + +export const PRODUCT_SECTION_HINTS: Record = { + voice_playground: 'Voice agent playground — LLM, STT, and TTS', + playground: 'Text playground and experiments', + chat: 'Chat conversations', + telephony: 'Telephony and live calls', + evaluators: 'Evaluator definitions and runs', + metrics: 'Metrics and scoring', + judge_alignment: 'Judge alignment workflows', + prompt_optimization: 'Prompt optimization jobs', + personas: 'Persona generation', + agents: 'Agent configuration', + prompt_partials: 'Prompt partials', + conversation_evaluations: 'Conversation evaluations', + test_agent: 'Test agent sessions', + other: 'Other product usage', +} + +export const CALL_IMPORT_HINT = + 'Call import batch — CSV upload or manual audio recordings' diff --git a/frontend/src/pages/usage/usageTheme.ts b/frontend/src/pages/usage/usageTheme.ts new file mode 100644 index 00000000..fc4b06be --- /dev/null +++ b/frontend/src/pages/usage/usageTheme.ts @@ -0,0 +1,18 @@ +/** Matches platform primary buttons (soft gold, not solid yellow). */ +export const usageTheme = { + pillActive: + 'bg-[#fef9c3] border border-[#facc15] text-[#a16207] font-semibold shadow-sm', + pillInactive: + 'text-gray-600 hover:bg-[#fefce8] hover:text-[#854d0e] border border-transparent', + pillMuted: 'bg-gray-50 border border-gray-200 text-gray-600', + link: 'text-primary-600 hover:text-primary-700', + linkStrong: 'text-[#a16207] hover:text-[#854d0e] font-medium', + chipBase: 'bg-[#fefce8] border border-[#fde047]', + chipContent: 'text-[#854d0e]', + panel: 'rounded-xl border border-gray-200 bg-white shadow-sm ring-1 ring-[#fde047]/30', + panelHeader: 'bg-[#fefce8]/50 border-b border-[#fde047]/40', + focusRing: 'focus:border-[#facc15] focus:ring-2 focus:ring-[#fef9c3]', + selectHighlight: 'bg-[#fefce8] text-[#854d0e] font-medium', + applyBtn: + 'rounded-lg bg-[#fef9c3] border border-[#facc15] px-4 py-1.5 text-sm font-semibold text-[#a16207] hover:bg-[#fef08a]', +} diff --git a/frontend/src/pages/usage/usageTimezone.ts b/frontend/src/pages/usage/usageTimezone.ts new file mode 100644 index 00000000..f222a038 --- /dev/null +++ b/frontend/src/pages/usage/usageTimezone.ts @@ -0,0 +1,11 @@ +export function getUsageTimezone(): string { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' + } catch { + return 'UTC' + } +} + +export function formatUsageTimezoneLabel(tz: string): string { + return tz.replace(/_/g, ' ') +} diff --git a/tests/test_services/test_evaluators/test_call_data_transcript.py b/tests/test_services/test_evaluators/test_call_data_transcript.py new file mode 100644 index 00000000..ef2e2707 --- /dev/null +++ b/tests/test_services/test_evaluators/test_call_data_transcript.py @@ -0,0 +1,34 @@ +"""Tests for call_data transcript extraction.""" + +from app.services.evaluators.call_data_transcript import extract_transcript_from_call_data + + +def test_vapi_extracts_transcript_from_messages_when_transcript_field_empty(): + call_data = { + "transcript": "", + "endedReason": "call.in-progress.error-assistant-did-not-receive-customer-audio", + "messages": [ + {"role": "assistant", "message": "Hello, how can I help?"}, + {"role": "user", "message": "Hi there"}, + ], + } + text, segments = extract_transcript_from_call_data(call_data, "vapi") + assert "Hello, how can I help?" in text + assert "Hi there" in text + assert len(segments) == 2 + + +def test_hydrate_helper_permanent_vapi_reason(): + from app.workers.tasks import process_evaluator_result as task_module + + class _Result: + audio_s3_key = None + transcription = None + provider_platform = "vapi" + call_data = { + "endedReason": "call.in-progress.error-assistant-did-not-receive-customer-audio", + } + + msg = task_module._permanent_input_failure_message(_Result(), db=None) + assert msg is not None + assert "microphone" in msg.lower() diff --git a/tests/test_services/test_usage/test_bucket_context.py b/tests/test_services/test_usage/test_bucket_context.py new file mode 100644 index 00000000..5cedb81c --- /dev/null +++ b/tests/test_services/test_usage/test_bucket_context.py @@ -0,0 +1,40 @@ +"""Tests for usage bucket context helpers.""" + +from uuid import uuid4 + +from app.services.usage.bucket_context import ( + build_bucket_context, + context_bucket_token, + legacy_resource_context, + parse_context_bucket_token, + resource_id_from_context, +) + + +def test_build_and_parse_context_roundtrip(): + rid = uuid4() + ctx = build_bucket_context( + resource_id=rid, + resource_type="call_import_evaluation", + extra={"agent_id": str(uuid4())}, + ) + token = context_bucket_token(ctx) + parsed = parse_context_bucket_token(token) + assert parsed["resource_id"] == str(rid) + assert parsed["resource_type"] == "call_import_evaluation" + assert "agent_id" in parsed + + +def test_extra_rejects_metric_keys(): + ctx = build_bucket_context( + resource_id=uuid4(), + extra={"prompt_tokens": "999", "agent_id": str(uuid4())}, + ) + assert "prompt_tokens" not in ctx + assert "agent_id" in ctx + + +def test_legacy_resource_context(): + rid = uuid4() + ctx = legacy_resource_context(rid, "call_import") + assert resource_id_from_context(ctx) == rid diff --git a/tests/test_services/test_usage/test_call_import_context.py b/tests/test_services/test_usage/test_call_import_context.py new file mode 100644 index 00000000..6d798f65 --- /dev/null +++ b/tests/test_services/test_usage/test_call_import_context.py @@ -0,0 +1,120 @@ +"""Tests for call-import usage attribution context.""" + +from uuid import uuid4 + +from app.services.usage.bucket_context import build_bucket_context +from app.services.usage.call_import_context import ( + call_import_evaluation_usage_context, + call_import_ids_from_usage_context, + call_import_row_usage_context, + enrich_usage_context_workspace, +) + + +def test_evaluation_row_gets_distinct_bucket_context(): + org_id = uuid4() + ws_id = uuid4() + eval_id = uuid4() + import_id = uuid4() + row_a = uuid4() + row_b = uuid4() + source_a = uuid4() + source_b = uuid4() + + ctx_a = call_import_evaluation_usage_context( + organization_id=org_id, + workspace_id=ws_id, + evaluation_id=eval_id, + call_import_id=import_id, + evaluation_row_id=row_a, + call_import_row_id=source_a, + ) + ctx_b = call_import_evaluation_usage_context( + organization_id=org_id, + workspace_id=ws_id, + evaluation_id=eval_id, + call_import_id=import_id, + evaluation_row_id=row_b, + call_import_row_id=source_b, + ) + + bucket_a = build_bucket_context( + resource_id=ctx_a.resource_id, + resource_type=ctx_a.resource_type, + extra=ctx_a.extra, + ) + bucket_b = build_bucket_context( + resource_id=ctx_b.resource_id, + resource_type=ctx_b.resource_type, + extra=ctx_b.extra, + ) + assert bucket_a != bucket_b + assert bucket_a["evaluation_row_id"] == str(row_a) + assert bucket_a["call_import_id"] == str(import_id) + + +def test_row_only_transcribe_context(): + org_id = uuid4() + import_id = uuid4() + row_id = uuid4() + ctx = call_import_row_usage_context( + organization_id=org_id, + workspace_id=None, + call_import_id=import_id, + call_import_row_id=row_id, + ) + assert ctx.extra is not None + assert ctx.extra["call_import_row_id"] == str(row_id) + assert ctx.resource_type == "call_import" + + +def test_call_import_ids_from_evaluation_context(): + org_id = uuid4() + eval_id = uuid4() + import_id = uuid4() + row_id = uuid4() + source_id = uuid4() + ctx = call_import_evaluation_usage_context( + organization_id=org_id, + workspace_id=uuid4(), + evaluation_id=eval_id, + call_import_id=import_id, + evaluation_row_id=row_id, + call_import_row_id=source_id, + ) + ids = call_import_ids_from_usage_context(ctx) + assert ids["call_import_id"] == import_id + assert ids["evaluation_id"] == eval_id + assert ids["evaluation_row_id"] == row_id + assert ids["call_import_row_id"] == source_id + + +def test_enrich_usage_context_workspace_noop_when_set(): + ws_id = uuid4() + ctx = call_import_evaluation_usage_context( + organization_id=uuid4(), + workspace_id=ws_id, + evaluation_id=uuid4(), + call_import_id=uuid4(), + ) + assert enrich_usage_context_workspace(ctx).workspace_id == ws_id + + +def test_enrich_usage_context_workspace_from_resolver(): + org_id = uuid4() + import_id = uuid4() + ws_id = uuid4() + ctx = call_import_evaluation_usage_context( + organization_id=org_id, + workspace_id=None, + evaluation_id=uuid4(), + call_import_id=import_id, + ) + from unittest.mock import patch + + with patch( + "app.services.usage.call_import_context.resolve_workspace_id_for_usage_context", + return_value=ws_id, + ): + enriched = enrich_usage_context_workspace(ctx) + assert enriched.workspace_id == ws_id diff --git a/tests/test_services/test_usage/test_llm_usage.py b/tests/test_services/test_usage/test_llm_usage.py index 0fdd77bf..824de3cd 100644 --- a/tests/test_services/test_usage/test_llm_usage.py +++ b/tests/test_services/test_usage/test_llm_usage.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import date +from types import SimpleNamespace from typing import Any, Dict, List, Optional from unittest.mock import MagicMock from uuid import uuid4 @@ -105,6 +106,9 @@ def srem(self, key: str, *members: str) -> int: def smembers(self, key: str) -> set: return set(self.sets.get(key, set())) + def sismember(self, key: str, member: str) -> bool: + return member in self.sets.get(key, set()) + def exists(self, key: str) -> int: return int(key in self.hashes or key in self.sets or key in self.kv) @@ -210,9 +214,10 @@ def test_record_increments_pending_and_counts_zero_token_calls(fake_redis, org_c assert prompt == 10 assert completion == 5 assert calls == 2 + assert any(k.rsplit("|", 1)[0].endswith("|llm") for k in fields) -def test_record_skipped_without_context(fake_redis): +def test_record_skipped_without_organization(fake_redis): usage_mod.record_llm_usage( "gpt-test", UsageSnapshot(prompt_tokens=10, completion_tokens=5), @@ -221,7 +226,137 @@ def test_record_skipped_without_context(fake_redis): assert fake_redis.sets == {} -def test_flush_commits_and_acks_claim(fake_redis, org_ctx): +def test_record_with_organization_id_without_context(fake_redis): + org_id = uuid4() + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=3, completion_tokens=1), + organization_id=org_id, + usage_date=date(2026, 8, 11), + ) + pending_key = usage_mod._pending_hash_key(org_id) + fields = fake_redis.hgetall(pending_key) + assert fields + assert sum(int(v) for k, v in fields.items() if k.endswith("|call_count")) == 1 + assert any("|other|" in k for k in fields) + + +def test_record_call_usage(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_call_usage( + "voice-agent-call", + audio_seconds=42, + usage_date=date(2026, 8, 11), + ) + + pending_key = usage_mod._pending_hash_key(org_id) + fields = fake_redis.hgetall(pending_key) + audio = sum(int(v) for k, v in fields.items() if k.endswith("|audio_seconds")) + calls = sum(int(v) for k, v in fields.items() if k.endswith("|call_count")) + assert audio == 42 + assert calls == 1 + assert any(k.rsplit("|", 1)[0].endswith("|llm") for k in fields) + + +def test_agent_usage_context_reuses_single_bucket(fake_redis): + """Stable agent context avoids per-call Redis/DB bucket explosion.""" + from app.services.usage.context import usage_context_for_evaluator_result + + org_id = uuid4() + workspace_id = uuid4() + agent_id = uuid4() + prefixes = set() + for idx in range(3): + result = SimpleNamespace( + id=uuid4(), + result_id=f"res-{idx}", + organization_id=org_id, + workspace_id=workspace_id, + evaluator_id=uuid4(), + agent_id=agent_id, + ) + ctx = usage_context_for_evaluator_result(result) + with llm_usage_context(ctx): + usage_mod.record_call_usage( + "voice-agent-call", + audio_seconds=10, + usage_date=date(2026, 8, 13), + ) + fields = fake_redis.hgetall(usage_mod._pending_hash_key(org_id)) + prefixes.update(k.rsplit("|", 1)[0] for k in fields if k.endswith("|call_count")) + + assert len(prefixes) == 1 + calls = sum(int(v) for k, v in fake_redis.hgetall(usage_mod._pending_hash_key(org_id)).items() if k.endswith("|call_count")) + assert calls == 3 + + +def test_record_stt_usage(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_stt_usage( + "nova-2", + audio_seconds=12.2, + usage_date=date(2026, 8, 11), + ) + + pending_key = usage_mod._pending_hash_key(org_id) + fields = fake_redis.hgetall(pending_key) + audio = sum(int(v) for k, v in fields.items() if k.endswith("|audio_seconds")) + calls = sum(int(v) for k, v in fields.items() if k.endswith("|call_count")) + assert audio == 13 # ceil + assert calls == 1 + assert any(k.rsplit("|", 1)[0].endswith("|stt") for k in fields) + + +def test_record_tts_usage(fake_redis, org_ctx): + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_tts_usage( + "eleven_flash_v2_5", + characters=142, + usage_date=date(2026, 8, 11), + ) + + pending_key = usage_mod._pending_hash_key(org_id) + fields = fake_redis.hgetall(pending_key) + chars = sum(int(v) for k, v in fields.items() if k.endswith("|tts_characters")) + calls = sum(int(v) for k, v in fields.items() if k.endswith("|call_count")) + assert chars == 142 + assert calls == 1 + assert any(k.rsplit("|", 1)[0].endswith("|tts") for k in fields) + + +def test_redis_failure_buffers_to_postgres(fake_redis, org_ctx, monkeypatch): + org_id, _workspace_id, ctx = org_ctx + + def _boom_pipeline(): + raise usage_mod.redis.RedisError("redis down") + + monkeypatch.setattr(fake_redis, "pipeline", _boom_pipeline) + + captured = {} + + def _fake_buffer(organization_id, bucket, deltas): + captured["organization_id"] = organization_id + captured["bucket"] = bucket + captured["deltas"] = deltas + + monkeypatch.setattr(usage_mod, "_buffer_to_postgres", _fake_buffer) + + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=4, completion_tokens=2), + usage_date=date(2026, 8, 11), + ) + + assert captured["organization_id"] == org_id + assert captured["deltas"]["prompt_tokens"] == 4 + assert captured["bucket"]["usage_kind"] == "llm" + + +def test_flush_commits_and_acks_claim(fake_redis, org_ctx, monkeypatch): org_id, _workspace_id, ctx = org_ctx with llm_usage_context(ctx): usage_mod.record_llm_usage( @@ -232,6 +367,7 @@ def test_flush_commits_and_acks_claim(fake_redis, org_ctx): db = MagicMock() db.execute.return_value = MagicMock(rowcount=1) + monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda _db, _org: 0) flushed = usage_mod.flush_usage_to_catalog(db, org_id) assert flushed == 1 @@ -242,7 +378,7 @@ def test_flush_commits_and_acks_claim(fake_redis, org_ctx): assert fake_redis.get(usage_mod._flush_lock_key(org_id)) is None -def test_flush_restores_redis_when_db_fails(fake_redis, org_ctx): +def test_flush_restores_redis_when_db_fails(fake_redis, org_ctx, monkeypatch): org_id, _workspace_id, ctx = org_ctx with llm_usage_context(ctx): usage_mod.record_llm_usage( @@ -253,6 +389,7 @@ def test_flush_restores_redis_when_db_fails(fake_redis, org_ctx): db = MagicMock() db.execute.side_effect = RuntimeError("db down") + monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda _db, _org: 0) flushed = usage_mod.flush_usage_to_catalog(db, org_id) assert flushed == 0 @@ -270,7 +407,7 @@ def test_flush_restores_redis_when_db_fails(fake_redis, org_ctx): assert not any(k.startswith("usage:flushing:") for k in fake_redis.hashes) -def test_flush_drops_pending_when_organization_missing(fake_redis, org_ctx): +def test_flush_drops_pending_when_organization_missing(fake_redis, org_ctx, monkeypatch): """Unknown org FK must not restore Redis (avoids infinite beat retries).""" from sqlalchemy.exc import IntegrityError @@ -291,6 +428,7 @@ def test_flush_drops_pending_when_organization_missing(fake_redis, org_ctx): 'constraint "llm_usage_daily_organization_id_fkey"' ), ) + monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda _db, _org: 0) flushed = usage_mod.flush_usage_to_catalog(db, org_id) assert flushed == 0 @@ -300,7 +438,7 @@ def test_flush_drops_pending_when_organization_missing(fake_redis, org_ctx): assert not any(k.startswith("usage:flushing:") for k in fake_redis.hashes) -def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx): +def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx, monkeypatch): org_id, _workspace_id, ctx = org_ctx with llm_usage_context(ctx): usage_mod.record_llm_usage( @@ -313,6 +451,7 @@ def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx): db_a.execute.return_value = MagicMock(rowcount=0) # force INSERT path db_b = MagicMock() db_b.execute.return_value = MagicMock(rowcount=1) + monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda _db, _org: 0) first = usage_mod.flush_usage_to_catalog(db_a, org_id) second = usage_mod.flush_usage_to_catalog(db_b, org_id) @@ -385,3 +524,64 @@ def test_ensure_uses_workspace_and_section_hints(): finally: reset_usage_hints(hint_tokens) reset_usage_context(ctx_token) + + +def test_parse_legacy_resource_bucket_prefix(): + rid = uuid4() + parsed = usage_mod._parse_bucket_prefix( + f"{uuid4()}|call_imports|nova-2|{rid}|call_import|2026-08-11|stt" + ) + assert parsed is not None + assert parsed["usage_kind"] == "stt" + assert parsed["context"]["resource_id"] == str(rid) + assert parsed["context"]["resource_type"] == "call_import" + + +def test_parse_context_bucket_prefix(): + ctx = '{"resource_id":"00000000-0000-0000-0000-000000000001","resource_type":"call_import_evaluation"}' + parsed = usage_mod._parse_bucket_prefix( + f"{uuid4()}|call_imports|gpt-4|{ctx}|2026-08-11|llm" + ) + assert parsed is not None + assert parsed["usage_kind"] == "llm" + assert parsed["context"]["resource_id"] == "00000000-0000-0000-0000-000000000001" + + +def test_upsert_bucket_sql_uses_valid_empty_jsonb_literal(): + """Regression: '{{}}'::jsonb is invalid JSON and breaks catalog flush.""" + import inspect + + source = inspect.getsource(usage_mod._upsert_bucket) + assert "'{{}}'::jsonb" not in source + assert "'{}'::jsonb" in source + + +def test_upsert_bucket_matches_legacy_resource_context_key(monkeypatch): + """Per-row context must merge into an existing evaluation-level bucket.""" + org_id = uuid4() + evaluation_id = uuid4() + bucket = { + "workspace_id": uuid4(), + "product_section": "call_import_evaluations", + "model": "gpt-test", + "context": { + "resource_id": str(evaluation_id), + "resource_type": "call_import_evaluation", + "evaluation_row_id": str(uuid4()), + }, + "usage_date": date(2026, 8, 12), + "usage_kind": "llm", + } + deltas = {"prompt_tokens": 10, "completion_tokens": 5, "call_count": 1} + + exact_update = MagicMock(rowcount=0) + legacy_update = MagicMock(rowcount=1) + db = MagicMock() + db.execute.side_effect = [exact_update, legacy_update] + + usage_mod._upsert_bucket(db, org_id, bucket, deltas) + + assert db.execute.call_count == 2 + legacy_sql = str(db.execute.call_args_list[1][0][0]) + assert "context->>'resource_id'" in legacy_sql + assert "context->>'resource_type'" in legacy_sql diff --git a/tests/test_services/test_usage/test_usage_dates.py b/tests/test_services/test_usage/test_usage_dates.py new file mode 100644 index 00000000..b73290c9 --- /dev/null +++ b/tests/test_services/test_usage/test_usage_dates.py @@ -0,0 +1,32 @@ +"""Tests for usage date timezone mapping.""" + +from datetime import date + +from app.services.usage.dates import usage_date_filter_bounds, usage_local_today + + +def test_usage_date_filter_bounds_india_late_night(): + # 1:26 AM IST on Aug 13 is still Aug 12 in UTC usage_date bucket. + start = date(2026, 8, 13) + end = date(2026, 8, 13) + filter_start, filter_end = usage_date_filter_bounds(start, end, "Asia/Kolkata") + assert filter_start == date(2026, 8, 12) + assert filter_end == date(2026, 8, 13) + + +def test_usage_date_filter_bounds_without_tz_uses_exact_dates(): + start = date(2026, 8, 13) + end = date(2026, 8, 13) + assert usage_date_filter_bounds(start, end, None) == (start, end) + + +def test_usage_date_filter_bounds_utc_matches_calendar_days(): + start = date(2026, 8, 13) + end = date(2026, 8, 13) + assert usage_date_filter_bounds(start, end, "UTC") == (start, end) + + +def test_usage_local_today_respects_timezone(): + today_utc = usage_local_today("UTC") + today_india = usage_local_today("Asia/Kolkata") + assert today_utc <= today_india or today_india <= today_utc diff --git a/tests/test_services/test_usage/test_usage_labels.py b/tests/test_services/test_usage/test_usage_labels.py new file mode 100644 index 00000000..233fe900 --- /dev/null +++ b/tests/test_services/test_usage/test_usage_labels.py @@ -0,0 +1,287 @@ +"""Tests for usage label helpers.""" + +from uuid import UUID + +from app.services.usage.usage_labels import ( + build_usage_resource_label, + format_entity_label, + labels_for_resource_buckets, + usage_kind_label, + UsageNameResolver, +) + + +class _FakeEvaluation: + def __init__(self, id, name): + self.id = id + self.name = name + + +class _FakeCallImport: + def __init__(self, id, dataset=None, original_filename=None): + self.id = id + self.dataset = dataset + self.original_filename = original_filename + + +class _FakeCallImportRow: + def __init__(self, id, conversation_id): + self.id = id + self.conversation_id = conversation_id + + +class _FakeTTSComparison: + def __init__(self, id, name=None, simulation_id=None): + self.id = id + self.name = name + self.simulation_id = simulation_id + + +class _FakeAgent: + def __init__(self, id, name=None, agent_id=None): + self.id = id + self.name = name + self.agent_id = agent_id + + +class _FakeQuery: + def __init__(self, rows): + self._rows = rows + + def filter(self, *args, **kwargs): + return self + + def join(self, *args, **kwargs): + return self + + def all(self): + return self._rows + + +class _FakeDb: + def __init__(self, evaluations=(), imports=(), rows=(), tts_comparisons=(), agents=()): + self._evaluations = evaluations + self._imports = imports + self._rows = rows + self._tts_comparisons = tts_comparisons + self._agents = agents + + def query(self, *entities): + if len(entities) == 1: + model = entities[0] + if model.__name__ == "CallImportEvaluation": + return _FakeQuery(self._evaluations) + if model.__name__ == "CallImport": + return _FakeQuery(self._imports) + if model.__name__ == "CallImportRow": + return _FakeQuery(self._rows) + if model.__name__ == "TTSComparison": + return _FakeQuery(self._tts_comparisons) + if model.__name__ == "Agent": + return _FakeQuery(self._agents) + raise AssertionError(f"unexpected model {model}") + return _FakeQuery(()) + + +EVAL_ID = UUID("984039ab-cdef-4567-8901-234567890abc") +IMPORT_ID = UUID("3111d376-e5f6-7890-abcd-ef1234567890") +ROW_ID = UUID("fedcba98-7654-3210-fedc-ba9876543210") +COMP_ID = UUID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") +AGENT_ID = UUID("bbbbbbbb-cccc-dddd-eeee-ffffffffffff") +ORG_ID = UUID("11111111-2222-3333-4444-555555555555") + + +def test_format_entity_label_custom_name_and_default(): + assert format_entity_label("test", EVAL_ID, "Evaluation") == "test-984039ab" + assert format_entity_label(None, EVAL_ID, "Evaluation") == "Evaluation-984039ab" + + +def test_call_import_label_includes_dataset_and_tags(): + db = _FakeDb( + imports=[ + _FakeCallImport( + IMPORT_ID, + dataset="QA batch", + original_filename="4 manual recordings", + ) + ] + ) + # Extend fake db for tag query + class _TagQuery: + def join(self, *args, **kwargs): + return self + + def filter(self, *args, **kwargs): + return self + + def all(self): + return [(IMPORT_ID, "support")] + + def query_router(*entities): + if len(entities) == 1: + model = entities[0] + if model.__name__ == "CallImportTagAssignment": + return _TagQuery() + if model.__name__ == "CallImportEvaluation": + return _FakeQuery(()) + if model.__name__ == "CallImport": + return _FakeQuery(db._imports) + if model.__name__ == "CallImportRow": + return _FakeQuery(()) + return _TagQuery() + + db.query = query_router # type: ignore[method-assign] + + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload([{"call_import_id": str(IMPORT_ID)}]) + label = resolver.call_import_name(str(IMPORT_ID)) + assert "4 manual recordings" in label + assert "QA batch" in label + assert "support" in label + + +def test_call_import_label_prefers_filename_over_dataset(): + db = _FakeDb( + imports=[ + _FakeCallImport( + IMPORT_ID, + dataset="calls", + original_filename="unauthenticated sheet.xlsx", + ) + ] + ) + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload([{"call_import_id": str(IMPORT_ID)}]) + assert resolver.call_import_name(str(IMPORT_ID)) == ( + "unauthenticated sheet.xlsx (calls)-3111d376" + ) + + +def test_build_usage_resource_label_hierarchical(): + db = _FakeDb( + evaluations=[_FakeEvaluation(EVAL_ID, "March QA pass")], + imports=[ + _FakeCallImport( + IMPORT_ID, + dataset="calls", + original_filename="unauthenticated sheet.xlsx", + ) + ], + rows=[_FakeCallImportRow(ROW_ID, "ext-call-8842")], + ) + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload( + [ + { + "evaluation_id": str(EVAL_ID), + "call_import_id": str(IMPORT_ID), + "call_import_row_id": str(ROW_ID), + "resource_type": "call_import_evaluation", + "resource_id": str(EVAL_ID), + } + ] + ) + label = build_usage_resource_label( + { + "evaluation_id": str(EVAL_ID), + "call_import_id": str(IMPORT_ID), + "call_import_row_id": str(ROW_ID), + "resource_type": "call_import_evaluation", + "resource_id": str(EVAL_ID), + }, + "call_import_evaluation", + resolver, + ) + assert label == ( + "unauthenticated sheet.xlsx (calls)-3111d376 / March QA pass-984039ab / ext-call-8842" + ) + + +def test_build_usage_resource_label_default_evaluation_name(): + db = _FakeDb(evaluations=[_FakeEvaluation(EVAL_ID, None)]) + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload( + [ + { + "evaluation_id": str(EVAL_ID), + "resource_type": "call_import_evaluation", + "resource_id": str(EVAL_ID), + } + ] + ) + label = build_usage_resource_label( + {"evaluation_id": str(EVAL_ID)}, + "call_import_evaluation", + resolver, + ) + assert label == "Evaluation-984039ab" + + +def test_build_usage_resource_label_tts_comparison_with_simulation_id(): + db = _FakeDb( + tts_comparisons=[ + _FakeTTSComparison( + COMP_ID, + name="elevenlabs benchmark", + simulation_id="781879", + ) + ] + ) + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload( + [ + { + "resource_type": "tts_comparison", + "resource_id": str(COMP_ID), + } + ] + ) + label = build_usage_resource_label( + { + "resource_type": "tts_comparison", + "resource_id": str(COMP_ID), + }, + "tts_comparison", + resolver, + ) + assert label == "elevenlabs benchmark #781879" + + +def test_build_usage_resource_label_agent_with_short_id(): + db = _FakeDb( + agents=[_FakeAgent(AGENT_ID, name="Support bot", agent_id="482910")] + ) + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload( + [ + { + "resource_type": "agent", + "resource_id": str(AGENT_ID), + } + ] + ) + label = build_usage_resource_label( + {"resource_type": "agent", "resource_id": str(AGENT_ID)}, + "agent", + resolver, + ) + assert label == "Support bot #482910" + + +def test_labels_for_resource_buckets_empty_context_uses_bucket_id(): + db = _FakeDb( + agents=[_FakeAgent(AGENT_ID, name="Support bot", agent_id="482910")] + ) + resolver = UsageNameResolver(db, ORG_ID) + resolver.preload([{"resource_type": "agent", "resource_id": str(AGENT_ID)}]) + labels = labels_for_resource_buckets( + [(str(AGENT_ID), "agent", [{}])], + resolver, + ) + assert labels[str(AGENT_ID)] == "Support bot #482910" + + +def test_usage_kind_label(): + assert usage_kind_label("stt") == "STT" + assert usage_kind_label("llm") == "LLM" + assert usage_kind_label("tts") == "TTS" diff --git a/tests/test_workers/test_process_evaluator_result_helpers.py b/tests/test_workers/test_process_evaluator_result_helpers.py index 794248a3..0600c22f 100644 --- a/tests/test_workers/test_process_evaluator_result_helpers.py +++ b/tests/test_workers/test_process_evaluator_result_helpers.py @@ -1,28 +1,37 @@ -"""Helper tests for process_evaluator_result task module.""" +"""Unit tests for process_evaluator_result helper utilities.""" + +import importlib +from types import SimpleNamespace +from uuid import uuid4 -import importlib.util -from pathlib import Path -_TASK_PATH = Path(__file__).resolve().parents[2] / "app" / "workers" / "tasks" / "process_evaluator_result.py" -_TASK_SPEC = importlib.util.spec_from_file_location("process_evaluator_result_under_test", _TASK_PATH) -process_evaluator_result = importlib.util.module_from_spec(_TASK_SPEC) -assert _TASK_SPEC is not None and _TASK_SPEC.loader is not None -_TASK_SPEC.loader.exec_module(process_evaluator_result) +def _task_module(): + return importlib.import_module("app.workers.tasks.process_evaluator_result") def test_extract_audio_url_supports_smallest_recordings(): + task_module = _task_module() call_data = {"recording_url": "https://audio.smallest.ai/call.wav"} - audio_url = process_evaluator_result._extract_audio_url(call_data, "smallest") + assert task_module._extract_audio_url(call_data, "smallest") == "https://audio.smallest.ai/call.wav" - assert audio_url == "https://audio.smallest.ai/call.wav" -"""Unit tests for process_evaluator_result helper utilities.""" +def test_should_record_external_agent_call_usage_only_for_provider_calls(): + task_module = _task_module() + agent_id = uuid4() -def test_extract_audio_url_supports_smallest_recordings(): - import importlib + external = SimpleNamespace(agent_id=agent_id, provider_platform="vapi") + internal = SimpleNamespace(agent_id=agent_id, provider_platform=None) + simulation = SimpleNamespace(agent_id=agent_id, provider_platform="") + no_agent = SimpleNamespace(agent_id=None, provider_platform="vapi") - task_module = importlib.import_module("app.workers.tasks.process_evaluator_result") + assert task_module._should_record_external_agent_call_usage(external) is True + assert task_module._should_record_external_agent_call_usage(internal) is False + assert task_module._should_record_external_agent_call_usage(simulation) is False + assert task_module._should_record_external_agent_call_usage(no_agent) is False - call_data = {"recording_url": "https://audio.smallest.ai/call.wav"} - assert task_module._extract_audio_url(call_data, "smallest") == "https://audio.smallest.ai/call.wav" +def test_resolve_call_duration_seconds_prefers_result_field(): + task_module = _task_module() + result = SimpleNamespace(duration_seconds=12.4, call_data={}) + assert task_module._resolve_call_duration_seconds(result) == 13 + From 09b9793483c7679b938e6603c474d79d7e556267 Mon Sep 17 00:00:00 2001 From: M Sami Date: Thu, 13 Aug 2026 11:49:00 +0530 Subject: [PATCH 17/55] fix: committed claims management in db --- .../063_usage_kind_stt_and_buffer.py | 44 ++++++++++ app/services/usage/llm_usage.py | 84 +++++++++++++++++-- ...st_call_import_diarization_and_eval_llm.py | 2 + .../test_usage/test_llm_usage.py | 4 +- 4 files changed, 124 insertions(+), 10 deletions(-) diff --git a/app/migrations/063_usage_kind_stt_and_buffer.py b/app/migrations/063_usage_kind_stt_and_buffer.py index 1cbb51d9..fb1d9e4c 100644 --- a/app/migrations/063_usage_kind_stt_and_buffer.py +++ b/app/migrations/063_usage_kind_stt_and_buffer.py @@ -146,12 +146,33 @@ def _ensure_unique_bucket_index(db: Session) -> None: ) +def _ensure_llm_usage_daily_base_columns(db: Session) -> None: + """Align pre-062 tables with the 062 schema before dedupe/index steps.""" + if not _column_exists(db, "llm_usage_daily", "resource_id"): + db.execute(text("ALTER TABLE llm_usage_daily ADD COLUMN resource_id UUID")) + if not _column_exists(db, "llm_usage_daily", "resource_type"): + db.execute( + text("ALTER TABLE llm_usage_daily ADD COLUMN resource_type VARCHAR(64)") + ) + if not _column_exists(db, "llm_usage_daily", "updated_at"): + db.execute( + text( + """ + ALTER TABLE llm_usage_daily + ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + """ + ) + ) + + def upgrade(db: Session): if not _table_exists(db, "llm_usage_daily"): print("llm_usage_daily missing; run 062 first — skipping 063") db.commit() return + _ensure_llm_usage_daily_base_columns(db) + if not _column_exists(db, "llm_usage_daily", "usage_kind"): db.execute( text( @@ -223,10 +244,33 @@ def upgrade(db: Session): ) print("Created usage_pending_buffer") + if not _table_exists(db, "usage_committed_claims"): + db.execute( + text( + """ + CREATE TABLE usage_committed_claims ( + claim_key TEXT PRIMARY KEY, + organization_id UUID NOT NULL, + committed_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_usage_committed_claims_committed_at + ON usage_committed_claims (committed_at) + """ + ) + ) + print("Created usage_committed_claims") + db.commit() def downgrade(db: Session): + db.execute(text("DROP TABLE IF EXISTS usage_committed_claims")) db.execute(text("DROP TABLE IF EXISTS usage_pending_buffer")) db.execute(text("DROP INDEX IF EXISTS ix_llm_usage_daily_org_kind_date")) # Keep usage_kind/audio_seconds columns on downgrade to avoid data loss. diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py index 33fda254..6b3cb65d 100644 --- a/app/services/usage/llm_usage.py +++ b/app/services/usage/llm_usage.py @@ -522,24 +522,88 @@ def _restore_buckets_to_pending( _CLAIM_COMMITTED_TTL_SECONDS = 24 * 60 * 60 +_CLAIM_COMMITTED_REDIS_RETRIES = 5 def _claim_committed_key(claim_key: str) -> str: return f"usage:claim_done:{claim_key}" -def _mark_claim_committed(claim_key: str) -> None: +def _record_claim_committed_pg( + db: Session, claim_key: str, organization_id: UUID +) -> None: try: - _client().set(_claim_committed_key(claim_key), "1", ex=_CLAIM_COMMITTED_TTL_SECONDS) - except redis.RedisError: - pass + db.execute( + text( + """ + INSERT INTO usage_committed_claims (claim_key, organization_id) + VALUES (:claim_key, CAST(:organization_id AS uuid)) + ON CONFLICT (claim_key) DO NOTHING + """ + ), + { + "claim_key": claim_key, + "organization_id": str(organization_id), + }, + ) + except Exception as exc: + logger.debug("usage committed claim pg write skipped: {}", exc) + + +def _is_claim_committed_pg(claim_key: str) -> bool: + try: + from app.database import SessionLocal + + db = SessionLocal() + try: + return ( + db.execute( + text( + """ + SELECT 1 FROM usage_committed_claims + WHERE claim_key = :claim_key + """ + ), + {"claim_key": claim_key}, + ).first() + is not None + ) + finally: + db.close() + except Exception: + return False + + +def _mark_claim_committed(claim_key: str) -> bool: + for attempt in range(_CLAIM_COMMITTED_REDIS_RETRIES): + try: + _client().set( + _claim_committed_key(claim_key), + "1", + ex=_CLAIM_COMMITTED_TTL_SECONDS, + ) + return True + except redis.RedisError: + if attempt + 1 < _CLAIM_COMMITTED_REDIS_RETRIES: + time.sleep(0.05 * (attempt + 1)) + return False def _is_claim_committed(claim_key: str) -> bool: try: - return bool(_client().exists(_claim_committed_key(claim_key))) + if _client().exists(_claim_committed_key(claim_key)): + return True except redis.RedisError: - return False + pass + return _is_claim_committed_pg(claim_key) + + +def _finalize_committed_claim( + db: Session, claim_key: str, organization_id: UUID +) -> None: + _record_claim_committed_pg(db, claim_key, organization_id) + _mark_claim_committed(claim_key) + _ack_claim(claim_key, organization_id) def _has_pending_usage(organization_id: UUID) -> bool: @@ -897,6 +961,8 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = deltas, ) flushed += 1 + if claim_key: + _record_claim_committed_pg(db, claim_key, organization_id) db.commit() except Exception as exc: db.rollback() @@ -907,8 +973,10 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = exc, ) if claim_key: - _mark_claim_committed(claim_key) - _ack_claim(claim_key, organization_id) + _finalize_committed_claim( + db, claim_key, organization_id + ) + db.commit() claim_key = None buckets = {} else: diff --git a/tests/test_api/test_call_import_diarization_and_eval_llm.py b/tests/test_api/test_call_import_diarization_and_eval_llm.py index 936460d7..cb890fdc 100644 --- a/tests/test_api/test_call_import_diarization_and_eval_llm.py +++ b/tests/test_api/test_call_import_diarization_and_eval_llm.py @@ -32,6 +32,8 @@ def _make_fake_row(): return SimpleNamespace( id=uuid4(), organization_id=uuid4(), + call_import_id=uuid4(), + workspace_id=uuid4(), recording_s3_key="s3://bucket/key.wav", transcript="production value from CSV", transcript_source="csv", diff --git a/tests/test_services/test_usage/test_llm_usage.py b/tests/test_services/test_usage/test_llm_usage.py index 824de3cd..d39bb501 100644 --- a/tests/test_services/test_usage/test_llm_usage.py +++ b/tests/test_services/test_usage/test_llm_usage.py @@ -460,8 +460,8 @@ def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx, monkeypatch assert second == 0 assert db_a.commit.call_count == 1 assert db_b.commit.call_count == 0 - # INSERT once for the claimed bucket; second flusher never upserts. - assert db_a.execute.call_count == 2 # UPDATE miss + INSERT + # One bucket upsert (2 UPDATE misses + SAVEPOINT/INSERT/RELEASE) plus committed-claim row. + assert db_a.execute.call_count == 6 assert db_b.execute.call_count == 0 From 5c51608ab8f3b4613b81c7834a5c155f7c8bdc8f Mon Sep 17 00:00:00 2001 From: M Sami Date: Thu, 13 Aug 2026 12:01:06 +0530 Subject: [PATCH 18/55] refactor: enhanced Redis handling and database transaction integrity --- app/services/usage/llm_usage.py | 163 +++++++++++++----- .../test_usage/test_llm_usage.py | 63 +++++++ 2 files changed, 179 insertions(+), 47 deletions(-) diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py index 6b3cb65d..b992b9e5 100644 --- a/app/services/usage/llm_usage.py +++ b/app/services/usage/llm_usage.py @@ -522,60 +522,104 @@ def _restore_buckets_to_pending( _CLAIM_COMMITTED_TTL_SECONDS = 24 * 60 * 60 -_CLAIM_COMMITTED_REDIS_RETRIES = 5 +_CLAIM_COMMITTED_REDIS_RETRIES = 3 +_ORPHAN_RECOVERY_INTERVAL_SEC = 30 +_COMMITTED_CLAIMS_PRUNE_INTERVAL_SEC = 3600 +_COMMITTED_CLAIMS_RETENTION_DAYS = 2 def _claim_committed_key(claim_key: str) -> str: return f"usage:claim_done:{claim_key}" +def _redis_interval_gate(key: str, interval_sec: int) -> bool: + """Return True when the gated work should run (interval lock acquired).""" + try: + return bool(_client().set(key, "1", nx=True, ex=interval_sec)) + except redis.RedisError: + return False + + def _record_claim_committed_pg( db: Session, claim_key: str, organization_id: UUID ) -> None: + db.execute( + text( + """ + INSERT INTO usage_committed_claims (claim_key, organization_id) + VALUES (:claim_key, CAST(:organization_id AS uuid)) + ON CONFLICT (claim_key) DO NOTHING + """ + ), + { + "claim_key": claim_key, + "organization_id": str(organization_id), + }, + ) + + +def _committed_claim_keys_in_pg(claim_keys: List[str]) -> set[str]: + if not claim_keys: + return set() try: - db.execute( - text( - """ - INSERT INTO usage_committed_claims (claim_key, organization_id) - VALUES (:claim_key, CAST(:organization_id AS uuid)) - ON CONFLICT (claim_key) DO NOTHING - """ - ), - { - "claim_key": claim_key, - "organization_id": str(organization_id), - }, - ) - except Exception as exc: - logger.debug("usage committed claim pg write skipped: {}", exc) + from app.database import SessionLocal + db = SessionLocal() + try: + rows = db.execute( + text( + """ + SELECT claim_key + FROM usage_committed_claims + WHERE claim_key = ANY(CAST(:claim_keys AS text[])) + """ + ), + {"claim_keys": claim_keys}, + ).all() + return {row[0] for row in rows} + finally: + db.close() + except Exception: + return set() -def _is_claim_committed_pg(claim_key: str) -> bool: + +def _prune_old_committed_claims() -> None: try: from app.database import SessionLocal db = SessionLocal() try: - return ( - db.execute( - text( - """ - SELECT 1 FROM usage_committed_claims - WHERE claim_key = :claim_key - """ - ), - {"claim_key": claim_key}, - ).first() - is not None + db.execute( + text( + f""" + DELETE FROM usage_committed_claims + WHERE committed_at < now() - interval '{_COMMITTED_CLAIMS_RETENTION_DAYS} days' + """ + ) ) + db.commit() + except Exception as exc: + db.rollback() + logger.debug("usage committed claim prune skipped: {}", exc) finally: db.close() except Exception: - return False + pass -def _mark_claim_committed(claim_key: str) -> bool: - for attempt in range(_CLAIM_COMMITTED_REDIS_RETRIES): +def _discard_committed_claim(claim_key: str, organization_id: UUID) -> None: + """Best-effort Redis cleanup when usage was not (or must not be) persisted.""" + _mark_claim_committed(claim_key, fast=True) + _ack_claim(claim_key, organization_id) + + +def _is_claim_committed_pg(claim_key: str) -> bool: + return claim_key in _committed_claim_keys_in_pg([claim_key]) + + +def _mark_claim_committed(claim_key: str, *, fast: bool = False) -> bool: + max_attempts = 2 if fast else _CLAIM_COMMITTED_REDIS_RETRIES + for attempt in range(max_attempts): try: _client().set( _claim_committed_key(claim_key), @@ -584,7 +628,7 @@ def _mark_claim_committed(claim_key: str) -> bool: ) return True except redis.RedisError: - if attempt + 1 < _CLAIM_COMMITTED_REDIS_RETRIES: + if attempt + 1 < max_attempts and not fast: time.sleep(0.05 * (attempt + 1)) return False @@ -598,14 +642,6 @@ def _is_claim_committed(claim_key: str) -> bool: return _is_claim_committed_pg(claim_key) -def _finalize_committed_claim( - db: Session, claim_key: str, organization_id: UUID -) -> None: - _record_claim_committed_pg(db, claim_key, organization_id) - _mark_claim_committed(claim_key) - _ack_claim(claim_key, organization_id) - - def _has_pending_usage(organization_id: UUID) -> bool: try: client = _client() @@ -973,10 +1009,14 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = exc, ) if claim_key: - _finalize_committed_claim( - db, claim_key, organization_id - ) - db.commit() + try: + _record_claim_committed_pg( + db, claim_key, organization_id + ) + db.commit() + except Exception: + db.rollback() + _discard_committed_claim(claim_key, organization_id) claim_key = None buckets = {} else: @@ -993,11 +1033,11 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = except redis.RedisError: pass claim_key = None - return flushed + _flush_pending_buffer(db, organization_id) + return _flush_pending_buffer(db, organization_id) if skipped: _restore_buckets_to_pending(organization_id, skipped) if claim_key: - _mark_claim_committed(claim_key) + _mark_claim_committed(claim_key, fast=True) _ack_claim(claim_key, organization_id) claim_key = None finally: @@ -1009,8 +1049,13 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = def _recover_orphaned_claims() -> None: + if not _redis_interval_gate( + "usage:orphan_recovery:due", _ORPHAN_RECOVERY_INTERVAL_SEC + ): + return try: client = _client() + candidates: List[Tuple[str, UUID]] = [] for claim_key in client.scan_iter(match="usage:flushing:*", count=100): parts = claim_key.split(":") if len(parts) < 4: @@ -1021,13 +1066,37 @@ def _recover_orphaned_claims() -> None: continue if client.exists(_flush_lock_key(org_id)): continue - if _is_claim_committed(claim_key): + candidates.append((claim_key, org_id)) + + if not candidates: + return + + pipe = client.pipeline() + for claim_key, _org_id in candidates: + pipe.exists(_claim_committed_key(claim_key)) + redis_committed_flags = pipe.execute() + + needs_pg: List[Tuple[str, UUID]] = [] + for index, (claim_key, org_id) in enumerate(candidates): + if redis_committed_flags[index]: + client.delete(claim_key) + continue + needs_pg.append((claim_key, org_id)) + + pg_committed = _committed_claim_keys_in_pg([key for key, _ in needs_pg]) + for claim_key, org_id in needs_pg: + if claim_key in pg_committed: client.delete(claim_key) continue buckets = _read_hash_buckets(claim_key) if buckets: _restore_buckets_to_pending(org_id, buckets) client.delete(claim_key) + + if _redis_interval_gate( + "usage:committed_claims:prune", _COMMITTED_CLAIMS_PRUNE_INTERVAL_SEC + ): + _prune_old_committed_claims() except redis.RedisError as exc: logger.warning("llm usage orphan claim recovery failed: {}", exc) diff --git a/tests/test_services/test_usage/test_llm_usage.py b/tests/test_services/test_usage/test_llm_usage.py index d39bb501..4abdf609 100644 --- a/tests/test_services/test_usage/test_llm_usage.py +++ b/tests/test_services/test_usage/test_llm_usage.py @@ -41,6 +41,10 @@ def expire(self, key: str, ttl: int): self._ops.append(("expire", key, ttl)) return self + def exists(self, key: str): + self._ops.append(("exists", key)) + return self + def execute(self): results = [] for op in self._ops: @@ -51,6 +55,8 @@ def execute(self): results.append(self._client.sadd(op[1], *op[2])) elif kind == "expire": results.append(self._client.expire(op[1], op[2])) + elif kind == "exists": + results.append(self._client.exists(op[1])) self._ops.clear() return results @@ -129,6 +135,9 @@ def delete(self, *keys: str) -> int: def expire(self, key: str, ttl: int) -> bool: return self.exists(key) == 1 + def pipeline(self): + return _FakePipeline(self) + def set(self, key: str, value: str, nx: bool = False, ex: Optional[int] = None) -> Optional[bool]: if nx and key in self.kv: return None @@ -407,6 +416,39 @@ def test_flush_restores_redis_when_db_fails(fake_redis, org_ctx, monkeypatch): assert not any(k.startswith("usage:flushing:") for k in fake_redis.hashes) +def test_flush_restores_redis_when_committed_claim_insert_fails( + fake_redis, org_ctx, monkeypatch +): + """Committed-claim insert must share the usage transaction; failure rolls back.""" + org_id, _workspace_id, ctx = org_ctx + with llm_usage_context(ctx): + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=5, completion_tokens=2), + usage_date=date(2026, 8, 11), + ) + + db = MagicMock() + + def _execute_side_effect(statement, *_args, **_kwargs): + if "usage_committed_claims" in str(statement): + raise RuntimeError("usage_committed_claims unavailable") + return MagicMock(rowcount=0) + + db.execute.side_effect = _execute_side_effect + monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda _db, _org: 0) + + flushed = usage_mod.flush_usage_to_catalog(db, org_id) + assert flushed == 0 + db.rollback.assert_called() + db.commit.assert_not_called() + + pending = fake_redis.hgetall(usage_mod._pending_hash_key(org_id)) + prompt = sum(int(v) for k, v in pending.items() if k.endswith("|prompt_tokens")) + assert prompt == 5 + assert str(org_id) in fake_redis.smembers("usage:pending:orgs") + + def test_flush_drops_pending_when_organization_missing(fake_redis, org_ctx, monkeypatch): """Unknown org FK must not restore Redis (avoids infinite beat retries).""" from sqlalchemy.exc import IntegrityError @@ -585,3 +627,24 @@ def test_upsert_bucket_matches_legacy_resource_context_key(monkeypatch): legacy_sql = str(db.execute.call_args_list[1][0][0]) assert "context->>'resource_id'" in legacy_sql assert "context->>'resource_type'" in legacy_sql + + +def test_orphan_recovery_runs_at_most_once_per_interval(fake_redis, monkeypatch): + org_id = uuid4() + claim_key = f"usage:flushing:{org_id}:{uuid4()}" + fake_redis.hashes[claim_key] = {"bucket|prompt_tokens": 3} + + pg_lookups = 0 + + def _count_pg_lookup(keys): + nonlocal pg_lookups + pg_lookups += 1 + return set() + + monkeypatch.setattr(usage_mod, "_committed_claim_keys_in_pg", _count_pg_lookup) + monkeypatch.setattr(usage_mod, "_prune_old_committed_claims", lambda: None) + + usage_mod._recover_orphaned_claims() + usage_mod._recover_orphaned_claims() + + assert pg_lookups == 1 From 4dd9fdc519dd4f7485b59a3db9ec7b1306a8c4e6 Mon Sep 17 00:00:00 2001 From: M Sami Date: Thu, 13 Aug 2026 12:17:01 +0530 Subject: [PATCH 19/55] refactor: usage commited claims --- app/migrations/068_usage_committed_claims.py | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 app/migrations/068_usage_committed_claims.py diff --git a/app/migrations/068_usage_committed_claims.py b/app/migrations/068_usage_committed_claims.py new file mode 100644 index 00000000..25c59fb6 --- /dev/null +++ b/app/migrations/068_usage_committed_claims.py @@ -0,0 +1,57 @@ +"""Migration: usage_committed_claims for durable flush idempotency.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Create usage_committed_claims when missing (063 may have run before this table was added)" +) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def upgrade(db: Session): + if _table_exists(db, "usage_committed_claims"): + print("usage_committed_claims already exists, skipping 068") + db.commit() + return + + db.execute( + text( + """ + CREATE TABLE usage_committed_claims ( + claim_key TEXT PRIMARY KEY, + organization_id UUID NOT NULL, + committed_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_usage_committed_claims_committed_at + ON usage_committed_claims (committed_at) + """ + ) + ) + print("Created usage_committed_claims") + db.commit() + + +def downgrade(db: Session): + db.execute(text("DROP TABLE IF EXISTS usage_committed_claims")) + db.commit() From cbe1c47f8c7fca5e7063bcc529eb2276ec860b73 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 14 Aug 2026 11:24:20 +0530 Subject: [PATCH 20/55] feat: usage pricing and integrate usage context across various routes --- README.md | 84 +- app/api/v1/api.py | 2 + app/api/v1/routes/agents.py | 147 +- app/api/v1/routes/call_import_evaluations.py | 10 + app/api/v1/routes/org_usage.py | 236 +- app/api/v1/routes/usage_pricing.py | 241 ++ app/api/v1/routes/voice_playground.py | 32 +- app/cli.py | 291 +- app/config/models.json | 3054 +++++++++++------ app/migrations/069_usage_pricing.py | 190 + app/migrations/070_usage_margin_multiplier.py | 45 + app/migrations/071_reseed_pricing_catalog.py | 37 + app/migrations/072_usage_pricing_phase1.py | 202 ++ .../073_usage_cost_recompute_jobs.py | 72 + ...pricing_rates_source_and_effective_from.py | 81 + app/models/database.py | 41 + app/services/ai/stt_clients/google.py | 25 + app/services/call_import_user_insights.py | 33 +- app/services/usage/llm_usage.py | 170 +- app/services/usage/pricing.py | 912 +++++ app/services/usage/pricing_cache.py | 87 + app/services/usage/pricing_jobs.py | 145 + app/services/usage/pricing_ops.py | 137 + app/services/usage/pricing_overrides.py | 530 +++ app/services/usage/usage_costs.py | 51 + app/workers/config.py | 10 +- app/workers/tasks/__init__.py | 2 + app/workers/tasks/recompute_usage_costs.py | 79 + docker-compose.observability.yml | 11 + docker-compose.yml | 57 + frontend/src/App.tsx | 2 + frontend/src/lib/api.ts | 169 + frontend/src/pages/usage/SearchableSelect.tsx | 7 +- frontend/src/pages/usage/Usage.tsx | 267 +- .../pages/usage/UsageCostBreakdownModal.tsx | 133 + frontend/src/pages/usage/UsageFiltersBar.tsx | 27 +- frontend/src/pages/usage/UsagePricing.tsx | 393 +++ frontend/src/pages/usage/usageProductHints.ts | 3 + scripts/merge_pricing_into_models_json.py | 107 + scripts/sync_pricing_catalog_from_litellm.py | 428 +++ .../test_usage/test_llm_usage.py | 32 + .../test_usage/test_org_usage_costs.py | 76 + .../test_services/test_usage/test_pricing.py | 211 ++ .../test_usage/test_pricing_cache.py | 91 + .../test_usage/test_pricing_ops.py | 10 + .../test_usage/test_pricing_overrides.py | 72 + .../test_usage/test_usage_costs.py | 33 + .../test_workers/test_usage_queue_routing.py | 18 + 48 files changed, 7777 insertions(+), 1316 deletions(-) create mode 100644 app/api/v1/routes/usage_pricing.py create mode 100644 app/migrations/069_usage_pricing.py create mode 100644 app/migrations/070_usage_margin_multiplier.py create mode 100644 app/migrations/071_reseed_pricing_catalog.py create mode 100644 app/migrations/072_usage_pricing_phase1.py create mode 100644 app/migrations/073_usage_cost_recompute_jobs.py create mode 100644 app/migrations/074_pricing_rates_source_and_effective_from.py create mode 100644 app/services/usage/pricing.py create mode 100644 app/services/usage/pricing_cache.py create mode 100644 app/services/usage/pricing_jobs.py create mode 100644 app/services/usage/pricing_ops.py create mode 100644 app/services/usage/pricing_overrides.py create mode 100644 app/services/usage/usage_costs.py create mode 100644 app/workers/tasks/recompute_usage_costs.py create mode 100644 frontend/src/pages/usage/UsageCostBreakdownModal.tsx create mode 100644 frontend/src/pages/usage/UsagePricing.tsx create mode 100644 scripts/merge_pricing_into_models_json.py create mode 100644 scripts/sync_pricing_catalog_from_litellm.py create mode 100644 tests/test_services/test_usage/test_org_usage_costs.py create mode 100644 tests/test_services/test_usage/test_pricing.py create mode 100644 tests/test_services/test_usage/test_pricing_cache.py create mode 100644 tests/test_services/test_usage/test_pricing_ops.py create mode 100644 tests/test_services/test_usage/test_pricing_overrides.py create mode 100644 tests/test_services/test_usage/test_usage_costs.py create mode 100644 tests/test_workers/test_usage_queue_routing.py diff --git a/README.md b/README.md index cc48559e..009ca348 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,21 @@ There are two ways to run the application: This will automatically: - Pull pre-built images from GitHub Container Registry (no build required!) - - Start all services (database, Redis, API, worker) + - Start all services: `db`, `redis`, `api`, `media`, `worker`, `worker-imports`, `worker-usage`, `beat` - Run database migrations automatically on startup + + | Service | Purpose | + |---------|---------| + | `db` | PostgreSQL | + | `redis` | Redis (Celery broker + usage counters) | + | `api` | HTTP API + frontend | + | `media` | Live voice WebSocket media server | + | `worker` | Celery: `celery`, `audio-metrics` queues | + | `worker-imports` | Celery: `imports`, `diarization`, `eval-control`, `evaluations` | + | `worker-usage` | Celery: `usage` queue (flush Redis counters + cost recompute) | + | `beat` | Celery Beat scheduler (periodic `flush_usage_counters`; requires `worker-usage`) | + + **Usage costs:** token/cost rollups stay stale without `worker-usage` and `beat` (or the equivalent processes from `eai start-all`). **Using a specific version:** ```bash @@ -137,20 +150,29 @@ docker compose up -d url: "redis://localhost:6379/0" ``` -4. **Start the application and worker** +4. **Start the application and workers** + + **Infra only (optional):** if Postgres/Redis run in Docker but the app runs locally: + ```bash + docker compose up -d db redis + ``` - **Option A: Start both together (Recommended)** + **Option A: Start everything together (Recommended)** ```bash eai start-all --config config.yml ``` - This single command will: - - Start the API server - - Start the Celery worker (for background task processing) - - Run database migrations automatically - - Build the frontend (if needed) + This single command spawns: + - API server (uvicorn) + - Telephony media server (`media` port, default 8001) + - Celery worker (`celery`, `audio-metrics`) + - Celery worker (`imports`, `diarization`, `eval-control`, `evaluations`) + - Celery worker (`usage` — flush + cost recompute) + - Celery Beat (periodic usage flush) + + It also runs database migrations and builds the frontend when needed. - Press `Ctrl+C` to stop both services together. + Press `Ctrl+C` to stop all processes. **Option B: Start separately (for advanced use)** @@ -247,7 +269,7 @@ make test-docker-db TEST_DB_HOST=localhost TEST_DB_PORT=5432 TEST_DB_NAME=effici ### Start Application and Worker Together (Recommended) ```bash -# Start both app and worker with default config.yml +# Start API + all workers + beat with default config.yml eai start-all # Start with custom config @@ -264,9 +286,17 @@ eai start-all --no-reload --no-build-frontend # Customize worker log level eai start-all --worker-loglevel debug + +# Skip dedicated workers (not recommended for production) +eai start-all --no-imports-worker +eai start-all --no-usage-worker +eai start-all --no-telephony-worker + +# Tune usage worker concurrency (default: 4, thread pool) +eai start-all --usage-worker-concurrency 8 ``` -**Note:** This is the recommended way to run EfficientAI. It starts both the API server and Celery worker in a single command. Press `Ctrl+C` to stop both services. +**Note:** This is the recommended local-dev workflow. One command spawns the API, telephony media server, three Celery workers (`celery,audio-metrics` · `imports,…` · `usage`), and Celery Beat. Press `Ctrl+C` to stop all processes. For Docker deployments, use `docker compose up -d` instead (separate containers per role; see Quick Start). ### Start Application Only ```bash @@ -340,7 +370,37 @@ eai worker --loglevel debug celery -A app.workers.celery_app worker --loglevel=info ``` -**Note:** The worker is required for processing background tasks (transcription, evaluation, etc.). If you use `eai start-all`, the worker starts automatically. Only use this command if you need to run the worker separately. +**Note:** Workers are required for background tasks (transcription, evaluation, usage cost flush, etc.). If you use `eai start-all`, they start automatically. Only use `eai worker` if you need to run a worker separately (e.g. `eai worker --queues usage` for the usage queue only). + +### Usage Pricing Ops +Manage model pricing rates and backfill stored usage costs on `llm_usage_daily` rollups. + +```bash +# Upsert model_pricing_rates from app/config/models.json +eai usage seed-rates --config config.yml + +# Compare models.json pricing vs Postgres +eai usage diff-rates --config config.yml + +# Backfill / recompute costs (sync, runs in this process) +eai usage recompute --config config.yml --sync + +# Enqueue Celery recompute on the usage queue (requires --organization-id) +eai usage recompute --config config.yml --organization-id + +# Optional: fetch LiteLLM prices into pricing_catalog.json +eai usage sync-litellm --local +eai usage sync-litellm --local --write-models +``` + +**After migrations or catalog changes:** +```bash +eai migrate +eai usage seed-rates --config config.yml +eai usage recompute --config config.yml --sync +``` + +Requires `worker-usage` (or `eai start-all`) for async recompute; Beat + `worker-usage` keep Redis usage counters flushed into Postgres on a schedule (`USAGE_FLUSH_BEAT_SECONDS`, default 120). ### Generate Config File ```bash diff --git a/app/api/v1/api.py b/app/api/v1/api.py index 51d81945..c1802238 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -46,6 +46,7 @@ dashboard, llm_gateway, org_usage, + usage_pricing, ) api_router = APIRouter() @@ -95,3 +96,4 @@ api_router.include_router(dashboard.router) api_router.include_router(llm_gateway.router) api_router.include_router(org_usage.router) +api_router.include_router(usage_pricing.router) diff --git a/app/api/v1/routes/agents.py b/app/api/v1/routes/agents.py index 4c4b295e..17094877 100644 --- a/app/api/v1/routes/agents.py +++ b/app/api/v1/routes/agents.py @@ -255,6 +255,7 @@ def _scenario_draft_responses(scenarios) -> list[GeneratedScenarioDraftResponse] async def generate_test_prompt( data: GenerateTestPromptRequest, organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), api_key: str = Depends(get_api_key), db: Session = Depends(get_db), ): @@ -262,6 +263,11 @@ async def generate_test_prompt( from app.services.testing.agent_test_setup_generation import ( generate_test_prompt_from_production, ) + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) if not data.production_prompt.strip(): raise HTTPException(400, "Production prompt is required") @@ -271,19 +277,26 @@ async def generate_test_prompt( ) try: - result = generate_test_prompt_from_production( - data.production_prompt, - agent_name=data.agent_name, - language=data.language, - call_type=data.call_type, - additional_context=data.additional_context, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - llm_config=data.llm_config, - credential_id=data.credential_id, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.AGENTS, + ) + ): + result = generate_test_prompt_from_production( + data.production_prompt, + agent_name=data.agent_name, + language=data.language, + call_type=data.call_type, + additional_context=data.additional_context, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + llm_config=data.llm_config, + credential_id=data.credential_id, + ) return GenerateTestPromptResponse( sections=_test_prompt_section_responses(result.sections), test_agent_prompt=result.test_agent_prompt, @@ -301,6 +314,7 @@ async def generate_test_prompt( async def generate_scenarios_from_prompt( data: GenerateScenariosFromPromptRequest, organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), api_key: str = Depends(get_api_key), db: Session = Depends(get_db), ): @@ -308,6 +322,11 @@ async def generate_scenarios_from_prompt( from app.services.testing.agent_test_setup_generation import ( generate_scenarios_from_test_prompt, ) + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) if not data.test_agent_prompt.strip(): raise HTTPException(400, "Test agent prompt is required") @@ -317,20 +336,27 @@ async def generate_scenarios_from_prompt( ) try: - result = generate_scenarios_from_test_prompt( - data.test_agent_prompt, - agent_name=data.agent_name, - scenario_count=data.scenario_count, - language=data.language, - call_type=data.call_type, - additional_context=data.additional_context, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - llm_config=data.llm_config, - credential_id=data.credential_id, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.AGENTS, + ) + ): + result = generate_scenarios_from_test_prompt( + data.test_agent_prompt, + agent_name=data.agent_name, + scenario_count=data.scenario_count, + language=data.language, + call_type=data.call_type, + additional_context=data.additional_context, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + llm_config=data.llm_config, + credential_id=data.credential_id, + ) return GenerateScenariosFromPromptResponse( scenarios=_scenario_draft_responses(result.scenarios), provider=result.provider, @@ -347,6 +373,7 @@ async def generate_scenarios_from_prompt( async def generate_test_setup( data: GenerateTestSetupRequest, organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), api_key: str = Depends(get_api_key), db: Session = Depends(get_db), ): @@ -355,6 +382,11 @@ async def generate_test_setup( generate_scenarios_from_test_prompt, generate_test_prompt_from_production, ) + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) if not data.production_prompt.strip(): raise HTTPException(400, "Production prompt is required") @@ -364,33 +396,40 @@ async def generate_test_setup( ) try: - prompt_result = generate_test_prompt_from_production( - data.production_prompt, - agent_name=data.agent_name, - language=data.language, - call_type=data.call_type, - additional_context=data.additional_context, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - llm_config=data.llm_config, - credential_id=data.credential_id, - ) - scenario_result = generate_scenarios_from_test_prompt( - prompt_result.test_agent_prompt, - agent_name=data.agent_name, - scenario_count=data.scenario_count, - language=data.language, - call_type=data.call_type, - additional_context=data.additional_context, - llm_provider=provider_enum, - llm_model=model_str, - organization_id=organization_id, - db=db, - llm_config=data.llm_config, - credential_id=data.credential_id, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.AGENTS, + ) + ): + prompt_result = generate_test_prompt_from_production( + data.production_prompt, + agent_name=data.agent_name, + language=data.language, + call_type=data.call_type, + additional_context=data.additional_context, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + llm_config=data.llm_config, + credential_id=data.credential_id, + ) + scenario_result = generate_scenarios_from_test_prompt( + prompt_result.test_agent_prompt, + agent_name=data.agent_name, + scenario_count=data.scenario_count, + language=data.language, + call_type=data.call_type, + additional_context=data.additional_context, + llm_provider=provider_enum, + llm_model=model_str, + organization_id=organization_id, + db=db, + llm_config=data.llm_config, + credential_id=data.credential_id, + ) return GenerateTestSetupResponse( sections=_test_prompt_section_responses(prompt_result.sections), test_agent_prompt=prompt_result.test_agent_prompt, diff --git a/app/api/v1/routes/call_import_evaluations.py b/app/api/v1/routes/call_import_evaluations.py index 880393c7..528f60f3 100644 --- a/app/api/v1/routes/call_import_evaluations.py +++ b/app/api/v1/routes/call_import_evaluations.py @@ -3028,10 +3028,19 @@ def _explain_period_deltas( from app.services.ai.llm_resolver import get_llm_provider_and_model from app.services.call_import_user_insights import _call_llm, _parse_json_object + from app.services.usage.call_import_context import ( + call_import_evaluation_usage_context, + ) provider_enum, model_str = get_llm_provider_and_model( organization_id, db, provider_hint, model_hint ) + usage_ctx = call_import_evaluation_usage_context( + organization_id=organization_id, + workspace_id=evaluation.workspace_id, + evaluation_id=evaluation.id, + call_import_id=evaluation.call_import_id, + ) try: text = _call_llm( db, @@ -3051,6 +3060,7 @@ def _explain_period_deltas( ], temperature=0.3, max_tokens=900, + usage_ctx=usage_ctx, ) except Exception as exc: logger.warning("[PeriodDeltaExplain] LLM call failed: {}", exc) diff --git a/app/api/v1/routes/org_usage.py b/app/api/v1/routes/org_usage.py index 4c702138..2d6854f6 100644 --- a/app/api/v1/routes/org_usage.py +++ b/app/api/v1/routes/org_usage.py @@ -27,6 +27,7 @@ Workspace, ) from app.services.usage.llm_usage import flush_usage_to_catalog +from app.services.usage.usage_costs import costs_from_micro from app.services.usage.usage_labels import ( labels_for_call_import_ids, labels_for_resource_buckets, @@ -87,6 +88,19 @@ def _label_row_order(): ) +class UsageCosts(BaseModel): + input_cost_usd: float = 0 + output_cost_usd: float = 0 + cache_read_cost_usd: float = 0 + cache_write_cost_usd: float = 0 + reasoning_cost_usd: float = 0 + audio_cost_usd: float = 0 + tts_cost_usd: float = 0 + total_cost_usd: float = 0 + currency: str = "USD" + has_unpriced_usage: bool = False + + class UsageTotals(BaseModel): prompt_tokens: int = 0 completion_tokens: int = 0 @@ -97,6 +111,15 @@ class UsageTotals(BaseModel): audio_seconds: int = 0 tts_characters: int = 0 call_count: int = 0 + input_cost_micro_usd: int = 0 + output_cost_micro_usd: int = 0 + cache_read_cost_micro_usd: int = 0 + cache_creation_cost_micro_usd: int = 0 + reasoning_cost_micro_usd: int = 0 + audio_cost_micro_usd: int = 0 + tts_cost_micro_usd: int = 0 + total_cost_micro_usd: int = 0 + costs: UsageCosts = Field(default_factory=UsageCosts) class UsageSummaryResponse(BaseModel): @@ -127,6 +150,15 @@ class UsageBreakdownRow(BaseModel): audio_seconds: int = 0 tts_characters: int = 0 call_count: int = 0 + input_cost_micro_usd: int = 0 + output_cost_micro_usd: int = 0 + cache_read_cost_micro_usd: int = 0 + cache_creation_cost_micro_usd: int = 0 + reasoning_cost_micro_usd: int = 0 + audio_cost_micro_usd: int = 0 + tts_cost_micro_usd: int = 0 + total_cost_micro_usd: int = 0 + costs: UsageCosts = Field(default_factory=UsageCosts) class UsageBreakdownResponse(BaseModel): @@ -438,7 +470,7 @@ def _apply_filters( query = query.filter( LLMUsageDaily.context["call_import_id"].astext == str(call_import_id) ) - if evaluation_id is not None: + if evaluation_id is not None and evaluation_id != resource_id: if db is not None: query = query.filter( _evaluation_scope_filter(evaluation_id, organization_id, db) @@ -847,6 +879,123 @@ def _evaluation_label_map( return labels +def _unpriced_usage_condition(): + return and_( + LLMUsageDaily.pricing_rate_id.is_(None), + _usage_row_weight() > 0, + ) + + +def _has_unpriced_usage_column(): + return func.coalesce(func.bool_or(_unpriced_usage_condition()), False).label( + "has_unpriced_usage" + ) + + +def _usage_cost_sum_columns(): + return ( + func.coalesce(func.sum(LLMUsageDaily.input_cost_micro_usd), 0).label( + "input_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.output_cost_micro_usd), 0).label( + "output_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.cache_read_cost_micro_usd), 0).label( + "cache_read_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.cache_creation_cost_micro_usd), 0).label( + "cache_creation_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.reasoning_cost_micro_usd), 0).label( + "reasoning_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.audio_cost_micro_usd), 0).label( + "audio_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.tts_cost_micro_usd), 0).label( + "tts_cost_micro_usd" + ), + func.coalesce(func.sum(LLMUsageDaily.total_cost_micro_usd), 0).label( + "total_cost_micro_usd" + ), + ) + + +def _attach_costs(metrics: dict, *, has_unpriced_usage: bool = False) -> dict: + costs = costs_from_micro( + input_cost_micro_usd=metrics.get("input_cost_micro_usd", 0), + output_cost_micro_usd=metrics.get("output_cost_micro_usd", 0), + cache_read_cost_micro_usd=metrics.get("cache_read_cost_micro_usd", 0), + cache_creation_cost_micro_usd=metrics.get("cache_creation_cost_micro_usd", 0), + reasoning_cost_micro_usd=metrics.get("reasoning_cost_micro_usd", 0), + audio_cost_micro_usd=metrics.get("audio_cost_micro_usd", 0), + tts_cost_micro_usd=metrics.get("tts_cost_micro_usd", 0), + total_cost_micro_usd=metrics.get("total_cost_micro_usd", 0), + has_unpriced_usage=has_unpriced_usage, + ) + return {**metrics, "costs": costs} + + +def _usage_totals_from_row(row) -> dict: + prompt = int(row.prompt_tokens) + completion = int(row.completion_tokens) + metrics = { + "prompt_tokens": prompt, + "completion_tokens": completion, + "total_tokens": prompt + completion, + "cache_read_tokens": int(row.cache_read_tokens), + "cache_creation_tokens": int(row.cache_creation_tokens), + "reasoning_tokens": int(row.reasoning_tokens), + "audio_seconds": int(row.audio_seconds), + "tts_characters": int(row.tts_characters), + "call_count": int(row.call_count), + "input_cost_micro_usd": int(getattr(row, "input_cost_micro_usd", 0) or 0), + "output_cost_micro_usd": int(getattr(row, "output_cost_micro_usd", 0) or 0), + "cache_read_cost_micro_usd": int( + getattr(row, "cache_read_cost_micro_usd", 0) or 0 + ), + "cache_creation_cost_micro_usd": int( + getattr(row, "cache_creation_cost_micro_usd", 0) or 0 + ), + "reasoning_cost_micro_usd": int( + getattr(row, "reasoning_cost_micro_usd", 0) or 0 + ), + "audio_cost_micro_usd": int(getattr(row, "audio_cost_micro_usd", 0) or 0), + "tts_cost_micro_usd": int(getattr(row, "tts_cost_micro_usd", 0) or 0), + "total_cost_micro_usd": int(getattr(row, "total_cost_micro_usd", 0) or 0), + } + return _attach_costs( + metrics, + has_unpriced_usage=bool(getattr(row, "has_unpriced_usage", False)), + ) + + +def _breakdown_metrics_from_tuple(metrics: tuple) -> dict: + prompt = int(metrics[0]) + completion = int(metrics[1]) + base = { + "prompt_tokens": prompt, + "completion_tokens": completion, + "total_tokens": prompt + completion, + "cache_read_tokens": int(metrics[2]), + "cache_creation_tokens": int(metrics[3]), + "reasoning_tokens": int(metrics[4]), + "audio_seconds": int(metrics[5]), + "tts_characters": int(metrics[6]), + "call_count": int(metrics[7]), + "input_cost_micro_usd": int(metrics[8]), + "output_cost_micro_usd": int(metrics[9]), + "cache_read_cost_micro_usd": int(metrics[10]), + "cache_creation_cost_micro_usd": int(metrics[11]), + "reasoning_cost_micro_usd": int(metrics[12]), + "audio_cost_micro_usd": int(metrics[13]), + "tts_cost_micro_usd": int(metrics[14]), + "total_cost_micro_usd": int(metrics[15]), + } + has_unpriced = bool(metrics[16]) if len(metrics) > 16 else False + return _attach_costs(base, has_unpriced_usage=has_unpriced) + + def _last_updated(db: Session, organization_id: UUID) -> Optional[datetime]: return ( db.query(func.max(LLMUsageDaily.updated_at)) @@ -890,6 +1039,8 @@ def _summary_aggregate_query( func.coalesce(func.sum(LLMUsageDaily.audio_seconds), 0).label("audio_seconds"), func.coalesce(func.sum(LLMUsageDaily.tts_characters), 0).label("tts_characters"), func.coalesce(func.sum(LLMUsageDaily.call_count), 0).label("call_count"), + *_usage_cost_sum_columns(), + _has_unpriced_usage_column(), ), organization_id=organization_id, start=start, @@ -953,19 +1104,7 @@ def get_usage_summary( dataset=dataset, tag_id=tag_id, ).one() - prompt = int(row.prompt_tokens) - completion = int(row.completion_tokens) - totals = { - "prompt_tokens": prompt, - "completion_tokens": completion, - "total_tokens": prompt + completion, - "cache_read_tokens": int(row.cache_read_tokens), - "cache_creation_tokens": int(row.cache_creation_tokens), - "reasoning_tokens": int(row.reasoning_tokens), - "audio_seconds": int(row.audio_seconds), - "tts_characters": int(row.tts_characters), - "call_count": int(row.call_count), - } + totals = _usage_totals_from_row(row) return UsageSummaryResponse( start=display_start, end=display_end, @@ -1032,6 +1171,8 @@ def get_usage_breakdown( func.coalesce(func.sum(LLMUsageDaily.audio_seconds), 0).label("audio_seconds"), func.coalesce(func.sum(LLMUsageDaily.tts_characters), 0).label("tts_characters"), func.coalesce(func.sum(LLMUsageDaily.call_count), 0).label("call_count"), + *_usage_cost_sum_columns(), + _has_unpriced_usage_column(), ] select_cols = [dim] @@ -1124,15 +1265,7 @@ def get_usage_breakdown( UsageBreakdownRow( workspace_id=ws_id, workspace_name=workspace_names.get(ws_id) if ws_id else "Unknown", - prompt_tokens=int(metrics[0]), - completion_tokens=int(metrics[1]), - total_tokens=int(metrics[0]) + int(metrics[1]), - cache_read_tokens=int(metrics[2]), - cache_creation_tokens=int(metrics[3]), - reasoning_tokens=int(metrics[4]), - audio_seconds=int(metrics[5]), - tts_characters=int(metrics[6]), - call_count=int(metrics[7]), + **_breakdown_metrics_from_tuple(metrics), ) ) elif group_by == "product_section": @@ -1142,15 +1275,7 @@ def get_usage_breakdown( UsageBreakdownRow( product_section=section, product_section_label=SECTION_LABELS.get(section or "", section), - prompt_tokens=int(metrics[0]), - completion_tokens=int(metrics[1]), - total_tokens=int(metrics[0]) + int(metrics[1]), - cache_read_tokens=int(metrics[2]), - cache_creation_tokens=int(metrics[3]), - reasoning_tokens=int(metrics[4]), - audio_seconds=int(metrics[5]), - tts_characters=int(metrics[6]), - call_count=int(metrics[7]), + **_breakdown_metrics_from_tuple(metrics), ) ) elif group_by == "model": @@ -1159,15 +1284,7 @@ def get_usage_breakdown( rows.append( UsageBreakdownRow( model=model_name, - prompt_tokens=int(metrics[0]), - completion_tokens=int(metrics[1]), - total_tokens=int(metrics[0]) + int(metrics[1]), - cache_read_tokens=int(metrics[2]), - cache_creation_tokens=int(metrics[3]), - reasoning_tokens=int(metrics[4]), - audio_seconds=int(metrics[5]), - tts_characters=int(metrics[6]), - call_count=int(metrics[7]), + **_breakdown_metrics_from_tuple(metrics), ) ) elif group_by == "usage_kind": @@ -1176,15 +1293,7 @@ def get_usage_breakdown( rows.append( UsageBreakdownRow( usage_kind=kind, - prompt_tokens=int(metrics[0]), - completion_tokens=int(metrics[1]), - total_tokens=int(metrics[0]) + int(metrics[1]), - cache_read_tokens=int(metrics[2]), - cache_creation_tokens=int(metrics[3]), - reasoning_tokens=int(metrics[4]), - audio_seconds=int(metrics[5]), - tts_characters=int(metrics[6]), - call_count=int(metrics[7]), + **_breakdown_metrics_from_tuple(metrics), ) ) elif group_by == "call_import": @@ -1205,15 +1314,7 @@ def get_usage_breakdown( UsageBreakdownRow( call_import_id=cid, call_import_label=label, - prompt_tokens=int(metrics[0]), - completion_tokens=int(metrics[1]), - total_tokens=int(metrics[0]) + int(metrics[1]), - cache_read_tokens=int(metrics[2]), - cache_creation_tokens=int(metrics[3]), - reasoning_tokens=int(metrics[4]), - audio_seconds=int(metrics[5]), - tts_characters=int(metrics[6]), - call_count=int(metrics[7]), + **_breakdown_metrics_from_tuple(metrics), ) ) else: @@ -1241,15 +1342,7 @@ def get_usage_breakdown( ), product_section=section, product_section_label=SECTION_LABELS.get(section or "", section), - prompt_tokens=int(metrics[0]), - completion_tokens=int(metrics[1]), - total_tokens=int(metrics[0]) + int(metrics[1]), - cache_read_tokens=int(metrics[2]), - cache_creation_tokens=int(metrics[3]), - reasoning_tokens=int(metrics[4]), - audio_seconds=int(metrics[5]), - tts_characters=int(metrics[6]), - call_count=int(metrics[7]), + **_breakdown_metrics_from_tuple(metrics), ) ) @@ -1278,6 +1371,7 @@ def get_usage_filters( resource_id: Optional[UUID] = Query(None), usage_kind: Optional[str] = Query(None), call_import_id: Optional[UUID] = Query(None), + evaluation_id: Optional[UUID] = Query(None), dataset: Optional[str] = Query(None), tag_id: Optional[UUID] = Query(None), q: Optional[str] = Query(None, description="Optional resource label search"), @@ -1288,6 +1382,8 @@ def get_usage_filters( flush_usage_to_catalog(db, organization_id) + scoped_resource_id = resource_id or evaluation_id + workspace_base = _filtered_query( db, organization_id=organization_id, @@ -1312,7 +1408,9 @@ def get_usage_filters( end=filter_end, workspace_id=workspace_id, product_section=product_section, + resource_id=scoped_resource_id, call_import_id=call_import_id, + evaluation_id=evaluation_id, dataset=dataset, tag_id=tag_id, ) @@ -1323,8 +1421,10 @@ def get_usage_filters( end=filter_end, workspace_id=workspace_id, product_section=product_section, + resource_id=scoped_resource_id, usage_kind=usage_kind, call_import_id=call_import_id, + evaluation_id=evaluation_id, dataset=dataset, tag_id=tag_id, ) diff --git a/app/api/v1/routes/usage_pricing.py b/app/api/v1/routes/usage_pricing.py new file mode 100644 index 00000000..0840a29a --- /dev/null +++ b/app/api/v1/routes/usage_pricing.py @@ -0,0 +1,241 @@ +"""Org-scoped usage pricing overrides and recompute API.""" + +from __future__ import annotations + +from datetime import date, datetime +from typing import List, Optional +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from app.core.auth.rbac import require_admin +from app.database import get_db +from app.dependencies import get_organization_id +from app.services.usage.pricing_jobs import ( + create_recompute_job, + enqueue_recompute_job, + get_recompute_job, + job_to_dict, +) +from app.services.usage.pricing_overrides import ( + delete_override, + get_effective_rate, + list_effective_pricing, + list_overrides, + upsert_override, +) + +router = APIRouter( + prefix="/organizations/usage/pricing", + tags=["Usage"], + dependencies=[Depends(require_admin)], +) + + +class PricingRatesUsd(BaseModel): + input_per_1m: Optional[float] = None + output_per_1m: Optional[float] = None + cache_read_per_1m: Optional[float] = None + cache_write_per_1m: Optional[float] = None + reasoning_per_1m: Optional[float] = None + audio_per_minute: Optional[float] = None + tts_per_1m_characters: Optional[float] = None + + +class PricingOverrideResponse(BaseModel): + id: str + organization_id: str + model: str + usage_kind: str + effective_from: date + effective_to: Optional[date] = None + rates: PricingRatesUsd + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + recompute_enqueued: Optional[bool] = None + recompute_job_id: Optional[str] = None + + +class PricingOverrideUpsertRequest(BaseModel): + usage_kind: str = "llm" + effective_from: date + effective_to: Optional[date] = None + rates: PricingRatesUsd + recompute: bool = True + + +class EffectivePricingResponse(BaseModel): + model: str + usage_kind: str + as_of: date + catalog_rates: Optional[PricingRatesUsd] = None + catalog_rate_id: Optional[str] = None + override: Optional[PricingOverrideResponse] = None + effective_rates: Optional[PricingRatesUsd] = None + effective_source: Optional[str] = None + effective_rate_id: Optional[str] = None + has_override: bool = False + + +class PricingOverrideDeleteResponse(BaseModel): + deleted: bool + model: str + usage_kind: str + recompute_enqueued: bool = False + recompute_job_id: Optional[str] = None + + +class UsageRecomputeRequest(BaseModel): + start_date: Optional[date] = None + end_date: Optional[date] = None + model: Optional[str] = None + usage_kind: Optional[str] = None + + +class UsageRecomputeJobResponse(BaseModel): + id: UUID + organization_id: UUID + status: str + model: Optional[str] = None + usage_kind: Optional[str] = None + start_date: Optional[date] = None + end_date: Optional[date] = None + updated_rows: int = 0 + error_message: Optional[str] = None + celery_task_id: Optional[str] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + + +@router.get("", response_model=List[EffectivePricingResponse]) +def list_effective_usage_pricing( + usage_kind: Optional[str] = Query(None), + model: Optional[str] = Query(None), + as_of: Optional[date] = Query(None), + limit: int = Query(200, ge=1, le=1000), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + day = as_of or date.today() + rows = list_effective_pricing( + db, + organization_id=organization_id, + usage_kind=usage_kind, + model=model, + as_of=day, + limit=limit, + ) + return rows + + +@router.get("/overrides", response_model=List[PricingOverrideResponse]) +def list_usage_pricing_overrides( + model: Optional[str] = Query(None), + usage_kind: Optional[str] = Query(None), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + return list_overrides( + db, + organization_id=organization_id, + model=model, + usage_kind=usage_kind, + ) + + +@router.get("/overrides/{model}", response_model=EffectivePricingResponse) +def get_usage_pricing_override_effective( + model: str, + usage_kind: str = Query("llm"), + as_of: Optional[date] = Query(None), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + return get_effective_rate( + db, + organization_id=organization_id, + model=model, + usage_kind=usage_kind, + as_of=as_of or date.today(), + ) + + +@router.put("/overrides/{model}", response_model=PricingOverrideResponse) +def upsert_usage_pricing_override( + model: str, + body: PricingOverrideUpsertRequest, + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + return upsert_override( + db, + organization_id=organization_id, + model=model, + usage_kind=body.usage_kind, + effective_from=body.effective_from, + effective_to=body.effective_to, + rates=body.rates.model_dump(exclude_unset=True), + recompute=body.recompute, + ) + + +@router.delete("/overrides/{model}", response_model=PricingOverrideDeleteResponse) +def delete_usage_pricing_override( + model: str, + usage_kind: str = Query("llm"), + effective_from: Optional[date] = Query(None), + recompute: bool = Query(True), + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + return delete_override( + db, + organization_id=organization_id, + model=model, + usage_kind=usage_kind, + effective_from=effective_from, + recompute=recompute, + ) + + +@router.post( + "/recompute", + response_model=UsageRecomputeJobResponse, + status_code=status.HTTP_202_ACCEPTED, +) +def trigger_usage_cost_recompute( + body: UsageRecomputeRequest, + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + if ( + body.start_date is not None + and body.end_date is not None + and body.end_date < body.start_date + ): + raise HTTPException(status_code=400, detail="end_date must be >= start_date") + + job = create_recompute_job( + db, + organization_id=organization_id, + model=body.model, + usage_kind=body.usage_kind, + start_date=body.start_date, + end_date=body.end_date, + ) + enqueue_recompute_job(db, job) + db.refresh(job) + return UsageRecomputeJobResponse(**job_to_dict(job)) + + +@router.get("/recompute/{job_id}", response_model=UsageRecomputeJobResponse) +def get_usage_cost_recompute_job( + job_id: UUID, + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + job = get_recompute_job(db, organization_id=organization_id, job_id=job_id) + return UsageRecomputeJobResponse(**job_to_dict(job)) diff --git a/app/api/v1/routes/voice_playground.py b/app/api/v1/routes/voice_playground.py index 8cc4519d..40477c25 100644 --- a/app/api/v1/routes/voice_playground.py +++ b/app/api/v1/routes/voice_playground.py @@ -398,6 +398,7 @@ def _serialize_custom_voice(voice: CustomTTSVoice) -> Dict[str, Any]: async def generate_sample_texts( data: GenerateSamplesRequest, organization_id: UUID = Depends(get_organization_id), + workspace_id: UUID = Depends(get_workspace_id), api_key: str = Depends(get_api_key), db: Session = Depends(get_db), ): @@ -460,16 +461,29 @@ async def generate_sample_texts( {"role": "user", "content": user_prompt}, ] + from app.services.usage.context import ( + LLMUsageContext, + LLMUsageProductSection, + llm_usage_context, + ) + try: - result = llm_service.generate_response( - messages=messages, - llm_provider=provider_enum, - llm_model=llm_model_str, - organization_id=organization_id, - db=db, - llm_config=request_llm_config, - task_defaults={"temperature": 0.8, "max_tokens": max_tokens}, - ) + with llm_usage_context( + LLMUsageContext( + organization_id=organization_id, + workspace_id=workspace_id, + product_section=LLMUsageProductSection.VOICE_PLAYGROUND, + ) + ): + result = llm_service.generate_response( + messages=messages, + llm_provider=provider_enum, + llm_model=llm_model_str, + organization_id=organization_id, + db=db, + llm_config=request_llm_config, + task_defaults={"temperature": 0.8, "max_tokens": max_tokens}, + ) except Exception as e: logger.error(f"[VoicePlayground] LLM generation failed: {e}") raise HTTPException(500, f"LLM generation failed: {str(e)}") diff --git a/app/cli.py b/app/cli.py index 453b2a2d..eed0274a 100644 --- a/app/cli.py +++ b/app/cli.py @@ -623,6 +623,20 @@ def _handle_signal(sig, frame): "is enabled; 32 threads can exhaust per-shard SQLAlchemy pools." ), ) +@click.option( + "--usage-worker/--no-usage-worker", + default=True, + help=( + "Also start a dedicated worker for the `usage` queue (flush + cost recompute; " + "default: True)." + ), +) +@click.option( + "--usage-worker-concurrency", + default=4, + type=int, + help="Concurrency for the usage worker (default: 4; thread pool).", +) @click.option( "--telephony-worker/--no-telephony-worker", default=True, @@ -646,6 +660,8 @@ def start_all( worker_loglevel: str, imports_worker: bool, imports_worker_concurrency: int, + usage_worker: bool, + usage_worker_concurrency: int, telephony_worker: bool, media_port: Optional[int], ): @@ -692,6 +708,7 @@ def start_all( # Store worker processes for cleanup. worker_process = None worker_imports_process = None + worker_usage_process = None beat_process = None telephony_process = None @@ -712,11 +729,12 @@ def _terminate(proc, label: str): def cleanup_processes(): """Clean up spawned processes.""" - nonlocal worker_process, worker_imports_process, beat_process, telephony_process + nonlocal worker_process, worker_imports_process, worker_usage_process, beat_process, telephony_process _terminate(telephony_process, "Telephony media server") _terminate(beat_process, "Celery beat") _terminate(worker_process, "Celery worker (default)") _terminate(worker_imports_process, "Celery worker (imports)") + _terminate(worker_usage_process, "Celery worker (usage)") # Register cleanup on exit atexit.register(cleanup_processes) @@ -870,6 +888,30 @@ def _stream_telephony(): prefix="[WORKER-IMPORTS]", ) + if usage_worker: + from app.workers.config import USAGE_WORKER_QUEUE + + worker_usage_process = _spawn_worker( + [ + "celery", + "-A", + "app.workers.celery_app", + "worker", + f"--loglevel={worker_loglevel}", + "-Q", + USAGE_WORKER_QUEUE, + "-P", + "threads", + "-c", + str(usage_worker_concurrency), + ], + label=( + f"Celery worker ({USAGE_WORKER_QUEUE} queue, " + f"pool=threads, concurrency={usage_worker_concurrency})" + ), + prefix="[WORKER-USAGE]", + ) + beat_process = _spawn_worker( [ "celery", @@ -920,6 +962,15 @@ def _stream_telephony(): ) else: click.echo(" Workers: default queue only (--no-imports-worker)") + if usage_worker: + from app.workers.config import USAGE_WORKER_QUEUE + + click.echo( + f" Usage worker: {USAGE_WORKER_QUEUE} queue " + f"(concurrency={usage_worker_concurrency})" + ) + else: + click.echo(" Usage worker: disabled (--no-usage-worker)") if telephony_worker: telephony_public = (settings.VOBIZ_WEBHOOK_BASE_URL or "").strip() click.echo(f" Telephony edge: http://localhost:{bind_media_port} (local)") @@ -1198,6 +1249,244 @@ def sharding_rebalance_slices( catalog.close() +@click.group() +def usage(): + """Usage pricing ops (seed rates, diff catalog, recompute costs).""" + pass + + +main.add_command(usage) + + +def _load_cli_config(config: str) -> Path: + config_path = Path(config) + if not config_path.exists(): + click.echo(f"❌ Config file not found: {config}", err=True) + sys.exit(1) + from app.config import load_config_from_file + + load_config_from_file(str(config_path)) + return config_path + + +@usage.command("seed-rates") +@click.option( + "--config", + "-c", + type=click.Path(exists=True, readable=True), + default="config.yml", + help="Path to configuration YAML file", +) +@click.option( + "--effective-from", + type=click.DateTime(formats=["%Y-%m-%d"]), + default=None, + help="Effective date for seeded rates (default: 2020-01-01)", +) +def usage_seed_rates(config: str, effective_from): + """Upsert model_pricing_rates from models.json pricing blocks.""" + _load_cli_config(config) + from app.database import SessionLocal + from app.services.usage.pricing_ops import seed_rates_from_models_json + + day = effective_from.date() if effective_from else None + db = SessionLocal() + try: + count = seed_rates_from_models_json(db, effective_from=day) + db.commit() + click.echo(f"✅ Seeded/updated {count} pricing rate row(s)") + finally: + db.close() + + +@usage.command("diff-rates") +@click.option( + "--config", + "-c", + type=click.Path(exists=True, readable=True), + default="config.yml", + help="Path to configuration YAML file", +) +@click.option( + "--effective-from", + type=click.DateTime(formats=["%Y-%m-%d"]), + default=None, + help="Compare rates at this effective_from date (default: 2020-01-01)", +) +@click.option("--json", "as_json", is_flag=True, help="Print machine-readable JSON") +def usage_diff_rates(config: str, effective_from, as_json: bool): + """Diff models.json pricing blocks vs model_pricing_rates in Postgres.""" + import json as json_module + + _load_cli_config(config) + from app.database import SessionLocal + from app.services.usage.pricing_ops import diff_models_json_vs_db + + day = effective_from.date() if effective_from else None + db = SessionLocal() + try: + report = diff_models_json_vs_db(db, effective_from=day) + finally: + db.close() + + if as_json: + click.echo(json_module.dumps(report, indent=2, default=str)) + return + + click.echo(f"effective_from: {report['effective_from']}") + click.echo( + f"models.json priced: {report['models_json_count']} | " + f"database rows: {report['database_count']} | " + f"in_sync: {report['in_sync']}" + ) + if report["only_in_models_json"]: + click.echo(f"\nOnly in models.json ({len(report['only_in_models_json'])}):") + for item in report["only_in_models_json"][:20]: + click.echo(f" - {item['model']} ({item['usage_kind']})") + if report["only_in_database"]: + click.echo(f"\nOnly in database ({len(report['only_in_database'])}):") + for item in report["only_in_database"][:20]: + click.echo(f" - {item['model']} ({item['usage_kind']})") + if report["mismatches"]: + click.echo(f"\nMismatched rates ({len(report['mismatches'])}):") + for item in report["mismatches"][:20]: + click.echo(f" - {item['model']} ({item['usage_kind']})") + for field, values in item["fields"].items(): + click.echo( + f" {field}: json={values['models_json']} db={values['database']}" + ) + missing = report["missing_pricing_blocks"] + if missing: + click.echo(f"\nmodels.json entries missing pricing blocks ({len(missing)}):") + for model in missing[:20]: + click.echo(f" - {model}") + unresolved = report["litellm_unresolved"] + if unresolved: + click.echo(f"\nLiteLLM unresolved ({len(unresolved)}):") + for item in unresolved[:20]: + click.echo(f" - {item.get('model')} ({item.get('reason', 'unresolved')})") + + +@usage.command("recompute") +@click.option( + "--config", + "-c", + type=click.Path(exists=True, readable=True), + default="config.yml", + help="Path to configuration YAML file", +) +@click.option("--organization-id", default=None, help="Scope recompute to one org UUID") +@click.option("--model", default=None, help="Scope recompute to one model") +@click.option("--usage-kind", default=None, help="Scope recompute to llm/stt/tts") +@click.option("--start-date", type=click.DateTime(formats=["%Y-%m-%d"]), default=None) +@click.option("--end-date", type=click.DateTime(formats=["%Y-%m-%d"]), default=None) +@click.option( + "--async/--sync", + "run_async", + default=True, + help="Enqueue Celery task (default) or run synchronously in this process", +) +def usage_recompute( + config: str, + organization_id: Optional[str], + model: Optional[str], + usage_kind: Optional[str], + start_date, + end_date, + run_async: bool, +): + """Backfill or recompute stored usage costs on llm_usage_daily rollups.""" + from uuid import UUID + + _load_cli_config(config) + start = start_date.date() if start_date else None + end = end_date.date() if end_date else None + org_uuid = UUID(organization_id) if organization_id else None + + if run_async: + if org_uuid is None: + click.echo( + "❌ --organization-id is required for async recompute (creates a tracked job).", + err=True, + ) + click.echo( + "💡 Use --sync to recompute in this process without an org scope, or pass --organization-id.", + err=True, + ) + sys.exit(1) + + from app.database import SessionLocal + from app.services.usage.pricing_jobs import ( + create_recompute_job, + enqueue_recompute_job, + job_to_dict, + ) + + db = SessionLocal() + try: + job = create_recompute_job( + db, + organization_id=org_uuid, + model=model, + usage_kind=usage_kind, + start_date=start, + end_date=end, + ) + enqueue_recompute_job(db, job) + db.refresh(job) + payload = job_to_dict(job) + click.echo(f"✅ Enqueued recompute job {payload['id']} (status={payload['status']})") + if payload.get("celery_task_id"): + click.echo(f" Celery task: {payload['celery_task_id']}") + except Exception as exc: + click.echo(f"❌ Failed to enqueue recompute job: {exc}", err=True) + sys.exit(1) + finally: + db.close() + return + + from app.database import SessionLocal + from app.services.usage.pricing import recompute_usage_costs + + db = SessionLocal() + try: + updated = recompute_usage_costs( + db, + organization_id=org_uuid, + model=model, + usage_kind=usage_kind, + start_date=start, + end_date=end, + ) + click.echo(f"✅ Recomputed costs for {updated} rollup row(s)") + finally: + db.close() + + +@usage.command("sync-litellm") +@click.option("--local", is_flag=True, help="Use bundled LiteLLM model_cost JSON") +@click.option( + "--write-models", + is_flag=True, + help="Merge generated pricing into app/config/models.json", +) +@click.option("--stdout", is_flag=True, help="Print pricing_catalog.json to stdout") +def usage_sync_litellm(local: bool, write_models: bool, stdout: bool): + """Fetch LiteLLM prices and regenerate pricing_catalog.json.""" + import subprocess + import sys as sys_module + + script = Path(__file__).resolve().parent.parent / "scripts" / "sync_pricing_catalog_from_litellm.py" + cmd = [sys_module.executable, str(script)] + if local: + cmd.append("--local") + if write_models: + cmd.append("--write-models") + if stdout: + cmd.append("--stdout") + subprocess.run(cmd, check=True) + + if __name__ == "__main__": main() diff --git a/app/config/models.json b/app/config/models.json index d727a14d..38f24cca 100644 --- a/app/config/models.json +++ b/app/config/models.json @@ -1,1029 +1,2025 @@ -{ - "whisper-1": { - "provider": "openai", - "model_type": "stt", - "description": "General-purpose speech recognition (verbose_json + word/segment timestamps)" - }, - "gpt-4o-transcribe": { - "provider": "openai", - "model_type": "stt", - "description": "GPT-4o speech-to-text; higher accuracy, no granular word timestamps" - }, - "gpt-4o-mini-transcribe": { - "provider": "openai", - "model_type": "stt", - "description": "Cheaper / faster GPT-4o transcription; no granular word timestamps" - }, - "gpt-4o-transcribe-diarize": { - "provider": "openai", - "model_type": "stt", - "description": "Transcription model that identifies who's speaking when" - }, - "gpt-realtime-whisper": { - "provider": "openai", - "model_type": "stt", - "description": "Streaming speech-to-text for realtime transcription" - }, - "gpt-5.6": { - "provider": "openai", - "model_type": "llm", - "description": "Preview frontier model for select partners; broad availability coming soon" - }, - "gpt-5.5": { - "provider": "openai", - "model_type": "llm", - "description": "OpenAI flagship — complex reasoning, agentic coding, 1M context" - }, - "gpt-5.5-pro": { - "provider": "openai", - "model_type": "llm", - "description": "Higher-accuracy GPT-5.5 variant with parallel test-time compute" - }, - "gpt-5.4": { - "provider": "openai", - "model_type": "llm", - "description": "Affordable frontier model for coding and professional work" - }, - "gpt-5.4-pro": { - "provider": "openai", - "model_type": "llm", - "description": "Higher-accuracy GPT-5.4 variant" - }, - "gpt-5.4-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Strong mini model for coding, computer use, and subagents" - }, - "gpt-5.4-nano": { - "provider": "openai", - "model_type": "llm", - "description": "Cheapest GPT-5.4-class model for simple high-volume tasks" - }, - "gpt-5.3-codex": { - "provider": "openai", - "model_type": "llm", - "description": "Most capable agentic coding model" - }, - "gpt-5.2": { - "provider": "openai", - "model_type": "llm", - "description": "Previous frontier model for professional work with configurable reasoning" - }, - "gpt-5.2-pro": { - "provider": "openai", - "model_type": "llm", - "description": "Previous pro model for professional work" - }, - "gpt-5.2-codex": { - "provider": "openai", - "model_type": "llm", - "description": "Intelligent coding model optimized for long-horizon agentic tasks (deprecated)" - }, - "gpt-5.1": { - "provider": "openai", - "model_type": "llm", - "description": "Best model for coding and agentic tasks with configurable reasoning effort" - }, - "gpt-5.1-codex": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.1 optimized for agentic coding in Codex (deprecated)" - }, - "gpt-5.1-codex-max": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.1-codex optimized for long-running tasks (deprecated)" - }, - "gpt-5.1-codex-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Smaller, cost-effective GPT-5.1-Codex variant (deprecated)" - }, - "gpt-5": { - "provider": "openai", - "model_type": "llm", - "description": "Intelligent reasoning model for coding and agentic tasks" - }, - "gpt-5-pro": { - "provider": "openai", - "model_type": "llm", - "description": "Higher-accuracy GPT-5 variant" - }, - "gpt-5-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Near-frontier intelligence for cost-sensitive, low-latency workloads" - }, - "gpt-5-nano": { - "provider": "openai", - "model_type": "llm", - "description": "Fastest, most cost-efficient GPT-5 variant" - }, - "gpt-5-codex": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5 optimized for agentic coding in Codex (deprecated)" - }, - "codex-mini-latest": { - "provider": "openai", - "model_type": "llm", - "description": "Fast reasoning model optimized for the Codex CLI (deprecated)" - }, - "o3-pro": { - "provider": "openai", - "model_type": "llm", - "description": "o3 with more compute for better responses" - }, - "o3": { - "provider": "openai", - "model_type": "llm", - "description": "Reasoning model for complex tasks (succeeded by GPT-5)" - }, - "o3-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Small o3 alternative (deprecated)" - }, - "o3-deep-research": { - "provider": "openai", - "model_type": "llm", - "description": "OpenAI flagship \u2014 complex reasoning, agentic coding, 1M context" - }, - "o4-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Fast, cost-efficient reasoning model (succeeded by GPT-5 mini)" - }, - "o4-mini-deep-research": { - "provider": "openai", - "model_type": "llm", - "description": "Faster, more affordable deep research model (deprecated)" - }, - "o1-pro": { - "provider": "openai", - "model_type": "llm", - "description": "o1 with more compute for better responses (deprecated)" - }, - "o1": { - "provider": "openai", - "model_type": "llm", - "description": "Previous full o-series reasoning model (deprecated)" - }, - "o1-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Small o1 alternative (deprecated)" - }, - "o1-preview": { - "provider": "openai", - "model_type": "llm", - "description": "Preview of the first o-series reasoning model (deprecated)" - }, - "gpt-4.1": { - "provider": "openai", - "model_type": "llm", - "description": "Smartest non-reasoning GPT-4.x model" - }, - "gpt-4.1-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Smaller, faster GPT-4.1 variant" - }, - "gpt-4.1-nano": { - "provider": "openai", - "model_type": "llm", - "description": "Fastest, most cost-efficient GPT-4.1 variant (deprecated)" - }, - "gpt-4o": { - "provider": "openai", - "model_type": "llm", - "description": "Fast, intelligent, flexible GPT model (deprecated)" - }, - "gpt-4o-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Fast, affordable small model for focused tasks (deprecated)" - }, - "gpt-4o-search-preview": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-4o with web search in Chat Completions (deprecated)" - }, - "gpt-4o-mini-search-preview": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-4o mini with web search in Chat Completions (deprecated)" - }, - "gpt-4-turbo": { - "provider": "openai", - "model_type": "llm", - "description": "Older high-intelligence GPT model (deprecated)" - }, - "gpt-4-turbo-preview": { - "provider": "openai", - "model_type": "llm", - "description": "Older fast GPT model preview (deprecated)" - }, - "gpt-4": { - "provider": "openai", - "model_type": "llm", - "description": "Older high-intelligence GPT model (deprecated)" - }, - "gpt-4.5-preview": { - "provider": "openai", - "model_type": "llm", - "description": "Deprecated large GPT preview model" - }, - "gpt-3.5-turbo": { - "provider": "openai", - "model_type": "llm", - "description": "Legacy GPT model for cheaper chat tasks (deprecated)" - }, - "computer-use-preview": { - "provider": "openai", - "model_type": "llm", - "description": "Specialized model for computer use tool (deprecated)" - }, - "gpt-4o-audio-preview": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-4o with audio input/output on Chat Completions (deprecated)" - }, - "gpt-4o-mini-audio-preview": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-4o mini with audio input/output on Chat Completions (deprecated)" - }, - "gpt-audio-1.5": { - "provider": "openai", - "model_type": "llm", - "description": "Best voice model for audio in/out with Chat Completions" - }, - "gpt-audio": { - "provider": "openai", - "model_type": "llm", - "description": "Audio inputs and outputs with Chat Completions API" - }, - "gpt-audio-mini": { - "provider": "openai", - "model_type": "llm", - "description": "Cost-efficient audio in/out with Chat Completions (deprecated)" - }, - "chat-latest": { - "provider": "openai", - "model_type": "llm", - "description": "Latest Instant model used in ChatGPT; not recommended for most API use" - }, - "gpt-5.3-chat": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.3 Instant model used in ChatGPT (deprecated)" - }, - "gpt-5.2-chat": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.2 model used in ChatGPT (deprecated)" - }, - "gpt-5.1-chat": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.1 model used in ChatGPT (deprecated)" - }, - "gpt-5-chat": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5 model used in ChatGPT (deprecated)" - }, - "chatgpt-4o-latest": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-4o model used in ChatGPT (deprecated)" - }, - "gpt-oss-120b": { - "provider": "openai", - "model_type": "llm", - "description": "Most powerful OpenAI open-weight model (Apache 2.0)" - }, - "gpt-oss-20b": { - "provider": "openai", - "model_type": "llm", - "description": "Medium-sized OpenAI open-weight model for low latency (Apache 2.0)" - }, - "gpt-5.6-sol": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.6 flagship — frontier reasoning, agentic coding, and computer use (1M context)", - "featured": true, - "featured_rank": 1, - "highlights": ["Flagship", "1M context", "Agentic coding"] - }, - "gpt-5.6-terra": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.6 balanced tier — strong everyday performance at lower cost than Sol" - }, - "gpt-5.6-luna": { - "provider": "openai", - "model_type": "llm", - "description": "GPT-5.6 fast tier — lowest-cost model for high-volume classification and extraction" - }, - "gpt-5.6": { - "provider": "openai", - "model_type": "llm", - "description": "OpenAI API alias for gpt-5.6-sol" - }, - "gpt-4o-mini-tts": { - "provider": "openai", - "model_type": "tts", - "description": "Text-to-speech powered by GPT-4o mini (deprecated)", - "featured": true, - "featured_rank": 3, - "highlights": [ - "Natural prosody", - "Multiple voices", - "Studio quality" - ] - }, - "claude-sonnet-4.5": { - "provider": "anthropic", - "model_type": "llm" - }, - "claude-opus-4.5": { - "provider": "anthropic", - "model_type": "llm" - }, - "claude-haiku-4.5": { - "provider": "anthropic", - "model_type": "llm" - }, - "claude-sonnet-4.6": { - "provider": "anthropic", - "model_type": "llm" - }, - "claude-opus-4.6": { - "provider": "anthropic", - "model_type": "llm" - }, - "claude-opus-4-7": { - "provider": "anthropic", - "model_type": "llm", - "description": "Claude Opus 4.7 \u2014 stronger coding, vision, and complex multi-step tasks" - }, - "claude-opus-4-8": { - "provider": "anthropic", - "model_type": "llm", - "description": "Claude Opus 4.8 \u2014 most capable Opus-tier model; 1M context, adaptive thinking" - }, - "claude-fable-5": { - "provider": "anthropic", - "model_type": "llm", - "description": "Claude Fable 5 \u2014 Anthropic's most capable widely released model for demanding agentic work" - }, - "claude-mythos-5": { - "provider": "anthropic", - "model_type": "llm", - "description": "Claude Mythos 5 \u2014 Fable 5 capabilities; limited availability via Project Glasswing" - }, - "claude-sonnet-5": { - "provider": "anthropic", - "model_type": "llm", - "description": "Claude Sonnet 5 \u2014 latest Sonnet tier; near-Opus quality with lower latency and cost", - "featured": true, - "featured_rank": 4, - "highlights": ["Latest Sonnet", "Agentic workflows", "Cost-efficient"] - }, - "grok-4.3": { - "provider": "xai", - "model_type": "llm", - "description": "xAI flagship \u2014 low hallucination, agentic tool calling, 1M context" - }, - "grok-build-0.1": { - "provider": "xai", - "model_type": "llm", - "description": "xAI fast coding model trained for agentic coding (early access)" - }, - "grok-4.20-0309-reasoning": { - "provider": "xai", - "model_type": "llm", - "description": "Grok 4.20 reasoning snapshot (Mar 2026)" - }, - "grok-4.20-0309-non-reasoning": { - "provider": "xai", - "model_type": "llm", - "description": "Grok 4.20 non-reasoning snapshot (Mar 2026)" - }, - "grok-4.20-multi-agent-0309": { - "provider": "xai", - "model_type": "llm", - "description": "Grok 4.20 multi-agent snapshot (Mar 2026)" - }, - "deepseek-v4-pro": { - "provider": "fireworks", - "model_type": "llm", - "description": "DeepSeek V4 Pro \u2014 frontier MoE reasoning and coding (1M context)" - }, - "deepseek-v4-flash": { - "provider": "fireworks", - "model_type": "llm", - "description": "DeepSeek V4 Flash \u2014 fast extraction, classification, and search" - }, - "kimi-k2p6": { - "provider": "fireworks", - "model_type": "llm", - "description": "Kimi K2.6 \u2014 native multimodal agentic model for long-horizon coding" - }, - "kimi-k2p5": { - "provider": "fireworks", - "model_type": "llm", - "description": "Kimi K2.5 \u2014 unified vision/text agentic model with controllable reasoning" - }, - "glm-5p1": { - "provider": "fireworks", - "model_type": "llm", - "description": "GLM 5.1 \u2014 frontier open model for reasoning and agentic workflows" - }, - "minimax-m2p7": { - "provider": "fireworks", - "model_type": "llm", - "description": "MiniMax M2.7 \u2014 MoE model for complex agent harnesses and productivity tasks" - }, - "minimax-m2p5": { - "provider": "fireworks", - "model_type": "llm", - "description": "MiniMax M2.5 \u2014 fast coding and agentic tool use at low cost" - }, - "qwen3p6-plus": { - "provider": "fireworks", - "model_type": "llm", - "description": "Qwen 3.6 Plus \u2014 flagship multimodal model (Fireworks exclusive outside Alibaba)" - }, - "gpt-oss-120b": { - "provider": "fireworks", - "model_type": "llm", - "description": "OpenAI gpt-oss-120b \u2014 high-quality open-weight model for general reasoning" - }, - "gpt-oss-20b": { - "provider": "fireworks", - "model_type": "llm", - "description": "OpenAI gpt-oss-20b \u2014 fast open-weight model for chat and classification" - }, - "firefunction-v2": { - "provider": "fireworks", - "model_type": "llm", - "description": "FireFunction V2 \u2014 Fireworks function-calling optimized model" - }, - "google-speech-v2": { - "provider": "google", - "model_type": "stt" - }, - "gemini-2.5-pro-stt": { - "provider": "google", - "model_type": "stt", - "description": "Gemini 2.5 Pro used for audio transcription via LiteLLM proxy (audio input -> text)" - }, - "gemini-2.5-flash-stt": { - "provider": "google", - "model_type": "stt", - "description": "Gemini 2.5 Flash used for audio transcription via LiteLLM proxy (fast, cost-efficient)", - "featured": true, - "featured_rank": 6, - "highlights": [ - "Fast transcription", - "Cost-efficient", - "Gemini 2.5" - ] - }, - "gemini-2.5-flash-lite-stt": { - "provider": "google", - "model_type": "stt", - "description": "Gemini 2.5 Flash Lite used for audio transcription via LiteLLM proxy (lowest cost)" - }, - "gemini-2.5-pro": { - "provider": "google", - "model_type": "llm" - }, - "gemini-2.5-flash": { - "provider": "google", - "model_type": "llm" - }, - "gemini-2.5-flash-lite": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 2.5 Flash Lite \u2014 lowest-latency 2.5 model; ideal for diarisation, classification, and other low-reasoning multimodal tasks" - }, - "gemini-3-pro-preview": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 3 Pro \u2014 advanced reasoning and multimodal understanding" - }, - "gemini-3-flash-preview": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 3 Flash \u2014 frontier-class performance at lower cost" - }, - "gemini-3.1-pro-preview": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 3.1 Pro \u2014 latest reasoning model with 1M context" - }, - "gemini-3.5-flash": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 3.5 Flash \u2014 GA flagship Flash model for agentic coding and long-horizon tasks", - "featured": true, - "featured_rank": 7, - "highlights": ["GA stable", "1M context", "Agentic coding"] - }, - "gemini-3.5-flash-lite": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 3.5 Flash Lite \u2014 fastest, lowest-cost 3.5 model for high-throughput extraction, classification, and subagent workflows" - }, - "gemini-3.1-flash-lite": { - "provider": "google", - "model_type": "llm", - "description": "Gemini 3.1 Flash Lite \u2014 cost-efficient stable model for high-volume lightweight tasks" - }, - "azure-speech-v1": { - "provider": "azure", - "model_type": "stt", - "description": "Azure Speech-to-text (batch and realtime)" - }, - "azure-openai-gpt4": { - "provider": "azure", - "model_type": "llm", - "description": "Legacy alias — maps to gpt-4 deployment; prefer azure-gpt-5-mini if that is your deployment name" - }, - "azure-gpt-4o": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-4o — catalog key maps to deployment name gpt-4o" - }, - "azure-gpt-4o-mini": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-4o mini" - }, - "azure-gpt-4.1": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-4.1" - }, - "azure-gpt-4.1-mini": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-4.1 mini" - }, - "azure-gpt-4.1-nano": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-4.1 nano" - }, - "azure-gpt-5": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-5" - }, - "azure-gpt-5-mini": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-5 mini — pick this if your deployment is named gpt-5-mini" - }, - "azure-gpt-5-nano": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-5 nano" - }, - "azure-gpt-5.1": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-5.1" - }, - "azure-gpt-5.2": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI GPT-5.2" - }, - "azure-o3": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI o3 reasoning model" - }, - "azure-o3-mini": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI o3 mini" - }, - "azure-o4-mini": { - "provider": "azure", - "model_type": "llm", - "description": "Azure OpenAI o4 mini" - }, - "azure-tts-v1": { - "provider": "azure", - "model_type": "tts", - "description": "Azure neural text-to-speech" - }, - "aws-transcribe": { - "provider": "aws", - "model_type": "stt" - }, - "aws-bedrock-claude": { - "provider": "aws", - "model_type": "llm" - }, - "aws-polly": { - "provider": "aws", - "model_type": "tts" - }, - "deepgram-flux": { - "provider": "deepgram", - "model_type": "stt", - "description": "Conversational STT with integrated turn detection for voice agents", - "featured": true, - "featured_rank": 4, - "highlights": [ - "Turn detection", - "Voice agents", - "Low latency" - ] - }, - "deepgram-nova-3": { - "provider": "deepgram", - "model_type": "stt", - "description": "Highest-performing general-purpose ASR model" - }, - "deepgram-nova-3-general": { - "provider": "deepgram", - "model_type": "stt" - }, - "deepgram-nova-3-general-preview-12-2025": { - "provider": "deepgram", - "model_type": "stt" - }, - "deepgram-nova-2": { - "provider": "deepgram", - "model_type": "stt", - "description": "Available for languages not yet supported by Nova-3" - }, - "pulse-v4": { - "provider": "smallest", - "model_type": "stt", - "description": "Smallest Pulse v4 speech-to-text (batch and realtime)" - }, - "cartesia-sonic-3": { - "provider": "cartesia", - "model_type": "tts", - "description": "Flagship streaming TTS with laughter, volume/speed/emotion controls, 42 languages", - "featured": true, - "featured_rank": 1, - "highlights": [ - "Ultra-low latency", - "42 languages", - "Emotion controls" - ] - }, - "cartesia-sonic-3-mini": { - "provider": "cartesia", - "model_type": "tts" - }, - "cartesia-sonic-3-nano": { - "provider": "cartesia", - "model_type": "tts" - }, - "eleven_v3": { - "provider": "elevenlabs", - "model_type": "tts", - "description": "Human-like and expressive speech generation", - "languages": "70+ languages" - }, - "eleven_ttv_v3": { - "provider": "elevenlabs", - "model_type": "tts", - "description": "Human-like and expressive voice design model (Text to Voice)", - "languages": "70+ languages" - }, - "eleven_multilingual_v2": { - "provider": "elevenlabs", - "model_type": "tts", - "description": "Most lifelike model with rich emotional expression", - "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru" - }, - "eleven_flash_v2_5": { - "provider": "elevenlabs", - "model_type": "tts", - "description": "Ultra-fast model optimized for real-time use (~75ms)", - "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru, hu, no, vi", - "featured": true, - "featured_rank": 2, - "highlights": [ - "~75ms latency", - "Real-time", - "Multilingual" - ] - }, - "eleven_turbo_v2_5": { - "provider": "elevenlabs", - "model_type": "tts", - "description": "High quality, low-latency model (~250ms-300ms)", - "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru, hu, no, vi" - }, - "eleven_multilingual_sts_v2": { - "provider": "elevenlabs", - "model_type": "sts", - "description": "State-of-the-art multilingual voice changer model (Speech to Speech)", - "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru" - }, - "eleven_multilingual_ttv_v2": { - "provider": "elevenlabs", - "model_type": "tts", - "description": "State-of-the-art multilingual voice designer model (Text to Voice)", - "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru" - }, - "eleven_english_sts_v2": { - "provider": "elevenlabs", - "model_type": "sts", - "description": "English-only voice changer model (Speech to Speech)", - "languages": "en" - }, - "scribe_v2_realtime": { - "provider": "elevenlabs", - "model_type": "stt", - "description": "Real-time speech recognition model", - "languages": "90+ languages", - "featured": true, - "featured_rank": 5, - "highlights": [ - "Real-time STT", - "90+ languages", - "Streaming" - ] - }, - "scribe_v2": { - "provider": "elevenlabs", - "model_type": "stt", - "description": "Most accurate transcription model with keyterm prompting and entity detection", - "languages": "90+ languages" - }, - "eleven_text_to_sound_v2": { - "provider": "elevenlabs", - "model_type": "sound_effects", - "description": "Sound effects generation from text prompts" - }, - "music_v1": { - "provider": "elevenlabs", - "model_type": "music", - "description": "Studio-grade music generation from text prompts", - "languages": "en, es, de, ja, and more" - }, - "murf-falcon": { - "provider": "murf", - "model_type": "tts", - "description": "Murf FALCON \u2013 ultra-low latency streaming TTS", - "languages": "en, es, de, fr, hi, ja, ko, pt, it, zh, and 10+ more", - "voices_source_file": "murf_falcon_voices.json" - }, - "murf-gen2": { - "provider": "murf", - "model_type": "tts", - "description": "Murf GEN2 \u2013 high-quality, natural-sounding TTS", - "languages": "en, es, de, fr, hi, ja, ko, pt, it, zh, and 10+ more", - "voices_source_file": "murf_gen2_voices.json" - }, - "voicemaker-ai3": { - "provider": "voicemaker", - "model_type": "tts", - "description": "VoiceMaker AI3 neural TTS voices", - "languages": "en-US, en-GB, en-AU, es-ES, fr-FR, de-DE, ja-JP, zh-CN", - "voices_source_file": "voicemaker_voices.json" - }, - "voicemaker-proplus": { - "provider": "voicemaker", - "model_type": "tts", - "description": "VoiceMaker ProPlus expressive multilingual voices", - "languages": "multi-lang + regional accents", - "voices_source_file": "voicemaker_voices.json" - }, - "lightning-v3.1": { - "provider": "smallest", - "model_type": "tts", - "description": "Smallest Lightning v3.1 low-latency neural TTS" - }, - "sarvam-30b": { - "provider": "sarvam", - "model_type": "llm", - "description": "Sarvam 30B \u2014 balanced Indian-language + English chat model (64K context)" - }, - "sarvam-105b": { - "provider": "sarvam", - "model_type": "llm", - "description": "Sarvam 105B \u2014 flagship MoE for complex reasoning, coding, and agentic workflows (128K context)" - }, - "saaras:v3": { - "provider": "sarvam", - "model_type": "stt", - "description": "State-of-the-art Sarvam STT model with transcribe/translate modes" - }, - "bulbul:v3": { - "provider": "sarvam", - "model_type": "tts", - "description": "High quality Indian language TTS", - "voices": [ - { - "id": "aditya", - "name": "Aditya", - "gender": "Male" - }, - { - "id": "ritu", - "name": "Ritu", - "gender": "Female" - }, - { - "id": "ashutosh", - "name": "Ashutosh", - "gender": "Male" - }, - { - "id": "priya", - "name": "Priya", - "gender": "Female" - }, - { - "id": "neha", - "name": "Neha", - "gender": "Female" - }, - { - "id": "rahul", - "name": "Rahul", - "gender": "Male" - }, - { - "id": "pooja", - "name": "Pooja", - "gender": "Female" - }, - { - "id": "rohan", - "name": "Rohan", - "gender": "Male" - }, - { - "id": "simran", - "name": "Simran", - "gender": "Female" - }, - { - "id": "kavya", - "name": "Kavya", - "gender": "Female" - }, - { - "id": "amit", - "name": "Amit", - "gender": "Male" - }, - { - "id": "dev", - "name": "Dev", - "gender": "Male" - }, - { - "id": "ishita", - "name": "Ishita", - "gender": "Female" - }, - { - "id": "shreya", - "name": "Shreya", - "gender": "Female" - }, - { - "id": "ratan", - "name": "Ratan", - "gender": "Male" - }, - { - "id": "varun", - "name": "Varun", - "gender": "Male" - }, - { - "id": "manan", - "name": "Manan", - "gender": "Male" - }, - { - "id": "sumit", - "name": "Sumit", - "gender": "Male" - }, - { - "id": "roopa", - "name": "Roopa", - "gender": "Female" - }, - { - "id": "kabir", - "name": "Kabir", - "gender": "Male" - }, - { - "id": "aayan", - "name": "Aayan", - "gender": "Male" - }, - { - "id": "shubh", - "name": "Shubh", - "gender": "Male" - }, - { - "id": "advait", - "name": "Advait", - "gender": "Male" - }, - { - "id": "amelia", - "name": "Amelia", - "gender": "Female" - }, - { - "id": "sophia", - "name": "Sophia", - "gender": "Female" - }, - { - "id": "anand", - "name": "Anand", - "gender": "Male" - }, - { - "id": "tanya", - "name": "Tanya", - "gender": "Female" - }, - { - "id": "tarun", - "name": "Tarun", - "gender": "Male" - }, - { - "id": "sunny", - "name": "Sunny", - "gender": "Male" - }, - { - "id": "mani", - "name": "Mani", - "gender": "Male" - }, - { - "id": "gokul", - "name": "Gokul", - "gender": "Male" - }, - { - "id": "vijay", - "name": "Vijay", - "gender": "Male" - }, - { - "id": "shruti", - "name": "Shruti", - "gender": "Female" - }, - { - "id": "suhani", - "name": "Suhani", - "gender": "Female" - }, - { - "id": "mohit", - "name": "Mohit", - "gender": "Male" - }, - { - "id": "kavitha", - "name": "Kavitha", - "gender": "Female" - }, - { - "id": "rehan", - "name": "Rehan", - "gender": "Male" - }, - { - "id": "soham", - "name": "Soham", - "gender": "Male" - }, - { - "id": "rupali", - "name": "Rupali", - "gender": "Female" - } - ] - } -} +{ + "whisper-1": { + "provider": "openai", + "model_type": "stt", + "description": "General-purpose speech recognition (verbose_json + word/segment timestamps)", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.006 + } + }, + "gpt-4o-transcribe": { + "provider": "openai", + "model_type": "stt", + "description": "GPT-4o speech-to-text; higher accuracy, no granular word timestamps", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00372 + } + }, + "gpt-4o-mini-transcribe": { + "provider": "openai", + "model_type": "stt", + "description": "Cheaper / faster GPT-4o transcription; no granular word timestamps", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00186 + } + }, + "gpt-4o-transcribe-diarize": { + "provider": "openai", + "model_type": "stt", + "description": "Transcription model that identifies who's speaking when", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00372 + } + }, + "gpt-realtime-whisper": { + "provider": "openai", + "model_type": "stt", + "description": "Streaming speech-to-text for realtime transcription", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.01698 + } + }, + "gpt-5.6": { + "provider": "openai", + "model_type": "llm", + "description": "OpenAI API alias for gpt-5.6-sol", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 30.0, + "cache_read_per_1m": 0.5, + "cache_write_per_1m": 6.25 + } + }, + "gpt-5.5": { + "provider": "openai", + "model_type": "llm", + "description": "OpenAI flagship — complex reasoning, agentic coding, 1M context", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 30.0, + "cache_read_per_1m": 0.5 + } + }, + "gpt-5.5-pro": { + "provider": "openai", + "model_type": "llm", + "description": "Higher-accuracy GPT-5.5 variant with parallel test-time compute", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 30.0, + "output_per_1m": 180.0, + "cache_read_per_1m": 3.0 + } + }, + "gpt-5.4": { + "provider": "openai", + "model_type": "llm", + "description": "Affordable frontier model for coding and professional work", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 15.0, + "cache_read_per_1m": 0.25 + } + }, + "gpt-5.4-pro": { + "provider": "openai", + "model_type": "llm", + "description": "Higher-accuracy GPT-5.4 variant", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 30.0, + "output_per_1m": 180.0, + "cache_read_per_1m": 3.0 + } + }, + "gpt-5.4-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Strong mini model for coding, computer use, and subagents", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.75, + "output_per_1m": 4.5, + "cache_read_per_1m": 0.075 + } + }, + "gpt-5.4-nano": { + "provider": "openai", + "model_type": "llm", + "description": "Cheapest GPT-5.4-class model for simple high-volume tasks", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.2, + "output_per_1m": 1.25, + "cache_read_per_1m": 0.02 + } + }, + "gpt-5.3-codex": { + "provider": "openai", + "model_type": "llm", + "description": "Most capable agentic coding model", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.75, + "output_per_1m": 14.0, + "cache_read_per_1m": 0.175 + } + }, + "gpt-5.2": { + "provider": "openai", + "model_type": "llm", + "description": "Previous frontier model for professional work with configurable reasoning", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.75, + "output_per_1m": 14.0, + "cache_read_per_1m": 0.175 + } + }, + "gpt-5.2-pro": { + "provider": "openai", + "model_type": "llm", + "description": "Previous pro model for professional work", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 21.0, + "output_per_1m": 168.0 + } + }, + "gpt-5.2-codex": { + "provider": "openai", + "model_type": "llm", + "description": "Intelligent coding model optimized for long-horizon agentic tasks (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.75, + "output_per_1m": 14.0, + "cache_read_per_1m": 0.175 + } + }, + "gpt-5.1": { + "provider": "openai", + "model_type": "llm", + "description": "Best model for coding and agentic tasks with configurable reasoning effort", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "gpt-5.1-codex": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.1 optimized for agentic coding in Codex (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "gpt-5.1-codex-max": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.1-codex optimized for long-running tasks (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "gpt-5.1-codex-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Smaller, cost-effective GPT-5.1-Codex variant (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.25, + "output_per_1m": 2.0, + "cache_read_per_1m": 0.025 + } + }, + "gpt-5": { + "provider": "openai", + "model_type": "llm", + "description": "Intelligent reasoning model for coding and agentic tasks", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "gpt-5-pro": { + "provider": "openai", + "model_type": "llm", + "description": "Higher-accuracy GPT-5 variant", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 15.0, + "output_per_1m": 120.0 + } + }, + "gpt-5-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Near-frontier intelligence for cost-sensitive, low-latency workloads", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.25, + "output_per_1m": 2.0, + "cache_read_per_1m": 0.025 + } + }, + "gpt-5-nano": { + "provider": "openai", + "model_type": "llm", + "description": "Fastest, most cost-efficient GPT-5 variant", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.05, + "output_per_1m": 0.4, + "cache_read_per_1m": 0.005 + } + }, + "gpt-5-codex": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5 optimized for agentic coding in Codex (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "codex-mini-latest": { + "provider": "openai", + "model_type": "llm", + "description": "Fast reasoning model optimized for the Codex CLI (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.5, + "output_per_1m": 6.0, + "cache_read_per_1m": 0.375 + } + }, + "o3-pro": { + "provider": "openai", + "model_type": "llm", + "description": "o3 with more compute for better responses", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 20.0, + "output_per_1m": 80.0 + } + }, + "o3": { + "provider": "openai", + "model_type": "llm", + "description": "Reasoning model for complex tasks (succeeded by GPT-5)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 8.0, + "cache_read_per_1m": 0.5 + } + }, + "o3-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Small o3 alternative (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.1, + "output_per_1m": 4.4, + "cache_read_per_1m": 0.55 + } + }, + "o3-deep-research": { + "provider": "openai", + "model_type": "llm", + "description": "OpenAI flagship — complex reasoning, agentic coding, 1M context", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 10.0, + "output_per_1m": 40.0, + "cache_read_per_1m": 2.5 + } + }, + "o4-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Fast, cost-efficient reasoning model (succeeded by GPT-5 mini)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.1, + "output_per_1m": 4.4, + "cache_read_per_1m": 0.275 + } + }, + "o4-mini-deep-research": { + "provider": "openai", + "model_type": "llm", + "description": "Faster, more affordable deep research model (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 8.0, + "cache_read_per_1m": 0.5 + } + }, + "o1-pro": { + "provider": "openai", + "model_type": "llm", + "description": "o1 with more compute for better responses (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 150.0, + "output_per_1m": 600.0 + } + }, + "o1": { + "provider": "openai", + "model_type": "llm", + "description": "Previous full o-series reasoning model (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 15.0, + "output_per_1m": 60.0, + "cache_read_per_1m": 7.5 + } + }, + "o1-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Small o1 alternative (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.21, + "output_per_1m": 4.84, + "cache_read_per_1m": 0.605 + } + }, + "o1-preview": { + "provider": "openai", + "model_type": "llm", + "description": "Preview of the first o-series reasoning model (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 15.0, + "output_per_1m": 60.0, + "cache_read_per_1m": 7.5 + } + }, + "gpt-4.1": { + "provider": "openai", + "model_type": "llm", + "description": "Smartest non-reasoning GPT-4.x model", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 8.0, + "cache_read_per_1m": 0.5 + } + }, + "gpt-4.1-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Smaller, faster GPT-4.1 variant", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.4, + "output_per_1m": 1.6, + "cache_read_per_1m": 0.1 + } + }, + "gpt-4.1-nano": { + "provider": "openai", + "model_type": "llm", + "description": "Fastest, most cost-efficient GPT-4.1 variant (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.1, + "output_per_1m": 0.4, + "cache_read_per_1m": 0.025 + } + }, + "gpt-4o": { + "provider": "openai", + "model_type": "llm", + "description": "Fast, intelligent, flexible GPT model (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 10.0, + "cache_read_per_1m": 1.25 + } + }, + "gpt-4o-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Fast, affordable small model for focused tasks (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.15, + "output_per_1m": 0.6, + "cache_read_per_1m": 0.075 + } + }, + "gpt-4o-search-preview": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-4o with web search in Chat Completions (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 10.0, + "cache_read_per_1m": 1.25 + } + }, + "gpt-4o-mini-search-preview": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-4o mini with web search in Chat Completions (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.15, + "output_per_1m": 0.6, + "cache_read_per_1m": 0.075 + } + }, + "gpt-4-turbo": { + "provider": "openai", + "model_type": "llm", + "description": "Older high-intelligence GPT model (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 10.0, + "output_per_1m": 30.0 + } + }, + "gpt-4-turbo-preview": { + "provider": "openai", + "model_type": "llm", + "description": "Older fast GPT model preview (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 10.0, + "output_per_1m": 30.0 + } + }, + "gpt-4": { + "provider": "openai", + "model_type": "llm", + "description": "Older high-intelligence GPT model (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 30.0, + "output_per_1m": 60.0 + } + }, + "gpt-4.5-preview": { + "provider": "openai", + "model_type": "llm", + "description": "Deprecated large GPT preview model", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 75.0, + "output_per_1m": 150.0, + "cache_read_per_1m": 37.5 + } + }, + "gpt-3.5-turbo": { + "provider": "openai", + "model_type": "llm", + "description": "Legacy GPT model for cheaper chat tasks (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.5, + "output_per_1m": 1.5 + } + }, + "computer-use-preview": { + "provider": "openai", + "model_type": "llm", + "description": "Specialized model for computer use tool (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 3.0, + "output_per_1m": 12.0 + } + }, + "gpt-4o-audio-preview": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-4o with audio input/output on Chat Completions (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 10.0 + } + }, + "gpt-4o-mini-audio-preview": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-4o mini with audio input/output on Chat Completions (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.15, + "output_per_1m": 0.6 + } + }, + "gpt-audio-1.5": { + "provider": "openai", + "model_type": "llm", + "description": "Best voice model for audio in/out with Chat Completions", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 10.0 + } + }, + "gpt-audio": { + "provider": "openai", + "model_type": "llm", + "description": "Audio inputs and outputs with Chat Completions API", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 10.0 + } + }, + "gpt-audio-mini": { + "provider": "openai", + "model_type": "llm", + "description": "Cost-efficient audio in/out with Chat Completions (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.6, + "output_per_1m": 2.4 + } + }, + "chat-latest": { + "provider": "openai", + "model_type": "llm", + "description": "Latest Instant model used in ChatGPT; not recommended for most API use", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "gpt-5.3-chat": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.3 Instant model used in ChatGPT (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.75, + "output_per_1m": 14.0, + "cache_read_per_1m": 0.175 + } + }, + "gpt-5.2-chat": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.2 model used in ChatGPT (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.75, + "output_per_1m": 14.0, + "cache_read_per_1m": 0.175 + } + }, + "gpt-5.1-chat": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.1 model used in ChatGPT (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.38, + "output_per_1m": 11.0, + "cache_read_per_1m": 0.14 + } + }, + "gpt-5-chat": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5 model used in ChatGPT (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "chatgpt-4o-latest": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-4o model used in ChatGPT (deprecated)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 15.0 + } + }, + "gpt-oss-120b": { + "provider": "fireworks", + "model_type": "llm", + "description": "OpenAI gpt-oss-120b — high-quality open-weight model for general reasoning", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.15, + "output_per_1m": 0.6, + "cache_read_per_1m": 0.015 + } + }, + "gpt-oss-20b": { + "provider": "fireworks", + "model_type": "llm", + "description": "OpenAI gpt-oss-20b — fast open-weight model for chat and classification", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.07, + "output_per_1m": 0.3, + "cache_read_per_1m": 0.035 + } + }, + "gpt-5.6-sol": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.6 flagship — frontier reasoning, agentic coding, and computer use (1M context)", + "featured": true, + "featured_rank": 1, + "highlights": [ + "Flagship", + "1M context", + "Agentic coding" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 30.0, + "cache_read_per_1m": 0.5, + "cache_write_per_1m": 6.25 + } + }, + "gpt-5.6-terra": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.6 balanced tier — strong everyday performance at lower cost than Sol", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 12.0, + "cache_read_per_1m": 0.2, + "cache_write_per_1m": 2.5 + } + }, + "gpt-5.6-luna": { + "provider": "openai", + "model_type": "llm", + "description": "GPT-5.6 fast tier — lowest-cost model for high-volume classification and extraction", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.2, + "output_per_1m": 1.2, + "cache_read_per_1m": 0.02, + "cache_write_per_1m": 0.25 + } + }, + "gpt-4o-mini-tts": { + "provider": "openai", + "model_type": "tts", + "description": "Text-to-speech powered by GPT-4o mini (deprecated)", + "featured": true, + "featured_rank": 3, + "highlights": [ + "Natural prosody", + "Multiple voices", + "Studio quality" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 2.5 + } + }, + "claude-sonnet-4.5": { + "provider": "anthropic", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 3.0, + "output_per_1m": 15.0 + } + }, + "claude-opus-4.5": { + "provider": "anthropic", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 25.0 + } + }, + "claude-haiku-4.5": { + "provider": "anthropic", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.0, + "output_per_1m": 5.0, + "cache_read_per_1m": 0.1, + "cache_write_per_1m": 1.25 + } + }, + "claude-sonnet-4.6": { + "provider": "anthropic", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 3.0, + "output_per_1m": 15.0, + "cache_read_per_1m": 0.3, + "cache_write_per_1m": 3.75 + } + }, + "claude-opus-4.6": { + "provider": "anthropic", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 25.0, + "cache_read_per_1m": 0.5, + "cache_write_per_1m": 6.25 + } + }, + "claude-opus-4-7": { + "provider": "anthropic", + "model_type": "llm", + "description": "Claude Opus 4.7 — stronger coding, vision, and complex multi-step tasks", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 25.0, + "cache_read_per_1m": 0.5, + "cache_write_per_1m": 6.25 + } + }, + "claude-opus-4-8": { + "provider": "anthropic", + "model_type": "llm", + "description": "Claude Opus 4.8 — most capable Opus-tier model; 1M context, adaptive thinking", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 5.0, + "output_per_1m": 25.0, + "cache_read_per_1m": 0.5, + "cache_write_per_1m": 6.25 + } + }, + "claude-fable-5": { + "provider": "anthropic", + "model_type": "llm", + "description": "Claude Fable 5 — Anthropic's most capable widely released model for demanding agentic work", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 10.0, + "output_per_1m": 50.0, + "cache_read_per_1m": 1.0, + "cache_write_per_1m": 12.5 + } + }, + "claude-mythos-5": { + "provider": "anthropic", + "model_type": "llm", + "description": "Claude Mythos 5 — Fable 5 capabilities; limited availability via Project Glasswing", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 10.0, + "output_per_1m": 50.0, + "cache_read_per_1m": 1.0, + "cache_write_per_1m": 12.5 + } + }, + "claude-sonnet-5": { + "provider": "anthropic", + "model_type": "llm", + "description": "Claude Sonnet 5 — latest Sonnet tier; near-Opus quality with lower latency and cost", + "featured": true, + "featured_rank": 4, + "highlights": [ + "Latest Sonnet", + "Agentic workflows", + "Cost-efficient" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.2, + "cache_write_per_1m": 2.5 + } + }, + "grok-4.3": { + "provider": "xai", + "model_type": "llm", + "description": "xAI flagship — low hallucination, agentic tool calling, 1M context", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 2.5, + "cache_read_per_1m": 0.2 + } + }, + "grok-build-0.1": { + "provider": "xai", + "model_type": "llm", + "description": "xAI fast coding model trained for agentic coding (early access)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 3.0, + "output_per_1m": 15.0, + "cache_read_per_1m": 0.75 + } + }, + "grok-4.20-0309-reasoning": { + "provider": "xai", + "model_type": "llm", + "description": "Grok 4.20 reasoning snapshot (Mar 2026)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 2.5, + "cache_read_per_1m": 0.2 + } + }, + "grok-4.20-0309-non-reasoning": { + "provider": "xai", + "model_type": "llm", + "description": "Grok 4.20 non-reasoning snapshot (Mar 2026)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.2, + "output_per_1m": 0.5, + "cache_read_per_1m": 0.05 + } + }, + "grok-4.20-multi-agent-0309": { + "provider": "xai", + "model_type": "llm", + "description": "Grok 4.20 multi-agent snapshot (Mar 2026)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 3.0, + "output_per_1m": 15.0 + } + }, + "deepseek-v4-pro": { + "provider": "fireworks", + "model_type": "llm", + "description": "DeepSeek V4 Pro — frontier MoE reasoning and coding (1M context)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.74, + "output_per_1m": 3.48, + "cache_read_per_1m": 0.145 + } + }, + "deepseek-v4-flash": { + "provider": "fireworks", + "model_type": "llm", + "description": "DeepSeek V4 Flash — fast extraction, classification, and search", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.14, + "output_per_1m": 0.28, + "cache_read_per_1m": 0.028 + } + }, + "kimi-k2p6": { + "provider": "fireworks", + "model_type": "llm", + "description": "Kimi K2.6 — native multimodal agentic model for long-horizon coding", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.95, + "output_per_1m": 4.0, + "cache_read_per_1m": 0.16 + } + }, + "kimi-k2p5": { + "provider": "fireworks", + "model_type": "llm", + "description": "Kimi K2.5 — unified vision/text agentic model with controllable reasoning", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.6, + "output_per_1m": 3.0, + "cache_read_per_1m": 0.1 + } + }, + "glm-5p1": { + "provider": "fireworks", + "model_type": "llm", + "description": "GLM 5.1 — frontier open model for reasoning and agentic workflows", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.4, + "output_per_1m": 4.4, + "cache_read_per_1m": 0.26 + } + }, + "minimax-m2p7": { + "provider": "fireworks", + "model_type": "llm", + "description": "MiniMax M2.7 — MoE model for complex agent harnesses and productivity tasks", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.3, + "output_per_1m": 1.2, + "cache_read_per_1m": 0.06 + } + }, + "minimax-m2p5": { + "provider": "fireworks", + "model_type": "llm", + "description": "MiniMax M2.5 — fast coding and agentic tool use at low cost", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.3, + "output_per_1m": 1.2, + "cache_read_per_1m": 0.06 + } + }, + "qwen3p6-plus": { + "provider": "fireworks", + "model_type": "llm", + "description": "Qwen 3.6 Plus — flagship multimodal model (Fireworks exclusive outside Alibaba)", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.325, + "output_per_1m": 1.95 + } + }, + "firefunction-v2": { + "provider": "fireworks", + "model_type": "llm", + "description": "FireFunction V2 — Fireworks function-calling optimized model", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.9, + "output_per_1m": 0.9 + } + }, + "google-speech-v2": { + "provider": "google", + "model_type": "stt", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.0015 + } + }, + "gemini-2.5-pro-stt": { + "provider": "google", + "model_type": "stt", + "description": "Gemini 2.5 Pro used for audio transcription via LiteLLM proxy (audio input -> text)", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.0015 + } + }, + "gemini-2.5-flash-stt": { + "provider": "google", + "model_type": "stt", + "description": "Gemini 2.5 Flash used for audio transcription via LiteLLM proxy (fast, cost-efficient)", + "featured": true, + "featured_rank": 6, + "highlights": [ + "Fast transcription", + "Cost-efficient", + "Gemini 2.5" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.0015 + } + }, + "gemini-2.5-flash-lite-stt": { + "provider": "google", + "model_type": "stt", + "description": "Gemini 2.5 Flash Lite used for audio transcription via LiteLLM proxy (lowest cost)", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00042 + } + }, + "gemini-2.5-pro": { + "provider": "google", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "gemini-2.5-flash": { + "provider": "google", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.3, + "output_per_1m": 2.5, + "cache_read_per_1m": 0.03, + "reasoning_per_1m": 2.5 + } + }, + "gemini-2.5-flash-lite": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 2.5 Flash Lite — lowest-latency 2.5 model; ideal for diarisation, classification, and other low-reasoning multimodal tasks", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.1, + "output_per_1m": 0.4, + "cache_read_per_1m": 0.01, + "reasoning_per_1m": 0.4 + } + }, + "gemini-3-pro-preview": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 3 Pro — advanced reasoning and multimodal understanding", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 12.0, + "cache_read_per_1m": 0.2 + } + }, + "gemini-3-flash-preview": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 3 Flash — frontier-class performance at lower cost", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.5, + "output_per_1m": 3.0, + "cache_read_per_1m": 0.05, + "reasoning_per_1m": 3.0 + } + }, + "gemini-3.1-pro-preview": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 3.1 Pro — latest reasoning model with 1M context", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 12.0, + "cache_read_per_1m": 0.2 + } + }, + "gemini-3.5-flash": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 3.5 Flash — GA flagship Flash model for agentic coding and long-horizon tasks", + "featured": true, + "featured_rank": 7, + "highlights": [ + "GA stable", + "1M context", + "Agentic coding" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.5, + "output_per_1m": 9.0, + "cache_read_per_1m": 0.15, + "reasoning_per_1m": 9.0 + } + }, + "gemini-3.5-flash-lite": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 3.5 Flash Lite — fastest, lowest-cost 3.5 model for high-throughput extraction, classification, and subagent workflows", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.3, + "output_per_1m": 2.5, + "cache_read_per_1m": 0.03, + "reasoning_per_1m": 2.5 + } + }, + "gemini-3.1-flash-lite": { + "provider": "google", + "model_type": "llm", + "description": "Gemini 3.1 Flash Lite — cost-efficient stable model for high-volume lightweight tasks", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.25, + "output_per_1m": 1.5, + "cache_read_per_1m": 0.025, + "reasoning_per_1m": 1.5 + } + }, + "azure-speech-v1": { + "provider": "azure", + "model_type": "stt", + "description": "Azure Speech-to-text (batch and realtime)", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.01668 + } + }, + "azure-openai-gpt4": { + "provider": "azure", + "model_type": "llm", + "description": "Legacy alias — maps to gpt-4 deployment; prefer azure-gpt-5-mini if that is your deployment name", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 30.0, + "output_per_1m": 60.0 + } + }, + "azure-gpt-4o": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-4o — catalog key maps to deployment name gpt-4o", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.5, + "output_per_1m": 10.0, + "cache_read_per_1m": 1.25 + } + }, + "azure-gpt-4o-mini": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-4o mini", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.165, + "output_per_1m": 0.66, + "cache_read_per_1m": 0.075 + } + }, + "azure-gpt-4.1": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-4.1", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 8.0, + "cache_read_per_1m": 0.5 + } + }, + "azure-gpt-4.1-mini": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-4.1 mini", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.4, + "output_per_1m": 1.6, + "cache_read_per_1m": 0.1 + } + }, + "azure-gpt-4.1-nano": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-4.1 nano", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.1, + "output_per_1m": 0.4, + "cache_read_per_1m": 0.025 + } + }, + "azure-gpt-5": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-5", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "azure-gpt-5-mini": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-5 mini — pick this if your deployment is named gpt-5-mini", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.25, + "output_per_1m": 2.0, + "cache_read_per_1m": 0.025 + } + }, + "azure-gpt-5-nano": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-5 nano", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 0.05, + "output_per_1m": 0.4, + "cache_read_per_1m": 0.005 + } + }, + "azure-gpt-5.1": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-5.1", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.25, + "output_per_1m": 10.0, + "cache_read_per_1m": 0.125 + } + }, + "azure-gpt-5.2": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI GPT-5.2", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.75, + "output_per_1m": 14.0, + "cache_read_per_1m": 0.175 + } + }, + "azure-o3": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI o3 reasoning model", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 2.0, + "output_per_1m": 8.0, + "cache_read_per_1m": 0.5 + } + }, + "azure-o3-mini": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI o3 mini", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.1, + "output_per_1m": 4.4, + "cache_read_per_1m": 0.55 + } + }, + "azure-o4-mini": { + "provider": "azure", + "model_type": "llm", + "description": "Azure OpenAI o4 mini", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 1.1, + "output_per_1m": 4.4, + "cache_read_per_1m": 0.275 + } + }, + "azure-tts-v1": { + "provider": "azure", + "model_type": "tts", + "description": "Azure neural text-to-speech", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 15.0 + } + }, + "aws-transcribe": { + "provider": "aws", + "model_type": "stt", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.006 + } + }, + "aws-bedrock-claude": { + "provider": "aws", + "model_type": "llm", + "pricing": { + "source": "litellm_import", + "usage_kind": "llm", + "input_per_1m": 3.0, + "output_per_1m": 15.0, + "cache_read_per_1m": 0.3, + "cache_write_per_1m": 3.75 + } + }, + "aws-polly": { + "provider": "aws", + "model_type": "tts", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 16.0 + } + }, + "deepgram-flux": { + "provider": "deepgram", + "model_type": "stt", + "description": "Conversational STT with integrated turn detection for voice agents", + "featured": true, + "featured_rank": 4, + "highlights": [ + "Turn detection", + "Voice agents", + "Low latency" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00432 + } + }, + "deepgram-nova-3": { + "provider": "deepgram", + "model_type": "stt", + "description": "Highest-performing general-purpose ASR model", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00432 + } + }, + "deepgram-nova-3-general": { + "provider": "deepgram", + "model_type": "stt", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00432 + } + }, + "deepgram-nova-3-general-preview-12-2025": { + "provider": "deepgram", + "model_type": "stt", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00432 + } + }, + "deepgram-nova-2": { + "provider": "deepgram", + "model_type": "stt", + "description": "Available for languages not yet supported by Nova-3", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00432 + } + }, + "pulse-v4": { + "provider": "smallest", + "model_type": "stt", + "description": "Smallest Pulse v4 speech-to-text (batch and realtime)", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.006 + } + }, + "cartesia-sonic-3": { + "provider": "cartesia", + "model_type": "tts", + "description": "Flagship streaming TTS with laughter, volume/speed/emotion controls, 42 languages", + "featured": true, + "featured_rank": 1, + "highlights": [ + "Ultra-low latency", + "42 languages", + "Emotion controls" + ], + "pricing": { + "source": "cartesia.ai credits ~$50/1M chars (Pro tier, 1 credit/char)", + "usage_kind": "tts", + "tts_per_1m_characters": 50.0 + } + }, + "cartesia-sonic-3-mini": { + "provider": "cartesia", + "model_type": "tts", + "pricing": { + "source": "cartesia.ai ~$40/1M chars (mid-tier estimate)", + "usage_kind": "tts", + "tts_per_1m_characters": 40.0 + } + }, + "cartesia-sonic-3-nano": { + "provider": "cartesia", + "model_type": "tts", + "pricing": { + "source": "cartesia.ai Scale ~$37/1M chars", + "usage_kind": "tts", + "tts_per_1m_characters": 37.0 + } + }, + "eleven_v3": { + "provider": "elevenlabs", + "model_type": "tts", + "description": "Human-like and expressive speech generation", + "languages": "70+ languages", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_ttv_v3": { + "provider": "elevenlabs", + "model_type": "tts", + "description": "Human-like and expressive voice design model (Text to Voice)", + "languages": "70+ languages", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_multilingual_v2": { + "provider": "elevenlabs", + "model_type": "tts", + "description": "Most lifelike model with rich emotional expression", + "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_flash_v2_5": { + "provider": "elevenlabs", + "model_type": "tts", + "description": "Ultra-fast model optimized for real-time use (~75ms)", + "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru, hu, no, vi", + "featured": true, + "featured_rank": 2, + "highlights": [ + "~75ms latency", + "Real-time", + "Multilingual" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_turbo_v2_5": { + "provider": "elevenlabs", + "model_type": "tts", + "description": "High quality, low-latency model (~250ms-300ms)", + "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru, hu, no, vi", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_multilingual_sts_v2": { + "provider": "elevenlabs", + "model_type": "sts", + "description": "State-of-the-art multilingual voice changer model (Speech to Speech)", + "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_multilingual_ttv_v2": { + "provider": "elevenlabs", + "model_type": "tts", + "description": "State-of-the-art multilingual voice designer model (Text to Voice)", + "languages": "en, ja, zh, de, hi, fr, ko, pt, it, es, id, nl, tr, fil, pl, sv, bg, ro, ar, cs, el, fi, hr, ms, sk, da, ta, uk, ru", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "eleven_english_sts_v2": { + "provider": "elevenlabs", + "model_type": "sts", + "description": "English-only voice changer model (Speech to Speech)", + "languages": "en", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "scribe_v2_realtime": { + "provider": "elevenlabs", + "model_type": "stt", + "description": "Real-time speech recognition model", + "languages": "90+ languages", + "featured": true, + "featured_rank": 5, + "highlights": [ + "Real-time STT", + "90+ languages", + "Streaming" + ], + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00366 + } + }, + "scribe_v2": { + "provider": "elevenlabs", + "model_type": "stt", + "description": "Most accurate transcription model with keyterm prompting and entity detection", + "languages": "90+ languages", + "pricing": { + "source": "litellm_import", + "usage_kind": "stt", + "audio_per_minute": 0.00366 + } + }, + "eleven_text_to_sound_v2": { + "provider": "elevenlabs", + "model_type": "sound_effects", + "description": "Sound effects generation from text prompts", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "music_v1": { + "provider": "elevenlabs", + "model_type": "music", + "description": "Studio-grade music generation from text prompts", + "languages": "en, es, de, ja, and more", + "pricing": { + "source": "litellm_import", + "usage_kind": "tts", + "tts_per_1m_characters": 180.0 + } + }, + "murf-falcon": { + "provider": "murf", + "model_type": "tts", + "description": "Murf FALCON – ultra-low latency streaming TTS", + "languages": "en, es, de, fr, hi, ja, ko, pt, it, zh, and 10+ more", + "voices_source_file": "murf_falcon_voices.json", + "pricing": { + "source": "murf.ai API $0.01/1K chars", + "usage_kind": "tts", + "tts_per_1m_characters": 10.0 + } + }, + "murf-gen2": { + "provider": "murf", + "model_type": "tts", + "description": "Murf GEN2 – high-quality, natural-sounding TTS", + "languages": "en, es, de, fr, hi, ja, ko, pt, it, zh, and 10+ more", + "voices_source_file": "murf_gen2_voices.json", + "pricing": { + "source": "murf.ai API $0.03/1K chars", + "usage_kind": "tts", + "tts_per_1m_characters": 30.0 + } + }, + "voicemaker-ai3": { + "provider": "voicemaker", + "model_type": "tts", + "description": "VoiceMaker AI3 neural TTS voices", + "languages": "en-US, en-GB, en-AU, es-ES, fr-FR, de-DE, ja-JP, zh-CN", + "voices_source_file": "voicemaker_voices.json", + "pricing": { + "source": "developer.voicemaker.in $25/1M chars × 1× (AI3)", + "usage_kind": "tts", + "tts_per_1m_characters": 25.0 + } + }, + "voicemaker-proplus": { + "provider": "voicemaker", + "model_type": "tts", + "description": "VoiceMaker ProPlus expressive multilingual voices", + "languages": "multi-lang + regional accents", + "voices_source_file": "voicemaker_voices.json", + "pricing": { + "source": "developer.voicemaker.in $25/1M chars × 2× (ProPlus Turbo)", + "usage_kind": "tts", + "tts_per_1m_characters": 50.0 + } + }, + "lightning-v3.1": { + "provider": "smallest", + "model_type": "tts", + "description": "Smallest Lightning v3.1 low-latency neural TTS", + "pricing": { + "source": "smallest.ai $0.175/10K chars", + "usage_kind": "tts", + "tts_per_1m_characters": 17.5 + } + }, + "sarvam-30b": { + "provider": "sarvam", + "model_type": "llm", + "description": "Sarvam 30B — balanced Indian-language + English chat model (64K context)", + "pricing": { + "source": "sarvam.ai ₹2.5 / ₹1.5 / ₹10 per 1M tokens @ 83 INR/USD", + "usage_kind": "llm", + "input_per_1m": 0.03012, + "output_per_1m": 0.120482, + "cache_read_per_1m": 0.018072 + } + }, + "sarvam-105b": { + "provider": "sarvam", + "model_type": "llm", + "description": "Sarvam 105B — flagship MoE for complex reasoning, coding, and agentic workflows (128K context)", + "pricing": { + "source": "sarvam.ai ₹4 / ₹2.5 / ₹16 per 1M tokens @ 83 INR/USD", + "usage_kind": "llm", + "input_per_1m": 0.048193, + "output_per_1m": 0.192771, + "cache_read_per_1m": 0.03012 + } + }, + "saaras:v3": { + "provider": "sarvam", + "model_type": "stt", + "description": "State-of-the-art Sarvam STT model with transcribe/translate modes", + "pricing": { + "source": "sarvam.ai ₹30/hour audio @ 83 INR/USD", + "usage_kind": "stt", + "audio_per_minute": 0.006 + } + }, + "bulbul:v3": { + "provider": "sarvam", + "model_type": "tts", + "description": "High quality Indian language TTS", + "voices": [ + { + "id": "aditya", + "name": "Aditya", + "gender": "Male" + }, + { + "id": "ritu", + "name": "Ritu", + "gender": "Female" + }, + { + "id": "ashutosh", + "name": "Ashutosh", + "gender": "Male" + }, + { + "id": "priya", + "name": "Priya", + "gender": "Female" + }, + { + "id": "neha", + "name": "Neha", + "gender": "Female" + }, + { + "id": "rahul", + "name": "Rahul", + "gender": "Male" + }, + { + "id": "pooja", + "name": "Pooja", + "gender": "Female" + }, + { + "id": "rohan", + "name": "Rohan", + "gender": "Male" + }, + { + "id": "simran", + "name": "Simran", + "gender": "Female" + }, + { + "id": "kavya", + "name": "Kavya", + "gender": "Female" + }, + { + "id": "amit", + "name": "Amit", + "gender": "Male" + }, + { + "id": "dev", + "name": "Dev", + "gender": "Male" + }, + { + "id": "ishita", + "name": "Ishita", + "gender": "Female" + }, + { + "id": "shreya", + "name": "Shreya", + "gender": "Female" + }, + { + "id": "ratan", + "name": "Ratan", + "gender": "Male" + }, + { + "id": "varun", + "name": "Varun", + "gender": "Male" + }, + { + "id": "manan", + "name": "Manan", + "gender": "Male" + }, + { + "id": "sumit", + "name": "Sumit", + "gender": "Male" + }, + { + "id": "roopa", + "name": "Roopa", + "gender": "Female" + }, + { + "id": "kabir", + "name": "Kabir", + "gender": "Male" + }, + { + "id": "aayan", + "name": "Aayan", + "gender": "Male" + }, + { + "id": "shubh", + "name": "Shubh", + "gender": "Male" + }, + { + "id": "advait", + "name": "Advait", + "gender": "Male" + }, + { + "id": "amelia", + "name": "Amelia", + "gender": "Female" + }, + { + "id": "sophia", + "name": "Sophia", + "gender": "Female" + }, + { + "id": "anand", + "name": "Anand", + "gender": "Male" + }, + { + "id": "tanya", + "name": "Tanya", + "gender": "Female" + }, + { + "id": "tarun", + "name": "Tarun", + "gender": "Male" + }, + { + "id": "sunny", + "name": "Sunny", + "gender": "Male" + }, + { + "id": "mani", + "name": "Mani", + "gender": "Male" + }, + { + "id": "gokul", + "name": "Gokul", + "gender": "Male" + }, + { + "id": "vijay", + "name": "Vijay", + "gender": "Male" + }, + { + "id": "shruti", + "name": "Shruti", + "gender": "Female" + }, + { + "id": "suhani", + "name": "Suhani", + "gender": "Female" + }, + { + "id": "mohit", + "name": "Mohit", + "gender": "Male" + }, + { + "id": "kavitha", + "name": "Kavitha", + "gender": "Female" + }, + { + "id": "rehan", + "name": "Rehan", + "gender": "Male" + }, + { + "id": "soham", + "name": "Soham", + "gender": "Male" + }, + { + "id": "rupali", + "name": "Rupali", + "gender": "Female" + } + ], + "pricing": { + "source": "sarvam.ai ₹30/10K chars @ 83 INR/USD", + "usage_kind": "tts", + "tts_per_1m_characters": 36.144578 + } + }, + "voice-agent-call": { + "provider": "telephony", + "model_type": "llm", + "description": "External voice-agent call duration (Vapi, Retell, ElevenLabs, Smallest); billed per minute", + "pricing": { + "source": "manual", + "usage_kind": "llm", + "audio_per_minute": 0.05 + } + }, + "unknown": { + "provider": "internal", + "model_type": "llm", + "description": "Fallback when model name is missing from usage records", + "pricing": { + "source": "manual", + "usage_kind": "llm", + "input_per_1m": 0, + "output_per_1m": 0 + } + } +} diff --git a/app/migrations/069_usage_pricing.py b/app/migrations/069_usage_pricing.py new file mode 100644 index 00000000..0d8f8a76 --- /dev/null +++ b/app/migrations/069_usage_pricing.py @@ -0,0 +1,190 @@ +"""Migration: usage pricing catalog, org overrides, and cost columns on rollups.""" + +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Add model_pricing_catalog, org_model_pricing_overrides, usage_pricing_mode, " + "and cost columns on llm_usage_daily" +) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _seed_pricing_catalog(db: Session) -> int: + from app.services.usage.pricing import DEFAULT_CATALOG_EFFECTIVE_FROM, seed_pricing_catalog + + return seed_pricing_catalog(db, effective_from=DEFAULT_CATALOG_EFFECTIVE_FROM) + + +def upgrade(db: Session): + if not _column_exists(db, "organizations", "usage_pricing_mode"): + db.execute( + text( + """ + ALTER TABLE organizations + ADD COLUMN usage_pricing_mode VARCHAR(32) NOT NULL DEFAULT 'platform_managed' + """ + ) + ) + print("Added organizations.usage_pricing_mode") + + if not _table_exists(db, "model_pricing_catalog"): + db.execute( + text( + """ + CREATE TABLE model_pricing_catalog ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + model VARCHAR(255) NOT NULL, + usage_kind VARCHAR(16) NOT NULL DEFAULT 'llm', + effective_from DATE NOT NULL DEFAULT CURRENT_DATE, + effective_to DATE, + input_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + output_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + cache_read_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + cache_creation_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + reasoning_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + audio_micro_usd_per_second BIGINT NOT NULL DEFAULT 0, + tts_micro_usd_per_million_chars BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT uq_model_pricing_catalog_model_kind_from + UNIQUE (model, usage_kind, effective_from) + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_model_pricing_catalog_lookup + ON model_pricing_catalog (model, usage_kind, effective_from DESC) + """ + ) + ) + print("Created model_pricing_catalog") + + if not _table_exists(db, "org_model_pricing_overrides"): + db.execute( + text( + """ + CREATE TABLE org_model_pricing_overrides ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL + REFERENCES organizations(id) ON DELETE CASCADE, + model VARCHAR(255) NOT NULL, + usage_kind VARCHAR(16) NOT NULL DEFAULT 'llm', + effective_from DATE NOT NULL DEFAULT CURRENT_DATE, + effective_to DATE, + input_micro_usd_per_million BIGINT, + output_micro_usd_per_million BIGINT, + cache_read_micro_usd_per_million BIGINT, + cache_creation_micro_usd_per_million BIGINT, + reasoning_micro_usd_per_million BIGINT, + audio_micro_usd_per_second BIGINT, + tts_micro_usd_per_million_chars BIGINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT uq_org_model_pricing_override + UNIQUE (organization_id, model, usage_kind, effective_from) + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_org_model_pricing_overrides_lookup + ON org_model_pricing_overrides ( + organization_id, model, usage_kind, effective_from DESC + ) + """ + ) + ) + print("Created org_model_pricing_overrides") + + cost_columns = [ + ("input_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("output_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("cache_read_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("cache_creation_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("reasoning_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("audio_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("tts_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("total_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("pricing_rate_source", "VARCHAR(16)"), + ("pricing_rate_id", "UUID"), + ] + if _table_exists(db, "llm_usage_daily"): + for column, col_type in cost_columns: + if not _column_exists(db, "llm_usage_daily", column): + db.execute( + text( + f"ALTER TABLE llm_usage_daily ADD COLUMN {column} {col_type}" + ) + ) + print("Added cost columns to llm_usage_daily") + + db.commit() + + if _table_exists(db, "model_pricing_catalog"): + seeded = _seed_pricing_catalog(db) + if seeded: + print(f"Seeded {seeded} model pricing catalog row(s)") + db.commit() + + +def downgrade(db: Session): + if _table_exists(db, "llm_usage_daily"): + for column in ( + "input_cost_micro_usd", + "output_cost_micro_usd", + "cache_read_cost_micro_usd", + "cache_creation_cost_micro_usd", + "reasoning_cost_micro_usd", + "audio_cost_micro_usd", + "tts_cost_micro_usd", + "total_cost_micro_usd", + "pricing_rate_source", + "pricing_rate_id", + ): + if _column_exists(db, "llm_usage_daily", column): + db.execute(text(f"ALTER TABLE llm_usage_daily DROP COLUMN {column}")) + + db.execute(text("DROP TABLE IF EXISTS org_model_pricing_overrides")) + db.execute(text("DROP TABLE IF EXISTS model_pricing_catalog")) + + if _column_exists(db, "organizations", "usage_pricing_mode"): + db.execute(text("ALTER TABLE organizations DROP COLUMN usage_pricing_mode")) + + db.commit() diff --git a/app/migrations/070_usage_margin_multiplier.py b/app/migrations/070_usage_margin_multiplier.py new file mode 100644 index 00000000..9a6c3e03 --- /dev/null +++ b/app/migrations/070_usage_margin_multiplier.py @@ -0,0 +1,45 @@ +"""Migration: org-level usage margin multiplier for priced rollups.""" + +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = "Add organizations.usage_margin_multiplier for usage cost markup" + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def upgrade(db: Session): + if not _column_exists(db, "organizations", "usage_margin_multiplier"): + db.execute( + text( + """ + ALTER TABLE organizations + ADD COLUMN usage_margin_multiplier DOUBLE PRECISION NOT NULL DEFAULT 1.0 + """ + ) + ) + print("Added organizations.usage_margin_multiplier") + db.commit() + + +def downgrade(db: Session): + if _column_exists(db, "organizations", "usage_margin_multiplier"): + db.execute( + text("ALTER TABLE organizations DROP COLUMN usage_margin_multiplier") + ) + db.commit() diff --git a/app/migrations/071_reseed_pricing_catalog.py b/app/migrations/071_reseed_pricing_catalog.py new file mode 100644 index 00000000..ad17a360 --- /dev/null +++ b/app/migrations/071_reseed_pricing_catalog.py @@ -0,0 +1,37 @@ +"""Migration: re-seed model_pricing_rates from models.json after effective_from fix.""" + +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.services.usage.pricing import DEFAULT_RATES_EFFECTIVE_FROM, seed_pricing_rates + +description = "Re-seed model_pricing_rates from models.json pricing blocks" + + +def upgrade(db: Session): + table = "model_pricing_rates" + exists = db.execute( + text("SELECT to_regclass('public.model_pricing_rates')") + ).scalar() + if not exists: + table = "model_pricing_catalog" + db.execute( + text( + f""" + UPDATE {table} + SET effective_from = CAST(:effective_from AS date) + WHERE effective_from > CAST(:effective_from AS date) + """ + ), + {"effective_from": DEFAULT_RATES_EFFECTIVE_FROM.isoformat()}, + ) + seeded = seed_pricing_rates(db, effective_from=DEFAULT_RATES_EFFECTIVE_FROM) + db.commit() + if seeded: + print(f"Re-seeded {seeded} model pricing rate row(s)") + + +def downgrade(db: Session): + pass diff --git a/app/migrations/072_usage_pricing_phase1.py b/app/migrations/072_usage_pricing_phase1.py new file mode 100644 index 00000000..68254deb --- /dev/null +++ b/app/migrations/072_usage_pricing_phase1.py @@ -0,0 +1,202 @@ +"""Migration: Phase 1 plan alignment — model_pricing_rates, buffer costs, strip extras.""" + +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = ( + "Rename model_pricing_catalog to model_pricing_rates, add currency/source, " + "cost columns on usage_pending_buffer, drop margin/BYOK org columns" +) + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def _ensure_model_pricing_rates(db: Session) -> None: + if _table_exists(db, "model_pricing_catalog") and not _table_exists( + db, "model_pricing_rates" + ): + db.execute( + text("ALTER TABLE model_pricing_catalog RENAME TO model_pricing_rates") + ) + db.execute( + text( + """ + ALTER INDEX IF EXISTS uq_model_pricing_catalog_model_kind_from + RENAME TO uq_model_pricing_rates_model_kind_from + """ + ) + ) + db.execute( + text( + """ + ALTER INDEX IF EXISTS ix_model_pricing_catalog_lookup + RENAME TO ix_model_pricing_rates_lookup + """ + ) + ) + print("Renamed model_pricing_catalog -> model_pricing_rates") + + if not _table_exists(db, "model_pricing_rates"): + db.execute( + text( + """ + CREATE TABLE model_pricing_rates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + model VARCHAR(255) NOT NULL, + usage_kind VARCHAR(16) NOT NULL DEFAULT 'llm', + effective_from DATE NOT NULL DEFAULT CURRENT_DATE, + effective_to DATE, + currency VARCHAR(8) NOT NULL DEFAULT 'USD', + source VARCHAR(32) NOT NULL DEFAULT 'catalog', + input_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + output_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + cache_read_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + cache_creation_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + reasoning_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, + audio_micro_usd_per_second BIGINT NOT NULL DEFAULT 0, + tts_micro_usd_per_million_chars BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT uq_model_pricing_rates_model_kind_from + UNIQUE (model, usage_kind, effective_from) + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX ix_model_pricing_rates_lookup + ON model_pricing_rates (model, usage_kind, effective_from DESC) + """ + ) + ) + print("Created model_pricing_rates") + + if not _column_exists(db, "model_pricing_rates", "currency"): + db.execute( + text( + """ + ALTER TABLE model_pricing_rates + ADD COLUMN currency VARCHAR(8) NOT NULL DEFAULT 'USD' + """ + ) + ) + if not _column_exists(db, "model_pricing_rates", "source"): + db.execute( + text( + """ + ALTER TABLE model_pricing_rates + ADD COLUMN source VARCHAR(32) NOT NULL DEFAULT 'catalog' + """ + ) + ) + + +def _add_buffer_cost_columns(db: Session) -> None: + if not _table_exists(db, "usage_pending_buffer"): + return + cost_columns = [ + ("input_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("output_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("cache_read_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("cache_creation_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("reasoning_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("audio_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("tts_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("total_cost_micro_usd", "BIGINT NOT NULL DEFAULT 0"), + ("pricing_rate_source", "VARCHAR(16)"), + ("pricing_rate_id", "UUID"), + ] + for column, col_type in cost_columns: + if not _column_exists(db, "usage_pending_buffer", column): + db.execute( + text( + f"ALTER TABLE usage_pending_buffer ADD COLUMN {column} {col_type}" + ) + ) + print("Ensured cost columns on usage_pending_buffer") + + +def _strip_org_extras(db: Session) -> None: + if _column_exists(db, "organizations", "usage_margin_multiplier"): + db.execute( + text("ALTER TABLE organizations DROP COLUMN usage_margin_multiplier") + ) + print("Dropped organizations.usage_margin_multiplier") + if _column_exists(db, "organizations", "usage_pricing_mode"): + db.execute(text("ALTER TABLE organizations DROP COLUMN usage_pricing_mode")) + print("Dropped organizations.usage_pricing_mode") + + +def upgrade(db: Session): + _ensure_model_pricing_rates(db) + _add_buffer_cost_columns(db) + _strip_org_extras(db) + db.commit() + + from app.services.usage.pricing import DEFAULT_RATES_EFFECTIVE_FROM, seed_pricing_rates + + seeded = seed_pricing_rates(db, effective_from=DEFAULT_RATES_EFFECTIVE_FROM) + db.commit() + if seeded: + print(f"Seeded {seeded} model_pricing_rates row(s)") + + +def downgrade(db: Session): + if _table_exists(db, "usage_pending_buffer"): + for column in ( + "input_cost_micro_usd", + "output_cost_micro_usd", + "cache_read_cost_micro_usd", + "cache_creation_cost_micro_usd", + "reasoning_cost_micro_usd", + "audio_cost_micro_usd", + "tts_cost_micro_usd", + "total_cost_micro_usd", + "pricing_rate_source", + "pricing_rate_id", + ): + if _column_exists(db, "usage_pending_buffer", column): + db.execute( + text(f"ALTER TABLE usage_pending_buffer DROP COLUMN {column}") + ) + + if _table_exists(db, "model_pricing_rates") and not _table_exists( + db, "model_pricing_catalog" + ): + db.execute( + text("ALTER TABLE model_pricing_rates RENAME TO model_pricing_catalog") + ) + + db.commit() diff --git a/app/migrations/073_usage_cost_recompute_jobs.py b/app/migrations/073_usage_cost_recompute_jobs.py new file mode 100644 index 00000000..55f18192 --- /dev/null +++ b/app/migrations/073_usage_cost_recompute_jobs.py @@ -0,0 +1,72 @@ +"""Migration: async usage cost recompute job tracking.""" + +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = "Add usage_cost_recompute_jobs table for async cost backfill/recompute" + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.tables + WHERE table_name = :table_name + """ + ), + {"table_name": table}, + ).first() + is not None + ) + + +def upgrade(db: Session) -> None: + if _table_exists(db, "usage_cost_recompute_jobs"): + print("usage_cost_recompute_jobs already exists, skipping...") + return + + db.execute( + text( + """ + CREATE TABLE usage_cost_recompute_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + status VARCHAR(32) NOT NULL DEFAULT 'pending', + model VARCHAR(255), + usage_kind VARCHAR(16), + start_date DATE, + end_date DATE, + updated_rows BIGINT NOT NULL DEFAULT 0, + error_message TEXT, + celery_task_id VARCHAR(255), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + completed_at TIMESTAMPTZ + ) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_usage_cost_recompute_jobs_organization_id + ON usage_cost_recompute_jobs(organization_id) + """ + ) + ) + db.execute( + text( + """ + CREATE INDEX IF NOT EXISTS ix_usage_cost_recompute_jobs_org_status + ON usage_cost_recompute_jobs(organization_id, status) + """ + ) + ) + print("Created usage_cost_recompute_jobs table") + + +def downgrade(db: Session) -> None: + db.execute(text("DROP TABLE IF EXISTS usage_cost_recompute_jobs")) diff --git a/app/migrations/074_pricing_rates_source_and_effective_from.py b/app/migrations/074_pricing_rates_source_and_effective_from.py new file mode 100644 index 00000000..6f78f8b4 --- /dev/null +++ b/app/migrations/074_pricing_rates_source_and_effective_from.py @@ -0,0 +1,81 @@ +"""Migration: widen pricing source column and normalize effective_from baseline.""" + +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.services.usage.pricing import DEFAULT_RATES_EFFECTIVE_FROM, seed_pricing_rates + +description = ( + "Widen model_pricing_rates.source to VARCHAR(255) and normalize effective_from " + "to 2020-01-01 baseline" +) + +_BASELINE = DEFAULT_RATES_EFFECTIVE_FROM.isoformat() + + +def _table_exists(db: Session, table: str) -> bool: + return ( + db.execute( + text("SELECT to_regclass(:table_name)"), + {"table_name": f"public.{table}"}, + ).scalar() + is not None + ) + + +def upgrade(db: Session) -> None: + table = "model_pricing_rates" + if not _table_exists(db, table): + table = "model_pricing_catalog" + if not _table_exists(db, table): + print("No pricing rates table found, skipping") + return + + db.execute( + text( + f""" + ALTER TABLE {table} + ALTER COLUMN source TYPE VARCHAR(255) + """ + ) + ) + print(f"Widened {table}.source to VARCHAR(255)") + + db.execute( + text( + f""" + DELETE FROM {table} newer + USING {table} baseline + WHERE newer.model = baseline.model + AND newer.usage_kind = baseline.usage_kind + AND newer.effective_from > CAST(:baseline AS date) + AND baseline.effective_from = CAST(:baseline AS date) + """ + ), + {"baseline": _BASELINE}, + ) + db.execute( + text( + f""" + UPDATE {table} + SET effective_from = CAST(:baseline AS date) + WHERE effective_from > CAST(:baseline AS date) + """ + ), + {"baseline": _BASELINE}, + ) + print(f"Normalized {table} effective_from to {_BASELINE}") + + seeded = seed_pricing_rates(db, effective_from=DEFAULT_RATES_EFFECTIVE_FROM) + from app.services.usage.pricing_cache import invalidate_all_pricing_cache + + invalidate_all_pricing_cache() + db.commit() + if seeded: + print(f"Re-seeded {seeded} pricing rate row(s) at {_BASELINE}") + + +def downgrade(db: Session) -> None: + pass diff --git a/app/models/database.py b/app/models/database.py index 00b99fdb..99fbbbba 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -2779,6 +2779,31 @@ class JudgeRun(Base): dataset = relationship("JudgeDataset", back_populates="runs") +class UsageCostRecomputeJob(Base): + """Async job tracking for retroactive usage cost recompute.""" + + __tablename__ = "usage_cost_recompute_jobs" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + organization_id = Column( + UUID(as_uuid=True), + ForeignKey("organizations.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + status = Column(String(32), nullable=False, default="pending", server_default="pending") + model = Column(String(255), nullable=True) + usage_kind = Column(String(16), nullable=True) + start_date = Column(Date, nullable=True) + end_date = Column(Date, nullable=True) + updated_rows = Column(BigInteger, nullable=False, default=0, server_default="0") + error_message = Column(String, nullable=True) + celery_task_id = Column(String(255), nullable=True, index=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + completed_at = Column(DateTime(timezone=True), nullable=True) + + class LLMUsageDaily(Base): """Daily LLM/STT usage rollups for org-scoped Usage reporting.""" @@ -2812,6 +2837,22 @@ class LLMUsageDaily(Base): audio_seconds = Column(BigInteger, nullable=False, default=0, server_default="0") tts_characters = Column(BigInteger, nullable=False, default=0, server_default="0") call_count = Column(BigInteger, nullable=False, default=0, server_default="0") + input_cost_micro_usd = Column(BigInteger, nullable=False, default=0, server_default="0") + output_cost_micro_usd = Column(BigInteger, nullable=False, default=0, server_default="0") + cache_read_cost_micro_usd = Column( + BigInteger, nullable=False, default=0, server_default="0" + ) + cache_creation_cost_micro_usd = Column( + BigInteger, nullable=False, default=0, server_default="0" + ) + reasoning_cost_micro_usd = Column( + BigInteger, nullable=False, default=0, server_default="0" + ) + audio_cost_micro_usd = Column(BigInteger, nullable=False, default=0, server_default="0") + tts_cost_micro_usd = Column(BigInteger, nullable=False, default=0, server_default="0") + total_cost_micro_usd = Column(BigInteger, nullable=False, default=0, server_default="0") + pricing_rate_source = Column(String(16), nullable=True) + pricing_rate_id = Column(UUID(as_uuid=True), nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() diff --git a/app/services/ai/stt_clients/google.py b/app/services/ai/stt_clients/google.py index 2bc100a1..84caf40d 100644 --- a/app/services/ai/stt_clients/google.py +++ b/app/services/ai/stt_clients/google.py @@ -83,6 +83,28 @@ def _build_transcription_prompt(language: Optional[str]) -> str: ) +def _gemini_stt_usage_ctx(config_model: str): + """Merge Gemini multimodal STT tags into the active usage context.""" + from app.services.usage.context import LLMUsageContext, get_usage_context + + tags = { + "stt_backend": "gemini_multimodal", + "config_model": (config_model or "").strip(), + "usage_split": "llm_tokens_and_stt_seconds", + } + base = get_usage_context() + if base is None: + return None + return LLMUsageContext( + organization_id=base.organization_id, + workspace_id=base.workspace_id, + product_section=base.product_section, + resource_id=base.resource_id, + resource_type=base.resource_type, + extra={**(base.extra or {}), **tags}, + ) + + def transcribe_google( audio_file_path: str, model: str, @@ -158,10 +180,12 @@ def transcribe_google( from app.services.usage.normalize import normalize_llm_usage gemini_model = _strip_stt_suffix(model) + usage_ctx = _gemini_stt_usage_ctx(model) record_llm_usage( gemini_model, normalize_llm_usage(raw_response=response), organization_id=organization_id, + ctx=usage_ctx, ) from app.services.usage.llm_usage import probe_audio_seconds, record_stt_usage @@ -171,6 +195,7 @@ def transcribe_google( gemini_model, audio_seconds=audio_seconds, organization_id=organization_id, + ctx=usage_ctx, count_call=False, ) except Exception as exc: diff --git a/app/services/call_import_user_insights.py b/app/services/call_import_user_insights.py index bf723e6a..7484a34b 100644 --- a/app/services/call_import_user_insights.py +++ b/app/services/call_import_user_insights.py @@ -21,6 +21,8 @@ Metric, ModelProvider, ) +from app.services.usage.context import LLMUsageContext + from app.models.schemas import ( EvaluationUserInsightItem, EvaluationUserInsightsState, @@ -222,17 +224,28 @@ def _call_llm( *, temperature: float, max_tokens: int, + usage_ctx: Optional[LLMUsageContext] = None, ) -> str: - result = llm_service.generate_response( - messages=messages, - llm_provider=provider, - llm_model=model, - organization_id=organization_id, - db=db, - temperature=temperature, - max_tokens=max_tokens, - ) - return str(result.get("text") or "") + from app.services.usage.context import get_usage_context, llm_usage_context + + effective_ctx = usage_ctx or get_usage_context() + + def _run() -> str: + result = llm_service.generate_response( + messages=messages, + llm_provider=provider, + llm_model=model, + organization_id=organization_id, + db=db, + temperature=temperature, + max_tokens=max_tokens, + ) + return str(result.get("text") or "") + + if effective_ctx is not None: + with llm_usage_context(effective_ctx): + return _run() + return _run() def run_extraction_batch( diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py index b992b9e5..0fdb48f1 100644 --- a/app/services/usage/llm_usage.py +++ b/app/services/usage/llm_usage.py @@ -240,14 +240,18 @@ def _buffer_to_postgres( logger.warning("usage postgres fallback unavailable: {}", exc) return + usage_date = bucket["usage_date"] + if isinstance(usage_date, str): + usage_date = date.fromisoformat(usage_date) + usage_kind = bucket.get("usage_kind") or USAGE_KIND_LLM params = { "organization_id": str(organization_id), "workspace_id": str(bucket["workspace_id"]) if bucket.get("workspace_id") else None, "product_section": bucket["product_section"], "model": bucket["model"], "context": json.dumps(bucket.get("context") or {}), - "usage_date": bucket["usage_date"].isoformat(), - "usage_kind": bucket.get("usage_kind") or USAGE_KIND_LLM, + "usage_date": usage_date.isoformat(), + "usage_kind": usage_kind, "prompt_tokens": int(deltas.get("prompt_tokens", 0)), "completion_tokens": int(deltas.get("completion_tokens", 0)), "cache_read_tokens": int(deltas.get("cache_read_tokens", 0)), @@ -259,6 +263,18 @@ def _buffer_to_postgres( } db = SessionLocal() try: + from app.services.usage.pricing import cost_fields_from_deltas + + params.update( + cost_fields_from_deltas( + deltas, + organization_id=organization_id, + model=bucket["model"], + usage_kind=usage_kind, + usage_date=usage_date, + db=db, + ) + ) db.execute( text( """ @@ -267,7 +283,12 @@ def _buffer_to_postgres( context, usage_date, usage_kind, prompt_tokens, completion_tokens, cache_read_tokens, cache_creation_tokens, reasoning_tokens, audio_seconds, - tts_characters, call_count, created_at + tts_characters, call_count, + input_cost_micro_usd, output_cost_micro_usd, + cache_read_cost_micro_usd, cache_creation_cost_micro_usd, + reasoning_cost_micro_usd, audio_cost_micro_usd, tts_cost_micro_usd, + total_cost_micro_usd, pricing_rate_source, pricing_rate_id, + created_at ) VALUES ( gen_random_uuid(), CAST(:organization_id AS uuid), CAST(:workspace_id AS uuid), :product_section, :model, @@ -275,7 +296,13 @@ def _buffer_to_postgres( CAST(:usage_date AS date), :usage_kind, :prompt_tokens, :completion_tokens, :cache_read_tokens, :cache_creation_tokens, :reasoning_tokens, :audio_seconds, - :tts_characters, :call_count, now() + :tts_characters, :call_count, + :input_cost_micro_usd, :output_cost_micro_usd, + :cache_read_cost_micro_usd, :cache_creation_cost_micro_usd, + :reasoning_cost_micro_usd, :audio_cost_micro_usd, :tts_cost_micro_usd, + :total_cost_micro_usd, :pricing_rate_source, + CAST(:pricing_rate_id AS uuid), + now() ) """ ), @@ -715,6 +742,8 @@ def _upsert_bucket( organization_id: UUID, bucket: Dict[str, Any], deltas: Dict[str, int], + *, + pricing_resolver: Any = None, ) -> None: context = bucket.get("context") or {} params = { @@ -772,64 +801,75 @@ def _upsert_bucket( text(f"UPDATE llm_usage_daily SET {update_set} {exact_context_where}"), params, ) - if result.rowcount: - return - - result = db.execute( - text(f"UPDATE llm_usage_daily SET {update_set} {legacy_context_where}"), - params, - ) - if result.rowcount: - return - - db.execute(text("SAVEPOINT llm_usage_bucket_insert")) - try: - db.execute( - text( - """ - INSERT INTO llm_usage_daily ( - id, organization_id, workspace_id, product_section, model, - context, usage_date, usage_kind, - prompt_tokens, completion_tokens, cache_read_tokens, - cache_creation_tokens, reasoning_tokens, audio_seconds, - tts_characters, call_count, - created_at, updated_at - ) VALUES ( - gen_random_uuid(), CAST(:organization_id AS uuid), - CAST(:workspace_id AS uuid), :product_section, :model, - CAST(:context AS jsonb), CAST(:usage_date AS date), - :usage_kind, - :prompt_tokens, :completion_tokens, :cache_read_tokens, - :cache_creation_tokens, :reasoning_tokens, :audio_seconds, - :tts_characters, :call_count, - now(), now() - ) - """ - ), - params, - ) - db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) - except IntegrityError as exc: - if not _is_unique_violation(exc): - db.execute(text("ROLLBACK TO SAVEPOINT llm_usage_bucket_insert")) - db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) - raise - db.execute(text("ROLLBACK TO SAVEPOINT llm_usage_bucket_insert")) + if not result.rowcount: result = db.execute( text(f"UPDATE llm_usage_daily SET {update_set} {legacy_context_where}"), params, ) if not result.rowcount: - result = db.execute( - text(f"UPDATE llm_usage_daily SET {update_set} {exact_context_where}"), - params, - ) - db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) - if not result.rowcount: - logger.warning( - "llm usage upsert unique conflict but no matching bucket for org {}", - organization_id, - ) + db.execute(text("SAVEPOINT llm_usage_bucket_insert")) + try: + db.execute( + text( + """ + INSERT INTO llm_usage_daily ( + id, organization_id, workspace_id, product_section, model, + context, usage_date, usage_kind, + prompt_tokens, completion_tokens, cache_read_tokens, + cache_creation_tokens, reasoning_tokens, audio_seconds, + tts_characters, call_count, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), CAST(:organization_id AS uuid), + CAST(:workspace_id AS uuid), :product_section, :model, + CAST(:context AS jsonb), CAST(:usage_date AS date), + :usage_kind, + :prompt_tokens, :completion_tokens, :cache_read_tokens, + :cache_creation_tokens, :reasoning_tokens, :audio_seconds, + :tts_characters, :call_count, + now(), now() + ) + """ + ), + params, + ) + db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) + except IntegrityError as exc: + if not _is_unique_violation(exc): + db.execute(text("ROLLBACK TO SAVEPOINT llm_usage_bucket_insert")) + db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) + raise + db.execute(text("ROLLBACK TO SAVEPOINT llm_usage_bucket_insert")) + result = db.execute( + text(f"UPDATE llm_usage_daily SET {update_set} {legacy_context_where}"), + params, + ) + if not result.rowcount: + result = db.execute( + text(f"UPDATE llm_usage_daily SET {update_set} {exact_context_where}"), + params, + ) + db.execute(text("RELEASE SAVEPOINT llm_usage_bucket_insert")) + if not result.rowcount: + logger.warning( + "llm usage upsert unique conflict but no matching bucket for org {}", + organization_id, + ) + + try: + from app.services.usage.pricing import PricingResolver, apply_cost_to_bucket + + resolver = pricing_resolver + if resolver is None: + resolver = PricingResolver(db) + apply_cost_to_bucket( + db, + organization_id=organization_id, + bucket=bucket, + resolver=resolver, + ) + except Exception as exc: + logger.warning("usage cost apply failed: {}", exc) def _is_unique_violation(exc: BaseException) -> bool: @@ -854,8 +894,13 @@ def _is_missing_organization_fk(exc: BaseException) -> bool: return "llm_usage_daily_organization_id_fkey" in text_blob -def _flush_pending_buffer(db: Session, organization_id: UUID) -> int: +def _flush_pending_buffer( + db: Session, organization_id: UUID, *, pricing_resolver: Any = None +) -> int: """Drain Postgres write-ahead rows into llm_usage_daily.""" + from app.services.usage.pricing import PricingResolver + + resolver = pricing_resolver or PricingResolver(db) try: rows = db.execute( text( @@ -910,6 +955,7 @@ def _flush_pending_buffer(db: Session, organization_id: UUID) -> int: organization_id, bucket, deltas, + pricing_resolver=resolver, ) ids.append(str(row["id"])) flushed += 1 @@ -966,11 +1012,14 @@ def _catalog_flush_recently(organization_id: UUID) -> bool: def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = False) -> int: """Claim Redis deltas + drain PG buffer into llm_usage_daily.""" + from app.services.usage.pricing import PricingResolver + _recover_orphaned_claims() skip_redis_flush = not force and _catalog_flush_recently(organization_id) if skip_redis_flush and _has_pending_usage(organization_id): skip_redis_flush = False flushed = 0 + pricing_resolver = PricingResolver(db) if not skip_redis_flush: redis_locked = _acquire_flush_lock(organization_id) claim_key = None @@ -995,6 +1044,7 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = organization_id, parsed, deltas, + pricing_resolver=pricing_resolver, ) flushed += 1 if claim_key: @@ -1032,8 +1082,8 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = _client().delete(claim_key) except redis.RedisError: pass - claim_key = None - return _flush_pending_buffer(db, organization_id) + claim_key = None + return _flush_pending_buffer(db, organization_id) if skipped: _restore_buckets_to_pending(organization_id, skipped) if claim_key: diff --git a/app/services/usage/pricing.py b/app/services/usage/pricing.py new file mode 100644 index 00000000..762add7d --- /dev/null +++ b/app/services/usage/pricing.py @@ -0,0 +1,912 @@ +"""Usage pricing: rate resolution, cost computation, seed, and rollup backfill.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from datetime import date +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Tuple +from uuid import UUID + +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.services.usage.pricing_cache import ( + get_cached_rate_payload, + pricing_cache_key, + set_cached_rate_payload, +) + +DEFAULT_RATES_EFFECTIVE_FROM = date(2020, 1, 1) +# Back-compat alias for older migrations. +DEFAULT_CATALOG_EFFECTIVE_FROM = DEFAULT_RATES_EFFECTIVE_FROM + +USAGE_KIND_LLM = "llm" +USAGE_KIND_STT = "stt" +USAGE_KIND_TTS = "tts" + +MICRO_USD_PER_UNIT = 1_000_000 +RATE_SOURCE_CATALOG = "catalog" +RATE_SOURCE_OVERRIDE = "override" + +_MODELS_JSON_PATH = ( + Path(__file__).resolve().parent.parent.parent / "config" / "models.json" +) + +_RATES_TABLE = "model_pricing_rates" +_RATES_TABLE_CACHE: Optional[str] = None + + +def _rates_table(db: Session) -> str: + global _RATES_TABLE_CACHE + if _RATES_TABLE_CACHE: + return _RATES_TABLE_CACHE + row = db.execute(text("SELECT to_regclass('public.model_pricing_rates')")).scalar() + if row: + _RATES_TABLE_CACHE = "model_pricing_rates" + return _RATES_TABLE_CACHE + row = db.execute(text("SELECT to_regclass('public.model_pricing_catalog')")).scalar() + if row: + _RATES_TABLE_CACHE = "model_pricing_catalog" + return _RATES_TABLE_CACHE + _RATES_TABLE_CACHE = _RATES_TABLE + return _RATES_TABLE_CACHE + + +@dataclass(frozen=True) +class RateCard: + source: str + rate_id: UUID + input_micro_usd_per_million: int = 0 + output_micro_usd_per_million: int = 0 + cache_read_micro_usd_per_million: int = 0 + cache_creation_micro_usd_per_million: int = 0 + reasoning_micro_usd_per_million: int = 0 + audio_micro_usd_per_second: int = 0 + tts_micro_usd_per_million_chars: int = 0 + + +@dataclass(frozen=True) +class CostBreakdown: + input_cost_micro_usd: int = 0 + output_cost_micro_usd: int = 0 + cache_read_cost_micro_usd: int = 0 + cache_creation_cost_micro_usd: int = 0 + reasoning_cost_micro_usd: int = 0 + audio_cost_micro_usd: int = 0 + tts_cost_micro_usd: int = 0 + total_cost_micro_usd: int = 0 + pricing_rate_source: Optional[str] = None + pricing_rate_id: Optional[UUID] = None + + +@dataclass(frozen=True) +class UsageMetrics: + prompt_tokens: int = 0 + completion_tokens: int = 0 + cache_read_tokens: int = 0 + cache_creation_tokens: int = 0 + reasoning_tokens: int = 0 + audio_seconds: int = 0 + tts_characters: int = 0 + + +def _int(value: Any, default: int = 0) -> int: + try: + return int(value or 0) + except (TypeError, ValueError): + return default + + +def _normalize_rate_source(value: Any) -> str: + source = str(value or "catalog").strip() or "catalog" + if len(source) > 255: + return source[:255] + return source + + +def _usage_kind_for_model_type(model_type: Optional[str]) -> str: + if model_type == "stt": + return USAGE_KIND_STT + if model_type in {"tts", "sts", "sound_effects", "music"}: + return USAGE_KIND_TTS + return USAGE_KIND_LLM + + +def _usd_per_million_to_micro(value: Any) -> int: + if value is None: + return 0 + try: + return int(round(float(value) * MICRO_USD_PER_UNIT)) + except (TypeError, ValueError): + return 0 + + +def _usd_per_minute_to_micro_per_second(value: Any) -> int: + if value is None: + return 0 + try: + return int(round(float(value) * MICRO_USD_PER_UNIT / 60.0)) + except (TypeError, ValueError): + return 0 + + +def _normalize_pricing_block( + pricing: Dict[str, Any], *, model_type: Optional[str] +) -> Dict[str, Any]: + """Convert models.json pricing block (plan USD fields or legacy micro fields) to DB shape.""" + usage_kind = pricing.get("usage_kind") or _usage_kind_for_model_type(model_type) + source = str(pricing.get("source") or pricing.get("_price_source") or "catalog") + + def micro(field_micro: str, field_usd: str) -> int: + if pricing.get(field_micro) is not None: + return _int(pricing.get(field_micro)) + return _usd_per_million_to_micro(pricing.get(field_usd)) + + audio_micro = _int(pricing.get("audio_micro_usd_per_second")) + if not audio_micro: + audio_micro = _usd_per_minute_to_micro_per_second(pricing.get("audio_per_minute")) + + tts_micro = _int(pricing.get("tts_micro_usd_per_million_chars")) + if not tts_micro: + tts_micro = _usd_per_million_to_micro(pricing.get("tts_per_1m_characters")) + + return { + "usage_kind": usage_kind, + "source": source, + "currency": str(pricing.get("currency") or "USD"), + "input_micro_usd_per_million": micro( + "input_micro_usd_per_million", "input_per_1m" + ), + "output_micro_usd_per_million": micro( + "output_micro_usd_per_million", "output_per_1m" + ), + "cache_read_micro_usd_per_million": micro( + "cache_read_micro_usd_per_million", "cache_read_per_1m" + ), + "cache_creation_micro_usd_per_million": micro( + "cache_creation_micro_usd_per_million", "cache_write_per_1m" + ), + "reasoning_micro_usd_per_million": micro( + "reasoning_micro_usd_per_million", "reasoning_per_1m" + ), + "audio_micro_usd_per_second": audio_micro, + "tts_micro_usd_per_million_chars": tts_micro, + } + + +def _catalog_lookup_models(model: str, usage_kind: str) -> Tuple[str, ...]: + candidates: List[str] = [] + for item in ( + model, + f"azure-{model}" if not model.startswith("azure-") else model[len("azure-") :], + ): + if item and item not in candidates: + candidates.append(item) + return tuple(candidates) + + +def _scaled_cost(units: int, rate_per_million: int) -> int: + if not units or not rate_per_million: + return 0 + return (int(units) * int(rate_per_million)) // MICRO_USD_PER_UNIT + + +def metrics_from_deltas(deltas: Dict[str, Any]) -> UsageMetrics: + return UsageMetrics( + prompt_tokens=_int(deltas.get("prompt_tokens")), + completion_tokens=_int(deltas.get("completion_tokens")), + cache_read_tokens=_int(deltas.get("cache_read_tokens")), + cache_creation_tokens=_int(deltas.get("cache_creation_tokens")), + reasoning_tokens=_int(deltas.get("reasoning_tokens")), + audio_seconds=_int(deltas.get("audio_seconds")), + tts_characters=_int(deltas.get("tts_characters")), + ) + + +def cost_fields_from_deltas( + deltas: Dict[str, Any], + *, + organization_id: UUID, + model: str, + usage_kind: str, + usage_date: date, + db: Session, + resolver: Optional["PricingResolver"] = None, +) -> Dict[str, Any]: + """Compute persisted cost columns for one pending-buffer delta row.""" + pricing = resolver or PricingResolver(db) + rate = pricing.resolve_rate( + organization_id=organization_id, + model=model, + usage_kind=usage_kind, + usage_date=usage_date, + ) + costs = compute_cost(metrics_from_deltas(deltas), rate) + return { + "input_cost_micro_usd": costs.input_cost_micro_usd, + "output_cost_micro_usd": costs.output_cost_micro_usd, + "cache_read_cost_micro_usd": costs.cache_read_cost_micro_usd, + "cache_creation_cost_micro_usd": costs.cache_creation_cost_micro_usd, + "reasoning_cost_micro_usd": costs.reasoning_cost_micro_usd, + "audio_cost_micro_usd": costs.audio_cost_micro_usd, + "tts_cost_micro_usd": costs.tts_cost_micro_usd, + "total_cost_micro_usd": costs.total_cost_micro_usd, + "pricing_rate_source": costs.pricing_rate_source, + "pricing_rate_id": str(costs.pricing_rate_id) + if costs.pricing_rate_id + else None, + } + + +def compute_cost(metrics: UsageMetrics, rate: Optional[RateCard]) -> CostBreakdown: + if rate is None: + return CostBreakdown() + + input_cost = _scaled_cost(metrics.prompt_tokens, rate.input_micro_usd_per_million) + output_cost = _scaled_cost( + metrics.completion_tokens, rate.output_micro_usd_per_million + ) + cache_read_cost = _scaled_cost( + metrics.cache_read_tokens, rate.cache_read_micro_usd_per_million + ) + cache_creation_cost = _scaled_cost( + metrics.cache_creation_tokens, rate.cache_creation_micro_usd_per_million + ) + reasoning_cost = _scaled_cost( + metrics.reasoning_tokens, rate.reasoning_micro_usd_per_million + ) + audio_cost = 0 + if metrics.audio_seconds and rate.audio_micro_usd_per_second: + audio_cost = int(metrics.audio_seconds) * int(rate.audio_micro_usd_per_second) + tts_cost = _scaled_cost( + metrics.tts_characters, rate.tts_micro_usd_per_million_chars + ) + total = ( + input_cost + + output_cost + + cache_read_cost + + cache_creation_cost + + reasoning_cost + + audio_cost + + tts_cost + ) + return CostBreakdown( + input_cost_micro_usd=input_cost, + output_cost_micro_usd=output_cost, + cache_read_cost_micro_usd=cache_read_cost, + cache_creation_cost_micro_usd=cache_creation_cost, + reasoning_cost_micro_usd=reasoning_cost, + audio_cost_micro_usd=audio_cost, + tts_cost_micro_usd=tts_cost, + total_cost_micro_usd=total, + pricing_rate_source=rate.source, + pricing_rate_id=rate.rate_id, + ) + + +def _rate_card_from_row(row: Any, *, source: str) -> RateCard: + return RateCard( + source=source, + rate_id=row["id"], + input_micro_usd_per_million=_int(row["input_micro_usd_per_million"]), + output_micro_usd_per_million=_int(row["output_micro_usd_per_million"]), + cache_read_micro_usd_per_million=_int(row["cache_read_micro_usd_per_million"]), + cache_creation_micro_usd_per_million=_int( + row["cache_creation_micro_usd_per_million"] + ), + reasoning_micro_usd_per_million=_int(row["reasoning_micro_usd_per_million"]), + audio_micro_usd_per_second=_int(row["audio_micro_usd_per_second"]), + tts_micro_usd_per_million_chars=_int(row["tts_micro_usd_per_million_chars"]), + ) + + +def _merge_override_with_catalog( + override_row: Any, catalog: Optional[RateCard] +) -> RateCard: + def pick(column: str, attr: str) -> int: + value = override_row.get(column) + if value is not None: + return _int(value) + if catalog is not None: + return getattr(catalog, attr) + return 0 + + return RateCard( + source=RATE_SOURCE_OVERRIDE, + rate_id=override_row["id"], + input_micro_usd_per_million=pick( + "input_micro_usd_per_million", "input_micro_usd_per_million" + ), + output_micro_usd_per_million=pick( + "output_micro_usd_per_million", "output_micro_usd_per_million" + ), + cache_read_micro_usd_per_million=pick( + "cache_read_micro_usd_per_million", "cache_read_micro_usd_per_million" + ), + cache_creation_micro_usd_per_million=pick( + "cache_creation_micro_usd_per_million", + "cache_creation_micro_usd_per_million", + ), + reasoning_micro_usd_per_million=pick( + "reasoning_micro_usd_per_million", "reasoning_micro_usd_per_million" + ), + audio_micro_usd_per_second=pick( + "audio_micro_usd_per_second", "audio_micro_usd_per_second" + ), + tts_micro_usd_per_million_chars=pick( + "tts_micro_usd_per_million_chars", "tts_micro_usd_per_million_chars" + ), + ) + + +def _rate_card_to_cache_payload(card: RateCard) -> Dict[str, Any]: + return { + "source": card.source, + "rate_id": str(card.rate_id), + "input_micro_usd_per_million": card.input_micro_usd_per_million, + "output_micro_usd_per_million": card.output_micro_usd_per_million, + "cache_read_micro_usd_per_million": card.cache_read_micro_usd_per_million, + "cache_creation_micro_usd_per_million": card.cache_creation_micro_usd_per_million, + "reasoning_micro_usd_per_million": card.reasoning_micro_usd_per_million, + "audio_micro_usd_per_second": card.audio_micro_usd_per_second, + "tts_micro_usd_per_million_chars": card.tts_micro_usd_per_million_chars, + } + + +def _rate_card_from_cache_payload(payload: Dict[str, Any]) -> RateCard: + return RateCard( + source=str(payload["source"]), + rate_id=UUID(str(payload["rate_id"])), + input_micro_usd_per_million=_int(payload.get("input_micro_usd_per_million")), + output_micro_usd_per_million=_int(payload.get("output_micro_usd_per_million")), + cache_read_micro_usd_per_million=_int( + payload.get("cache_read_micro_usd_per_million") + ), + cache_creation_micro_usd_per_million=_int( + payload.get("cache_creation_micro_usd_per_million") + ), + reasoning_micro_usd_per_million=_int( + payload.get("reasoning_micro_usd_per_million") + ), + audio_micro_usd_per_second=_int(payload.get("audio_micro_usd_per_second")), + tts_micro_usd_per_million_chars=_int( + payload.get("tts_micro_usd_per_million_chars") + ), + ) + + +class PricingResolver: + """Cached pricing lookups for flush/recompute batches.""" + + def __init__(self, db: Session): + self._db = db + self._memory_cache: Dict[Tuple[str, ...], Optional[RateCard]] = {} + + def resolve_rate( + self, + *, + organization_id: UUID, + model: str, + usage_kind: str, + usage_date: date, + ) -> Optional[RateCard]: + kind = usage_kind or USAGE_KIND_LLM + memory_key = (str(organization_id), model, kind, usage_date.isoformat()) + if memory_key in self._memory_cache: + return self._memory_cache[memory_key] + + redis_key = pricing_cache_key( + organization_id=organization_id, + model=model, + usage_kind=kind, + usage_date=usage_date, + ) + cached = get_cached_rate_payload(redis_key) + if cached: + card = _rate_card_from_cache_payload(cached) + self._memory_cache[memory_key] = card + return card + + override = self._load_override_rate(organization_id, model, kind, usage_date) + if override is not None: + set_cached_rate_payload(redis_key, _rate_card_to_cache_payload(override)) + self._memory_cache[memory_key] = override + return override + + catalog = self._resolve_catalog(model, kind, usage_date) + set_cached_rate_payload( + redis_key, + _rate_card_to_cache_payload(catalog) if catalog else None, + ) + self._memory_cache[memory_key] = catalog + return catalog + + def _resolve_catalog( + self, model: str, usage_kind: str, usage_date: date + ) -> Optional[RateCard]: + for candidate in _catalog_lookup_models(model, usage_kind): + card = self._load_catalog_rate(candidate, usage_kind, usage_date) + if card is not None: + return card + return None + + def _load_catalog_rate( + self, model: str, usage_kind: str, usage_date: date + ) -> Optional[RateCard]: + table = _rates_table(self._db) + row = self._db.execute( + text( + f""" + SELECT + id, + input_micro_usd_per_million, + output_micro_usd_per_million, + cache_read_micro_usd_per_million, + cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million, + audio_micro_usd_per_second, + tts_micro_usd_per_million_chars + FROM {table} + WHERE model = :model + AND usage_kind = :usage_kind + AND effective_from <= :usage_date + AND (effective_to IS NULL OR effective_to >= :usage_date) + ORDER BY effective_from DESC + LIMIT 1 + """ + ), + { + "model": model, + "usage_kind": usage_kind, + "usage_date": usage_date.isoformat(), + }, + ).mappings().first() + if not row: + return None + return _rate_card_from_row(row, source=RATE_SOURCE_CATALOG) + + def _load_override_rate( + self, + organization_id: UUID, + model: str, + usage_kind: str, + usage_date: date, + ) -> Optional[RateCard]: + row = self._db.execute( + text( + """ + SELECT + id, + input_micro_usd_per_million, + output_micro_usd_per_million, + cache_read_micro_usd_per_million, + cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million, + audio_micro_usd_per_second, + tts_micro_usd_per_million_chars + FROM org_model_pricing_overrides + WHERE organization_id = CAST(:organization_id AS uuid) + AND model = :model + AND usage_kind = :usage_kind + AND effective_from <= :usage_date + AND (effective_to IS NULL OR effective_to >= :usage_date) + ORDER BY effective_from DESC + LIMIT 1 + """ + ), + { + "organization_id": str(organization_id), + "model": model, + "usage_kind": usage_kind, + "usage_date": usage_date.isoformat(), + }, + ).mappings().first() + if not row: + return None + catalog = self._resolve_catalog(model, usage_kind, usage_date) + return _merge_override_with_catalog(row, catalog) + + +def _pricing_entries_from_models_json() -> Dict[str, Dict[str, Any]]: + if not _MODELS_JSON_PATH.exists(): + return {} + try: + with open(_MODELS_JSON_PATH, "r", encoding="utf-8") as handle: + payload = json.load(handle) + except (OSError, json.JSONDecodeError): + return {} + entries: Dict[str, Dict[str, Any]] = {} + for model_name, config in payload.items(): + if model_name.startswith("_") or not isinstance(config, dict): + continue + pricing = config.get("pricing") + if not isinstance(pricing, dict): + continue + entries[model_name] = _normalize_pricing_block( + pricing, model_type=config.get("model_type") + ) + return entries + + +def seed_pricing_rates(db: Session, *, effective_from: Optional[date] = None) -> int: + """Upsert global rates from models.json pricing blocks (seed only; DB is runtime truth).""" + day = effective_from or DEFAULT_RATES_EFFECTIVE_FROM + table = _rates_table(db) + has_currency = ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = 'currency' + """ + ), + {"table_name": table}, + ).first() + is not None + ) + inserted = 0 + for model_name, pricing in _pricing_entries_from_models_json().items(): + usage_kind = pricing.get("usage_kind") or USAGE_KIND_LLM + base_params = { + "model": model_name, + "usage_kind": usage_kind, + "effective_from": day.isoformat(), + "input_micro_usd_per_million": _int( + pricing.get("input_micro_usd_per_million") + ), + "output_micro_usd_per_million": _int( + pricing.get("output_micro_usd_per_million") + ), + "cache_read_micro_usd_per_million": _int( + pricing.get("cache_read_micro_usd_per_million") + ), + "cache_creation_micro_usd_per_million": _int( + pricing.get("cache_creation_micro_usd_per_million") + ), + "reasoning_micro_usd_per_million": _int( + pricing.get("reasoning_micro_usd_per_million") + ), + "audio_micro_usd_per_second": _int( + pricing.get("audio_micro_usd_per_second") + ), + "tts_micro_usd_per_million_chars": _int( + pricing.get("tts_micro_usd_per_million_chars") + ), + } + if has_currency: + sql = f""" + INSERT INTO {table} ( + id, model, usage_kind, effective_from, currency, source, + input_micro_usd_per_million, + output_micro_usd_per_million, + cache_read_micro_usd_per_million, + cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million, + audio_micro_usd_per_second, + tts_micro_usd_per_million_chars, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), :model, :usage_kind, CAST(:effective_from AS date), + :currency, :source, + :input_micro_usd_per_million, + :output_micro_usd_per_million, + :cache_read_micro_usd_per_million, + :cache_creation_micro_usd_per_million, + :reasoning_micro_usd_per_million, + :audio_micro_usd_per_second, + :tts_micro_usd_per_million_chars, + now(), now() + ) + ON CONFLICT (model, usage_kind, effective_from) DO UPDATE SET + currency = EXCLUDED.currency, + source = EXCLUDED.source, + input_micro_usd_per_million = EXCLUDED.input_micro_usd_per_million, + output_micro_usd_per_million = EXCLUDED.output_micro_usd_per_million, + cache_read_micro_usd_per_million = EXCLUDED.cache_read_micro_usd_per_million, + cache_creation_micro_usd_per_million = EXCLUDED.cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million = EXCLUDED.reasoning_micro_usd_per_million, + audio_micro_usd_per_second = EXCLUDED.audio_micro_usd_per_second, + tts_micro_usd_per_million_chars = EXCLUDED.tts_micro_usd_per_million_chars, + updated_at = now() + """ + params = { + **base_params, + "currency": pricing.get("currency") or "USD", + "source": _normalize_rate_source(pricing.get("source")), + } + else: + sql = f""" + INSERT INTO {table} ( + id, model, usage_kind, effective_from, + input_micro_usd_per_million, + output_micro_usd_per_million, + cache_read_micro_usd_per_million, + cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million, + audio_micro_usd_per_second, + tts_micro_usd_per_million_chars, + created_at, updated_at + ) VALUES ( + gen_random_uuid(), :model, :usage_kind, CAST(:effective_from AS date), + :input_micro_usd_per_million, + :output_micro_usd_per_million, + :cache_read_micro_usd_per_million, + :cache_creation_micro_usd_per_million, + :reasoning_micro_usd_per_million, + :audio_micro_usd_per_second, + :tts_micro_usd_per_million_chars, + now(), now() + ) + ON CONFLICT (model, usage_kind, effective_from) DO UPDATE SET + input_micro_usd_per_million = EXCLUDED.input_micro_usd_per_million, + output_micro_usd_per_million = EXCLUDED.output_micro_usd_per_million, + cache_read_micro_usd_per_million = EXCLUDED.cache_read_micro_usd_per_million, + cache_creation_micro_usd_per_million = EXCLUDED.cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million = EXCLUDED.reasoning_micro_usd_per_million, + audio_micro_usd_per_second = EXCLUDED.audio_micro_usd_per_second, + tts_micro_usd_per_million_chars = EXCLUDED.tts_micro_usd_per_million_chars, + updated_at = now() + """ + params = base_params + result = db.execute(text(sql), params) + if result.rowcount: + inserted += 1 + if inserted: + from app.services.usage.pricing_cache import invalidate_all_pricing_cache + + invalidate_all_pricing_cache() + return inserted + + +def seed_pricing_catalog(db: Session, *, effective_from: Optional[date] = None) -> int: + """Back-compat alias.""" + return seed_pricing_rates(db, effective_from=effective_from) + + +def apply_cost_to_bucket( + db: Session, + *, + organization_id: UUID, + bucket: Dict[str, Any], + resolver: Optional[PricingResolver] = None, +) -> bool: + """Recompute and persist cost columns for one rollup bucket from row totals.""" + context = bucket.get("context") or {} + workspace_id = bucket.get("workspace_id") + usage_kind = bucket.get("usage_kind") or USAGE_KIND_LLM + usage_date = bucket["usage_date"] + if isinstance(usage_date, str): + usage_date = date.fromisoformat(usage_date) + + base_params = { + "organization_id": str(organization_id), + "workspace_id": str(workspace_id) if workspace_id else None, + "product_section": bucket["product_section"], + "model": bucket["model"], + "usage_date": usage_date.isoformat(), + "usage_kind": usage_kind, + } + exact_params = { + **base_params, + "context": json.dumps(context), + "context_resource_id": str(context.get("resource_id") or ""), + "context_resource_type": str(context.get("resource_type") or ""), + } + row = db.execute( + text( + """ + SELECT + prompt_tokens, completion_tokens, cache_read_tokens, + cache_creation_tokens, reasoning_tokens, audio_seconds, + tts_characters + FROM llm_usage_daily + WHERE organization_id = CAST(:organization_id AS uuid) + AND product_section = :product_section + AND model = :model + AND usage_date = CAST(:usage_date AS date) + AND usage_kind = :usage_kind + AND workspace_id IS NOT DISTINCT FROM CAST(:workspace_id AS uuid) + AND context = CAST(:context AS jsonb) + """ + ), + exact_params, + ).mappings().first() + if not row: + row = db.execute( + text( + """ + SELECT + prompt_tokens, completion_tokens, cache_read_tokens, + cache_creation_tokens, reasoning_tokens, audio_seconds, + tts_characters + FROM llm_usage_daily + WHERE organization_id = CAST(:organization_id AS uuid) + AND product_section = :product_section + AND model = :model + AND usage_date = CAST(:usage_date AS date) + AND usage_kind = :usage_kind + AND workspace_id IS NOT DISTINCT FROM CAST(:workspace_id AS uuid) + AND COALESCE(context->>'resource_id', '') = :context_resource_id + AND COALESCE(context->>'resource_type', '') = :context_resource_type + """ + ), + exact_params, + ).mappings().first() + if not row: + return False + + pricing = resolver or PricingResolver(db) + rate = pricing.resolve_rate( + organization_id=organization_id, + model=bucket["model"], + usage_kind=usage_kind, + usage_date=usage_date, + ) + costs = compute_cost( + UsageMetrics( + prompt_tokens=_int(row["prompt_tokens"]), + completion_tokens=_int(row["completion_tokens"]), + cache_read_tokens=_int(row["cache_read_tokens"]), + cache_creation_tokens=_int(row["cache_creation_tokens"]), + reasoning_tokens=_int(row["reasoning_tokens"]), + audio_seconds=_int(row["audio_seconds"]), + tts_characters=_int(row["tts_characters"]), + ), + rate, + ) + update_params = { + **exact_params, + "input_cost_micro_usd": costs.input_cost_micro_usd, + "output_cost_micro_usd": costs.output_cost_micro_usd, + "cache_read_cost_micro_usd": costs.cache_read_cost_micro_usd, + "cache_creation_cost_micro_usd": costs.cache_creation_cost_micro_usd, + "reasoning_cost_micro_usd": costs.reasoning_cost_micro_usd, + "audio_cost_micro_usd": costs.audio_cost_micro_usd, + "tts_cost_micro_usd": costs.tts_cost_micro_usd, + "total_cost_micro_usd": costs.total_cost_micro_usd, + "pricing_rate_source": costs.pricing_rate_source, + "pricing_rate_id": str(costs.pricing_rate_id) + if costs.pricing_rate_id + else None, + } + result = db.execute( + text( + """ + UPDATE llm_usage_daily SET + input_cost_micro_usd = :input_cost_micro_usd, + output_cost_micro_usd = :output_cost_micro_usd, + cache_read_cost_micro_usd = :cache_read_cost_micro_usd, + cache_creation_cost_micro_usd = :cache_creation_cost_micro_usd, + reasoning_cost_micro_usd = :reasoning_cost_micro_usd, + audio_cost_micro_usd = :audio_cost_micro_usd, + tts_cost_micro_usd = :tts_cost_micro_usd, + total_cost_micro_usd = :total_cost_micro_usd, + pricing_rate_source = :pricing_rate_source, + pricing_rate_id = CAST(:pricing_rate_id AS uuid), + updated_at = now() + WHERE organization_id = CAST(:organization_id AS uuid) + AND product_section = :product_section + AND model = :model + AND usage_date = CAST(:usage_date AS date) + AND usage_kind = :usage_kind + AND workspace_id IS NOT DISTINCT FROM CAST(:workspace_id AS uuid) + AND context = CAST(:context AS jsonb) + """ + ), + update_params, + ) + if result.rowcount: + return True + result = db.execute( + text( + """ + UPDATE llm_usage_daily SET + input_cost_micro_usd = :input_cost_micro_usd, + output_cost_micro_usd = :output_cost_micro_usd, + cache_read_cost_micro_usd = :cache_read_cost_micro_usd, + cache_creation_cost_micro_usd = :cache_creation_cost_micro_usd, + reasoning_cost_micro_usd = :reasoning_cost_micro_usd, + audio_cost_micro_usd = :audio_cost_micro_usd, + tts_cost_micro_usd = :tts_cost_micro_usd, + total_cost_micro_usd = :total_cost_micro_usd, + pricing_rate_source = :pricing_rate_source, + pricing_rate_id = CAST(:pricing_rate_id AS uuid), + updated_at = now() + WHERE organization_id = CAST(:organization_id AS uuid) + AND product_section = :product_section + AND model = :model + AND usage_date = CAST(:usage_date AS date) + AND usage_kind = :usage_kind + AND workspace_id IS NOT DISTINCT FROM CAST(:workspace_id AS uuid) + AND COALESCE(context->>'resource_id', '') = :context_resource_id + AND COALESCE(context->>'resource_type', '') = :context_resource_type + """ + ), + update_params, + ) + return bool(result.rowcount) + + +def recompute_usage_costs( + db: Session, + *, + organization_id: Optional[UUID] = None, + model: Optional[str] = None, + usage_kind: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, + batch_size: int = 500, + on_progress: Optional[Callable[[int], None]] = None, +) -> int: + """Recompute stored costs for existing rollup rows (backfill / override changes).""" + resolver = PricingResolver(db) + updated = 0 + last_id: Optional[str] = None + + while True: + params: Dict[str, Any] = {"batch_size": batch_size} + filters = ["1=1"] + if organization_id is not None: + filters.append("organization_id = CAST(:organization_id AS uuid)") + params["organization_id"] = str(organization_id) + if model is not None: + filters.append("model = :model") + params["model"] = model + if usage_kind is not None: + filters.append("usage_kind = :usage_kind") + params["usage_kind"] = usage_kind + if start_date is not None: + filters.append("usage_date >= CAST(:start_date AS date)") + params["start_date"] = start_date.isoformat() + if end_date is not None: + filters.append("usage_date <= CAST(:end_date AS date)") + params["end_date"] = end_date.isoformat() + if last_id is not None: + filters.append("id > CAST(:last_id AS uuid)") + params["last_id"] = last_id + + rows = db.execute( + text( + f""" + SELECT + id, organization_id, workspace_id, product_section, model, + context, usage_date, usage_kind + FROM llm_usage_daily + WHERE {' AND '.join(filters)} + ORDER BY id + LIMIT :batch_size + """ + ), + params, + ).mappings().all() + if not rows: + break + + for row in rows: + bucket = { + "workspace_id": row["workspace_id"], + "product_section": row["product_section"], + "model": row["model"], + "context": row["context"] or {}, + "usage_date": row["usage_date"], + "usage_kind": row["usage_kind"] or USAGE_KIND_LLM, + } + if apply_cost_to_bucket( + db, + organization_id=row["organization_id"], + bucket=bucket, + resolver=resolver, + ): + updated += 1 + last_id = str(row["id"]) + + db.commit() + if on_progress is not None: + on_progress(updated) + + if on_progress is not None: + on_progress(updated) + + return updated diff --git a/app/services/usage/pricing_cache.py b/app/services/usage/pricing_cache.py new file mode 100644 index 00000000..dd2ebb0f --- /dev/null +++ b/app/services/usage/pricing_cache.py @@ -0,0 +1,87 @@ +"""Redis cache for resolved usage pricing rates.""" + +from __future__ import annotations + +import json +from datetime import date +from typing import Optional +from uuid import UUID + +import redis +from loguru import logger + +from app.config import settings + +PRICING_CACHE_TTL_SEC = 3600 +PRICING_NULL_CACHE_TTL_SEC = 300 +PRICING_CACHE_PREFIX = "usage:pricing" + +_redis: redis.Redis | None = None + + +def _client() -> redis.Redis: + global _redis + if _redis is None: + _redis = redis.from_url(settings.REDIS_URL, decode_responses=True) + return _redis + + +def pricing_cache_key( + *, + organization_id: UUID, + model: str, + usage_kind: str, + usage_date: date, +) -> str: + return ( + f"{PRICING_CACHE_PREFIX}:{organization_id}:{model}:" + f"{usage_kind}:{usage_date.isoformat()}" + ) + + +def get_cached_rate_payload(key: str) -> Optional[dict]: + try: + raw = _client().get(key) + except redis.RedisError as exc: + logger.debug("pricing cache read skipped: {}", exc) + return None + if raw is None: + return None + if raw == "__null__": + return {} + try: + payload = json.loads(raw) + except json.JSONDecodeError: + return None + return payload if isinstance(payload, dict) else None + + +def set_cached_rate_payload(key: str, payload: Optional[dict]) -> None: + try: + value = "__null__" if not payload else json.dumps(payload) + ttl = PRICING_NULL_CACHE_TTL_SEC if not payload else PRICING_CACHE_TTL_SEC + _client().setex(key, ttl, value) + except redis.RedisError as exc: + logger.debug("pricing cache write skipped: {}", exc) + + +def invalidate_org_pricing_cache(organization_id: UUID) -> None: + _invalidate_pricing_cache_pattern(f"{PRICING_CACHE_PREFIX}:{organization_id}:*") + + +def invalidate_all_pricing_cache() -> None: + _invalidate_pricing_cache_pattern(f"{PRICING_CACHE_PREFIX}:*") + + +def _invalidate_pricing_cache_pattern(pattern: str) -> None: + try: + client = _client() + cursor = 0 + while True: + cursor, keys = client.scan(cursor=cursor, match=pattern, count=200) + if keys: + client.delete(*keys) + if cursor == 0: + break + except redis.RedisError as exc: + logger.debug("pricing cache invalidate skipped: {}", exc) diff --git a/app/services/usage/pricing_jobs.py b/app/services/usage/pricing_jobs.py new file mode 100644 index 00000000..5e01947a --- /dev/null +++ b/app/services/usage/pricing_jobs.py @@ -0,0 +1,145 @@ +"""Usage cost recompute job lifecycle.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +from typing import Any, Dict, Optional +from uuid import UUID + +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from app.models.database import UsageCostRecomputeJob + +ACTIVE_JOB_STATUSES = ("pending", "running") +TERMINAL_JOB_STATUSES = ("completed", "failed") + + +def _utcnow() -> datetime: + return datetime.now(timezone.utc) + + +def get_recompute_job( + db: Session, + *, + organization_id: UUID, + job_id: UUID, +) -> UsageCostRecomputeJob: + job = ( + db.query(UsageCostRecomputeJob) + .filter( + UsageCostRecomputeJob.id == job_id, + UsageCostRecomputeJob.organization_id == organization_id, + ) + .first() + ) + if job is None: + raise HTTPException(status_code=404, detail="Recompute job not found") + return job + + +def create_recompute_job( + db: Session, + *, + organization_id: UUID, + model: Optional[str] = None, + usage_kind: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None, +) -> UsageCostRecomputeJob: + active = ( + db.query(UsageCostRecomputeJob) + .filter( + UsageCostRecomputeJob.organization_id == organization_id, + UsageCostRecomputeJob.status.in_(ACTIVE_JOB_STATUSES), + ) + .first() + ) + if active is not None: + raise HTTPException( + status_code=409, + detail="A usage cost recompute job is already in progress", + ) + + job = UsageCostRecomputeJob( + organization_id=organization_id, + status="pending", + model=model, + usage_kind=usage_kind, + start_date=start_date, + end_date=end_date, + ) + db.add(job) + db.commit() + db.refresh(job) + return job + + +def enqueue_recompute_job(db: Session, job: UsageCostRecomputeJob) -> str: + from app.workers.tasks import recompute_usage_costs_task + + result = recompute_usage_costs_task.delay(job_id=str(job.id)) + job.celery_task_id = result.id + job.updated_at = _utcnow() + db.commit() + return result.id + + +def mark_job_running(db: Session, job_id: UUID) -> None: + job = db.query(UsageCostRecomputeJob).filter(UsageCostRecomputeJob.id == job_id).first() + if job is None: + return + job.status = "running" + job.updated_at = _utcnow() + db.commit() + + +def update_job_progress(db: Session, job_id: UUID, updated_rows: int) -> None: + job = db.query(UsageCostRecomputeJob).filter(UsageCostRecomputeJob.id == job_id).first() + if job is None: + return + job.updated_rows = updated_rows + job.updated_at = _utcnow() + db.commit() + + +def mark_job_completed(db: Session, job_id: UUID, updated_rows: int) -> None: + job = db.query(UsageCostRecomputeJob).filter(UsageCostRecomputeJob.id == job_id).first() + if job is None: + return + now = _utcnow() + job.status = "completed" + job.updated_rows = updated_rows + job.updated_at = now + job.completed_at = now + db.commit() + + +def mark_job_failed(db: Session, job_id: UUID, error_message: str) -> None: + job = db.query(UsageCostRecomputeJob).filter(UsageCostRecomputeJob.id == job_id).first() + if job is None: + return + now = _utcnow() + job.status = "failed" + job.error_message = error_message[:4000] + job.updated_at = now + job.completed_at = now + db.commit() + + +def job_to_dict(job: UsageCostRecomputeJob) -> Dict[str, Any]: + return { + "id": job.id, + "organization_id": job.organization_id, + "status": job.status, + "model": job.model, + "usage_kind": job.usage_kind, + "start_date": job.start_date, + "end_date": job.end_date, + "updated_rows": int(job.updated_rows or 0), + "error_message": job.error_message, + "celery_task_id": job.celery_task_id, + "created_at": job.created_at, + "updated_at": job.updated_at, + "completed_at": job.completed_at, + } diff --git a/app/services/usage/pricing_ops.py b/app/services/usage/pricing_ops.py new file mode 100644 index 00000000..abd55d4b --- /dev/null +++ b/app/services/usage/pricing_ops.py @@ -0,0 +1,137 @@ +"""Ops helpers for usage pricing catalog maintenance.""" + +from __future__ import annotations + +import json +from datetime import date +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.services.usage.pricing import ( + DEFAULT_RATES_EFFECTIVE_FROM, + _pricing_entries_from_models_json, + _rates_table, + seed_pricing_rates, +) + +_MODELS_JSON_PATH = ( + Path(__file__).resolve().parent.parent.parent / "config" / "models.json" +) +_CATALOG_JSON_PATH = ( + Path(__file__).resolve().parent.parent.parent / "config" / "pricing_catalog.json" +) + +_RATE_COMPARE_COLUMNS = ( + "input_micro_usd_per_million", + "output_micro_usd_per_million", + "cache_read_micro_usd_per_million", + "cache_creation_micro_usd_per_million", + "reasoning_micro_usd_per_million", + "audio_micro_usd_per_second", + "tts_micro_usd_per_million_chars", +) + + +def models_missing_pricing_blocks() -> List[str]: + if not _MODELS_JSON_PATH.exists(): + return [] + payload = json.loads(_MODELS_JSON_PATH.read_text(encoding="utf-8")) + missing: List[str] = [] + for model_name, config in payload.items(): + if model_name.startswith("_") or not isinstance(config, dict): + continue + pricing = config.get("pricing") + if not isinstance(pricing, dict): + missing.append(model_name) + return sorted(missing) + + +def litellm_unresolved_models() -> List[Dict[str, Any]]: + if not _CATALOG_JSON_PATH.exists(): + return [] + payload = json.loads(_CATALOG_JSON_PATH.read_text(encoding="utf-8")) + meta = payload.get("_metadata") + if not isinstance(meta, dict): + return [] + unresolved = meta.get("unresolved") + return unresolved if isinstance(unresolved, list) else [] + + +def _load_db_rates( + db: Session, *, effective_from: date +) -> Dict[Tuple[str, str], Dict[str, Any]]: + table = _rates_table(db) + rows = db.execute( + text( + f""" + SELECT model, usage_kind, {_rate_columns_sql()} + FROM {table} + WHERE effective_from = CAST(:effective_from AS date) + """ + ), + {"effective_from": effective_from.isoformat()}, + ).mappings().all() + return {(row["model"], row["usage_kind"]): dict(row) for row in rows} + + +def _rate_columns_sql() -> str: + return ", ".join(_RATE_COMPARE_COLUMNS) + + +def diff_models_json_vs_db( + db: Session, *, effective_from: Optional[date] = None +) -> Dict[str, Any]: + day = effective_from or DEFAULT_RATES_EFFECTIVE_FROM + json_rates = _pricing_entries_from_models_json() + db_rates = _load_db_rates(db, effective_from=day) + + json_keys = {(model, entry.get("usage_kind") or "llm") for model, entry in json_rates.items()} + db_keys = set(db_rates.keys()) + + only_in_json = sorted(json_keys - db_keys) + only_in_db = sorted(db_keys - json_keys) + mismatches: List[Dict[str, Any]] = [] + + for key in sorted(json_keys & db_keys): + model, usage_kind = key + expected = json_rates[model] + actual = db_rates[key] + field_diffs: Dict[str, Dict[str, int]] = {} + for column in _RATE_COMPARE_COLUMNS: + left = int(expected.get(column) or 0) + right = int(actual.get(column) or 0) + if left != right: + field_diffs[column] = {"models_json": left, "database": right} + if field_diffs: + mismatches.append( + { + "model": model, + "usage_kind": usage_kind, + "fields": field_diffs, + } + ) + + return { + "effective_from": day.isoformat(), + "models_json_count": len(json_rates), + "database_count": len(db_rates), + "only_in_models_json": [ + {"model": model, "usage_kind": kind} for model, kind in only_in_json + ], + "only_in_database": [ + {"model": model, "usage_kind": kind} for model, kind in only_in_db + ], + "mismatches": mismatches, + "missing_pricing_blocks": models_missing_pricing_blocks(), + "litellm_unresolved": litellm_unresolved_models(), + "in_sync": not only_in_json and not only_in_db and not mismatches, + } + + +def seed_rates_from_models_json( + db: Session, *, effective_from: Optional[date] = None +) -> int: + return seed_pricing_rates(db, effective_from=effective_from) diff --git a/app/services/usage/pricing_overrides.py b/app/services/usage/pricing_overrides.py new file mode 100644 index 00000000..7f55ef01 --- /dev/null +++ b/app/services/usage/pricing_overrides.py @@ -0,0 +1,530 @@ +"""Org-level usage pricing overrides.""" + +from __future__ import annotations + +from datetime import date, timedelta +from typing import Any, Dict, List, Optional, Set +from uuid import UUID + +from fastapi import HTTPException +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.services.usage.pricing import ( + PricingResolver, + USAGE_KIND_LLM, + USAGE_KIND_STT, + USAGE_KIND_TTS, + _int, + _pricing_entries_from_models_json, + _rates_table, + _usd_per_million_to_micro, + _usd_per_minute_to_micro_per_second, +) +from app.services.usage.pricing_cache import invalidate_org_pricing_cache +from app.services.usage.pricing_jobs import create_recompute_job, enqueue_recompute_job +from app.services.usage.usage_costs import micro_to_usd + +RATE_COLUMNS = ( + "input_micro_usd_per_million", + "output_micro_usd_per_million", + "cache_read_micro_usd_per_million", + "cache_creation_micro_usd_per_million", + "reasoning_micro_usd_per_million", + "audio_micro_usd_per_second", + "tts_micro_usd_per_million_chars", +) + +USD_RATE_FIELDS = { + "input_per_1m": "input_micro_usd_per_million", + "output_per_1m": "output_micro_usd_per_million", + "cache_read_per_1m": "cache_read_micro_usd_per_million", + "cache_write_per_1m": "cache_creation_micro_usd_per_million", + "reasoning_per_1m": "reasoning_micro_usd_per_million", + "audio_per_minute": "audio_micro_usd_per_second", + "tts_per_1m_characters": "tts_micro_usd_per_million_chars", +} + + +def _known_models(db: Session) -> Set[str]: + models = set(_pricing_entries_from_models_json().keys()) + table = _rates_table(db) + rows = db.execute(text(f"SELECT DISTINCT model FROM {table}")).scalars().all() + models.update(rows) + return models + + +def validate_model_name(db: Session, model: str) -> None: + if model not in _known_models(db): + raise HTTPException(status_code=400, detail=f"Unknown model: {model}") + + +def _validate_usage_kind(usage_kind: str) -> str: + kind = usage_kind or USAGE_KIND_LLM + if kind not in {USAGE_KIND_LLM, USAGE_KIND_STT, USAGE_KIND_TTS}: + raise HTTPException(status_code=400, detail=f"Invalid usage_kind: {kind}") + return kind + + +def _micro_to_optional_usd_per_1m(micro: Optional[int]) -> Optional[float]: + if micro is None: + return None + return micro_to_usd(_int(micro)) + + +def _micro_to_optional_usd_per_minute(micro_per_second: Optional[int]) -> Optional[float]: + if micro_per_second is None: + return None + return micro_to_usd(_int(micro_per_second) * 60) + + +def _rates_usd_from_row(row: Dict[str, Any]) -> Dict[str, Optional[float]]: + return { + "input_per_1m": _micro_to_optional_usd_per_1m(row.get("input_micro_usd_per_million")), + "output_per_1m": _micro_to_optional_usd_per_1m(row.get("output_micro_usd_per_million")), + "cache_read_per_1m": _micro_to_optional_usd_per_1m( + row.get("cache_read_micro_usd_per_million") + ), + "cache_write_per_1m": _micro_to_optional_usd_per_1m( + row.get("cache_creation_micro_usd_per_million") + ), + "reasoning_per_1m": _micro_to_optional_usd_per_1m( + row.get("reasoning_micro_usd_per_million") + ), + "audio_per_minute": _micro_to_optional_usd_per_minute( + row.get("audio_micro_usd_per_second") + ), + "tts_per_1m_characters": _micro_to_optional_usd_per_1m( + row.get("tts_micro_usd_per_million_chars") + ), + } + + +def _rate_card_to_usd_dict(card) -> Dict[str, float]: + return { + "input_per_1m": micro_to_usd(card.input_micro_usd_per_million), + "output_per_1m": micro_to_usd(card.output_micro_usd_per_million), + "cache_read_per_1m": micro_to_usd(card.cache_read_micro_usd_per_million), + "cache_write_per_1m": micro_to_usd(card.cache_creation_micro_usd_per_million), + "reasoning_per_1m": micro_to_usd(card.reasoning_micro_usd_per_million), + "audio_per_minute": micro_to_usd(card.audio_micro_usd_per_second * 60), + "tts_per_1m_characters": micro_to_usd(card.tts_micro_usd_per_million_chars), + } + + +def _override_row_to_dict(row: Any) -> Dict[str, Any]: + payload = dict(row) + for key in ("id", "organization_id"): + if payload.get(key) is not None: + payload[key] = str(payload[key]) + rates = _rates_usd_from_row(payload) + return { + "id": payload["id"], + "organization_id": payload["organization_id"], + "model": payload["model"], + "usage_kind": payload["usage_kind"], + "effective_from": payload["effective_from"], + "effective_to": payload.get("effective_to"), + "rates": rates, + "created_at": payload.get("created_at"), + "updated_at": payload.get("updated_at"), + } + + +def _usd_payload_to_micro_columns(payload: Dict[str, Any]) -> Dict[str, Optional[int]]: + columns: Dict[str, Optional[int]] = {} + for usd_field, micro_column in USD_RATE_FIELDS.items(): + if usd_field not in payload: + continue + value = payload.get(usd_field) + if value is None: + columns[micro_column] = None + continue + if usd_field == "audio_per_minute": + columns[micro_column] = _usd_per_minute_to_micro_per_second(value) + else: + columns[micro_column] = _usd_per_million_to_micro(value) + return columns + + +def list_overrides( + db: Session, + *, + organization_id: UUID, + model: Optional[str] = None, + usage_kind: Optional[str] = None, +) -> List[Dict[str, Any]]: + filters = ["organization_id = CAST(:organization_id AS uuid)"] + params: Dict[str, Any] = {"organization_id": str(organization_id)} + if model is not None: + filters.append("model = :model") + params["model"] = model + if usage_kind is not None: + filters.append("usage_kind = :usage_kind") + params["usage_kind"] = _validate_usage_kind(usage_kind) + + rows = db.execute( + text( + f""" + SELECT * + FROM org_model_pricing_overrides + WHERE {' AND '.join(filters)} + ORDER BY model ASC, usage_kind ASC, effective_from DESC + """ + ), + params, + ).mappings().all() + return [_override_row_to_dict(row) for row in rows] + + +def get_effective_rate( + db: Session, + *, + organization_id: UUID, + model: str, + usage_kind: str, + as_of: date, +) -> Dict[str, Any]: + validate_model_name(db, model) + kind = _validate_usage_kind(usage_kind) + resolver = PricingResolver(db) + effective = resolver.resolve_rate( + organization_id=organization_id, + model=model, + usage_kind=kind, + usage_date=as_of, + ) + catalog = resolver._resolve_catalog(model, kind, as_of) + override_row = db.execute( + text( + """ + SELECT * + FROM org_model_pricing_overrides + WHERE organization_id = CAST(:organization_id AS uuid) + AND model = :model + AND usage_kind = :usage_kind + AND effective_from <= :as_of + AND (effective_to IS NULL OR effective_to >= :as_of) + ORDER BY effective_from DESC + LIMIT 1 + """ + ), + { + "organization_id": str(organization_id), + "model": model, + "usage_kind": kind, + "as_of": as_of.isoformat(), + }, + ).mappings().first() + + return { + "model": model, + "usage_kind": kind, + "as_of": as_of, + "catalog_rates": _rate_card_to_usd_dict(catalog) if catalog else None, + "catalog_rate_id": str(catalog.rate_id) if catalog else None, + "override": _override_row_to_dict(override_row) if override_row else None, + "effective_rates": _rate_card_to_usd_dict(effective) if effective else None, + "effective_source": effective.source if effective else None, + "effective_rate_id": str(effective.rate_id) if effective else None, + "has_override": override_row is not None, + } + + +def list_effective_pricing( + db: Session, + *, + organization_id: UUID, + usage_kind: Optional[str] = None, + model: Optional[str] = None, + as_of: date, + limit: int = 200, +) -> List[Dict[str, Any]]: + if model: + return [ + get_effective_rate( + db, + organization_id=organization_id, + model=model, + usage_kind=usage_kind or USAGE_KIND_LLM, + as_of=as_of, + ) + ] + + keys: Set[tuple[str, str]] = set() + for entry in list_overrides(db, organization_id=organization_id, usage_kind=usage_kind): + keys.add((entry["model"], entry["usage_kind"])) + + table = _rates_table(db) + rate_filters = ["effective_from <= :as_of", "(effective_to IS NULL OR effective_to >= :as_of)"] + params: Dict[str, Any] = {"as_of": as_of.isoformat(), "limit": limit} + if usage_kind is not None: + rate_filters.append("usage_kind = :usage_kind") + params["usage_kind"] = _validate_usage_kind(usage_kind) + rate_rows = db.execute( + text( + f""" + SELECT DISTINCT model, usage_kind + FROM {table} + WHERE {' AND '.join(rate_filters)} + ORDER BY model ASC + LIMIT :limit + """ + ), + params, + ).mappings().all() + for row in rate_rows: + keys.add((row["model"], row["usage_kind"])) + + results: List[Dict[str, Any]] = [] + for model_name, kind in sorted(keys)[:limit]: + results.append( + get_effective_rate( + db, + organization_id=organization_id, + model=model_name, + usage_kind=kind, + as_of=as_of, + ) + ) + return results + + +def upsert_override( + db: Session, + *, + organization_id: UUID, + model: str, + usage_kind: str, + effective_from: date, + effective_to: Optional[date] = None, + rates: Dict[str, Any], + recompute: bool = True, +) -> Dict[str, Any]: + validate_model_name(db, model) + kind = _validate_usage_kind(usage_kind) + if effective_to is not None and effective_to < effective_from: + raise HTTPException(status_code=400, detail="effective_to must be >= effective_from") + + micro_columns = _usd_payload_to_micro_columns(rates) + if not micro_columns: + raise HTTPException(status_code=400, detail="At least one rate field is required") + + params: Dict[str, Any] = { + "organization_id": str(organization_id), + "model": model, + "usage_kind": kind, + "effective_from": effective_from.isoformat(), + "effective_to": effective_to.isoformat() if effective_to else None, + } + for column in RATE_COLUMNS: + params[column] = micro_columns.get(column) + + row = db.execute( + text( + """ + INSERT INTO org_model_pricing_overrides ( + organization_id, model, usage_kind, effective_from, effective_to, + input_micro_usd_per_million, output_micro_usd_per_million, + cache_read_micro_usd_per_million, cache_creation_micro_usd_per_million, + reasoning_micro_usd_per_million, audio_micro_usd_per_second, + tts_micro_usd_per_million_chars + ) VALUES ( + CAST(:organization_id AS uuid), :model, :usage_kind, + CAST(:effective_from AS date), CAST(:effective_to AS date), + :input_micro_usd_per_million, :output_micro_usd_per_million, + :cache_read_micro_usd_per_million, :cache_creation_micro_usd_per_million, + :reasoning_micro_usd_per_million, :audio_micro_usd_per_second, + :tts_micro_usd_per_million_chars + ) + ON CONFLICT (organization_id, model, usage_kind, effective_from) + DO UPDATE SET + effective_to = EXCLUDED.effective_to, + input_micro_usd_per_million = COALESCE( + EXCLUDED.input_micro_usd_per_million, + org_model_pricing_overrides.input_micro_usd_per_million + ), + output_micro_usd_per_million = COALESCE( + EXCLUDED.output_micro_usd_per_million, + org_model_pricing_overrides.output_micro_usd_per_million + ), + cache_read_micro_usd_per_million = COALESCE( + EXCLUDED.cache_read_micro_usd_per_million, + org_model_pricing_overrides.cache_read_micro_usd_per_million + ), + cache_creation_micro_usd_per_million = COALESCE( + EXCLUDED.cache_creation_micro_usd_per_million, + org_model_pricing_overrides.cache_creation_micro_usd_per_million + ), + reasoning_micro_usd_per_million = COALESCE( + EXCLUDED.reasoning_micro_usd_per_million, + org_model_pricing_overrides.reasoning_micro_usd_per_million + ), + audio_micro_usd_per_second = COALESCE( + EXCLUDED.audio_micro_usd_per_second, + org_model_pricing_overrides.audio_micro_usd_per_second + ), + tts_micro_usd_per_million_chars = COALESCE( + EXCLUDED.tts_micro_usd_per_million_chars, + org_model_pricing_overrides.tts_micro_usd_per_million_chars + ), + updated_at = now() + RETURNING * + """ + ), + params, + ).mappings().first() + db.commit() + + invalidate_org_pricing_cache(organization_id) + + recompute_job_id = None + recompute_enqueued = False + if recompute: + recompute_job_id = _enqueue_override_recompute( + db, + organization_id=organization_id, + model=model, + usage_kind=kind, + start_date=effective_from, + end_date=effective_to, + ) + recompute_enqueued = recompute_job_id is not None + + result = _override_row_to_dict(row) + result["recompute_enqueued"] = recompute_enqueued + result["recompute_job_id"] = recompute_job_id + return result + + +def delete_override( + db: Session, + *, + organization_id: UUID, + model: str, + usage_kind: str, + effective_from: Optional[date] = None, + recompute: bool = True, +) -> Dict[str, Any]: + validate_model_name(db, model) + kind = _validate_usage_kind(usage_kind) + filters = [ + "organization_id = CAST(:organization_id AS uuid)", + "model = :model", + "usage_kind = :usage_kind", + ] + params: Dict[str, Any] = { + "organization_id": str(organization_id), + "model": model, + "usage_kind": kind, + } + + if effective_from is not None: + filters.append("effective_from = CAST(:effective_from AS date)") + params["effective_from"] = effective_from.isoformat() + result = db.execute( + text( + f""" + DELETE FROM org_model_pricing_overrides + WHERE {' AND '.join(filters)} + RETURNING effective_from, effective_to + """ + ), + params, + ).mappings().first() + if not result: + raise HTTPException(status_code=404, detail="Override not found") + recompute_from = effective_from + recompute_to = result.get("effective_to") + db.commit() + else: + active = db.execute( + text( + f""" + SELECT id, effective_from, effective_to + FROM org_model_pricing_overrides + WHERE {' AND '.join(filters)} + AND effective_from <= CURRENT_DATE + AND (effective_to IS NULL OR effective_to >= CURRENT_DATE) + ORDER BY effective_from DESC + LIMIT 1 + """ + ), + params, + ).mappings().first() + if not active: + raise HTTPException(status_code=404, detail="No active override found") + recompute_from = active["effective_from"] + recompute_to = active.get("effective_to") + yesterday = date.today() - timedelta(days=1) + if active["effective_from"] > yesterday: + db.execute( + text( + """ + DELETE FROM org_model_pricing_overrides + WHERE id = CAST(:id AS uuid) + """ + ), + {"id": str(active["id"])}, + ) + else: + db.execute( + text( + """ + UPDATE org_model_pricing_overrides + SET effective_to = CAST(:effective_to AS date), updated_at = now() + WHERE id = CAST(:id AS uuid) + """ + ), + {"id": str(active["id"]), "effective_to": yesterday.isoformat()}, + ) + db.commit() + + invalidate_org_pricing_cache(organization_id) + + recompute_job_id = None + recompute_enqueued = False + if recompute: + recompute_job_id = _enqueue_override_recompute( + db, + organization_id=organization_id, + model=model, + usage_kind=kind, + start_date=recompute_from, + end_date=recompute_to, + ) + recompute_enqueued = recompute_job_id is not None + + return { + "deleted": True, + "model": model, + "usage_kind": kind, + "recompute_enqueued": recompute_enqueued, + "recompute_job_id": recompute_job_id, + } + + +def _enqueue_override_recompute( + db: Session, + *, + organization_id: UUID, + model: str, + usage_kind: str, + start_date: date, + end_date: Optional[date], +) -> Optional[str]: + try: + job = create_recompute_job( + db, + organization_id=organization_id, + model=model, + usage_kind=usage_kind, + start_date=start_date, + end_date=end_date, + ) + enqueue_recompute_job(db, job) + return str(job.id) + except HTTPException as exc: + if exc.status_code == 409: + return None + raise diff --git a/app/services/usage/usage_costs.py b/app/services/usage/usage_costs.py new file mode 100644 index 00000000..914de0b1 --- /dev/null +++ b/app/services/usage/usage_costs.py @@ -0,0 +1,51 @@ +"""Usage cost presentation helpers for API responses.""" + +from __future__ import annotations + +from typing import Any, Dict, Optional + +MICRO_USD_PER_DOLLAR = 1_000_000 + + +def micro_to_usd(micro: int) -> float: + return micro / MICRO_USD_PER_DOLLAR + + +def costs_from_micro( + *, + input_cost_micro_usd: int = 0, + output_cost_micro_usd: int = 0, + cache_read_cost_micro_usd: int = 0, + cache_creation_cost_micro_usd: int = 0, + reasoning_cost_micro_usd: int = 0, + audio_cost_micro_usd: int = 0, + tts_cost_micro_usd: int = 0, + total_cost_micro_usd: Optional[int] = None, + has_unpriced_usage: bool = False, + currency: str = "USD", +) -> Dict[str, Any]: + total_micro = ( + total_cost_micro_usd + if total_cost_micro_usd is not None + else ( + input_cost_micro_usd + + output_cost_micro_usd + + cache_read_cost_micro_usd + + cache_creation_cost_micro_usd + + reasoning_cost_micro_usd + + audio_cost_micro_usd + + tts_cost_micro_usd + ) + ) + return { + "input_cost_usd": micro_to_usd(input_cost_micro_usd), + "output_cost_usd": micro_to_usd(output_cost_micro_usd), + "cache_read_cost_usd": micro_to_usd(cache_read_cost_micro_usd), + "cache_write_cost_usd": micro_to_usd(cache_creation_cost_micro_usd), + "reasoning_cost_usd": micro_to_usd(reasoning_cost_micro_usd), + "audio_cost_usd": micro_to_usd(audio_cost_micro_usd), + "tts_cost_usd": micro_to_usd(tts_cost_micro_usd), + "total_cost_usd": micro_to_usd(total_micro), + "currency": currency, + "has_unpriced_usage": has_unpriced_usage, + } diff --git a/app/workers/config.py b/app/workers/config.py index 3448429d..e06336d0 100644 --- a/app/workers/config.py +++ b/app/workers/config.py @@ -74,6 +74,7 @@ # Queues consumed by the dedicated call-import / evaluation worker. IMPORTS_WORKER_QUEUES = "imports,diarization,eval-control,evaluations" EVAL_CONTROL_QUEUE = "eval-control" +USAGE_WORKER_QUEUE = "usage" # Create Celery app celery_app = Celery( @@ -137,13 +138,16 @@ "generate_evaluation_prompt_improvements": {"queue": "evaluations"}, "generate_agent_flowchart": {"queue": "celery"}, "map_agent_flowchart_prompt_sections": {"queue": "celery"}, - "flush_usage_counters": {"queue": "celery"}, + "flush_usage_counters": {"queue": USAGE_WORKER_QUEUE}, + "recompute_usage_costs": {"queue": USAGE_WORKER_QUEUE}, } -# Periodic flush of Redis LLM usage counters into catalog rollups. +# Periodic flush of Redis usage counters into catalog rollups (usage queue). +_flush_interval = float(os.environ.get("USAGE_FLUSH_BEAT_SECONDS", "120")) celery_app.conf.beat_schedule = { "flush-llm-usage-counters": { "task": "flush_usage_counters", - "schedule": 120.0, + "schedule": _flush_interval, + "options": {"queue": USAGE_WORKER_QUEUE}, }, } diff --git a/app/workers/tasks/__init__.py b/app/workers/tasks/__init__.py index 04eb0380..499d9a5d 100644 --- a/app/workers/tasks/__init__.py +++ b/app/workers/tasks/__init__.py @@ -23,6 +23,7 @@ from . import finalize_telephony_recording from . import call_import_bulk_ops from . import flush_usage_counters +from . import recompute_usage_costs from app.workers.concurrency import eval_dispatch from app.workers.concurrency import fair_dispatch from app.workers.concurrency import fair_diarization_dispatch @@ -117,3 +118,4 @@ call_import_bulk_ops.materialize_call_import_evaluation_task ) flush_usage_counters_task = flush_usage_counters.flush_usage_counters_task +recompute_usage_costs_task = recompute_usage_costs.recompute_usage_costs_task diff --git a/app/workers/tasks/recompute_usage_costs.py b/app/workers/tasks/recompute_usage_costs.py new file mode 100644 index 00000000..8340f797 --- /dev/null +++ b/app/workers/tasks/recompute_usage_costs.py @@ -0,0 +1,79 @@ +"""Celery task: recompute stored usage costs for rollup rows.""" + +from __future__ import annotations + +from datetime import date +from typing import Optional +from uuid import UUID + +from loguru import logger + +from app.database import SessionLocal +from app.workers.config import celery_app + + +@celery_app.task(name="recompute_usage_costs") +def recompute_usage_costs_task( + job_id: Optional[str] = None, + organization_id: Optional[str] = None, + model: Optional[str] = None, + usage_kind: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, +) -> dict: + from app.models.database import UsageCostRecomputeJob + from app.services.usage.pricing import recompute_usage_costs + from app.services.usage.pricing_jobs import ( + mark_job_completed, + mark_job_failed, + mark_job_running, + update_job_progress, + ) + + db = SessionLocal() + try: + if job_id: + job = ( + db.query(UsageCostRecomputeJob) + .filter(UsageCostRecomputeJob.id == UUID(job_id)) + .first() + ) + if job is None: + return {"updated_rows": 0, "error": "job not found"} + mark_job_running(db, job.id) + org_uuid = job.organization_id + model = job.model + usage_kind = job.usage_kind + start = job.start_date + end = job.end_date + progress_job_id = job.id + else: + org_uuid = UUID(organization_id) if organization_id else None + start = date.fromisoformat(start_date) if start_date else None + end = date.fromisoformat(end_date) if end_date else None + progress_job_id = None + + def _on_progress(updated_rows: int) -> None: + if progress_job_id is not None: + update_job_progress(db, progress_job_id, updated_rows) + + updated = recompute_usage_costs( + db, + organization_id=org_uuid, + model=model, + usage_kind=usage_kind, + start_date=start, + end_date=end, + on_progress=_on_progress if progress_job_id else None, + ) + if progress_job_id is not None: + mark_job_completed(db, progress_job_id, updated) + if updated: + logger.info("Recomputed usage costs for {} rollup row(s)", updated) + return {"updated_rows": updated, "job_id": job_id} + except Exception as exc: + if job_id: + mark_job_failed(db, UUID(job_id), str(exc)) + raise + finally: + db.close() diff --git a/docker-compose.observability.yml b/docker-compose.observability.yml index 402ac2e0..35b84190 100644 --- a/docker-compose.observability.yml +++ b/docker-compose.observability.yml @@ -62,6 +62,17 @@ services: loki-max-backoff: "800ms" loki-external-labels: "service=worker-imports,environment=docker" + worker-usage: + depends_on: + - loki + logging: + driver: loki + options: + loki-url: "http://localhost:3100/loki/api/v1/push" + loki-retries: "5" + loki-max-backoff: "800ms" + loki-external-labels: "service=worker-usage,environment=docker" + # Pin image tags instead of :latest to reduce exposure to stale third-party # binaries (e.g. Go stdlib CVEs in observability tooling). loki: diff --git a/docker-compose.yml b/docker-compose.yml index 552884f3..13be6295 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -193,5 +193,62 @@ services: # (each task may hold catalog + shard connections for tens of seconds). command: eai worker --config /app/config.yml --loglevel info --queues imports,diarization,eval-control,evaluations --pool threads --concurrency 12 + # Low-priority usage pricing: Redis flush + cost recompute/backfill. + worker-usage: + image: ghcr.io/efficientai-tech/efficientai-worker:${EFFICIENTAI_VERSION:-latest} + build: + context: . + dockerfile: docker/Dockerfile.worker + args: + INSTALL_EXTRAS: "" + container_name: efficientai_worker_usage + env_file: + - .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} + REDIS_URL: redis://redis:6379/0 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/0 + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./uploads:/app/uploads + - ./.data:/app/.data + - ./config.docker.yml:/app/config.yml:ro + command: eai worker --config /app/config.yml --loglevel info --queues usage --pool threads --concurrency 4 + + beat: + image: ghcr.io/efficientai-tech/efficientai-worker:${EFFICIENTAI_VERSION:-latest} + build: + context: . + dockerfile: docker/Dockerfile.worker + args: + INSTALL_EXTRAS: "" + container_name: efficientai_beat + env_file: + - .env + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-efficientai}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-efficientai} + REDIS_URL: redis://redis:6379/0 + CELERY_BROKER_URL: redis://redis:6379/0 + CELERY_RESULT_BACKEND: redis://redis:6379/0 + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + worker-usage: + condition: service_started + volumes: + - ./uploads:/app/uploads + - ./.data:/app/.data + - ./config.docker.yml:/app/config.yml:ro + command: celery -A app.workers.celery_app beat --loglevel info + volumes: postgres_data: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d18118b0..287bdd2d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import Dashboard from './pages/dashboard/Dashboard' // Usage import Usage from './pages/usage/Usage' +import UsagePricing from './pages/usage/UsagePricing' // Prompt Partials import PromptPartials from './pages/promptPartials/PromptPartials' @@ -186,6 +187,7 @@ function App() { } /> } /> } /> + } /> } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7e2c4369..6dfe8744 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -3017,6 +3017,26 @@ class ApiClient { audio_seconds: number tts_characters: number call_count: number + input_cost_micro_usd?: number + output_cost_micro_usd?: number + cache_read_cost_micro_usd?: number + cache_creation_cost_micro_usd?: number + reasoning_cost_micro_usd?: number + audio_cost_micro_usd?: number + tts_cost_micro_usd?: number + total_cost_micro_usd?: number + costs?: { + input_cost_usd: number + output_cost_usd: number + cache_read_cost_usd: number + cache_write_cost_usd: number + reasoning_cost_usd: number + audio_cost_usd: number + tts_cost_usd: number + total_cost_usd: number + currency: string + has_unpriced_usage: boolean + } } last_updated_at?: string | null }> { @@ -3067,6 +3087,26 @@ class ApiClient { audio_seconds: number tts_characters: number call_count: number + input_cost_micro_usd?: number + output_cost_micro_usd?: number + cache_read_cost_micro_usd?: number + cache_creation_cost_micro_usd?: number + reasoning_cost_micro_usd?: number + audio_cost_micro_usd?: number + tts_cost_micro_usd?: number + total_cost_micro_usd?: number + costs?: { + input_cost_usd: number + output_cost_usd: number + cache_read_cost_usd: number + cache_write_cost_usd: number + reasoning_cost_usd: number + audio_cost_usd: number + tts_cost_usd: number + total_cost_usd: number + currency: string + has_unpriced_usage: boolean + } }> total_count: number truncated_at_limit?: boolean @@ -3078,6 +3118,135 @@ class ApiClient { return response.data } + async listUsagePricingOverrides(params?: { + model?: string + usage_kind?: string + }): Promise< + Array<{ + id: string + organization_id: string + model: string + usage_kind: string + effective_from: string + effective_to?: string | null + rates: { + input_per_1m?: number | null + output_per_1m?: number | null + cache_read_per_1m?: number | null + cache_write_per_1m?: number | null + reasoning_per_1m?: number | null + audio_per_minute?: number | null + tts_per_1m_characters?: number | null + } + recompute_enqueued?: boolean + recompute_job_id?: string | null + }> + > { + const response = await this.client.get('/api/v1/organizations/usage/pricing/overrides', { + params, + }) + return response.data + } + + async upsertUsagePricingOverride( + model: string, + body: { + usage_kind?: string + effective_from: string + effective_to?: string + rates: { + input_per_1m?: number + output_per_1m?: number + cache_read_per_1m?: number + cache_write_per_1m?: number + reasoning_per_1m?: number + audio_per_minute?: number + tts_per_1m_characters?: number + } + recompute?: boolean + } + ): Promise<{ + id: string + model: string + usage_kind: string + effective_from: string + effective_to?: string | null + rates: Record + recompute_enqueued?: boolean + recompute_job_id?: string | null + }> { + const response = await this.client.put( + `/api/v1/organizations/usage/pricing/overrides/${encodeURIComponent(model)}`, + body + ) + return response.data + } + + async deleteUsagePricingOverride( + model: string, + params?: { usage_kind?: string; effective_from?: string; recompute?: boolean } + ): Promise<{ + deleted: boolean + model: string + usage_kind: string + recompute_enqueued?: boolean + recompute_job_id?: string | null + }> { + const response = await this.client.delete( + `/api/v1/organizations/usage/pricing/overrides/${encodeURIComponent(model)}`, + { params } + ) + return response.data + } + + async triggerUsageCostRecompute(body?: { + start_date?: string + end_date?: string + model?: string + usage_kind?: string + }): Promise<{ + id: string + organization_id: string + status: string + model?: string | null + usage_kind?: string | null + start_date?: string | null + end_date?: string | null + updated_rows: number + error_message?: string | null + celery_task_id?: string | null + created_at?: string | null + updated_at?: string | null + completed_at?: string | null + }> { + const response = await this.client.post( + '/api/v1/organizations/usage/pricing/recompute', + body ?? {} + ) + return response.data + } + + async getUsageCostRecomputeJob(jobId: string): Promise<{ + id: string + organization_id: string + status: string + model?: string | null + usage_kind?: string | null + start_date?: string | null + end_date?: string | null + updated_rows: number + error_message?: string | null + celery_task_id?: string | null + created_at?: string | null + updated_at?: string | null + completed_at?: string | null + }> { + const response = await this.client.get( + `/api/v1/organizations/usage/pricing/recompute/${jobId}` + ) + return response.data + } + async getOrgUsageFilters(params?: { start?: string end?: string diff --git a/frontend/src/pages/usage/SearchableSelect.tsx b/frontend/src/pages/usage/SearchableSelect.tsx index 66f625dd..61da046a 100644 --- a/frontend/src/pages/usage/SearchableSelect.tsx +++ b/frontend/src/pages/usage/SearchableSelect.tsx @@ -27,7 +27,9 @@ export default function SearchableSelect({ const [search, setSearch] = useState('') const rootRef = useRef(null) - const selected = options.find((o) => o.id === value) + const selected = options.find( + (o) => o.id === value || o.id.toLowerCase() === value.toLowerCase(), + ) const filtered = useMemo(() => { const q = search.trim().toLowerCase() @@ -102,7 +104,8 @@ export default function SearchableSelect({ + ) : null} + {isAdmin ? ( + + Pricing overrides + + ) : null} +
+ {summary?.last_updated_at ? ( +

+ Updated {new Date(summary.last_updated_at).toLocaleString()} +

+ ) : null} +
- - - - -
+
+ + + + +
- {(showAudio || - showTts || - totals?.cache_read_tokens || - totals?.cache_creation_tokens || - totals?.reasoning_tokens) ? ( -
- {showAudio ? ( +
+
+ {showAudio ? ( + + ) : null} + {showTts ? ( + + ) : null} + {(totals?.cache_read_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.cache_creation_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.reasoning_tokens || 0) > 0 ? ( + + ) : null} +
+
- ) : null} - {showTts ? ( - - ) : null} - {(totals?.cache_read_tokens || 0) > 0 ? ( - - ) : null} - {(totals?.cache_creation_tokens || 0) > 0 ? ( - - ) : null} - {(totals?.reasoning_tokens || 0) > 0 ? ( - - ) : null} +
- ) : null} +
setParams({ start: s, end: e })} @@ -1068,7 +1172,20 @@ export default function Usage() { }) } onEvaluationChange={(id) => { - const resource = filterOptions?.resources?.find((r) => r.id === id) + if (id.startsWith(USAGE_SECTION_SOURCE_PREFIX)) { + const section = id.slice(USAGE_SECTION_SOURCE_PREFIX.length) + setParams({ + product_section: section || null, + resource_id: null, + call_import_id: null, + model: null, + usage_kind: null, + }) + return + } + const resource = filterOptions?.resources?.find( + (r) => idKey(r.id) === idKey(id), + ) setParams({ resource_id: id || null, product_section: id ? resource?.product_section || null : null, @@ -1122,6 +1239,7 @@ export default function Usage() { Input tokens Output tokens Total tokens + Est. cost STT audio TTS chars Cache read @@ -1189,6 +1307,9 @@ export default function Usage() { {formatNumber(row.total_tokens)} + + {formatCostUsd(rowCostUsd(row))} + {row.audio_seconds ? formatAudio(row.audio_seconds) : '—'} @@ -1207,28 +1328,46 @@ export default function Usage() { )} + + setCostBreakdownOpen(false)} + costs={totals?.costs} + scopeLabel={scopeSubtitle} + />
) } +const statCardClass = 'border border-gray-200 ring-1 ring-[#fde047]/25 shadow-sm' + function StatCard({ label, value, valueLabel, loading, + emphasize = false, + className = '', }: { label: string value?: number valueLabel?: string loading?: boolean + emphasize?: boolean + className?: string }) { return ( - + -

- {label} -

-

+

{label}

+

{loading ? '—' : valueLabel ?? formatNumber(value || 0)}

diff --git a/frontend/src/pages/usage/UsageCostBreakdownModal.tsx b/frontend/src/pages/usage/UsageCostBreakdownModal.tsx new file mode 100644 index 00000000..9277de53 --- /dev/null +++ b/frontend/src/pages/usage/UsageCostBreakdownModal.tsx @@ -0,0 +1,133 @@ +import { X } from 'lucide-react' + +type UsageCosts = { + input_cost_usd: number + output_cost_usd: number + cache_read_cost_usd: number + cache_write_cost_usd: number + reasoning_cost_usd: number + audio_cost_usd: number + tts_cost_usd: number + total_cost_usd: number + has_unpriced_usage?: boolean +} + +type Props = { + isOpen: boolean + onClose: () => void + costs?: UsageCosts | null + scopeLabel?: string +} + +function formatCostUsd(usd?: number | null): string { + const amount = Number(usd || 0) + if (!amount) return '$0.00' + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 4, + }).format(amount) +} + +const LINE_ITEMS: Array<{ key: keyof UsageCosts; label: string }> = [ + { key: 'input_cost_usd', label: 'Input tokens' }, + { key: 'output_cost_usd', label: 'Output tokens' }, + { key: 'cache_read_cost_usd', label: 'Cache read' }, + { key: 'cache_write_cost_usd', label: 'Cache write' }, + { key: 'reasoning_cost_usd', label: 'Reasoning' }, + { key: 'audio_cost_usd', label: 'Audio / STT' }, + { key: 'tts_cost_usd', label: 'TTS' }, +] + +export default function UsageCostBreakdownModal({ + isOpen, + onClose, + costs, + scopeLabel, +}: Props) { + if (!isOpen) return null + + const rows = LINE_ITEMS.filter((item) => Number(costs?.[item.key] || 0) > 0) + + return ( +
+
+
+
+
+
+

+ Cost breakdown +

+ {scopeLabel ? ( +

{scopeLabel}

+ ) : null} +
+ +
+ + {costs?.has_unpriced_usage ? ( +

+ Some usage in this range has no pricing rate — estimated cost may be understated. +

+ ) : null} + +
+
+ Estimated total + + {formatCostUsd(costs?.total_cost_usd)} + +
+ {rows.length > 0 ? ( +
    + {rows.map((item) => ( +
  • + {item.label} + + {formatCostUsd(costs?.[item.key] as number)} + +
  • + ))} +
+ ) : ( +

+ No per-metric cost detail for this range. +

+ )} +
+ +
+ +
+
+
+
+ ) +} diff --git a/frontend/src/pages/usage/UsageFiltersBar.tsx b/frontend/src/pages/usage/UsageFiltersBar.tsx index 747fc2ed..eb736667 100644 --- a/frontend/src/pages/usage/UsageFiltersBar.tsx +++ b/frontend/src/pages/usage/UsageFiltersBar.tsx @@ -4,6 +4,7 @@ import { ChevronDown, Filter, SlidersHorizontal } from 'lucide-react' import SearchableSelect from './SearchableSelect' import UsageDateRangePicker from './UsageDateRangePicker' import { usageTheme } from './usageTheme' +import { CALL_IMPORT_PRODUCT_SECTIONS, USAGE_SECTION_SOURCE_PREFIX } from './usageProductHints' type Kind = '' | 'llm' | 'stt' | 'tts' @@ -12,6 +13,7 @@ type FilterOptions = { call_imports: Array<{ id: string; label: string }> evaluations: Array<{ id: string; label: string }> resources?: Array<{ id: string; label: string; type?: string; product_section?: string }> + product_sections?: Array<{ id: string; label: string }> models: string[] usage_kinds: Array<{ id: string; label: string }> datasets?: string[] @@ -30,6 +32,7 @@ type UsageFiltersBarProps = { tagId: string usageKind: Kind model: string + productSection?: string options?: FilterOptions filtersLoading?: boolean onDateApply: (start: string, end: string) => void @@ -60,6 +63,7 @@ export default function UsageFiltersBar({ tagId, usageKind, model, + productSection = '', options, filtersLoading, onDateApply, @@ -86,7 +90,20 @@ export default function UsageFiltersBar({ [options?.usage_kinds], ) - const sourceOptions = callImportId ? evaluations : resources + const sectionSourceOptions = useMemo(() => { + if (callImportId) return [] + return (options?.product_sections ?? []) + .filter((s) => !CALL_IMPORT_PRODUCT_SECTIONS.has(s.id)) + .map((s) => ({ + id: `${USAGE_SECTION_SOURCE_PREFIX}${s.id}`, + label: s.label, + })) + }, [callImportId, options?.product_sections]) + + const sourceOptions = callImportId ? evaluations : [...sectionSourceOptions, ...resources] + const sourceSelectValue = + evaluationId || + (productSection ? `${USAGE_SECTION_SOURCE_PREFIX}${productSection}` : '') const activeChips = useMemo((): ActiveChip[] => { const chips: ActiveChip[] = [] @@ -124,10 +141,10 @@ export default function UsageFiltersBar({ } } const sourceLabel = - sourceOptions.find((e) => e.id === evaluationId)?.label || + sourceOptions.find((e) => e.id === sourceSelectValue)?.label || evaluations.find((e) => e.id === evaluationId)?.label || resources.find((r) => r.id === evaluationId)?.label - if (evaluationId && sourceLabel) { + if ((evaluationId || productSection) && sourceLabel) { chips.push({ key: 'evaluation', label: sourceLabel, @@ -158,6 +175,8 @@ export default function UsageFiltersBar({ tagId, tags, evaluationId, + productSection, + sourceSelectValue, sourceOptions, evaluations, resources, @@ -286,7 +305,7 @@ export default function UsageFiltersBar({ ? 'All evaluations for import' : 'Agents, voice sims, telephony, …' } - value={evaluationId} + value={sourceSelectValue} options={sourceOptions} onChange={onEvaluationChange} emptyMessage="No matching usage in this range" diff --git a/frontend/src/pages/usage/UsagePricing.tsx b/frontend/src/pages/usage/UsagePricing.tsx new file mode 100644 index 00000000..f976d116 --- /dev/null +++ b/frontend/src/pages/usage/UsagePricing.tsx @@ -0,0 +1,393 @@ +import { useMemo, useState, type ReactNode } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { Card, CardBody, Spinner } from '@heroui/react' +import { Link } from 'react-router-dom' +import { ArrowLeft, DollarSign, Info, Plus, Trash2 } from 'lucide-react' +import { apiClient } from '../../lib/api' +import { useIsAdmin } from '../../hooks/useRole' +import { useToast } from '../../hooks/useToast' +import Button from '../../components/Button' +import SearchableSelect from './SearchableSelect' +import { usageTheme } from './usageTheme' + +type UsageKind = 'llm' | 'stt' | 'tts' + +type PricingRatesUsd = { + input_per_1m?: number | null + output_per_1m?: number | null + cache_read_per_1m?: number | null + cache_write_per_1m?: number | null + reasoning_per_1m?: number | null + audio_per_minute?: number | null + tts_per_1m_characters?: number | null +} + +type PricingOverride = { + id: string + model: string + usage_kind: string + effective_from: string + effective_to?: string | null + rates: PricingRatesUsd + recompute_enqueued?: boolean + recompute_job_id?: string | null +} + +type RateFieldKey = keyof PricingRatesUsd + +const EMPTY_RATES: PricingRatesUsd = { + input_per_1m: undefined, + output_per_1m: undefined, + cache_read_per_1m: undefined, + cache_write_per_1m: undefined, + reasoning_per_1m: undefined, + audio_per_minute: undefined, + tts_per_1m_characters: undefined, +} + +const RATE_FIELDS: Array<{ key: RateFieldKey; label: string; kinds: UsageKind[] }> = [ + { key: 'input_per_1m', label: 'Input / 1M tokens (USD)', kinds: ['llm'] }, + { key: 'output_per_1m', label: 'Output / 1M tokens (USD)', kinds: ['llm'] }, + { key: 'cache_read_per_1m', label: 'Cache read / 1M (USD)', kinds: ['llm'] }, + { key: 'cache_write_per_1m', label: 'Cache write / 1M (USD)', kinds: ['llm'] }, + { key: 'reasoning_per_1m', label: 'Reasoning / 1M (USD)', kinds: ['llm'] }, + { key: 'audio_per_minute', label: 'Audio / minute (USD)', kinds: ['llm', 'stt'] }, + { key: 'tts_per_1m_characters', label: 'TTS / 1M characters (USD)', kinds: ['tts'] }, +] + +const CONTROL_CLASS = `h-10 w-full rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm outline-none transition-colors hover:border-gray-300 ${usageTheme.focusRing}` + +function FormField({ + label, + children, + className = '', +}: { + label: string + children: ReactNode + className?: string +}) { + return ( +
+ {label} +
{children}
+
+ ) +} + +function formatUsd(value?: number | null): string { + if (value == null || Number.isNaN(value)) return '—' + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 6, + }).format(value) +} + +function todayIso(): string { + return new Date().toISOString().slice(0, 10) +} + +export default function UsagePricing() { + const isAdmin = useIsAdmin() + const queryClient = useQueryClient() + const { showToast, ToastContainer } = useToast() + + const [model, setModel] = useState('') + const [usageKind, setUsageKind] = useState('llm') + const [effectiveFrom, setEffectiveFrom] = useState(todayIso()) + const [rates, setRates] = useState(EMPTY_RATES) + + const { data: filters } = useQuery({ + queryKey: ['org-usage', 'filters'], + queryFn: () => apiClient.getOrgUsageFilters(), + enabled: isAdmin, + }) + + const { data: overrides = [], isLoading } = useQuery({ + queryKey: ['usage-pricing', 'overrides'], + queryFn: () => apiClient.listUsagePricingOverrides(), + enabled: isAdmin, + }) + + const modelOptions = useMemo(() => { + const fromUsage = filters?.models || [] + const fromOverrides = overrides.map((row) => row.model) + return Array.from(new Set([...fromUsage, ...fromOverrides])) + .sort() + .map((name) => ({ id: name, label: name })) + }, [filters?.models, overrides]) + + const visibleRateFields = useMemo( + () => RATE_FIELDS.filter((field) => field.kinds.includes(usageKind)), + [usageKind] + ) + + const saveMutation = useMutation({ + mutationFn: () => { + const payloadRates = Object.fromEntries( + Object.entries(rates).filter((entry): entry is [string, number] => { + const value = entry[1] + return value != null && !Number.isNaN(value) + }) + ) + return apiClient.upsertUsagePricingOverride(model, { + usage_kind: usageKind, + effective_from: effectiveFrom, + rates: payloadRates, + recompute: true, + }) + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['usage-pricing'] }) + queryClient.invalidateQueries({ queryKey: ['org-usage'] }) + setRates(EMPTY_RATES) + showToast( + data.recompute_enqueued + ? 'Override saved; cost recompute started' + : 'Override saved (recompute already running)', + 'success' + ) + }, + onError: (error: any) => { + showToast(error.response?.data?.detail || 'Failed to save override', 'error') + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (row: PricingOverride) => + apiClient.deleteUsagePricingOverride(row.model, { + usage_kind: row.usage_kind, + effective_from: row.effective_from, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['usage-pricing'] }) + queryClient.invalidateQueries({ queryKey: ['org-usage'] }) + showToast('Override removed', 'success') + }, + onError: (error: any) => { + showToast(error.response?.data?.detail || 'Failed to delete override', 'error') + }, + }) + + if (!isAdmin) { + return ( +
+ Organization admin access is required to manage pricing overrides. +
+ ) + } + + return ( +
+ + +
+ + + Back to usage + +
+
+ +
+
+
+

Pricing overrides

+
+ +
+ Set custom USD rates for this organization. Empty fields inherit the platform + catalog. Saving updates costs for matching usage and starts a scoped recompute. +
+
+
+

+ Per-model rates for this org. Leave blank to use the default catalog. +

+
+
+
+ + + +
+

Add or update override

+

+ Saving triggers a scoped cost recompute for this model. +

+
+ +
+ + + + + + setEffectiveFrom(e.target.value)} + /> + +
+ +
+

+ Rate overrides (USD) +

+
+ {visibleRateFields.map(({ key, label }) => ( + + { + const value = e.target.value + setRates((prev) => ({ + ...prev, + [key]: value === '' ? undefined : Number(value), + })) + }} + /> + + ))} +
+
+ +
+ + +
+
+
+ + + +
+

Current overrides

+ {!isLoading ? ( + {overrides.length} total + ) : null} +
+ + {isLoading ? ( +
+ +
+ ) : overrides.length === 0 ? ( +

+ No overrides yet. Platform catalog rates apply to all usage. +

+ ) : ( +
+ + + + + + + + + + + + + + {overrides.map((row) => ( + + + + + + + + + + + ))} + +
ModelKindEffectiveInputOutputAudioTTS +
+ {row.model} + {row.usage_kind} + {row.effective_from} + {row.effective_to ? ` → ${row.effective_to}` : ''} + + {formatUsd(row.rates.input_per_1m)} + + {formatUsd(row.rates.output_per_1m)} + + {formatUsd(row.rates.audio_per_minute)} + + {formatUsd(row.rates.tts_per_1m_characters)} + + +
+
+ )} +
+
+
+ ) +} diff --git a/frontend/src/pages/usage/usageProductHints.ts b/frontend/src/pages/usage/usageProductHints.ts index 4a057b1c..db382f23 100644 --- a/frontend/src/pages/usage/usageProductHints.ts +++ b/frontend/src/pages/usage/usageProductHints.ts @@ -45,3 +45,6 @@ export const PRODUCT_SECTION_HINTS: Record = { export const CALL_IMPORT_HINT = 'Call import batch — CSV upload or manual audio recordings' + +/** Prefix for product-area rows in the Source filter (not a resource UUID). */ +export const USAGE_SECTION_SOURCE_PREFIX = 'section:' diff --git a/scripts/merge_pricing_into_models_json.py b/scripts/merge_pricing_into_models_json.py new file mode 100644 index 00000000..c31060dd --- /dev/null +++ b/scripts/merge_pricing_into_models_json.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Merge pricing_catalog.json micro-USD rates into models.json plan-format pricing blocks.""" + +from __future__ import annotations + +import json +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +MODELS_JSON = REPO_ROOT / "app" / "config" / "models.json" +CATALOG_JSON = REPO_ROOT / "app" / "config" / "pricing_catalog.json" +MANUAL_JSON = REPO_ROOT / "app" / "config" / "pricing_manual.json" +MICRO = 1_000_000 + + +def _usd_per_m(micro: int) -> float: + return round(micro / MICRO, 8) + + +def _micro_entry_to_plan_pricing(entry: dict) -> dict: + usage_kind = entry.get("usage_kind") or "llm" + source = entry.get("_price_source") or ( + "litellm_import" if entry.get("_litellm_key") else "catalog" + ) + pricing: dict = {"source": source} + if usage_kind: + pricing["usage_kind"] = usage_kind + + if entry.get("input_micro_usd_per_million"): + pricing["input_per_1m"] = _usd_per_m(entry["input_micro_usd_per_million"]) + if entry.get("output_micro_usd_per_million"): + pricing["output_per_1m"] = _usd_per_m(entry["output_micro_usd_per_million"]) + if entry.get("cache_read_micro_usd_per_million"): + pricing["cache_read_per_1m"] = _usd_per_m( + entry["cache_read_micro_usd_per_million"] + ) + if entry.get("cache_creation_micro_usd_per_million"): + pricing["cache_write_per_1m"] = _usd_per_m( + entry["cache_creation_micro_usd_per_million"] + ) + if entry.get("reasoning_micro_usd_per_million"): + pricing["reasoning_per_1m"] = _usd_per_m( + entry["reasoning_micro_usd_per_million"] + ) + if entry.get("audio_micro_usd_per_second"): + pricing["audio_per_minute"] = round( + entry["audio_micro_usd_per_second"] * 60 / MICRO, 8 + ) + if entry.get("tts_micro_usd_per_million_chars"): + pricing["tts_per_1m_characters"] = _usd_per_m( + entry["tts_micro_usd_per_million_chars"] + ) + return pricing + + +def _load_catalog() -> dict: + merged: dict = {} + if CATALOG_JSON.exists(): + payload = json.loads(CATALOG_JSON.read_text(encoding="utf-8")) + merged.update( + { + k: v + for k, v in payload.items() + if not k.startswith("_") and isinstance(v, dict) + } + ) + if MANUAL_JSON.exists(): + payload = json.loads(MANUAL_JSON.read_text(encoding="utf-8")) + for key, value in payload.items(): + if not key.startswith("_") and isinstance(value, dict): + merged[key] = value + return merged + + +def merge() -> tuple[int, int]: + models = json.loads(MODELS_JSON.read_text(encoding="utf-8")) + catalog = _load_catalog() + updated = 0 + skipped = 0 + for model_name, cfg in models.items(): + if model_name.startswith("_") or not isinstance(cfg, dict): + continue + entry = catalog.get(model_name) + if not entry: + skipped += 1 + continue + pricing = _micro_entry_to_plan_pricing(entry) + if len(pricing) <= 1: + skipped += 1 + continue + cfg["pricing"] = pricing + updated += 1 + MODELS_JSON.write_text( + json.dumps(models, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + return updated, skipped + + +def main() -> int: + updated, skipped = merge() + print(f"merged pricing into {updated} model(s); skipped {skipped}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sync_pricing_catalog_from_litellm.py b/scripts/sync_pricing_catalog_from_litellm.py new file mode 100644 index 00000000..9d6375aa --- /dev/null +++ b/scripts/sync_pricing_catalog_from_litellm.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +"""Generate app/config/pricing_catalog.json from models.json + LiteLLM model_cost.""" + +from __future__ import annotations + +import argparse +import json +import importlib.util +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +REPO_ROOT = Path(__file__).resolve().parents[1] +MODELS_JSON = REPO_ROOT / "app" / "config" / "models.json" +MANUAL_JSON = REPO_ROOT / "app" / "config" / "pricing_manual.json" +OUTPUT_JSON = REPO_ROOT / "app" / "config" / "pricing_catalog.json" + +LITELLM_PROVIDER_PREFIX = { + "openai": "openai", + "anthropic": "anthropic", + "google": "gemini", + "azure": "azure", + "aws": "bedrock", + "deepseek": "deepseek", + "groq": "groq", + "xai": "xai", + "fireworks": "fireworks_ai", + "sarvam": "sarvam", + "deepgram": "deepgram", + "elevenlabs": "elevenlabs", + "cartesia": "cartesia", +} + +# Catalog key -> LiteLLM model_cost key (when auto-resolution fails). +EXPLICIT_LITELLM_KEYS: Dict[str, str] = { + "chat-latest": "gpt-5-chat-latest", + "azure-openai-gpt4": "gpt-4", + "aws-bedrock-claude": "anthropic.claude-sonnet-4-20250514-v1:0", + "aws-transcribe": "whisper-1", + "aws-polly": "aws_polly/neural", + "google-speech-v2": "gemini-2.5-flash", + "gemini-2.5-pro-stt": "gemini-2.5-flash", + "gemini-2.5-flash-stt": "gemini-2.5-flash", + "gemini-2.5-flash-lite-stt": "gemini-2.5-flash-lite", + "azure-speech-v1": "azure/speech/azure-stt", + "azure-tts-v1": "azure/speech/azure-tts", + "pulse-v4": "whisper-1", + "deepgram-flux": "deepgram/nova-3", + "deepgram-nova-3-general-preview-12-2025": "deepgram/nova-3-general", + "minimax-m2p5": "fireworks_ai/minimax-m2p7", + "qwen3p6-plus": "openrouter/qwen/qwen3.6-plus", + "grok-build-0.1": "xai/grok-3", + "grok-4.20-0309-non-reasoning": "xai/grok-4-fast-non-reasoning", + "grok-4.20-multi-agent-0309": "xai/grok-4", + # ElevenLabs: LiteLLM only prices a subset; map siblings to closest priced SKU. + "scribe_v2": "elevenlabs/scribe_v1", + "scribe_v2_realtime": "elevenlabs/scribe_v1", + "eleven_flash_v2_5": "elevenlabs/eleven_multilingual_v2", + "eleven_turbo_v2_5": "elevenlabs/eleven_multilingual_v2", + "eleven_ttv_v3": "elevenlabs/eleven_v3", + "eleven_multilingual_ttv_v2": "elevenlabs/eleven_multilingual_v2", + "eleven_english_sts_v2": "elevenlabs/eleven_multilingual_v2", + "eleven_multilingual_sts_v2": "elevenlabs/eleven_multilingual_v2", + "eleven_text_to_sound_v2": "elevenlabs/eleven_multilingual_v2", + "music_v1": "elevenlabs/eleven_multilingual_v2", +} + +MICRO_USD_PER_USD = 1_000_000 + + +def _azure_deployment_name(catalog_model: str) -> str: + if catalog_model == "azure-openai-gpt4": + return "gpt-4" + if catalog_model.startswith("azure-"): + return catalog_model[len("azure-") :] + return catalog_model + + +def _usage_kind(model_type: Optional[str]) -> str: + if model_type == "stt": + return "stt" + if model_type in {"tts", "sts", "sound_effects", "music"}: + return "tts" + return "llm" + + +def _fireworks_model(name: str) -> str: + if name.startswith("accounts/"): + return name + return f"accounts/fireworks/models/{name}" + + +def _per_million_micro_usd(cost_per_unit: float) -> int: + return int(round(float(cost_per_unit) * MICRO_USD_PER_USD * 1_000_000)) + + +def _per_second_micro_usd(cost_per_second: float) -> int: + return int(round(float(cost_per_second) * MICRO_USD_PER_USD)) + + +def _first_cost(info: Dict[str, Any], *keys: str) -> float: + for key in keys: + value = info.get(key) + if value is not None and float(value) > 0: + return float(value) + return 0.0 + + +def _litellm_candidates( + catalog_name: str, provider: str, model_type: str +) -> List[str]: + if catalog_name in EXPLICIT_LITELLM_KEYS: + return [EXPLICIT_LITELLM_KEYS[catalog_name]] + + prefix = LITELLM_PROVIDER_PREFIX.get(provider, provider) + candidates: List[str] = [] + + if provider == "azure" and model_type == "llm": + deployment = _azure_deployment_name(catalog_name) + candidates.extend( + [ + f"azure/{deployment}", + f"openai/{deployment}", + deployment, + ] + ) + elif provider == "azure" and model_type == "stt": + candidates.extend(["azure/speech/azure-stt", f"azure/{catalog_name}"]) + elif provider == "azure" and model_type == "tts": + candidates.extend(["azure/speech/azure-tts", f"azure/{catalog_name}"]) + elif provider == "deepgram": + stripped = ( + catalog_name[len("deepgram-") :] + if catalog_name.startswith("deepgram-") + else catalog_name + ) + candidates.extend([f"deepgram/{stripped}", stripped]) + elif provider == "fireworks" and model_type == "llm": + fw = _fireworks_model(catalog_name) + candidates.extend( + [ + f"fireworks_ai/{fw}", + f"fireworks_ai/{catalog_name}", + f"fireworks_ai/accounts/fireworks/models/{catalog_name}", + catalog_name, + ] + ) + elif provider == "elevenlabs": + candidates.extend([f"elevenlabs/{catalog_name}", catalog_name]) + elif provider == "google" and catalog_name.endswith("-stt"): + base = catalog_name[: -len("-stt")] + candidates.extend([base, f"gemini/{base}"]) + else: + candidates.extend([catalog_name, f"{prefix}/{catalog_name}"]) + + # De-dupe while preserving order. + seen = set() + ordered: List[str] = [] + for item in candidates: + if item not in seen: + seen.add(item) + ordered.append(item) + return ordered + + +def _resolve_litellm_key( + catalog_name: str, + provider: str, + model_type: str, + model_cost: Dict[str, Dict[str, Any]], +) -> Tuple[Optional[str], Optional[Dict[str, Any]]]: + for key in _litellm_candidates(catalog_name, provider, model_type): + info = model_cost.get(key) + if not info: + continue + if any("cost" in field and info.get(field) for field in info): + return key, info + + for key, info in model_cost.items(): + if key == "sample_spec": + continue + if key.endswith(f"/{catalog_name}") or key == catalog_name: + if any("cost" in field and info.get(field) for field in info): + return key, info + return None, None + + +def _convert_litellm_pricing( + info: Dict[str, Any], *, usage_kind: str +) -> Dict[str, int]: + entry: Dict[str, int] = { + "input_micro_usd_per_million": 0, + "output_micro_usd_per_million": 0, + "cache_read_micro_usd_per_million": 0, + "cache_creation_micro_usd_per_million": 0, + "reasoning_micro_usd_per_million": 0, + "audio_micro_usd_per_second": 0, + "tts_micro_usd_per_million_chars": 0, + } + + if usage_kind == "stt": + audio = _first_cost( + info, + "input_cost_per_second", + "output_cost_per_second", + ) + if not audio: + audio_token = _first_cost(info, "input_cost_per_audio_token") + if audio_token: + # LiteLLM audio-token STT models: ~25 audio tokens/sec (OpenAI convention). + audio = audio_token * 25.0 + entry["audio_micro_usd_per_second"] = _per_second_micro_usd(audio) + return entry + + if usage_kind == "tts": + per_char = _first_cost( + info, + "input_cost_per_character", + "output_cost_per_character", + ) + if per_char: + entry["tts_micro_usd_per_million_chars"] = _per_million_micro_usd(per_char) + return entry + per_token = _first_cost( + info, + "output_cost_per_token", + "input_cost_per_token", + ) + if per_token: + # Approximate chars/token ~= 4 when LiteLLM only exposes token pricing. + entry["tts_micro_usd_per_million_chars"] = _per_million_micro_usd( + per_token / 4.0 + ) + return entry + + entry["input_micro_usd_per_million"] = _per_million_micro_usd( + _first_cost(info, "input_cost_per_token", "input_cost_per_audio_token") + ) + entry["output_micro_usd_per_million"] = _per_million_micro_usd( + _first_cost(info, "output_cost_per_token") + ) + entry["cache_read_micro_usd_per_million"] = _per_million_micro_usd( + _first_cost(info, "cache_read_input_token_cost") + ) + entry["cache_creation_micro_usd_per_million"] = _per_million_micro_usd( + _first_cost(info, "cache_creation_input_token_cost") + ) + entry["reasoning_micro_usd_per_million"] = _per_million_micro_usd( + _first_cost(info, "output_cost_per_reasoning_token") + ) + return entry + + +def _load_model_cost(*, remote: bool) -> Dict[str, Dict[str, Any]]: + if remote: + from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map + + return get_model_cost_map( + url="https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" + ) + from litellm import model_cost + + return dict(model_cost) + + +def _load_manual_entries() -> Dict[str, Dict[str, Any]]: + if not MANUAL_JSON.exists(): + return {} + try: + payload = json.loads(MANUAL_JSON.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {} + if not isinstance(payload, dict): + return {} + return { + key: value + for key, value in payload.items() + if not key.startswith("_") and isinstance(value, dict) + } + + +def _apply_manual_entries( + catalog: Dict[str, Any], meta: Dict[str, Any], models: Dict[str, Any] +) -> None: + manual = _load_manual_entries() + if not manual: + return + meta.setdefault("manual", {}) + for catalog_name, entry in manual.items(): + if catalog_name in catalog: + continue + if catalog_name not in models or catalog_name.startswith("_"): + continue + model_type = str(models[catalog_name].get("model_type") or "llm") + usage_kind = entry.get("usage_kind") or _usage_kind(model_type) + pricing = { + k: int(v) + for k, v in entry.items() + if k != "usage_kind" and not k.startswith("_") and v is not None + } + if not any(pricing.values()): + continue + catalog[catalog_name] = { + "usage_kind": usage_kind, + **pricing, + "_price_source": entry.get("_price_source", "pricing_manual.json"), + "_litellm_proxy": True, + } + meta["manual"][catalog_name] = entry.get("_price_source", "pricing_manual.json") + meta["resolved"][catalog_name] = "manual" + meta["unresolved"] = [ + item for item in meta["unresolved"] if item.get("model") != catalog_name + ] + + +def build_catalog(*, remote: bool = True) -> Tuple[Dict[str, Any], Dict[str, Any]]: + models = json.loads(MODELS_JSON.read_text(encoding="utf-8")) + model_cost = _load_model_cost(remote=remote) + + catalog: Dict[str, Any] = {} + meta: Dict[str, Any] = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "source": "litellm_remote" if remote else "litellm_local", + "resolved": {}, + "unresolved": [], + } + + for catalog_name, cfg in sorted(models.items()): + if catalog_name.startswith("_") or not isinstance(cfg, dict): + continue + provider = str(cfg.get("provider") or "") + model_type = str(cfg.get("model_type") or "llm") + usage_kind = _usage_kind(model_type) + + litellm_key, info = _resolve_litellm_key( + catalog_name, provider, model_type, model_cost + ) + if not info: + meta["unresolved"].append( + { + "model": catalog_name, + "provider": provider, + "model_type": model_type, + } + ) + continue + + pricing = _convert_litellm_pricing(info, usage_kind=usage_kind) + if not any(pricing.values()): + meta["unresolved"].append( + { + "model": catalog_name, + "provider": provider, + "model_type": model_type, + "litellm_key": litellm_key, + "reason": "zero_cost", + } + ) + continue + + catalog[catalog_name] = { + "usage_kind": usage_kind, + **pricing, + "_litellm_key": litellm_key, + "_litellm_proxy": catalog_name not in {litellm_key, litellm_key.split("/")[-1]}, + } + meta["resolved"][catalog_name] = litellm_key + + _apply_manual_entries(catalog, meta, models) + catalog["_metadata"] = meta + return catalog, meta + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--local", + action="store_true", + help="Use bundled LiteLLM model_cost instead of fetching remote JSON", + ) + parser.add_argument( + "--stdout", + action="store_true", + help="Print JSON to stdout instead of writing pricing_catalog.json", + ) + parser.add_argument( + "--write-models", + action="store_true", + help="Also merge plan-format pricing blocks into app/config/models.json", + ) + args = parser.parse_args() + + catalog, meta = build_catalog(remote=not args.local) + payload = json.dumps(catalog, indent=2, sort_keys=True) + "\n" + if args.stdout: + sys.stdout.write(payload) + else: + OUTPUT_JSON.write_text(payload, encoding="utf-8") + + if args.write_models: + merge_path = REPO_ROOT / "scripts" / "merge_pricing_into_models_json.py" + spec = importlib.util.spec_from_file_location( + "merge_pricing_into_models_json", merge_path + ) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + updated, skipped = module.merge() + print( + f"models.json pricing: {updated} updated, {skipped} skipped", + file=sys.stderr, + ) + + resolved = len(meta["resolved"]) + unresolved = len(meta["unresolved"]) + print( + f"pricing catalog: {resolved} resolved, {unresolved} unresolved -> {OUTPUT_JSON if not args.stdout else 'stdout'}", + file=sys.stderr, + ) + if unresolved: + for item in meta["unresolved"]: + print(f" unresolved: {item['model']} ({item.get('reason', 'no_litellm_match')})", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_services/test_usage/test_llm_usage.py b/tests/test_services/test_usage/test_llm_usage.py index 4abdf609..28c06104 100644 --- a/tests/test_services/test_usage/test_llm_usage.py +++ b/tests/test_services/test_usage/test_llm_usage.py @@ -494,6 +494,10 @@ def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx, monkeypatch db_b = MagicMock() db_b.execute.return_value = MagicMock(rowcount=1) monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda _db, _org: 0) + monkeypatch.setattr( + "app.services.usage.pricing.apply_cost_to_bucket", + lambda *args, **kwargs: False, + ) first = usage_mod.flush_usage_to_catalog(db_a, org_id) second = usage_mod.flush_usage_to_catalog(db_b, org_id) @@ -600,6 +604,10 @@ def test_upsert_bucket_sql_uses_valid_empty_jsonb_literal(): def test_upsert_bucket_matches_legacy_resource_context_key(monkeypatch): """Per-row context must merge into an existing evaluation-level bucket.""" + monkeypatch.setattr( + "app.services.usage.pricing.apply_cost_to_bucket", + lambda *args, **kwargs: False, + ) org_id = uuid4() evaluation_id = uuid4() bucket = { @@ -629,6 +637,30 @@ def test_upsert_bucket_matches_legacy_resource_context_key(monkeypatch): assert "context->>'resource_type'" in legacy_sql +def test_upsert_bucket_applies_cost_after_update(monkeypatch): + org_id = uuid4() + bucket = { + "workspace_id": uuid4(), + "product_section": "chat", + "model": "gpt-test", + "context": {}, + "usage_date": date(2026, 8, 12), + "usage_kind": "llm", + } + deltas = {"prompt_tokens": 10, "completion_tokens": 5} + db = MagicMock() + db.execute.return_value = MagicMock(rowcount=1) + called: list[bool] = [] + monkeypatch.setattr( + "app.services.usage.pricing.apply_cost_to_bucket", + lambda *args, **kwargs: called.append(True) or True, + ) + + usage_mod._upsert_bucket(db, org_id, bucket, deltas) + + assert called + + def test_orphan_recovery_runs_at_most_once_per_interval(fake_redis, monkeypatch): org_id = uuid4() claim_key = f"usage:flushing:{org_id}:{uuid4()}" diff --git a/tests/test_services/test_usage/test_org_usage_costs.py b/tests/test_services/test_usage/test_org_usage_costs.py new file mode 100644 index 00000000..63c791c5 --- /dev/null +++ b/tests/test_services/test_usage/test_org_usage_costs.py @@ -0,0 +1,76 @@ +"""Tests for usage cost API response shaping.""" + +from types import SimpleNamespace + +from app.api.v1.routes.org_usage import ( + _attach_costs, + _breakdown_metrics_from_tuple, + _usage_totals_from_row, +) + + +def test_usage_totals_from_row_includes_nested_costs(): + row = SimpleNamespace( + prompt_tokens=100, + completion_tokens=50, + cache_read_tokens=0, + cache_creation_tokens=0, + reasoning_tokens=0, + audio_seconds=0, + tts_characters=0, + call_count=2, + input_cost_micro_usd=1_000_000, + output_cost_micro_usd=500_000, + cache_read_cost_micro_usd=0, + cache_creation_cost_micro_usd=0, + reasoning_cost_micro_usd=0, + audio_cost_micro_usd=0, + tts_cost_micro_usd=0, + total_cost_micro_usd=1_500_000, + has_unpriced_usage=True, + ) + totals = _usage_totals_from_row(row) + assert totals["total_tokens"] == 150 + assert totals["costs"]["total_cost_usd"] == 1.5 + assert totals["costs"]["has_unpriced_usage"] is True + + +def test_breakdown_metrics_from_tuple_includes_costs(): + metrics = ( + 10, + 5, + 0, + 0, + 0, + 0, + 0, + 1, + 100, + 50, + 0, + 0, + 0, + 0, + 0, + 150, + True, + ) + row = _breakdown_metrics_from_tuple(metrics) + assert row["costs"]["total_cost_usd"] == 0.00015 + assert row["costs"]["has_unpriced_usage"] is True + + +def test_attach_costs_uses_cache_write_field_name(): + payload = _attach_costs( + { + "input_cost_micro_usd": 0, + "output_cost_micro_usd": 0, + "cache_read_cost_micro_usd": 0, + "cache_creation_cost_micro_usd": 2_000_000, + "reasoning_cost_micro_usd": 0, + "audio_cost_micro_usd": 0, + "tts_cost_micro_usd": 0, + "total_cost_micro_usd": 2_000_000, + } + ) + assert payload["costs"]["cache_write_cost_usd"] == 2.0 diff --git a/tests/test_services/test_usage/test_pricing.py b/tests/test_services/test_usage/test_pricing.py new file mode 100644 index 00000000..6a526916 --- /dev/null +++ b/tests/test_services/test_usage/test_pricing.py @@ -0,0 +1,211 @@ +"""Tests for usage pricing computation and rate resolution.""" + +from __future__ import annotations + +from datetime import date +from unittest.mock import MagicMock +from uuid import uuid4 + +from app.services.usage.pricing import ( + CostBreakdown, + RateCard, + UsageMetrics, + _catalog_lookup_models, + _normalize_pricing_block, + _normalize_rate_source, + compute_cost, + RATE_SOURCE_CATALOG, +) + + +def test_compute_cost_llm_token_breakdown(): + rate = RateCard( + source=RATE_SOURCE_CATALOG, + rate_id=uuid4(), + input_micro_usd_per_million=1_000_000, + output_micro_usd_per_million=2_000_000, + cache_read_micro_usd_per_million=500_000, + cache_creation_micro_usd_per_million=1_250_000, + ) + costs = compute_cost( + UsageMetrics( + prompt_tokens=1_000_000, + completion_tokens=500_000, + cache_read_tokens=200_000, + cache_creation_tokens=100_000, + ), + rate, + ) + assert costs.input_cost_micro_usd == 1_000_000 + assert costs.output_cost_micro_usd == 1_000_000 + assert costs.cache_read_cost_micro_usd == 100_000 + assert costs.cache_creation_cost_micro_usd == 125_000 + assert costs.total_cost_micro_usd == 2_225_000 + assert costs.pricing_rate_source == RATE_SOURCE_CATALOG + + +def test_compute_cost_stt_audio_seconds(): + rate = RateCard( + source=RATE_SOURCE_CATALOG, + rate_id=uuid4(), + audio_micro_usd_per_second=100, + ) + costs = compute_cost(UsageMetrics(audio_seconds=90), rate) + assert costs.audio_cost_micro_usd == 9_000 + assert costs.total_cost_micro_usd == 9_000 + + +def test_compute_cost_tts_characters(): + rate = RateCard( + source=RATE_SOURCE_CATALOG, + rate_id=uuid4(), + tts_micro_usd_per_million_chars=15_000_000, + ) + costs = compute_cost(UsageMetrics(tts_characters=2_000_000), rate) + assert costs.tts_cost_micro_usd == 30_000_000 + assert costs.total_cost_micro_usd == 30_000_000 + + +def test_compute_cost_without_rate_is_zero(): + costs = compute_cost(UsageMetrics(prompt_tokens=10_000), None) + assert costs == CostBreakdown() + + +def test_normalize_rate_source_truncates_long_values(): + long_source = "x" * 300 + assert len(_normalize_rate_source(long_source)) == 255 + + +def test_normalize_pricing_block_plan_format(): + normalized = _normalize_pricing_block( + { + "input_per_1m": 2.5, + "output_per_1m": 10.0, + "cache_read_per_1m": 1.25, + "cache_write_per_1m": 0.0, + "source": "litellm_import", + }, + model_type="llm", + ) + assert normalized["usage_kind"] == "llm" + assert normalized["input_micro_usd_per_million"] == 2_500_000 + assert normalized["output_micro_usd_per_million"] == 10_000_000 + assert normalized["cache_read_micro_usd_per_million"] == 1_250_000 + assert normalized["source"] == "litellm_import" + + +def test_normalize_pricing_block_stt_audio_per_minute(): + normalized = _normalize_pricing_block( + {"audio_per_minute": 0.36, "usage_kind": "stt"}, + model_type="stt", + ) + assert normalized["usage_kind"] == "stt" + assert normalized["audio_micro_usd_per_second"] == 6_000 + + +def test_catalog_lookup_models_includes_azure_alias(): + assert _catalog_lookup_models("gpt-4o", "llm") == ("gpt-4o", "azure-gpt-4o") + assert _catalog_lookup_models("azure-gpt-4o", "llm") == ( + "azure-gpt-4o", + "gpt-4o", + ) + + +def test_compute_cost_reasoning_tokens(): + rate = RateCard( + source=RATE_SOURCE_CATALOG, + rate_id=uuid4(), + reasoning_micro_usd_per_million=3_000_000, + ) + costs = compute_cost(UsageMetrics(reasoning_tokens=1_000_000), rate) + assert costs.reasoning_cost_micro_usd == 3_000_000 + assert costs.total_cost_micro_usd == 3_000_000 + + +def test_cost_fields_from_deltas_voice_agent_call_audio(): + from app.services.usage.pricing import ( + RateCard, + _pricing_entries_from_models_json, + cost_fields_from_deltas, + ) + + entries = _pricing_entries_from_models_json() + assert "voice-agent-call" in entries + assert "unknown" in entries + assert entries["voice-agent-call"]["audio_micro_usd_per_second"] > 0 + + rate = RateCard( + source="catalog", + rate_id=uuid4(), + audio_micro_usd_per_second=833, # ~$0.05/min + ) + fields = cost_fields_from_deltas( + {"audio_seconds": 60, "call_count": 1}, + organization_id=uuid4(), + model="voice-agent-call", + usage_kind="llm", + usage_date=date(2026, 8, 11), + db=MagicMock(), + resolver=MagicMock(resolve_rate=MagicMock(return_value=rate)), + ) + assert fields["audio_cost_micro_usd"] == 60 * 833 + assert fields["total_cost_micro_usd"] == fields["audio_cost_micro_usd"] + assert fields["pricing_rate_source"] == "catalog" + + +def test_resolve_rate_ignores_stale_negative_cache(monkeypatch): + from unittest.mock import MagicMock + + import app.services.usage.pricing as pricing_mod + import app.services.usage.pricing_cache as pricing_cache_mod + from app.services.usage.pricing import RATE_SOURCE_CATALOG, PricingResolver + from tests.test_services.test_usage.test_pricing_cache import _FakeRedis + + pricing_mod._RATES_TABLE_CACHE = "model_pricing_rates" + + org_id = uuid4() + rate_id = uuid4() + usage_day = date(2026, 8, 11) + + fake_redis = _FakeRedis() + monkeypatch.setattr(pricing_cache_mod, "_client", lambda: fake_redis) + + redis_key = pricing_cache_mod.pricing_cache_key( + organization_id=org_id, + model="gpt-oss-120b", + usage_kind="llm", + usage_date=usage_day, + ) + pricing_cache_mod.set_cached_rate_payload(redis_key, None) + + row = { + "id": rate_id, + "input_micro_usd_per_million": 1_000_000, + "output_micro_usd_per_million": 2_000_000, + "cache_read_micro_usd_per_million": 0, + "cache_creation_micro_usd_per_million": 0, + "reasoning_micro_usd_per_million": 0, + "audio_micro_usd_per_second": 0, + "tts_micro_usd_per_million_chars": 0, + } + + override_result = MagicMock() + override_result.mappings.return_value.first.return_value = None + catalog_result = MagicMock() + catalog_result.mappings.return_value.first.return_value = row + + db = MagicMock() + db.execute.side_effect = [override_result, catalog_result] + + resolver = PricingResolver(db) + card = resolver.resolve_rate( + organization_id=org_id, + model="gpt-oss-120b", + usage_kind="llm", + usage_date=usage_day, + ) + + assert card is not None + assert card.rate_id == rate_id + assert card.source == RATE_SOURCE_CATALOG + assert fake_redis.store[redis_key] != "__null__" diff --git a/tests/test_services/test_usage/test_pricing_cache.py b/tests/test_services/test_usage/test_pricing_cache.py new file mode 100644 index 00000000..e4276240 --- /dev/null +++ b/tests/test_services/test_usage/test_pricing_cache.py @@ -0,0 +1,91 @@ +"""Tests for Redis-backed usage pricing cache.""" + +from __future__ import annotations + +from datetime import date +from uuid import uuid4 + +import app.services.usage.pricing_cache as pricing_cache_mod + + +class _FakeRedis: + def __init__(self): + self.store: dict[str, str] = {} + + def get(self, key: str): + return self.store.get(key) + + def setex(self, key: str, _ttl: int, value: str): + self.store[key] = value + + def scan(self, cursor: int, match: str, count: int): + keys = [key for key in self.store if key.startswith(match.rstrip("*"))] + return 0, keys + + def delete(self, *keys: str): + for key in keys: + self.store.pop(key, None) + + +def test_pricing_cache_roundtrip(monkeypatch): + fake = _FakeRedis() + monkeypatch.setattr(pricing_cache_mod, "_client", lambda: fake) + + org_id = uuid4() + key = pricing_cache_mod.pricing_cache_key( + organization_id=org_id, + model="gpt-4o", + usage_kind="llm", + usage_date=date(2026, 8, 13), + ) + payload = { + "source": "catalog", + "rate_id": str(uuid4()), + "input_micro_usd_per_million": 2_500_000, + "output_micro_usd_per_million": 10_000_000, + "cache_read_micro_usd_per_million": 0, + "cache_creation_micro_usd_per_million": 0, + "reasoning_micro_usd_per_million": 0, + "audio_micro_usd_per_second": 0, + "tts_micro_usd_per_million_chars": 0, + } + pricing_cache_mod.set_cached_rate_payload(key, payload) + cached = pricing_cache_mod.get_cached_rate_payload(key) + assert cached == payload + + +def test_pricing_cache_null_marker(monkeypatch): + fake = _FakeRedis() + monkeypatch.setattr(pricing_cache_mod, "_client", lambda: fake) + + org_id = uuid4() + key = pricing_cache_mod.pricing_cache_key( + organization_id=org_id, + model="unknown-model", + usage_kind="llm", + usage_date=date(2026, 8, 13), + ) + pricing_cache_mod.set_cached_rate_payload(key, None) + assert pricing_cache_mod.get_cached_rate_payload(key) == {} + + +def test_null_cache_uses_shorter_ttl(monkeypatch): + fake = _FakeRedis() + recorded: list[tuple[int, str]] = [] + + def _setex(key: str, ttl: int, value: str): + fake.store[key] = value + recorded.append((ttl, value)) + + fake.setex = _setex # type: ignore[method-assign] + monkeypatch.setattr(pricing_cache_mod, "_client", lambda: fake) + + key = "usage:pricing:test" + pricing_cache_mod.set_cached_rate_payload(key, None) + pricing_cache_mod.set_cached_rate_payload( + key + ":hit", + {"source": "catalog", "rate_id": str(uuid4())}, + ) + assert recorded[0][0] == pricing_cache_mod.PRICING_NULL_CACHE_TTL_SEC + assert recorded[0][1] == "__null__" + assert recorded[1][0] == pricing_cache_mod.PRICING_CACHE_TTL_SEC diff --git a/tests/test_services/test_usage/test_pricing_ops.py b/tests/test_services/test_usage/test_pricing_ops.py new file mode 100644 index 00000000..e5a3c51b --- /dev/null +++ b/tests/test_services/test_usage/test_pricing_ops.py @@ -0,0 +1,10 @@ +"""Tests for usage pricing ops helpers.""" + +from __future__ import annotations + +from app.services.usage.pricing_ops import models_missing_pricing_blocks + + +def test_models_missing_pricing_blocks_is_sorted_list(): + missing = models_missing_pricing_blocks() + assert missing == sorted(missing) diff --git a/tests/test_services/test_usage/test_pricing_overrides.py b/tests/test_services/test_usage/test_pricing_overrides.py new file mode 100644 index 00000000..3e18d191 --- /dev/null +++ b/tests/test_services/test_usage/test_pricing_overrides.py @@ -0,0 +1,72 @@ +"""Tests for org pricing override merge and service helpers.""" + +from __future__ import annotations + +from uuid import uuid4 + +from app.services.usage.pricing import ( + RATE_SOURCE_CATALOG, + RATE_SOURCE_OVERRIDE, + RateCard, + _merge_override_with_catalog, +) +from app.services.usage.pricing_overrides import _usd_payload_to_micro_columns + + +def test_merge_override_inherits_null_fields_from_catalog(): + catalog = RateCard( + source=RATE_SOURCE_CATALOG, + rate_id=uuid4(), + input_micro_usd_per_million=1_000_000, + output_micro_usd_per_million=2_000_000, + cache_read_micro_usd_per_million=500_000, + ) + override_id = uuid4() + merged = _merge_override_with_catalog( + { + "id": override_id, + "input_micro_usd_per_million": 3_000_000, + "output_micro_usd_per_million": None, + "cache_read_micro_usd_per_million": None, + "cache_creation_micro_usd_per_million": None, + "reasoning_micro_usd_per_million": None, + "audio_micro_usd_per_second": None, + "tts_micro_usd_per_million_chars": None, + }, + catalog, + ) + assert merged.source == RATE_SOURCE_OVERRIDE + assert merged.rate_id == override_id + assert merged.input_micro_usd_per_million == 3_000_000 + assert merged.output_micro_usd_per_million == 2_000_000 + assert merged.cache_read_micro_usd_per_million == 500_000 + + +def test_merge_override_without_catalog_uses_zero_defaults(): + override_id = uuid4() + merged = _merge_override_with_catalog( + { + "id": override_id, + "input_micro_usd_per_million": 100, + "output_micro_usd_per_million": None, + "cache_read_micro_usd_per_million": None, + "cache_creation_micro_usd_per_million": None, + "reasoning_micro_usd_per_million": None, + "audio_micro_usd_per_second": None, + "tts_micro_usd_per_million_chars": None, + }, + None, + ) + assert merged.input_micro_usd_per_million == 100 + assert merged.output_micro_usd_per_million == 0 + + +def test_usd_payload_to_micro_columns(): + columns = _usd_payload_to_micro_columns( + { + "input_per_1m": 1.5, + "audio_per_minute": 0.006, + } + ) + assert columns["input_micro_usd_per_million"] == 1_500_000 + assert columns["audio_micro_usd_per_second"] == 100 diff --git a/tests/test_services/test_usage/test_usage_costs.py b/tests/test_services/test_usage/test_usage_costs.py new file mode 100644 index 00000000..2e6d028c --- /dev/null +++ b/tests/test_services/test_usage/test_usage_costs.py @@ -0,0 +1,33 @@ +"""Tests for usage cost presentation helpers.""" + +from app.services.usage.usage_costs import costs_from_micro, micro_to_usd + + +def test_micro_to_usd(): + assert micro_to_usd(1_500_000) == 1.5 + assert micro_to_usd(0) == 0 + + +def test_costs_from_micro_maps_cache_write_and_unpriced_flag(): + costs = costs_from_micro( + input_cost_micro_usd=1_000_000, + output_cost_micro_usd=500_000, + cache_creation_cost_micro_usd=250_000, + total_cost_micro_usd=1_750_000, + has_unpriced_usage=True, + ) + assert costs["input_cost_usd"] == 1.0 + assert costs["output_cost_usd"] == 0.5 + assert costs["cache_write_cost_usd"] == 0.25 + assert costs["total_cost_usd"] == 1.75 + assert costs["currency"] == "USD" + assert costs["has_unpriced_usage"] is True + + +def test_costs_from_micro_sums_total_when_not_provided(): + costs = costs_from_micro( + input_cost_micro_usd=100, + output_cost_micro_usd=200, + audio_cost_micro_usd=300, + ) + assert costs["total_cost_usd"] == micro_to_usd(600) diff --git a/tests/test_workers/test_usage_queue_routing.py b/tests/test_workers/test_usage_queue_routing.py new file mode 100644 index 00000000..8ccdb6f3 --- /dev/null +++ b/tests/test_workers/test_usage_queue_routing.py @@ -0,0 +1,18 @@ +"""Tests for usage pricing Celery queue routing.""" + +from app.workers.config import USAGE_WORKER_QUEUE, celery_app + + +def test_flush_usage_counters_routes_to_usage_queue(): + routes = celery_app.conf.task_routes + assert routes["flush_usage_counters"]["queue"] == USAGE_WORKER_QUEUE + + +def test_recompute_usage_costs_routes_to_usage_queue(): + routes = celery_app.conf.task_routes + assert routes["recompute_usage_costs"]["queue"] == USAGE_WORKER_QUEUE + + +def test_usage_beat_flush_targets_usage_queue(): + schedule = celery_app.conf.beat_schedule["flush-llm-usage-counters"] + assert schedule["options"]["queue"] == USAGE_WORKER_QUEUE From 7d59a5babe804d695f21d861301c95fffb45b9f0 Mon Sep 17 00:00:00 2001 From: M Sami Date: Fri, 14 Aug 2026 18:45:37 +0530 Subject: [PATCH 21/55] feat: add usage flush configuration and enterprise entitlement checks for usage features --- README.md | 24 +- app/api/v1/routes/org_usage.py | 92 +++++-- app/api/v1/routes/settings.py | 3 + app/api/v1/routes/usage_pricing.py | 22 +- app/core/usage_entitlement.py | 55 ++++ app/dependencies.py | 24 ++ app/services/usage/access.py | 83 ++++++ app/services/usage/llm_usage.py | 256 ++++++++++++------ app/services/usage/retention.py | 41 +++ app/workers/config.py | 7 + app/workers/tasks/__init__.py | 2 + app/workers/tasks/prune_oss_usage_history.py | 22 ++ env.example | 10 + frontend/src/App.tsx | 27 +- frontend/src/lib/api.ts | 11 + .../pages/enterprise/EnterpriseUpgrade.tsx | 8 +- frontend/src/pages/usage/Usage.tsx | 82 +++++- .../src/pages/usage/UsageDateRangePicker.tsx | 34 ++- frontend/src/pages/usage/UsageFiltersBar.tsx | 9 +- frontend/src/store/licenseStore.ts | 16 +- tests/test_core/test_usage_entitlement.py | 49 ++++ .../test_usage/test_llm_usage.py | 57 +++- .../test_usage/test_usage_access.py | 67 +++++ .../test_usage/test_usage_retention.py | 68 +++++ 24 files changed, 930 insertions(+), 139 deletions(-) create mode 100644 app/core/usage_entitlement.py create mode 100644 app/services/usage/access.py create mode 100644 app/services/usage/retention.py create mode 100644 app/workers/tasks/prune_oss_usage_history.py create mode 100644 tests/test_core/test_usage_entitlement.py create mode 100644 tests/test_services/test_usage/test_usage_access.py create mode 100644 tests/test_services/test_usage/test_usage_retention.py diff --git a/README.md b/README.md index 009ca348..c5ad0a0b 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ celery -A app.workers.celery_app worker --loglevel=info **Note:** Workers are required for background tasks (transcription, evaluation, usage cost flush, etc.). If you use `eai start-all`, they start automatically. Only use `eai worker` if you need to run a worker separately (e.g. `eai worker --queues usage` for the usage queue only). ### Usage Pricing Ops -Manage model pricing rates and backfill stored usage costs on `llm_usage_daily` rollups. +Manage model pricing rates and backfill stored usage costs on `llm_usage_daily` rollups. Requires `worker-usage` + `beat` (or `eai start-all`). ```bash # Upsert model_pricing_rates from app/config/models.json @@ -382,12 +382,14 @@ eai usage seed-rates --config config.yml # Compare models.json pricing vs Postgres eai usage diff-rates --config config.yml -# Backfill / recompute costs (sync, runs in this process) +# Backfill costs in-process (all orgs; use after migrate or catalog change) eai usage recompute --config config.yml --sync -# Enqueue Celery recompute on the usage queue (requires --organization-id) +# Async recompute via usage queue (requires --organization-id) eai usage recompute --config config.yml --organization-id +# Optional scopes: --model, --usage-kind, --start-date, --end-date + # Optional: fetch LiteLLM prices into pricing_catalog.json eai usage sync-litellm --local eai usage sync-litellm --local --write-models @@ -400,7 +402,17 @@ eai usage seed-rates --config config.yml eai usage recompute --config config.yml --sync ``` -Requires `worker-usage` (or `eai start-all`) for async recompute; Beat + `worker-usage` keep Redis usage counters flushed into Postgres on a schedule (`USAGE_FLUSH_BEAT_SECONDS`, default 120). +**Flush / Usage UI tuning** — set in `.env` (see `env.example`): + +| Variable | Default | Purpose | +|----------|---------|---------| +| `USAGE_FLUSH_BUCKET_BATCH_SIZE` | `500` | Buckets per DB transaction | +| `USAGE_FLUSH_MAX_BATCHES_PER_RUN` | `30` | Batches per flush tick (≤ **15,000** buckets/run) | +| `USAGE_FLUSH_BEAT_SECONDS` | `120` | Beat interval (~2 min lag vs Redis) | +| `USAGE_FLUSH_LOCK_TTL_SECONDS` | `300` | Per-org flush lock TTL | +| `USAGE_API_FLUSH_COOLDOWN_SECONDS` | `60` | Min gap between Usage page catalog syncs | + +Usage UI calls `POST /api/v1/organizations/usage/catalog/sync` once per visit; summary/breakdown/filters are read-only. If Redis backlog grows, lower `USAGE_FLUSH_BEAT_SECONDS` or raise `USAGE_FLUSH_MAX_BATCHES_PER_RUN`. ### Generate Config File ```bash @@ -515,6 +527,10 @@ POSTGRES_PASSWORD=password POSTGRES_DB=efficientai SECRET_KEY=your-secret-key-here +# Usage cost flush (see README "Usage Pricing Ops"; full list in env.example) +# USAGE_FLUSH_BEAT_SECONDS=120 +# USAGE_FLUSH_MAX_BATCHES_PER_RUN=30 + # Optional: GCS blob storage (also set storage.blob_provider: gcs in config.yml) BLOB_STORAGE_PROVIDER=gcs GCS_BUCKET_NAME=your-gcs-bucket diff --git a/app/api/v1/routes/org_usage.py b/app/api/v1/routes/org_usage.py index 2d6854f6..95688b3c 100644 --- a/app/api/v1/routes/org_usage.py +++ b/app/api/v1/routes/org_usage.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import date, datetime, timezone +from app.services.usage.access import UsageAccessPolicy from app.services.usage.dates import usage_date_filter_bounds, usage_local_today from typing import List, Literal, Optional from uuid import UUID @@ -122,10 +123,21 @@ class UsageTotals(BaseModel): costs: UsageCosts = Field(default_factory=UsageCosts) +class UsageCatalogSyncResponse(BaseModel): + flushed_buckets: int = 0 + + +class UsagePolicyMeta(BaseModel): + extended_history: bool + max_history_days: Optional[int] = None + range_clamped: bool = False + + class UsageSummaryResponse(BaseModel): start: date end: date totals: UsageTotals + usage_policy: UsagePolicyMeta last_updated_at: Optional[datetime] = None @@ -168,6 +180,7 @@ class UsageBreakdownResponse(BaseModel): rows: List[UsageBreakdownRow] total_count: int truncated_at_limit: bool = False + usage_policy: UsagePolicyMeta last_updated_at: Optional[datetime] = None @@ -198,6 +211,14 @@ def _parse_usage_range( return display_start, display_end, filter_start, filter_end +def _usage_policy_meta(access) -> UsagePolicyMeta: + return UsagePolicyMeta( + extended_history=access.policy.extended_history, + max_history_days=access.policy.max_history_days, + range_clamped=access.range_clamped, + ) + + def _evaluation_id_expr(): return func.coalesce( LLMUsageDaily.context["evaluation_id"].astext, @@ -418,6 +439,7 @@ def _apply_filters( organization_id: UUID, start: date, end: date, + enforced_floor: Optional[date] = None, workspace_id: Optional[UUID], product_section: Optional[str], model: Optional[str], @@ -430,9 +452,12 @@ def _apply_filters( tag_id: Optional[UUID] = None, db: Optional[Session] = None, ): + effective_start = start + if enforced_floor is not None and effective_start < enforced_floor: + effective_start = enforced_floor query = query.filter( LLMUsageDaily.organization_id == organization_id, - LLMUsageDaily.usage_date >= start, + LLMUsageDaily.usage_date >= effective_start, LLMUsageDaily.usage_date <= end, ) if workspace_id is not None: @@ -506,6 +531,7 @@ def _filtered_query( organization_id: UUID, start: date, end: date, + enforced_floor: Optional[date] = None, workspace_id: Optional[UUID] = None, product_section: Optional[str] = None, model: Optional[str] = None, @@ -522,6 +548,7 @@ def _filtered_query( organization_id=organization_id, start=start, end=end, + enforced_floor=enforced_floor, workspace_id=workspace_id, product_section=product_section, model=model, @@ -1010,6 +1037,7 @@ def _summary_aggregate_query( organization_id: UUID, start: date, end: date, + enforced_floor: Optional[date] = None, workspace_id: Optional[UUID], product_section: Optional[str], model: Optional[str], @@ -1045,6 +1073,7 @@ def _summary_aggregate_query( organization_id=organization_id, start=start, end=end, + enforced_floor=enforced_floor, workspace_id=workspace_id, product_section=product_section, model=model, @@ -1059,6 +1088,16 @@ def _summary_aggregate_query( ) +@router.post("/catalog/sync", response_model=UsageCatalogSyncResponse) +def sync_usage_catalog( + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + """Drain Redis usage counters into Postgres (rate-limited per org via Redis).""" + flushed = flush_usage_to_catalog(db, organization_id) + return UsageCatalogSyncResponse(flushed_buckets=flushed) + + @router.get("/summary", response_model=UsageSummaryResponse) def get_usage_summary( start: Optional[date] = Query(None), @@ -1080,19 +1119,18 @@ def get_usage_summary( organization_id: UUID = Depends(get_organization_id), db: Session = Depends(get_db), ): - display_start, display_end, filter_start, filter_end = _parse_usage_range( - start, end, tz - ) + access = UsageAccessPolicy.resolve(organization_id, start, end, tz) + display_start = access.display_start + display_end = access.display_end if display_end < display_start: raise HTTPException(status_code=400, detail="end must be >= start") - flush_usage_to_catalog(db, organization_id) - row = _summary_aggregate_query( db, organization_id=organization_id, - start=filter_start, - end=filter_end, + start=access.filter_start, + end=access.filter_end, + enforced_floor=access.enforced_filter_floor, workspace_id=workspace_id, product_section=product_section, model=model, @@ -1109,6 +1147,7 @@ def get_usage_summary( start=display_start, end=display_end, totals=UsageTotals(**totals), + usage_policy=_usage_policy_meta(access), last_updated_at=_last_updated(db, organization_id), ) @@ -1137,14 +1176,12 @@ def get_usage_breakdown( organization_id: UUID = Depends(get_organization_id), db: Session = Depends(get_db), ): - display_start, display_end, filter_start, filter_end = _parse_usage_range( - start, end, tz - ) + access = UsageAccessPolicy.resolve(organization_id, start, end, tz) + display_start = access.display_start + display_end = access.display_end if display_end < display_start: raise HTTPException(status_code=400, detail="end must be >= start") - flush_usage_to_catalog(db, organization_id) - dim = { "workspace": LLMUsageDaily.workspace_id, "product_section": LLMUsageDaily.product_section, @@ -1186,8 +1223,9 @@ def get_usage_breakdown( query = _apply_filters( db.query(*select_cols, *aggregates), organization_id=organization_id, - start=filter_start, - end=filter_end, + start=access.filter_start, + end=access.filter_end, + enforced_floor=access.enforced_filter_floor, workspace_id=workspace_id, product_section=product_section, model=model, @@ -1227,8 +1265,9 @@ def get_usage_breakdown( label_query = _filtered_query( db, organization_id=organization_id, - start=filter_start, - end=filter_end, + start=access.filter_start, + end=access.filter_end, + enforced_floor=access.enforced_filter_floor, workspace_id=workspace_id, product_section=product_section, model=model, @@ -1243,8 +1282,9 @@ def get_usage_breakdown( label_query = _filtered_query( db, organization_id=organization_id, - start=filter_start, - end=filter_end, + start=access.filter_start, + end=access.filter_end, + enforced_floor=access.enforced_filter_floor, workspace_id=workspace_id, product_section=product_section, model=model, @@ -1353,6 +1393,7 @@ def get_usage_breakdown( rows=rows, total_count=len(rows), truncated_at_limit=len(rows) >= limit, + usage_policy=_usage_policy_meta(access), last_updated_at=_last_updated(db, organization_id), ) @@ -1378,9 +1419,10 @@ def get_usage_filters( organization_id: UUID = Depends(get_organization_id), db: Session = Depends(get_db), ): - _, _, filter_start, filter_end = _parse_usage_range(start, end, tz) - - flush_usage_to_catalog(db, organization_id) + access = UsageAccessPolicy.resolve(organization_id, start, end, tz) + filter_start = access.filter_start + filter_end = access.filter_end + enforced_floor = access.enforced_filter_floor scoped_resource_id = resource_id or evaluation_id @@ -1389,6 +1431,7 @@ def get_usage_filters( organization_id=organization_id, start=filter_start, end=filter_end, + enforced_floor=enforced_floor, dataset=dataset, tag_id=tag_id, ) @@ -1397,6 +1440,7 @@ def get_usage_filters( organization_id=organization_id, start=filter_start, end=filter_end, + enforced_floor=enforced_floor, workspace_id=workspace_id, dataset=dataset, tag_id=tag_id, @@ -1406,6 +1450,7 @@ def get_usage_filters( organization_id=organization_id, start=filter_start, end=filter_end, + enforced_floor=enforced_floor, workspace_id=workspace_id, product_section=product_section, resource_id=scoped_resource_id, @@ -1419,6 +1464,7 @@ def get_usage_filters( organization_id=organization_id, start=filter_start, end=filter_end, + enforced_floor=enforced_floor, workspace_id=workspace_id, product_section=product_section, resource_id=scoped_resource_id, @@ -1433,6 +1479,7 @@ def get_usage_filters( organization_id=organization_id, start=filter_start, end=filter_end, + enforced_floor=enforced_floor, workspace_id=workspace_id, product_section=product_section, dataset=dataset, @@ -1443,6 +1490,7 @@ def get_usage_filters( organization_id=organization_id, start=filter_start, end=filter_end, + enforced_floor=enforced_floor, workspace_id=workspace_id, product_section=product_section, model=model, diff --git a/app/api/v1/routes/settings.py b/app/api/v1/routes/settings.py index 3c49c0cc..83225380 100644 --- a/app/api/v1/routes/settings.py +++ b/app/api/v1/routes/settings.py @@ -32,6 +32,7 @@ is_feature_enabled, ENTERPRISE_FEATURES, ) +from app.core.usage_entitlement import get_usage_policy REPORT_LOGO_CONTENT_TYPES = { @@ -85,12 +86,14 @@ def license_info(organization_id: UUID = Depends(get_organization_id)): data = get_license_info() all_licensed = data.get("features", []) if isinstance(data.get("features"), list) else [] enabled_for_org = [f for f in all_licensed if is_feature_enabled(f, organization_id)] + usage_policy = get_usage_policy(organization_id) return { "is_enterprise": bool(enabled_for_org), "enabled_features": enabled_for_org, "all_enterprise_features": ENTERPRISE_FEATURES, "feature_catalog": get_feature_catalog(), "organization": data.get("org_id"), + "usage_policy": usage_policy.as_dict(), } diff --git a/app/api/v1/routes/usage_pricing.py b/app/api/v1/routes/usage_pricing.py index 0840a29a..0716eef6 100644 --- a/app/api/v1/routes/usage_pricing.py +++ b/app/api/v1/routes/usage_pricing.py @@ -12,7 +12,7 @@ from app.core.auth.rbac import require_admin from app.database import get_db -from app.dependencies import get_organization_id +from app.dependencies import get_organization_id, require_enterprise_entitlement from app.services.usage.pricing_jobs import ( create_recompute_job, enqueue_recompute_job, @@ -27,10 +27,12 @@ upsert_override, ) +from app.services.usage.access import UsageAccessPolicy + router = APIRouter( prefix="/organizations/usage/pricing", tags=["Usage"], - dependencies=[Depends(require_admin)], + dependencies=[Depends(require_admin), Depends(require_enterprise_entitlement())], ) @@ -218,13 +220,25 @@ def trigger_usage_cost_recompute( ): raise HTTPException(status_code=400, detail="end_date must be >= start_date") + clamped_start = body.start_date + clamped_end = body.end_date + if body.start_date is not None or body.end_date is not None: + access = UsageAccessPolicy.resolve( + organization_id, + body.start_date, + body.end_date, + None, + ) + clamped_start = access.display_start if body.start_date is not None else None + clamped_end = access.display_end if body.end_date is not None else None + job = create_recompute_job( db, organization_id=organization_id, model=body.model, usage_kind=body.usage_kind, - start_date=body.start_date, - end_date=body.end_date, + start_date=clamped_start, + end_date=clamped_end, ) enqueue_recompute_job(db, job) db.refresh(job) diff --git a/app/core/usage_entitlement.py b/app/core/usage_entitlement.py new file mode 100644 index 00000000..cbedbf7d --- /dev/null +++ b/app/core/usage_entitlement.py @@ -0,0 +1,55 @@ +"""Enterprise entitlement for usage history (any catalog feature, not per-feature gates).""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional +from uuid import UUID + +from app.core.license import get_enabled_features, get_license_info + +OSS_USAGE_HISTORY_DAYS = 7 + + +@dataclass(frozen=True) +class UsagePolicySnapshot: + extended_history: bool + max_history_days: Optional[int] = None + + def as_dict(self) -> dict: + return { + "extended_history": self.extended_history, + "max_history_days": self.max_history_days, + } + + +def has_enterprise_entitlement(organization_id: Optional[UUID] = None) -> bool: + """Valid license with at least one catalog feature; org-scoped licenses must match.""" + if not get_enabled_features(): + return False + + info = get_license_info() + licensed_org = info.get("org_id") + if licensed_org is None: + return True + + if organization_id is None: + return False + + return str(organization_id) == str(licensed_org) + + +def deployment_has_entitlement() -> bool: + """Deployment-wide entitlement (license without org_id scoping).""" + if not get_enabled_features(): + return False + return get_license_info().get("org_id") is None + + +def get_usage_policy(organization_id: UUID) -> UsagePolicySnapshot: + if has_enterprise_entitlement(organization_id): + return UsagePolicySnapshot(extended_history=True, max_history_days=None) + return UsagePolicySnapshot( + extended_history=False, + max_history_days=OSS_USAGE_HISTORY_DAYS, + ) diff --git a/app/dependencies.py b/app/dependencies.py index c3597d75..5ac8f1ca 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -21,6 +21,7 @@ from app.core.auth import Principal, get_principal # noqa: F401 - re-exported from app.core.auth.rbac import get_org_role from app.core.license import is_feature_enabled +from app.core.usage_entitlement import has_enterprise_entitlement from app.database import get_db from app.models.database import RoleEnum, Workspace, WorkspaceMember from app.core.auth.capabilities import capability_denied_message @@ -291,3 +292,26 @@ def _check( ) return _check + + +def require_enterprise_entitlement(): + """ + FastAPI dependency: valid enterprise license with any catalog feature. + Distinct from require_enterprise_feature (per-feature product gates). + """ + + def _check(organization_id: UUID = Depends(get_organization_id)): + if not has_enterprise_entitlement(organization_id): + raise HTTPException( + status_code=403, + detail={ + "error": "enterprise_license_required", + "message": ( + "This capability requires a valid EfficientAI Enterprise license. " + "Set EFFICIENTAI_LICENSE in your environment with any enterprise " + "feature enabled. Contact sales@efficientai.com for a license key." + ), + }, + ) + + return _check diff --git a/app/services/usage/access.py b/app/services/usage/access.py new file mode 100644 index 00000000..9591bf2d --- /dev/null +++ b/app/services/usage/access.py @@ -0,0 +1,83 @@ +"""Usage read access policy — date window clamping and SQL floor for OSS tier.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, timedelta +from typing import Optional +from uuid import UUID + +from loguru import logger + +from app.core.usage_entitlement import ( + OSS_USAGE_HISTORY_DAYS, + UsagePolicySnapshot, + get_usage_policy, + has_enterprise_entitlement, +) +from app.services.usage.dates import usage_date_filter_bounds, usage_local_today + + +@dataclass(frozen=True) +class UsageAccessResult: + display_start: date + display_end: date + filter_start: date + filter_end: date + enforced_filter_floor: Optional[date] + policy: UsagePolicySnapshot + range_clamped: bool + + +def oss_usage_min_local_date(tz: Optional[str]) -> date: + """Earliest inclusive local calendar day allowed for OSS tier.""" + today = usage_local_today(tz) + return today - timedelta(days=OSS_USAGE_HISTORY_DAYS - 1) + + +class UsageAccessPolicy: + @staticmethod + def resolve( + organization_id: UUID, + start: Optional[date], + end: Optional[date], + tz: Optional[str], + ) -> UsageAccessResult: + today = usage_local_today(tz) + display_start = start or today + display_end = end or today + policy = get_usage_policy(organization_id) + range_clamped = False + enforced_floor: Optional[date] = None + + if not has_enterprise_entitlement(organization_id): + oss_min = oss_usage_min_local_date(tz) + if display_start < oss_min: + display_start = oss_min + range_clamped = True + enforced_floor = usage_date_filter_bounds(oss_min, oss_min, tz)[0] + + filter_start, filter_end = usage_date_filter_bounds( + display_start, display_end, tz + ) + + if enforced_floor is not None and filter_start < enforced_floor: + filter_start = enforced_floor + + if range_clamped: + logger.info( + "usage_history_clamped org_id={} effective_start={} effective_end={}", + organization_id, + display_start, + display_end, + ) + + return UsageAccessResult( + display_start=display_start, + display_end=display_end, + filter_start=filter_start, + filter_end=filter_end, + enforced_filter_floor=enforced_floor, + policy=policy, + range_clamped=range_clamped, + ) diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py index 0fdb48f1..51ffc6bc 100644 --- a/app/services/usage/llm_usage.py +++ b/app/services/usage/llm_usage.py @@ -2,10 +2,11 @@ from __future__ import annotations +import json import math +import os import time import uuid -import json from datetime import date, datetime, timezone from typing import Any, Dict, Iterable, List, Optional, Tuple from uuid import UUID @@ -34,7 +35,10 @@ _NONE = "__none__" _PENDING_TTL_SECONDS = 14 * 24 * 60 * 60 -_FLUSH_LOCK_TTL_SECONDS = 45 +_DEFAULT_FLUSH_BUCKET_BATCH_SIZE = 500 +_DEFAULT_FLUSH_MAX_BATCHES_PER_RUN = 30 +_DEFAULT_FLUSH_LOCK_TTL_SECONDS = 300 +_DEFAULT_API_FLUSH_COOLDOWN_SECONDS = 60 _FLUSH_LOCK_WAIT_SECONDS = 3.0 USAGE_KIND_LLM = "llm" USAGE_KIND_STT = "stt" @@ -59,6 +63,63 @@ """ +def _env_int(name: str, default: int, *, minimum: int = 1) -> int: + raw = os.environ.get(name) + if raw is None: + return default + try: + return max(minimum, int(raw)) + except (TypeError, ValueError): + logger.warning("invalid {}={!r}; using default {}", name, raw, default) + return default + + +def flush_bucket_batch_size() -> int: + """Max rollup buckets persisted per DB transaction during Redis flush.""" + return _env_int( + "USAGE_FLUSH_BUCKET_BATCH_SIZE", + _DEFAULT_FLUSH_BUCKET_BATCH_SIZE, + ) + + +def flush_max_batches_per_run() -> int: + """Max Redis flush batches per org per flush_usage_to_catalog invocation.""" + return _env_int( + "USAGE_FLUSH_MAX_BATCHES_PER_RUN", + _DEFAULT_FLUSH_MAX_BATCHES_PER_RUN, + ) + + +def _flush_lock_ttl_seconds() -> int: + return _env_int( + "USAGE_FLUSH_LOCK_TTL_SECONDS", + _DEFAULT_FLUSH_LOCK_TTL_SECONDS, + ) + + +def api_flush_cooldown_seconds() -> int: + """Min seconds between API-triggered catalog syncs per org (0 = no cooldown).""" + return _env_int( + "USAGE_API_FLUSH_COOLDOWN_SECONDS", + _DEFAULT_API_FLUSH_COOLDOWN_SECONDS, + minimum=0, + ) + + +def _split_buckets_for_flush( + buckets: Dict[str, Dict[str, int]], + batch_size: int, +) -> Tuple[Dict[str, Dict[str, int]], Dict[str, Dict[str, int]]]: + if batch_size <= 0 or len(buckets) <= batch_size: + return buckets, {} + prefixes = sorted(buckets.keys())[:batch_size] + batch = {prefix: buckets[prefix] for prefix in prefixes} + remainder = { + prefix: metrics for prefix, metrics in buckets.items() if prefix not in batch + } + return batch, remainder + + def _client() -> redis.Redis: global _redis if _redis is None: @@ -683,12 +744,13 @@ def _acquire_flush_lock(organization_id: UUID) -> bool: try: client = _client() lock_key = _flush_lock_key(organization_id) - if client.set(lock_key, "1", nx=True, ex=_FLUSH_LOCK_TTL_SECONDS): + lock_ttl = _flush_lock_ttl_seconds() + if client.set(lock_key, "1", nx=True, ex=lock_ttl): return True deadline = time.monotonic() + _FLUSH_LOCK_WAIT_SECONDS while time.monotonic() < deadline: time.sleep(0.05) - if client.set(lock_key, "1", nx=True, ex=_FLUSH_LOCK_TTL_SECONDS): + if client.set(lock_key, "1", nx=True, ex=lock_ttl): return True if client.get(lock_key) is None: continue @@ -872,6 +934,104 @@ def _upsert_bucket( logger.warning("usage cost apply failed: {}", exc) +def _upsert_claimed_buckets( + db: Session, + organization_id: UUID, + buckets: Dict[str, Dict[str, int]], + *, + pricing_resolver: Any, +) -> Tuple[int, Dict[str, Dict[str, int]]]: + """Persist claimed Redis buckets; return flushed count and unparseable buckets.""" + flushed = 0 + skipped: Dict[str, Dict[str, int]] = {} + for prefix, deltas in buckets.items(): + parsed = _parse_bucket_prefix(prefix) + if not parsed: + skipped[prefix] = deltas + logger.warning( + "llm usage skipped unparseable bucket prefix for org {}", + organization_id, + ) + continue + _upsert_bucket( + db, + organization_id, + parsed, + deltas, + pricing_resolver=pricing_resolver, + ) + flushed += 1 + return flushed, skipped + + +def _flush_redis_pending_to_catalog( + db: Session, + organization_id: UUID, + *, + pricing_resolver: Any, +) -> int: + """Drain Redis pending hash into llm_usage_daily in bounded batches.""" + batch_size = flush_bucket_batch_size() + max_batches = flush_max_batches_per_run() + flushed = 0 + + for _ in range(max_batches): + if not _has_pending_usage(organization_id): + break + + claim_key, claimed_buckets = _claim_pending(organization_id) + if not claim_key or not claimed_buckets: + break + + batch, remainder = _split_buckets_for_flush(claimed_buckets, batch_size) + skipped: Dict[str, Dict[str, int]] = {} + try: + batch_flushed, skipped = _upsert_claimed_buckets( + db, + organization_id, + batch, + pricing_resolver=pricing_resolver, + ) + if claim_key: + _record_claim_committed_pg(db, claim_key, organization_id) + db.commit() + flushed += batch_flushed + if remainder: + _restore_buckets_to_pending(organization_id, remainder) + if skipped: + _restore_buckets_to_pending(organization_id, skipped) + if claim_key: + _mark_claim_committed(claim_key, fast=True) + _ack_claim(claim_key, organization_id) + except Exception as exc: + db.rollback() + if _is_missing_organization_fk(exc): + logger.warning( + "llm usage flush dropped for unknown organization {}: {}", + organization_id, + exc, + ) + if claim_key: + try: + _record_claim_committed_pg(db, claim_key, organization_id) + db.commit() + except Exception: + db.rollback() + _discard_committed_claim(claim_key, organization_id) + return flushed + logger.warning("llm usage catalog flush failed, restoring redis: {}", exc) + restore_buckets = dict(claimed_buckets) + _restore_buckets_to_pending(organization_id, restore_buckets) + if claim_key: + try: + _client().delete(claim_key) + except redis.RedisError: + pass + return flushed + + return flushed + + def _is_unique_violation(exc: BaseException) -> bool: text_blob = " ".join( str(part) @@ -913,10 +1073,13 @@ def _flush_pending_buffer( FROM usage_pending_buffer WHERE organization_id = CAST(:organization_id AS uuid) ORDER BY created_at ASC - LIMIT 2000 + LIMIT :batch_limit """ ), - {"organization_id": str(organization_id)}, + { + "organization_id": str(organization_id), + "batch_limit": flush_bucket_batch_size(), + }, ).mappings().all() except Exception as exc: db.rollback() @@ -997,15 +1160,15 @@ def _flush_pending_buffer( return 0 -_CATALOG_FLUSH_COOLDOWN_SEC = 20 - - def _catalog_flush_recently(organization_id: UUID) -> bool: """Skip flush if another request flushed this org within the cooldown window.""" + cooldown = api_flush_cooldown_seconds() + if cooldown <= 0: + return False try: client = _client() key = f"usage:catalog_flush:{organization_id}" - return not client.set(key, "1", nx=True, ex=_CATALOG_FLUSH_COOLDOWN_SEC) + return not client.set(key, "1", nx=True, ex=cooldown) except redis.RedisError: return False @@ -1022,79 +1185,18 @@ def flush_usage_to_catalog(db: Session, organization_id: UUID, *, force: bool = pricing_resolver = PricingResolver(db) if not skip_redis_flush: redis_locked = _acquire_flush_lock(organization_id) - claim_key = None - buckets: Dict[str, Dict[str, int]] = {} try: if redis_locked: - claim_key, buckets = _claim_pending(organization_id) - if claim_key and buckets: - skipped: Dict[str, Dict[str, int]] = {} - try: - for prefix, deltas in buckets.items(): - parsed = _parse_bucket_prefix(prefix) - if not parsed: - skipped[prefix] = deltas - logger.warning( - "llm usage skipped unparseable bucket prefix for org {}", - organization_id, - ) - continue - _upsert_bucket( - db, - organization_id, - parsed, - deltas, - pricing_resolver=pricing_resolver, - ) - flushed += 1 - if claim_key: - _record_claim_committed_pg(db, claim_key, organization_id) - db.commit() - except Exception as exc: - db.rollback() - if _is_missing_organization_fk(exc): - logger.warning( - "llm usage flush dropped for unknown organization {}: {}", - organization_id, - exc, - ) - if claim_key: - try: - _record_claim_committed_pg( - db, claim_key, organization_id - ) - db.commit() - except Exception: - db.rollback() - _discard_committed_claim(claim_key, organization_id) - claim_key = None - buckets = {} - else: - logger.warning( - "llm usage catalog flush failed, restoring redis: {}", exc - ) - restore_buckets = dict(buckets) - if skipped: - restore_buckets.update(skipped) - _restore_buckets_to_pending(organization_id, restore_buckets) - if claim_key: - try: - _client().delete(claim_key) - except redis.RedisError: - pass - claim_key = None - return _flush_pending_buffer(db, organization_id) - if skipped: - _restore_buckets_to_pending(organization_id, skipped) - if claim_key: - _mark_claim_committed(claim_key, fast=True) - _ack_claim(claim_key, organization_id) - claim_key = None + flushed += _flush_redis_pending_to_catalog( + db, + organization_id, + pricing_resolver=pricing_resolver, + ) finally: if redis_locked: _release_flush_lock(organization_id) - flushed += _flush_pending_buffer(db, organization_id) + flushed += _flush_pending_buffer(db, organization_id, pricing_resolver=pricing_resolver) return flushed diff --git a/app/services/usage/retention.py b/app/services/usage/retention.py new file mode 100644 index 00000000..763cbbcc --- /dev/null +++ b/app/services/usage/retention.py @@ -0,0 +1,41 @@ +"""OSS usage rollup retention — per-org flush beyond history window.""" + +from __future__ import annotations + +from datetime import date, timedelta + +from sqlalchemy.orm import Session + +from loguru import logger + +from app.core.license import get_enabled_features, get_license_info +from app.core.usage_entitlement import OSS_USAGE_HISTORY_DAYS +from app.models.database import LLMUsageDaily + + +def oss_usage_cutoff_date() -> date: + return date.today() - timedelta(days=OSS_USAGE_HISTORY_DAYS - 1) + + +def prune_oss_usage_history(db: Session) -> dict: + """ + Delete rollup rows older than OSS window for non-entitled orgs. + Deployment-wide license → no deletes. Org-scoped license → skip licensed org. + """ + if get_enabled_features() and get_license_info().get("org_id") is None: + return {"deleted": 0} + + cutoff = oss_usage_cutoff_date() + licensed_org = get_license_info().get("org_id") + + query = db.query(LLMUsageDaily).filter(LLMUsageDaily.usage_date < cutoff) + if get_enabled_features() and licensed_org is not None: + query = query.filter(LLMUsageDaily.organization_id != licensed_org) + + deleted = query.delete(synchronize_session=False) + db.commit() + + if deleted: + logger.info("pruned_oss_usage_history deleted_rows={} cutoff={}", deleted, cutoff) + + return {"deleted": deleted, "cutoff": cutoff.isoformat()} diff --git a/app/workers/config.py b/app/workers/config.py index e06336d0..b43306e7 100644 --- a/app/workers/config.py +++ b/app/workers/config.py @@ -140,14 +140,21 @@ "map_agent_flowchart_prompt_sections": {"queue": "celery"}, "flush_usage_counters": {"queue": USAGE_WORKER_QUEUE}, "recompute_usage_costs": {"queue": USAGE_WORKER_QUEUE}, + "prune_oss_usage_history": {"queue": USAGE_WORKER_QUEUE}, } # Periodic flush of Redis usage counters into catalog rollups (usage queue). _flush_interval = float(os.environ.get("USAGE_FLUSH_BEAT_SECONDS", "120")) +_prune_interval = float(os.environ.get("USAGE_PRUNE_BEAT_SECONDS", "86400")) celery_app.conf.beat_schedule = { "flush-llm-usage-counters": { "task": "flush_usage_counters", "schedule": _flush_interval, "options": {"queue": USAGE_WORKER_QUEUE}, }, + "prune-oss-usage-history": { + "task": "prune_oss_usage_history", + "schedule": _prune_interval, + "options": {"queue": USAGE_WORKER_QUEUE}, + }, } diff --git a/app/workers/tasks/__init__.py b/app/workers/tasks/__init__.py index 499d9a5d..1f237e29 100644 --- a/app/workers/tasks/__init__.py +++ b/app/workers/tasks/__init__.py @@ -24,6 +24,7 @@ from . import call_import_bulk_ops from . import flush_usage_counters from . import recompute_usage_costs +from . import prune_oss_usage_history from app.workers.concurrency import eval_dispatch from app.workers.concurrency import fair_dispatch from app.workers.concurrency import fair_diarization_dispatch @@ -119,3 +120,4 @@ ) flush_usage_counters_task = flush_usage_counters.flush_usage_counters_task recompute_usage_costs_task = recompute_usage_costs.recompute_usage_costs_task +prune_oss_usage_history_task = prune_oss_usage_history.prune_oss_usage_history_task diff --git a/app/workers/tasks/prune_oss_usage_history.py b/app/workers/tasks/prune_oss_usage_history.py new file mode 100644 index 00000000..a91b186c --- /dev/null +++ b/app/workers/tasks/prune_oss_usage_history.py @@ -0,0 +1,22 @@ +"""Celery task: prune OSS usage rollup rows beyond history window.""" + +from __future__ import annotations + +from loguru import logger + +from app.database import SessionLocal +from app.workers.config import celery_app + + +@celery_app.task(name="prune_oss_usage_history") +def prune_oss_usage_history_task() -> dict: + from app.services.usage.retention import prune_oss_usage_history + + db = SessionLocal() + try: + result = prune_oss_usage_history(db) + finally: + db.close() + if result.get("deleted"): + logger.info("prune_oss_usage_history_task {}", result) + return result diff --git a/env.example b/env.example index cd34c25a..7e87c790 100644 --- a/env.example +++ b/env.example @@ -69,3 +69,13 @@ AUTH_LOCAL_ALLOW_SIGNUP=true # Enterprise license JWT (RS256). Obtain from the EfficientAI team. # EFFICIENTAI_LICENSE=eyJhbGciOi... +# ----------------------------------------------------------------------------- +# Usage cost flush (worker-usage + beat; optional overrides for api service too) +# See README "Usage Pricing Ops". Copy to .env for Docker Compose / local dev. +# ----------------------------------------------------------------------------- +USAGE_FLUSH_BUCKET_BATCH_SIZE=500 +USAGE_FLUSH_MAX_BATCHES_PER_RUN=30 +USAGE_FLUSH_BEAT_SECONDS=120 +USAGE_FLUSH_LOCK_TTL_SECONDS=300 +USAGE_API_FLUSH_COOLDOWN_SECONDS=60 + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 287bdd2d..7a078b20 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -132,6 +132,24 @@ function EnterpriseGate({ feature, children }: { feature: string; children: Reac return <>{children} } +function EnterpriseLicenseGate({ children }: { children: React.ReactNode }) { + const { hasExtendedUsageHistory, isLoaded } = useLicenseStore() + + if (!isLoaded) { + return ( +
+
+
+ ) + } + + if (!hasExtendedUsageHistory()) { + return + } + + return <>{children} +} + function App() { return ( @@ -187,7 +205,14 @@ function App() { } /> } /> } /> - } /> + + + + } + /> } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 6dfe8744..fc6ec8ec 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -100,12 +100,18 @@ export interface VoicePlaygroundBlindTestPair { y: VoicePlaygroundBlindTestAudioRef } +export interface UsagePolicy { + extended_history: boolean + max_history_days: number | null +} + export interface LicenseInfoResponse { is_enterprise: boolean enabled_features: string[] all_enterprise_features: string[] feature_catalog?: EnterpriseFeatureCatalog organization?: string + usage_policy?: UsagePolicy } export interface ReportBranding { @@ -2991,6 +2997,11 @@ class ApiClient { return response.data } + async syncOrgUsageCatalog(): Promise<{ flushed_buckets: number }> { + const response = await this.client.post('/api/v1/organizations/usage/catalog/sync') + return response.data + } + async getOrgUsageSummary(params: { start?: string end?: string diff --git a/frontend/src/pages/enterprise/EnterpriseUpgrade.tsx b/frontend/src/pages/enterprise/EnterpriseUpgrade.tsx index 8c990df7..46c5fc2e 100644 --- a/frontend/src/pages/enterprise/EnterpriseUpgrade.tsx +++ b/frontend/src/pages/enterprise/EnterpriseUpgrade.tsx @@ -3,9 +3,9 @@ import { useLicenseStore } from '../../store/licenseStore' const FALLBACK_TITLE = 'Enterprise Feature' -export default function EnterpriseUpgrade({ feature }: { feature: string }) { +export default function EnterpriseUpgrade({ feature }: { feature?: string }) { const getFeatureMeta = useLicenseStore((state) => state.getFeatureMeta) - const title = getFeatureMeta(feature)?.title ?? FALLBACK_TITLE + const title = feature ? (getFeatureMeta(feature)?.title ?? FALLBACK_TITLE) : 'Extended Usage History' return (
@@ -21,7 +21,9 @@ export default function EnterpriseUpgrade({ feature }: { feature: string }) {

- This feature is available with an EfficientAI Enterprise license. + {feature + ? 'This feature is available with an EfficientAI Enterprise license.' + : 'Extended usage history and pricing tools require a valid EfficientAI Enterprise license with any enabled feature.'}

diff --git a/frontend/src/pages/usage/Usage.tsx b/frontend/src/pages/usage/Usage.tsx index 1b049984..a87cf73c 100644 --- a/frontend/src/pages/usage/Usage.tsx +++ b/frontend/src/pages/usage/Usage.tsx @@ -8,8 +8,9 @@ import { useIsAdmin } from '../../hooks/useRole' import UsageFiltersBar from './UsageFiltersBar' import UsageDrillPath from './UsageDrillPath' import UsageCostBreakdownModal from './UsageCostBreakdownModal' -import { defaultUsageDateRange } from './UsageDateRangePicker' +import { defaultUsageDateRange, isRangeWithinMaxDays, rangeForDays } from './UsageDateRangePicker' import { getUsageTimezone } from './usageTimezone' +import { useLicenseStore } from '../../store/licenseStore' import { CALL_IMPORT_BATCH_HEADLINE, CALL_IMPORT_HINT, @@ -561,8 +562,35 @@ export default function Usage() { const usageKind = (searchParams.get('usage_kind') as Kind) || '' const productSection = searchParams.get('product_section') || '' const isAdmin = useIsAdmin() + const { usagePolicy, isLoaded: licenseLoaded, fetchLicense } = useLicenseStore() + const showOssUsageNotice = licenseLoaded && !usagePolicy.extended_history + const maxHistoryDays = !licenseLoaded + ? null + : usagePolicy.extended_history + ? null + : usagePolicy.max_history_days ?? 7 const [costBreakdownOpen, setCostBreakdownOpen] = useState(false) + useEffect(() => { + if (!licenseLoaded) { + void fetchLicense() + } + }, [licenseLoaded, fetchLicense]) + + useEffect(() => { + if (!licenseLoaded || usagePolicy.extended_history) return + const maxDays = usagePolicy.max_history_days ?? 7 + if (!isRangeWithinMaxDays(start, end, maxDays)) { + const r = rangeForDays(maxDays) + setSearchParams((prev) => { + const next = new URLSearchParams(prev) + next.set('start', r.start) + next.set('end', r.end) + return next + }) + } + }, [licenseLoaded, usagePolicy.extended_history, usagePolicy.max_history_days, start, end, setSearchParams]) + const showWorkspaceComposite = Boolean(workspaceId) && !callImportId && @@ -609,13 +637,25 @@ export default function Usage() { staleTime: 60 * 1000, } + const catalogSync = useQuery({ + queryKey: ['org-usage', 'catalog-sync'], + queryFn: () => apiClient.syncOrgUsageCatalog(), + staleTime: 60 * 1000, + refetchOnWindowFocus: false, + }) + + const usageReadsReady = catalogSync.isSuccess || catalogSync.isError + const { data: summary, isLoading: summaryLoading, isFetching: summaryFetching } = useQuery({ queryKey: ['org-usage', 'summary', dataParams], queryFn: () => apiClient.getOrgUsageSummary(dataParams), + enabled: usageReadsReady, ...usageQueryDefaults, placeholderData: keepPreviousData, }) + const usageStatsLoading = catalogSync.isLoading || summaryLoading + const { data: breakdown, isLoading: breakdownLoading, @@ -628,7 +668,7 @@ export default function Usage() { group_by: groupBy, limit: 100, }), - enabled: !showWorkspaceComposite, + enabled: usageReadsReady && !showWorkspaceComposite, ...usageQueryDefaults, placeholderData: (previousData, previousQuery) => { if (!previousQuery || previousQuery.queryKey[2] !== groupBy) return undefined @@ -648,7 +688,7 @@ export default function Usage() { group_by: 'call_import', limit: 100, }), - enabled: showWorkspaceComposite, + enabled: usageReadsReady && showWorkspaceComposite, ...usageQueryDefaults, }) @@ -664,13 +704,14 @@ export default function Usage() { group_by: 'resource', limit: 100, }), - enabled: showWorkspaceComposite, + enabled: usageReadsReady && showWorkspaceComposite, ...usageQueryDefaults, }) const { data: filterOptions, isFetching: filtersLoading } = useQuery({ queryKey: ['org-usage', 'filters', scopeParams], queryFn: () => apiClient.getOrgUsageFilters(scopeParams), + enabled: usageReadsReady, staleTime: 60 * 1000, placeholderData: (previousData, previousQuery) => { if (!previousQuery) return undefined @@ -1077,7 +1118,7 @@ export default function Usage() { Cost breakdown ) : null} - {isAdmin ? ( + {isAdmin && licenseLoaded && usagePolicy.extended_history ? (
+ {showOssUsageNotice ? ( +
+ Showing the last {maxHistoryDays} days of usage history. Set{' '} + + EFFICIENTAI_LICENSE + {' '} + on your server with any enterprise feature to unlock extended history. +
+ ) : null} +
- - - - + + + +
@@ -1110,31 +1161,31 @@ export default function Usage() { ) : null} {showTts ? ( - + ) : null} {(totals?.cache_read_tokens || 0) > 0 ? ( - + ) : null} {(totals?.cache_creation_tokens || 0) > 0 ? ( ) : null} {(totals?.reasoning_tokens || 0) > 0 ? ( - + ) : null}
@@ -1196,6 +1247,7 @@ export default function Usage() { onUsageKindChange={(k) => setParams({ usage_kind: k || null })} onModelChange={(v) => setParams({ model: v || null })} onClearAll={handleClearAll} + maxHistoryDays={maxHistoryDays} /> diff --git a/frontend/src/pages/usage/UsageDateRangePicker.tsx b/frontend/src/pages/usage/UsageDateRangePicker.tsx index 3ffeaeec..cd5718ab 100644 --- a/frontend/src/pages/usage/UsageDateRangePicker.tsx +++ b/frontend/src/pages/usage/UsageDateRangePicker.tsx @@ -9,6 +9,7 @@ type UsageDateRangePickerProps = { start: string end: string onApply: (start: string, end: string) => void + maxHistoryDays?: number | null } function toDateInput(d: Date): string { @@ -23,13 +24,18 @@ function formatDisplay(start: string, end: string): string { return `${start} → ${end}` } -function rangeForDays(days: number): { start: string; end: string } { +export function rangeForDays(days: number): { start: string; end: string } { const end = new Date() const start = new Date() start.setDate(start.getDate() - (days - 1)) return { start: toDateInput(start), end: toDateInput(end) } } +export function isRangeWithinMaxDays(start: string, end: string, maxDays: number): boolean { + const r = rangeForDays(maxDays) + return start >= r.start && end <= r.end +} + const QUICK_RANGES = [ { label: '1d', days: 1 }, { label: '7d', days: 7 }, @@ -44,6 +50,7 @@ export default function UsageDateRangePicker({ start, end, onApply, + maxHistoryDays = null, }: UsageDateRangePickerProps) { const [open, setOpen] = useState(false) const [mode, setMode] = useState('relative') @@ -52,13 +59,25 @@ export default function UsageDateRangePicker({ const [relDays, setRelDays] = useState(1) const rootRef = useRef(null) + const quickRanges = useMemo(() => { + if (!maxHistoryDays) return QUICK_RANGES + return QUICK_RANGES.filter((q) => q.days <= maxHistoryDays) + }, [maxHistoryDays]) + + const minStartDate = useMemo(() => { + if (!maxHistoryDays) return null + return rangeForDays(maxHistoryDays).start + }, [maxHistoryDays]) + + const maxRelDays = maxHistoryDays ?? 365 + const activeQuick = useMemo(() => { - for (const q of QUICK_RANGES) { + for (const q of quickRanges) { const r = rangeForDays(q.days) if (r.start === start && r.end === end) return q.label } return null - }, [start, end]) + }, [start, end, quickRanges]) useEffect(() => { if (open) { @@ -96,7 +115,7 @@ export default function UsageDateRangePicker({ return (
- {QUICK_RANGES.map((q) => ( + {quickRanges.map((q) => ( + ))} +
{showCostBreakdown ? (
) diff --git a/frontend/src/pages/usage/UsageCostBreakdownModal.tsx b/frontend/src/pages/usage/UsageCostBreakdownModal.tsx index 9277de53..645729ba 100644 --- a/frontend/src/pages/usage/UsageCostBreakdownModal.tsx +++ b/frontend/src/pages/usage/UsageCostBreakdownModal.tsx @@ -17,9 +17,10 @@ type Props = { onClose: () => void costs?: UsageCosts | null scopeLabel?: string + formatCost?: (usd?: number | null) => string } -function formatCostUsd(usd?: number | null): string { +function defaultFormatCostUsd(usd?: number | null): string { const amount = Number(usd || 0) if (!amount) return '$0.00' return new Intl.NumberFormat(undefined, { @@ -45,6 +46,7 @@ export default function UsageCostBreakdownModal({ onClose, costs, scopeLabel, + formatCost = defaultFormatCostUsd, }: Props) { if (!isOpen) return null @@ -93,7 +95,7 @@ export default function UsageCostBreakdownModal({
Estimated total - {formatCostUsd(costs?.total_cost_usd)} + {formatCost(costs?.total_cost_usd)}
{rows.length > 0 ? ( @@ -105,7 +107,7 @@ export default function UsageCostBreakdownModal({ > {item.label} - {formatCostUsd(costs?.[item.key] as number)} + {formatCost(costs?.[item.key] as number)} ))} diff --git a/frontend/src/pages/usage/UsagePricing.tsx b/frontend/src/pages/usage/UsagePricing.tsx index f976d116..92d159f3 100644 --- a/frontend/src/pages/usage/UsagePricing.tsx +++ b/frontend/src/pages/usage/UsagePricing.tsx @@ -9,6 +9,12 @@ import { useToast } from '../../hooks/useToast' import Button from '../../components/Button' import SearchableSelect from './SearchableSelect' import { usageTheme } from './usageTheme' +import { + formatUsageCostUsd, + getUsageDisplayCurrency, + setUsageDisplayCurrency, + type UsageDisplayCurrency, +} from '../../lib/usageCurrency' type UsageKind = 'llm' | 'stt' | 'tts' @@ -74,15 +80,6 @@ function FormField({ ) } -function formatUsd(value?: number | null): string { - if (value == null || Number.isNaN(value)) return '—' - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 6, - }).format(value) -} function todayIso(): string { return new Date().toISOString().slice(0, 10) @@ -93,6 +90,18 @@ export default function UsagePricing() { const queryClient = useQueryClient() const { showToast, ToastContainer } = useToast() + const [displayCurrency, setDisplayCurrency] = useState(() => + getUsageDisplayCurrency(), + ) + const { data: fxRate } = useQuery({ + queryKey: ['org-usage', 'fx-rate'], + queryFn: () => apiClient.getOrgUsageFxRate(), + staleTime: 60 * 60 * 1000, + }) + const inrRate = fxRate?.rate ?? 83 + const formatDisplayRate = (value?: number | null) => + formatUsageCostUsd(value, displayCurrency, inrRate) + const [model, setModel] = useState('') const [usageKind, setUsageKind] = useState('llm') const [effectiveFrom, setEffectiveFrom] = useState(todayIso()) @@ -197,6 +206,29 @@ export default function UsagePricing() {

Pricing overrides

+
+ {(['USD', 'INR'] as const).map((currency) => ( + + ))} +
- ))} -
- {showCostBreakdown ? ( - - ) : null} - {isAdmin && licenseLoaded && usagePolicy.extended_history ? ( - - Pricing overrides - - ) : null} -
- {summary?.last_updated_at ? ( -

- Updated {new Date(summary.last_updated_at).toLocaleString()} -

- ) : null} -
-
- - {showOssUsageNotice ? ( -
- Showing the last {maxHistoryDays} days of usage history. Set{' '} - - EFFICIENTAI_LICENSE - {' '} - on your server with any enterprise feature to unlock extended history. -
- ) : null} - -
-
- - - - -
- -
-
- {showAudio ? ( - - ) : null} - {showTts ? ( - - ) : null} - {(totals?.cache_read_tokens || 0) > 0 ? ( - - ) : null} - {(totals?.cache_creation_tokens || 0) > 0 ? ( - - ) : null} - {(totals?.reasoning_tokens || 0) > 0 ? ( - - ) : null} -
-
- -
-
-
- - setParams({ start: s, end: e })} - onWorkspaceChange={handleWorkspaceChange} - onCallImportChange={handleCallImportChange} - onDatasetChange={(v) => - setParams({ - dataset: v || null, - call_import_id: null, - resource_id: null, - }) - } - onTagChange={(v) => - setParams({ - tag_id: v || null, - call_import_id: null, - resource_id: null, - }) - } - onEvaluationChange={(id) => { - if (id.startsWith(USAGE_SECTION_SOURCE_PREFIX)) { - const section = id.slice(USAGE_SECTION_SOURCE_PREFIX.length) - setParams({ - product_section: section || null, - resource_id: null, - call_import_id: null, - model: null, - usage_kind: null, - }) - return - } - const resource = filterOptions?.resources?.find( - (r) => idKey(r.id) === idKey(id), - ) - setParams({ - resource_id: id || null, - product_section: id ? resource?.product_section || null : null, - model: null, - usage_kind: null, - }) - }} - onUsageKindChange={(k) => setParams({ usage_kind: k || null })} - onModelChange={(v) => setParams({ model: v || null })} - onClearAll={handleClearAll} - maxHistoryDays={maxHistoryDays} - /> - - -
- - {showTruncation ? ( -

- Showing the first 100 rows for this level. Narrow the date range or drill - further for complete detail. -

- ) : null} -
- - {tableLoading || breakdownStale ? ( -
- -
- ) : rows.length === 0 ? ( -
- No usage in this period for the current scope. Try 7d or 30d, or go back up a - level. -
- ) : ( -
- {tableFetching ? ( -
- -
- ) : null} - - - - - - - - - - - - - - - - {rows.map((row, idx) => { - const drillable = isRowDrillable(row) - const compositeHeadline = - 'rowKind' in row ? compositeRowHeadline(row) : null - const rowTitle = tableRowLabel(groupBy, row, filterOptions) - const showRowTitle = - 'rowKind' in row - ? row.rowKind === 'workspace_resource' || - (compositeHeadline && - rowTitle.toLowerCase() !== compositeHeadline.toLowerCase()) - : true - return ( - drillable && handleRowDrill(row)} - > - - - - - - - - - - - ) - })} - -
- {drillColumnLabel(groupBy, showWorkspaceComposite)} - LLM callsInput tokensOutput tokensTotal tokensEst. costSTT audioTTS charsCache read
- - - {'rowKind' in row ? ( - - {compositeHeadline} - - ) : null} - {showRowTitle ? ( - - {rowTitle} - - ) : !('rowKind' in row) ? ( - {rowTitle} - ) : null} - {'hint' in row && row.hint ? ( - - {row.hint} - - ) : null} - - {drillable ? ( - - ) : null} - - - {formatNumber(row.call_count)} - - {formatNumber(row.prompt_tokens)} - - {formatNumber(row.completion_tokens)} - - {formatNumber(row.total_tokens)} - - {formatCostUsd(rowCostUsd(row))} - - {row.audio_seconds ? formatAudio(row.audio_seconds) : '—'} - - {row.tts_characters ? formatNumber(row.tts_characters) : '—'} - - {formatNumber(row.cache_read_tokens)} -
-
- )} -
-
- - setCostBreakdownOpen(false)} - costs={totals?.costs} - scopeLabel={scopeSubtitle} - formatCost={formatCostUsd} - /> -
- ) -} - -const statCardClass = 'border border-gray-200 ring-1 ring-[#fde047]/25 shadow-sm' - -function StatCard({ - label, - value, - valueLabel, - loading, - emphasize = false, - className = '', -}: { - label: string - value?: number - valueLabel?: string - loading?: boolean - emphasize?: boolean - className?: string -}) { - return ( - - -

{label}

-

- {loading ? '—' : valueLabel ?? formatNumber(value || 0)} -

-
-
- ) -} +import { useEffect, useMemo, useState } from 'react' +import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { Link, useSearchParams } from 'react-router-dom' +import { Card, CardBody, Spinner } from '@heroui/react' +import { Activity, ChevronRight, CircleDollarSign } from 'lucide-react' +import { apiClient } from '../../lib/api' +import { useIsAdmin } from '../../hooks/useRole' +import UsageFiltersBar from './UsageFiltersBar' +import UsageDrillPath from './UsageDrillPath' +import UsageCostBreakdownModal from './UsageCostBreakdownModal' +import { defaultUsageDateRange, isRangeWithinMaxDays, rangeForDays } from './UsageDateRangePicker' +import { getUsageTimezone } from './usageTimezone' +import { + formatUsageCostUsd, + getUsageDisplayCurrency, + setUsageDisplayCurrency, + type UsageDisplayCurrency, +} from '../../lib/usageCurrency' +import { useLicenseStore } from '../../store/licenseStore' +import { + CALL_IMPORT_BATCH_HEADLINE, + CALL_IMPORT_HINT, + CALL_IMPORT_PRODUCT_SECTIONS, + PRODUCT_SECTION_HEADLINES, + PRODUCT_SECTION_HINTS, + USAGE_SECTION_SOURCE_PREFIX, +} from './usageProductHints' + +type DrillGroupBy = + | 'workspace' + | 'call_import' + | 'resource' + | 'model' + | 'usage_kind' + | 'product_section' +type Kind = '' | 'llm' | 'stt' | 'tts' + +type FilterOptions = { + workspaces: Array<{ id: string; name: string }> + call_imports: Array<{ id: string; label: string }> + evaluations: Array<{ id: string; label: string }> + resources?: Array<{ id: string; label: string; type?: string; product_section?: string }> + models: string[] + usage_kinds: Array<{ id: string; label: string }> + product_sections?: Array<{ id: string; label: string }> + datasets?: string[] + tags?: Array<{ id: string; label: string }> +} + +type UsageCosts = { + input_cost_usd: number + output_cost_usd: number + cache_read_cost_usd: number + cache_write_cost_usd: number + reasoning_cost_usd: number + audio_cost_usd: number + tts_cost_usd: number + total_cost_usd: number + currency: string + has_unpriced_usage: boolean +} + +type BreakdownRow = { + workspace_id?: string | null + workspace_name?: string | null + call_import_id?: string | null + call_import_label?: string | null + resource_id?: string | null + resource_type?: string | null + resource_label?: string | null + model?: string | null + usage_kind?: string | null + product_section?: string | null + product_section_label?: string | null + prompt_tokens: number + completion_tokens: number + total_tokens: number + cache_read_tokens: number + cache_creation_tokens: number + reasoning_tokens: number + audio_seconds: number + tts_characters: number + call_count: number + input_cost_micro_usd?: number + output_cost_micro_usd?: number + cache_read_cost_micro_usd?: number + cache_creation_cost_micro_usd?: number + reasoning_cost_micro_usd?: number + audio_cost_micro_usd?: number + tts_cost_micro_usd?: number + total_cost_micro_usd?: number + costs?: UsageCosts +} + +type WorkspaceSourceRow = BreakdownRow & { + rowKind: 'call_import' | 'workspace_resource' + hint: string +} + +const NON_COMPOSITE_RESOURCE_TYPES = new Set([ + 'call_import', + 'call_import_evaluation', +]) + +type TableRow = BreakdownRow | WorkspaceSourceRow + +const EMPTY_METRICS = { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + cache_read_tokens: 0, + cache_creation_tokens: 0, + reasoning_tokens: 0, + audio_seconds: 0, + tts_characters: 0, + call_count: 0, + input_cost_micro_usd: 0, + output_cost_micro_usd: 0, + cache_read_cost_micro_usd: 0, + cache_creation_cost_micro_usd: 0, + reasoning_cost_micro_usd: 0, + audio_cost_micro_usd: 0, + tts_cost_micro_usd: 0, + total_cost_micro_usd: 0, + costs: { + input_cost_usd: 0, + output_cost_usd: 0, + cache_read_cost_usd: 0, + cache_write_cost_usd: 0, + reasoning_cost_usd: 0, + audio_cost_usd: 0, + tts_cost_usd: 0, + total_cost_usd: 0, + currency: 'USD', + has_unpriced_usage: false, + }, +} + +function compositeRowHeadline(row: WorkspaceSourceRow): string { + if (row.rowKind === 'call_import') return CALL_IMPORT_BATCH_HEADLINE + const section = row.product_section || '' + return ( + PRODUCT_SECTION_HEADLINES[section] || + row.product_section_label || + section || + 'Other' + ) +} + +function usableResourceLabel(label: string | null | undefined): string | undefined { + if (!label || label === 'Unscoped') return undefined + return label +} + +function compositeRowTitle(row: WorkspaceSourceRow, options?: FilterOptions): string { + if (row.rowKind === 'call_import') { + return row.call_import_label || 'Call import batch' + } + const fromFilters = row.resource_id + ? options?.resources?.find((r) => idKey(r.id) === idKey(row.resource_id))?.label + : undefined + return ( + usableResourceLabel(row.resource_label) || + fromFilters || + row.product_section_label || + 'Unscoped usage' + ) +} + +function sortRows(rows: BreakdownRow[]): BreakdownRow[] { + return [...rows].sort((a, b) => b.total_tokens - a.total_tokens) +} + +function formatNumber(value: number): string { + return new Intl.NumberFormat().format(value || 0) +} + +function rowCostUsd(row: Pick): number { + if (row.costs?.total_cost_usd != null) return row.costs.total_cost_usd + return Number(row.total_cost_micro_usd || 0) / 1_000_000 +} + +function formatAudio(seconds: number): string { + const total = Math.max(0, Math.floor(seconds || 0)) + if (total < 60) return `${total}s` + const mins = Math.floor(total / 60) + const secs = total % 60 + if (mins < 60) return secs ? `${mins}m ${secs}s` : `${mins}m` + const hours = Math.floor(mins / 60) + const remMins = mins % 60 + return remMins ? `${hours}h ${remMins}m` : `${hours}h` +} + +function rowHasUsageForKind(row: BreakdownRow, kind: Kind): boolean { + if (!kind) return true + if (kind === 'llm') { + return (row.total_tokens ?? 0) > 0 || (row.call_count ?? 0) > 0 + } + if (kind === 'stt') { + return (row.audio_seconds ?? 0) > 0 || (row.call_count ?? 0) > 0 + } + if (kind === 'tts') { + return (row.tts_characters ?? 0) > 0 || (row.call_count ?? 0) > 0 + } + return true +} + +function filterRowsForUsageKind(rows: BreakdownRow[], kind: Kind): BreakdownRow[] { + if (!kind) return rows + return rows.filter((row) => rowHasUsageForKind(row, kind)) +} + +function rowHasAnyUsage(row: BreakdownRow): boolean { + return ( + (row.total_tokens ?? 0) > 0 || + (row.call_count ?? 0) > 0 || + (row.audio_seconds ?? 0) > 0 || + (row.tts_characters ?? 0) > 0 + ) +} + +function isUsageScopeActive( + workspaceId: string, + callImportId: string, + evaluationId: string, + dataset: string, + tagId: string, + usageKind: Kind, + model: string, + productSection: string, +): boolean { + return Boolean( + workspaceId || + callImportId || + evaluationId || + dataset || + tagId || + usageKind || + model || + productSection, + ) +} + +/** Drop zero rows when any filter is active; kind filter uses kind-specific metrics. */ +function filterTableRows( + rows: BreakdownRow[], + usageKind: Kind, + scopeActive: boolean, +): BreakdownRow[] { + if (usageKind) return filterRowsForUsageKind(rows, usageKind) + if (scopeActive) return rows.filter(rowHasAnyUsage) + return rows +} + +function drillGroupBy( + workspaceId: string, + callImportId: string, + evaluationId: string, + model: string, + productSection: string, +): DrillGroupBy { + if (productSection && model) return 'usage_kind' + if (productSection) return 'model' + if (evaluationId && model) return 'usage_kind' + if (evaluationId) return 'model' + if (callImportId) return 'resource' + if (workspaceId) return 'call_import' + return 'workspace' +} + +function enrichCallImportRows( + rawRows: BreakdownRow[], + options?: FilterOptions, +): BreakdownRow[] { + const labelById = new Map( + (options?.call_imports ?? []).map((c) => [idKey(c.id), c.label]), + ) + return sortRows( + rawRows + .filter((r) => r.call_import_id) + .map((row) => { + const key = idKey(row.call_import_id) + return { + ...row, + call_import_label: + labelById.get(key) || row.call_import_label || 'Call import', + } + }), + ) +} + +function buildWorkspaceCompositeRows( + callImportRaw: BreakdownRow[], + resourceRaw: BreakdownRow[], + options?: FilterOptions, + padMissingRows = true, +): WorkspaceSourceRow[] { + const resourceLabelById = new Map( + (options?.resources ?? []).map((r) => [idKey(r.id), r.label]), + ) + const importRows = enrichCallImportRows(callImportRaw, options) + const rows: WorkspaceSourceRow[] = [] + const shownImportIds = new Set() + + for (const row of importRows) { + if (!row.call_import_id) continue + shownImportIds.add(idKey(row.call_import_id)) + rows.push({ + ...row, + rowKind: 'call_import', + hint: CALL_IMPORT_HINT, + }) + } + + if (padMissingRows) { + for (const ci of options?.call_imports ?? []) { + const key = idKey(ci.id) + if (shownImportIds.has(key)) continue + shownImportIds.add(key) + rows.push({ + call_import_id: ci.id, + call_import_label: ci.label, + ...EMPTY_METRICS, + rowKind: 'call_import', + hint: CALL_IMPORT_HINT, + }) + } + } + + for (const row of resourceRaw) { + const section = row.product_section + if (!section || CALL_IMPORT_PRODUCT_SECTIONS.has(section)) continue + if (NON_COMPOSITE_RESOURCE_TYPES.has(row.resource_type || '')) continue + if ( + row.total_tokens === 0 && + row.call_count === 0 && + !row.audio_seconds && + !row.tts_characters + ) { + continue + } + if (!rowHasAnyUsage(row)) { + continue + } + rows.push({ + ...row, + rowKind: 'workspace_resource', + product_section: section, + product_section_label: row.product_section_label || section, + resource_label: + resourceLabelById.get(idKey(row.resource_id)) || + usableResourceLabel(row.resource_label), + hint: PRODUCT_SECTION_HINTS[section] || 'Product usage', + }) + } + + return sortRows(rows) as WorkspaceSourceRow[] +} + +function drillColumnLabel(groupBy: DrillGroupBy, composite = false): string { + if (composite) return 'Source' + if (groupBy === 'workspace') return 'Workspace' + if (groupBy === 'call_import') return 'Call import' + if (groupBy === 'resource') return 'Evaluation run' + if (groupBy === 'product_section') return 'Product area' + if (groupBy === 'model') return 'Model' + return 'Kind' +} + +function rowLabel(groupBy: DrillGroupBy, row: BreakdownRow, options?: FilterOptions): string { + if (groupBy === 'workspace') return row.workspace_name || 'Unknown' + if (groupBy === 'call_import') return row.call_import_label || 'Call import' + if (groupBy === 'resource') { + const fromFilters = row.resource_id + ? options?.resources?.find((r) => idKey(r.id) === idKey(row.resource_id))?.label + : undefined + return ( + usableResourceLabel(row.resource_label) || fromFilters || 'Unscoped' + ) + } + if (groupBy === 'model') return row.model || '—' + if (groupBy === 'product_section') + return row.product_section_label || row.product_section || '—' + if (row.usage_kind === 'stt') return 'STT' + if (row.usage_kind === 'llm') return 'LLM' + if (row.usage_kind === 'tts') return 'TTS' + return row.usage_kind || '—' +} + +function tableRowLabel( + groupBy: DrillGroupBy, + row: TableRow, + options?: FilterOptions, +): string { + if ('rowKind' in row) { + if (row.rowKind === 'call_import') return row.call_import_label || 'Call import batch' + return compositeRowTitle(row, options) + } + return rowLabel(groupBy, row, options) +} + +function idInOptions( + id: string, + options: Array<{ id: string }> | undefined, +): boolean { + if (!id || !options?.length) return false + const key = idKey(id) + return options.some((o) => idKey(o.id) === key) +} + +function idKey(id: string | null | undefined): string { + return id ? String(id).toLowerCase() : '' +} + +function mergeDrillRows( + groupBy: DrillGroupBy, + rawRows: BreakdownRow[], + options?: FilterOptions, + padMissingRows = true, +): BreakdownRow[] { + if (groupBy === 'workspace' && options?.workspaces?.length && padMissingRows) { + const byId = new Map( + rawRows + .filter((r) => r.workspace_id) + .map((r) => [idKey(r.workspace_id), r]), + ) + const merged: BreakdownRow[] = options.workspaces.map((ws) => ({ + workspace_id: ws.id, + workspace_name: ws.name, + ...(byId.get(idKey(ws.id)) ?? EMPTY_METRICS), + })) + for (const row of rawRows) { + if (!row.workspace_id) { + merged.push({ + ...row, + workspace_name: row.workspace_name || 'No workspace', + }) + } + } + return sortRows(merged) + } + + if (groupBy === 'call_import') { + if (rawRows.length === 0) return [] + + const labelById = new Map( + (options?.call_imports ?? []).map((c) => [idKey(c.id), c.label]), + ) + const shown = new Set() + const merged: BreakdownRow[] = [] + + for (const row of rawRows) { + if (row.call_import_id) { + const key = idKey(row.call_import_id) + shown.add(key) + merged.push({ + ...row, + call_import_label: + labelById.get(key) || row.call_import_label || 'Call import', + }) + } + } + + if (padMissingRows) { + for (const ci of options?.call_imports ?? []) { + const key = idKey(ci.id) + if (!shown.has(key)) { + merged.push({ + call_import_id: ci.id, + call_import_label: ci.label, + ...EMPTY_METRICS, + }) + } + } + } + + return sortRows(merged) + } + + if (groupBy === 'resource') { + if (rawRows.length === 0) return [] + + const evalLabelById = new Map( + (options?.evaluations ?? []).map((e) => [idKey(e.id), e.label]), + ) + const resourceLabelById = new Map( + (options?.resources ?? []).map((r) => [idKey(r.id), r.label]), + ) + const shown = new Set() + const merged: BreakdownRow[] = [] + + for (const row of rawRows) { + if (row.resource_id) { + const key = idKey(row.resource_id) + shown.add(key) + merged.push({ + ...row, + resource_label: + resourceLabelById.get(key) || + evalLabelById.get(key) || + usableResourceLabel(row.resource_label) || + 'Evaluation', + }) + } + } + + if (padMissingRows) { + for (const ev of options?.evaluations ?? []) { + const key = idKey(ev.id) + if (!shown.has(key)) { + merged.push({ + resource_id: ev.id, + resource_label: ev.label, + ...EMPTY_METRICS, + }) + } + } + } + + return sortRows(merged) + } + + if (groupBy === 'model' && options?.models?.length && padMissingRows) { + if (rawRows.length === 0) return [] + const byName = new Map( + rawRows.filter((r) => r.model).map((r) => [r.model!, r]), + ) + const merged = options.models.map((name) => ({ + model: name, + ...(byName.get(name) ?? EMPTY_METRICS), + })) + return sortRows(merged) + } + + if (groupBy === 'usage_kind') { + return sortRows( + rawRows.filter((row) => rowHasUsageForKind(row, row.usage_kind as Kind)), + ) + } + + return sortRows(rawRows) +} + +export default function Usage() { + const [searchParams, setSearchParams] = useSearchParams() + const defaultRange = useMemo(() => defaultUsageDateRange(), []) + const usageTimezone = useMemo(() => getUsageTimezone(), []) + + const start = searchParams.get('start') || defaultRange.start + const end = searchParams.get('end') || defaultRange.end + const workspaceId = searchParams.get('workspace_id') || '' + const callImportId = searchParams.get('call_import_id') || '' + const dataset = searchParams.get('dataset') || '' + const tagId = searchParams.get('tag_id') || '' + const evaluationId = searchParams.get('resource_id') || '' + const model = searchParams.get('model') || '' + const usageKind = (searchParams.get('usage_kind') as Kind) || '' + const productSection = searchParams.get('product_section') || '' + const isAdmin = useIsAdmin() + const { usagePolicy, isLoaded: licenseLoaded, fetchLicense } = useLicenseStore() + const showOssUsageNotice = licenseLoaded && !usagePolicy.extended_history + const maxHistoryDays = !licenseLoaded + ? null + : usagePolicy.extended_history + ? null + : usagePolicy.max_history_days ?? 7 + const [costBreakdownOpen, setCostBreakdownOpen] = useState(false) + const [displayCurrency, setDisplayCurrency] = useState(() => + getUsageDisplayCurrency(), + ) + + const { data: fxRate } = useQuery({ + queryKey: ['org-usage', 'fx-rate'], + queryFn: () => apiClient.getOrgUsageFxRate(), + staleTime: 60 * 60 * 1000, + }) + const inrRate = fxRate?.rate ?? 83 + const formatCostUsd = (usd?: number | null) => + formatUsageCostUsd(usd, displayCurrency, inrRate) + + useEffect(() => { + if (!licenseLoaded) { + void fetchLicense() + } + }, [licenseLoaded, fetchLicense]) + + useEffect(() => { + if (!licenseLoaded || usagePolicy.extended_history) return + const maxDays = usagePolicy.max_history_days ?? 7 + if (!isRangeWithinMaxDays(start, end, maxDays)) { + const r = rangeForDays(maxDays) + setSearchParams((prev) => { + const next = new URLSearchParams(prev) + next.set('start', r.start) + next.set('end', r.end) + return next + }) + } + }, [licenseLoaded, usagePolicy.extended_history, usagePolicy.max_history_days, start, end, setSearchParams]) + + const showWorkspaceComposite = + Boolean(workspaceId) && + !callImportId && + !evaluationId && + !productSection + + const groupBy = drillGroupBy( + workspaceId, + callImportId, + evaluationId, + model, + productSection, + ) + + const setParams = (updates: Record) => { + const next = new URLSearchParams(searchParams) + for (const [key, value] of Object.entries(updates)) { + if (!value) next.delete(key) + else next.set(key, value) + } + setSearchParams(next) + } + + const scopeParams = { + start, + end, + tz: usageTimezone, + workspace_id: workspaceId || undefined, + call_import_id: callImportId || undefined, + dataset: dataset || undefined, + tag_id: tagId || undefined, + product_section: productSection || undefined, + usage_kind: usageKind || undefined, + model: model || undefined, + resource_id: evaluationId || undefined, + } + + const dataParams = { + ...scopeParams, + evaluation_id: callImportId ? evaluationId || undefined : undefined, + } + + const usageQueryDefaults = { + staleTime: 0, + refetchOnMount: 'always' as const, + refetchOnWindowFocus: true, + } + + const { + data: summary, + isLoading: summaryLoading, + isFetching: summaryFetching, + isError: summaryError, + error: summaryQueryError, + } = useQuery({ + queryKey: ['org-usage', 'summary', dataParams], + queryFn: () => apiClient.getOrgUsageSummary(dataParams), + ...usageQueryDefaults, + placeholderData: keepPreviousData, + }) + + const usageStatsLoading = summaryLoading + + const { + data: breakdown, + isLoading: breakdownLoading, + isFetching: breakdownFetching, + } = useQuery({ + queryKey: ['org-usage', 'breakdown', groupBy, dataParams], + queryFn: () => + apiClient.getOrgUsageBreakdown({ + ...dataParams, + group_by: groupBy, + limit: 100, + }), + enabled: !showWorkspaceComposite, + ...usageQueryDefaults, + placeholderData: (previousData, previousQuery) => { + if (!previousQuery || previousQuery.queryKey[2] !== groupBy) return undefined + return previousData + }, + }) + + const { + data: importBreakdown, + isLoading: importBreakdownLoading, + isFetching: importBreakdownFetching, + } = useQuery({ + queryKey: ['org-usage', 'breakdown', 'call_import', dataParams], + queryFn: () => + apiClient.getOrgUsageBreakdown({ + ...dataParams, + group_by: 'call_import', + limit: 100, + }), + enabled: showWorkspaceComposite, + ...usageQueryDefaults, + }) + + const { + data: resourceBreakdown, + isLoading: resourceBreakdownLoading, + isFetching: resourceBreakdownFetching, + } = useQuery({ + queryKey: ['org-usage', 'breakdown', 'resource', dataParams], + queryFn: () => + apiClient.getOrgUsageBreakdown({ + ...dataParams, + group_by: 'resource', + limit: 100, + }), + enabled: showWorkspaceComposite, + ...usageQueryDefaults, + }) + + const { data: filterOptions, isFetching: filtersLoading } = useQuery({ + queryKey: ['org-usage', 'filters', scopeParams], + queryFn: () => apiClient.getOrgUsageFilters(scopeParams), + ...usageQueryDefaults, + placeholderData: (previousData, previousQuery) => { + if (!previousQuery) return undefined + const prevScope = previousQuery.queryKey[2] as typeof scopeParams + if (JSON.stringify(prevScope) !== JSON.stringify(scopeParams)) return undefined + return previousData + }, + }) + + useEffect(() => { + if (!filterOptions) return + const updates: Record = {} + if ( + workspaceId && + !idInOptions(workspaceId, filterOptions.workspaces) + ) { + updates.workspace_id = null + updates.call_import_id = null + updates.resource_id = null + updates.product_section = null + } + if ( + callImportId && + !idInOptions(callImportId, filterOptions.call_imports) + ) { + updates.call_import_id = null + updates.resource_id = null + updates.product_section = null + } + if ( + evaluationId && + !idInOptions(evaluationId, filterOptions.evaluations) && + !idInOptions(evaluationId, filterOptions.resources) + ) { + updates.resource_id = null + updates.product_section = null + } + if (model && !filterOptions.models?.includes(model)) { + updates.model = null + } + if ( + usageKind && + !filterOptions.usage_kinds?.some((k) => k.id === usageKind) + ) { + updates.usage_kind = null + } + if ( + productSection && + !filterOptions.product_sections?.some((s) => s.id === productSection) + ) { + updates.product_section = null + } + if (dataset && !filterOptions.datasets?.includes(dataset)) { + updates.dataset = null + } + if (tagId && !idInOptions(tagId, filterOptions.tags)) { + updates.tag_id = null + } + if (Object.keys(updates).length > 0) setParams(updates) + }, [ + filterOptions, + workspaceId, + callImportId, + dataset, + tagId, + evaluationId, + model, + usageKind, + productSection, + ]) + + const breakdownMatchesLevel = breakdown?.group_by === groupBy + const rawRows = useMemo((): BreakdownRow[] => { + if (!breakdown || !breakdownMatchesLevel) return [] + return breakdown.rows as BreakdownRow[] + }, [breakdown, breakdownMatchesLevel]) + + const mergeOptions = filterOptions + const scopeActive = isUsageScopeActive( + workspaceId, + callImportId, + evaluationId, + dataset, + tagId, + usageKind, + model, + productSection, + ) + const padMissingRows = !scopeActive + const filteredRawRows = useMemo( + () => filterTableRows(rawRows, usageKind, scopeActive), + [rawRows, usageKind, scopeActive], + ) + const rows: TableRow[] = useMemo(() => { + if (showWorkspaceComposite) { + const importReady = importBreakdown?.group_by === 'call_import' + const resourceReady = resourceBreakdown?.group_by === 'resource' + if (!importReady && !resourceReady) return [] + return buildWorkspaceCompositeRows( + importReady + ? filterTableRows(importBreakdown.rows as BreakdownRow[], usageKind, scopeActive) + : [], + resourceReady + ? filterTableRows(resourceBreakdown.rows as BreakdownRow[], usageKind, scopeActive) + : [], + mergeOptions, + padMissingRows, + ) + } + return mergeDrillRows(groupBy, filteredRawRows, mergeOptions, padMissingRows) + }, [ + showWorkspaceComposite, + importBreakdown, + resourceBreakdown, + groupBy, + filteredRawRows, + mergeOptions, + padMissingRows, + usageKind, + scopeActive, + ]) + + const breakdownStale = + !showWorkspaceComposite && breakdownFetching && !breakdownMatchesLevel + const tableLoading = showWorkspaceComposite + ? (importBreakdownLoading || resourceBreakdownLoading) && + !importBreakdown && + !resourceBreakdown + : breakdownLoading && !breakdown + const tableFetching = showWorkspaceComposite + ? importBreakdownFetching || resourceBreakdownFetching + : breakdownFetching + const totals = summary?.totals + const estimatedTotalCost = + totals?.costs?.total_cost_usd ?? (totals?.total_cost_micro_usd || 0) / 1_000_000 + const showCostBreakdown = + estimatedTotalCost > 0 || Boolean(totals?.costs?.has_unpriced_usage) + const showAudio = Boolean(totals?.audio_seconds) + const showTts = + Boolean(totals?.tts_characters) || + rows.some((r) => Boolean(r.tts_characters)) + + const showTruncation = + (!showWorkspaceComposite && + breakdownMatchesLevel && + Boolean(breakdown?.truncated_at_limit)) || + (showWorkspaceComposite && + Boolean( + importBreakdown?.truncated_at_limit || resourceBreakdown?.truncated_at_limit, + )) + + const workspaceLabel = + filterOptions?.workspaces?.find((w) => idKey(w.id) === idKey(workspaceId))?.name + const callImportLabel = + filterOptions?.call_imports?.find((c) => idKey(c.id) === idKey(callImportId))?.label + const evaluationLabel = + filterOptions?.resources?.find((r) => idKey(r.id) === idKey(evaluationId))?.label || + filterOptions?.evaluations?.find((e) => idKey(e.id) === idKey(evaluationId))?.label + + const productSectionLabel = + filterOptions?.product_sections?.find((s) => s.id === productSection)?.label + + const scopeSubtitle = model + ? model + : evaluationId + ? evaluationLabel || 'Evaluation' + : callImportId + ? callImportLabel || 'Call import' + : productSection + ? productSectionLabel || 'Product area' + : workspaceId + ? workspaceLabel || 'Workspace' + : 'Organization' + + const levelHint = (() => { + if (showWorkspaceComposite) { + return 'Call import batches and other product usage — click a row to drill down' + } + if (groupBy === 'workspace') return 'Click a workspace to drill down' + if (groupBy === 'call_import') return 'Click a call import to see evaluation runs' + if (groupBy === 'product_section') return 'Click a product area to see models used' + if (groupBy === 'resource') return 'Click an evaluation to see models used' + if (groupBy === 'model') return 'Click a model to see usage by kind' + return 'Token totals by LLM / STT / TTS' + })() + + const drillCrumbs = [ + { + label: 'Organization', + onClick: + workspaceId || + callImportId || + evaluationId || + model || + productSection + ? () => + setParams({ + workspace_id: null, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + : undefined, + }, + ...(workspaceId + ? [ + { + label: workspaceLabel || 'Workspace', + onClick: + callImportId || evaluationId || model || productSection + ? () => + setParams({ + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + : undefined, + }, + ] + : []), + ...(productSection + ? [ + { + label: productSectionLabel || 'Product area', + onClick: + model + ? () => setParams({ model: null, usage_kind: null }) + : undefined, + }, + ] + : []), + ...(callImportId + ? [ + { + label: callImportLabel || 'Call import', + onClick: + evaluationId || model + ? () => + setParams({ + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + : undefined, + }, + ] + : []), + ...(evaluationId + ? [ + { + label: evaluationLabel || 'Evaluation', + onClick: model + ? () => setParams({ model: null, usage_kind: null }) + : undefined, + }, + ] + : []), + ...(model ? [{ label: model }] : []), + ] + + const handleWorkspaceChange = (id: string) => { + setParams({ + workspace_id: id || null, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + } + + const handleCallImportChange = (id: string) => { + setParams({ + call_import_id: id || null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + } + + const handleClearAll = () => { + setParams({ + workspace_id: null, + call_import_id: null, + dataset: null, + tag_id: null, + resource_id: null, + usage_kind: null, + model: null, + product_section: null, + }) + } + + const handleRowDrill = (row: TableRow) => { + if ('rowKind' in row) { + if (row.rowKind === 'call_import' && row.call_import_id) { + setParams({ + call_import_id: row.call_import_id, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (row.rowKind === 'workspace_resource' && row.product_section) { + setParams({ + product_section: row.product_section, + call_import_id: null, + resource_id: row.resource_id || null, + model: null, + usage_kind: null, + }) + return + } + } + if (groupBy === 'workspace' && row.workspace_id) { + setParams({ + workspace_id: row.workspace_id, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (groupBy === 'call_import' && row.call_import_id) { + setParams({ + call_import_id: row.call_import_id, + resource_id: null, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (groupBy === 'product_section' && row.product_section) { + setParams({ + product_section: row.product_section, + call_import_id: null, + resource_id: null, + model: null, + usage_kind: null, + }) + return + } + if (groupBy === 'resource' && row.resource_id) { + setParams({ + resource_id: row.resource_id, + model: null, + usage_kind: null, + product_section: null, + }) + return + } + if (groupBy === 'model' && row.model) { + setParams({ model: row.model, usage_kind: null }) + } + } + + const isRowDrillable = (row: TableRow): boolean => { + if ('rowKind' in row) { + if (row.rowKind === 'call_import') return Boolean(row.call_import_id) + return Boolean(row.product_section) + } + if (groupBy === 'workspace') return Boolean(row.workspace_id) + if (groupBy === 'call_import') return Boolean(row.call_import_id) + if (groupBy === 'product_section') return Boolean(row.product_section) + if (groupBy === 'resource') return Boolean(row.resource_id) + if (groupBy === 'model') return Boolean(row.model) + return false + } + + return ( +
+
+
+

+ + Usage +

+

+ Cards show usage for {scopeSubtitle}. + Drill down: workspaces → call imports or product areas → evaluations / models. +

+
+
+
+
+ {(['USD', 'INR'] as const).map((currency) => ( + + ))} +
+ {showCostBreakdown ? ( + + ) : null} + {isAdmin && licenseLoaded && usagePolicy.extended_history ? ( + + Pricing overrides + + ) : null} +
+ {summary?.last_updated_at ? ( +

+ Updated {new Date(summary.last_updated_at).toLocaleString()} +

+ ) : null} +
+
+ + {summaryError ? ( +
+ Could not load usage data + {summaryQueryError instanceof Error && summaryQueryError.message + ? `: ${summaryQueryError.message}` + : '.'}{' '} + Try refreshing the page. +
+ ) : null} + + {showOssUsageNotice ? ( +
+ Showing the last {maxHistoryDays} days of usage history. Set{' '} + + EFFICIENTAI_LICENSE + {' '} + on your server with any enterprise feature to unlock extended history. +
+ ) : null} + +
+
+ + + + +
+ +
+
+ {showAudio ? ( + + ) : null} + {showTts ? ( + + ) : null} + {(totals?.cache_read_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.cache_creation_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.reasoning_tokens || 0) > 0 ? ( + + ) : null} +
+
+ +
+
+
+ + setParams({ start: s, end: e })} + onWorkspaceChange={handleWorkspaceChange} + onCallImportChange={handleCallImportChange} + onDatasetChange={(v) => + setParams({ + dataset: v || null, + call_import_id: null, + resource_id: null, + }) + } + onTagChange={(v) => + setParams({ + tag_id: v || null, + call_import_id: null, + resource_id: null, + }) + } + onEvaluationChange={(id) => { + if (id.startsWith(USAGE_SECTION_SOURCE_PREFIX)) { + const section = id.slice(USAGE_SECTION_SOURCE_PREFIX.length) + setParams({ + product_section: section || null, + resource_id: null, + call_import_id: null, + model: null, + usage_kind: null, + }) + return + } + const resource = filterOptions?.resources?.find( + (r) => idKey(r.id) === idKey(id), + ) + setParams({ + resource_id: id || null, + product_section: id ? resource?.product_section || null : null, + model: null, + usage_kind: null, + }) + }} + onUsageKindChange={(k) => setParams({ usage_kind: k || null })} + onModelChange={(v) => setParams({ model: v || null })} + onClearAll={handleClearAll} + maxHistoryDays={maxHistoryDays} + /> + + +
+ + {showTruncation ? ( +

+ Showing the first 100 rows for this level. Narrow the date range or drill + further for complete detail. +

+ ) : null} +
+ + {tableLoading || breakdownStale ? ( +
+ +
+ ) : rows.length === 0 ? ( +
+ No usage in this period for the current scope. Try 7d or 30d, or go back up a + level. +
+ ) : ( +
+ {tableFetching ? ( +
+ +
+ ) : null} + + + + + + + + + + + + + + + + {rows.map((row, idx) => { + const drillable = isRowDrillable(row) + const compositeHeadline = + 'rowKind' in row ? compositeRowHeadline(row) : null + const rowTitle = tableRowLabel(groupBy, row, filterOptions) + const showRowTitle = + 'rowKind' in row + ? row.rowKind === 'workspace_resource' || + (compositeHeadline && + rowTitle.toLowerCase() !== compositeHeadline.toLowerCase()) + : true + return ( + drillable && handleRowDrill(row)} + > + + + + + + + + + + + ) + })} + +
+ {drillColumnLabel(groupBy, showWorkspaceComposite)} + LLM callsInput tokensOutput tokensTotal tokensEst. costSTT audioTTS charsCache read
+ + + {'rowKind' in row ? ( + + {compositeHeadline} + + ) : null} + {showRowTitle ? ( + + {rowTitle} + + ) : !('rowKind' in row) ? ( + {rowTitle} + ) : null} + {'hint' in row && row.hint ? ( + + {row.hint} + + ) : null} + + {drillable ? ( + + ) : null} + + + {formatNumber(row.call_count)} + + {formatNumber(row.prompt_tokens)} + + {formatNumber(row.completion_tokens)} + + {formatNumber(row.total_tokens)} + + {formatCostUsd(rowCostUsd(row))} + + {row.audio_seconds ? formatAudio(row.audio_seconds) : '—'} + + {row.tts_characters ? formatNumber(row.tts_characters) : '—'} + + {formatNumber(row.cache_read_tokens)} +
+
+ )} +
+
+ + setCostBreakdownOpen(false)} + costs={totals?.costs} + scopeLabel={scopeSubtitle} + formatCost={formatCostUsd} + /> +
+ ) +} + +const statCardClass = 'border border-gray-200 ring-1 ring-[#fde047]/25 shadow-sm' + +function StatCard({ + label, + value, + valueLabel, + loading, + emphasize = false, + className = '', +}: { + label: string + value?: number + valueLabel?: string + loading?: boolean + emphasize?: boolean + className?: string +}) { + return ( + + +

{label}

+

+ {loading ? '—' : valueLabel ?? formatNumber(value || 0)} +

+
+
+ ) +} diff --git a/frontend/src/pages/usage/UsageDateRangePicker.tsx b/frontend/src/pages/usage/UsageDateRangePicker.tsx index cd5718ab..4c6ca19f 100644 --- a/frontend/src/pages/usage/UsageDateRangePicker.tsx +++ b/frontend/src/pages/usage/UsageDateRangePicker.tsx @@ -1,274 +1,273 @@ -import { useEffect, useMemo, useRef, useState } from 'react' -import { Calendar } from 'lucide-react' -import { usageTheme } from './usageTheme' -import { getUsageTimezone, formatUsageTimezoneLabel } from './usageTimezone' - -type Mode = 'relative' | 'absolute' - -type UsageDateRangePickerProps = { - start: string - end: string - onApply: (start: string, end: string) => void - maxHistoryDays?: number | null -} - -function toDateInput(d: Date): string { - const y = d.getFullYear() - const m = String(d.getMonth() + 1).padStart(2, '0') - const day = String(d.getDate()).padStart(2, '0') - return `${y}-${m}-${day}` -} - -function formatDisplay(start: string, end: string): string { - if (start === end) return start - return `${start} → ${end}` -} - -export function rangeForDays(days: number): { start: string; end: string } { - const end = new Date() - const start = new Date() - start.setDate(start.getDate() - (days - 1)) - return { start: toDateInput(start), end: toDateInput(end) } -} - -export function isRangeWithinMaxDays(start: string, end: string, maxDays: number): boolean { - const r = rangeForDays(maxDays) - return start >= r.start && end <= r.end -} - -const QUICK_RANGES = [ - { label: '1d', days: 1 }, - { label: '7d', days: 7 }, - { label: '30d', days: 30 }, - { label: '90d', days: 90 }, -] as const - -const RELATIVE_DAYS = [1, 2, 3, 4, 5, 6] -const RELATIVE_WEEKS = [1, 2, 3, 4] - -export default function UsageDateRangePicker({ - start, - end, - onApply, - maxHistoryDays = null, -}: UsageDateRangePickerProps) { - const [open, setOpen] = useState(false) - const [mode, setMode] = useState('relative') - const [draftStart, setDraftStart] = useState(start) - const [draftEnd, setDraftEnd] = useState(end) - const [relDays, setRelDays] = useState(1) - const rootRef = useRef(null) - - const quickRanges = useMemo(() => { - if (!maxHistoryDays) return QUICK_RANGES - return QUICK_RANGES.filter((q) => q.days <= maxHistoryDays) - }, [maxHistoryDays]) - - const minStartDate = useMemo(() => { - if (!maxHistoryDays) return null - return rangeForDays(maxHistoryDays).start - }, [maxHistoryDays]) - - const maxRelDays = maxHistoryDays ?? 365 - - const activeQuick = useMemo(() => { - for (const q of quickRanges) { - const r = rangeForDays(q.days) - if (r.start === start && r.end === end) return q.label - } - return null - }, [start, end, quickRanges]) - - useEffect(() => { - if (open) { - setDraftStart(start) - setDraftEnd(end) - } - }, [open, start, end]) - - useEffect(() => { - const onDoc = (e: MouseEvent) => { - if (!rootRef.current?.contains(e.target as Node)) setOpen(false) - } - document.addEventListener('mousedown', onDoc) - return () => document.removeEventListener('mousedown', onDoc) - }, []) - - const applyRelativeDays = (days: number) => { - const r = rangeForDays(days) - onApply(r.start, r.end) - setOpen(false) - } - - const applyAbsolute = () => { - if (!draftStart || !draftEnd) return - if (draftEnd < draftStart) return - onApply(draftStart, draftEnd) - setOpen(false) - } - - const pillClass = (active: boolean) => - `rounded-md px-2.5 py-1 text-xs font-medium transition-colors border ${ - active ? usageTheme.pillActive : usageTheme.pillInactive - }` - - return ( -
-
- {quickRanges.map((q) => ( - - ))} - -
- - - {formatDisplay(start, end)} - - {' '} - · {formatUsageTimezoneLabel(getUsageTimezone())} - - - - {open ? ( -
-
- - -
- - {mode === 'relative' ? ( -
-

- Dates use your local timezone ({formatUsageTimezoneLabel(getUsageTimezone())}). - Usage is stored by UTC day; filters include activity that happened on each - selected local day. -

-
-

Days

-
- {RELATIVE_DAYS.map((d) => ( - - ))} -
-
-
-

Weeks

-
- {RELATIVE_WEEKS.map((w) => ( - - ))} -
-
-
- - -
-
- ) : ( -
-
- - -
-
- - -
-
- )} -
- ) : null} -
- ) -} - -export function defaultUsageDateRange(): { start: string; end: string } { - const today = toDateInput(new Date()) - return { start: today, end: today } -} +import { useEffect, useMemo, useRef, useState } from 'react' +import { Calendar } from 'lucide-react' +import { usageTheme } from './usageTheme' +import { getUsageTimezone, formatUsageTimezoneLabel } from './usageTimezone' + +type Mode = 'relative' | 'absolute' + +type UsageDateRangePickerProps = { + start: string + end: string + onApply: (start: string, end: string) => void + maxHistoryDays?: number | null +} + +function toDateInput(d: Date): string { + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +function formatDisplay(start: string, end: string): string { + if (start === end) return start + return `${start} → ${end}` +} + +export function rangeForDays(days: number): { start: string; end: string } { + const end = new Date() + const start = new Date() + start.setDate(start.getDate() - (days - 1)) + return { start: toDateInput(start), end: toDateInput(end) } +} + +export function isRangeWithinMaxDays(start: string, end: string, maxDays: number): boolean { + const r = rangeForDays(maxDays) + return start >= r.start && end <= r.end +} + +const QUICK_RANGES = [ + { label: '1d', days: 1 }, + { label: '7d', days: 7 }, + { label: '30d', days: 30 }, + { label: '90d', days: 90 }, +] as const + +const RELATIVE_DAYS = [1, 2, 3, 4, 5, 6] +const RELATIVE_WEEKS = [1, 2, 3, 4] + +export default function UsageDateRangePicker({ + start, + end, + onApply, + maxHistoryDays = null, +}: UsageDateRangePickerProps) { + const [open, setOpen] = useState(false) + const [mode, setMode] = useState('relative') + const [draftStart, setDraftStart] = useState(start) + const [draftEnd, setDraftEnd] = useState(end) + const [relDays, setRelDays] = useState(1) + const rootRef = useRef(null) + + const quickRanges = useMemo(() => { + if (!maxHistoryDays) return QUICK_RANGES + return QUICK_RANGES.filter((q) => q.days <= maxHistoryDays) + }, [maxHistoryDays]) + + const minStartDate = useMemo(() => { + if (!maxHistoryDays) return null + return rangeForDays(maxHistoryDays).start + }, [maxHistoryDays]) + + const maxRelDays = maxHistoryDays ?? 365 + + const activeQuick = useMemo(() => { + for (const q of quickRanges) { + const r = rangeForDays(q.days) + if (r.start === start && r.end === end) return q.label + } + return null + }, [start, end, quickRanges]) + + useEffect(() => { + if (open) { + setDraftStart(start) + setDraftEnd(end) + } + }, [open, start, end]) + + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (!rootRef.current?.contains(e.target as Node)) setOpen(false) + } + document.addEventListener('mousedown', onDoc) + return () => document.removeEventListener('mousedown', onDoc) + }, []) + + const applyRelativeDays = (days: number) => { + const r = rangeForDays(days) + onApply(r.start, r.end) + setOpen(false) + } + + const applyAbsolute = () => { + if (!draftStart || !draftEnd) return + if (draftEnd < draftStart) return + onApply(draftStart, draftEnd) + setOpen(false) + } + + const pillClass = (active: boolean) => + `rounded-md px-2.5 py-1 text-xs font-medium transition-colors border ${ + active ? usageTheme.pillActive : usageTheme.pillInactive + }` + + return ( +
+
+ {quickRanges.map((q) => ( + + ))} + +
+ + + {formatDisplay(start, end)} + + {' '} + · {formatUsageTimezoneLabel(getUsageTimezone())} + + + + {open ? ( +
+
+ + +
+ + {mode === 'relative' ? ( +
+

+ Dates use your local timezone ({formatUsageTimezoneLabel(getUsageTimezone())}). + Usage is stored by UTC day; filters include activity that happened on each + selected local day. +

+
+

Days

+
+ {RELATIVE_DAYS.map((d) => ( + + ))} +
+
+
+

Weeks

+
+ {RELATIVE_WEEKS.map((w) => ( + + ))} +
+
+
+ + +
+
+ ) : ( +
+
+ + +
+
+ + +
+
+ )} +
+ ) : null} +
+ ) +} + +export function defaultUsageDateRange(): { start: string; end: string } { + return rangeForDays(7) +} diff --git a/tests/test_api/test_org_usage_routes.py b/tests/test_api/test_org_usage_routes.py new file mode 100644 index 00000000..f83e0af5 --- /dev/null +++ b/tests/test_api/test_org_usage_routes.py @@ -0,0 +1,12 @@ +"""Ensure org usage API routes are mounted on the v1 router.""" + +from app.api.v1.api import api_router + + +def test_org_usage_routes_are_registered(): + paths = {route.path for route in api_router.routes if hasattr(route, "path")} + assert "/organizations/usage/summary" in paths + assert "/organizations/usage/breakdown" in paths + assert "/organizations/usage/filters" in paths + assert "/organizations/usage/fx-rate" in paths + assert "/organizations/usage/pricing/overrides" in paths diff --git a/tests/test_services/test_usage/test_call_import_context.py b/tests/test_services/test_usage/test_call_import_context.py index 6d798f65..0466be72 100644 --- a/tests/test_services/test_usage/test_call_import_context.py +++ b/tests/test_services/test_usage/test_call_import_context.py @@ -9,33 +9,26 @@ call_import_row_usage_context, enrich_usage_context_workspace, ) +from app.services.usage.llm_usage import _bucket_prefix -def test_evaluation_row_gets_distinct_bucket_context(): +def test_evaluation_rows_share_bucket_context(): org_id = uuid4() ws_id = uuid4() eval_id = uuid4() import_id = uuid4() - row_a = uuid4() - row_b = uuid4() - source_a = uuid4() - source_b = uuid4() ctx_a = call_import_evaluation_usage_context( organization_id=org_id, workspace_id=ws_id, evaluation_id=eval_id, call_import_id=import_id, - evaluation_row_id=row_a, - call_import_row_id=source_a, ) ctx_b = call_import_evaluation_usage_context( organization_id=org_id, workspace_id=ws_id, evaluation_id=eval_id, call_import_id=import_id, - evaluation_row_id=row_b, - call_import_row_id=source_b, ) bucket_a = build_bucket_context( @@ -48,45 +41,75 @@ def test_evaluation_row_gets_distinct_bucket_context(): resource_type=ctx_b.resource_type, extra=ctx_b.extra, ) - assert bucket_a != bucket_b - assert bucket_a["evaluation_row_id"] == str(row_a) + assert bucket_a == bucket_b + assert bucket_a["evaluation_id"] == str(eval_id) assert bucket_a["call_import_id"] == str(import_id) + assert "evaluation_row_id" not in bucket_a + assert "call_import_row_id" not in bucket_a -def test_row_only_transcribe_context(): +def test_evaluation_rows_share_redis_bucket_prefix(): + from datetime import date + + org_id = uuid4() + ws_id = uuid4() + eval_id = uuid4() + import_id = uuid4() + model = "gpt-test" + usage_date = date(2026, 8, 15) + + def prefix_for(_row_id: object) -> str: + ctx = call_import_evaluation_usage_context( + organization_id=org_id, + workspace_id=ws_id, + evaluation_id=eval_id, + call_import_id=import_id, + ) + context = build_bucket_context( + resource_id=ctx.resource_id, + resource_type=ctx.resource_type, + extra=ctx.extra, + ) + return _bucket_prefix( + workspace_id=ws_id, + product_section=ctx.product_section.value, + model=model, + context=context, + usage_date=usage_date, + usage_kind="llm", + ) + + assert prefix_for(uuid4()) == prefix_for(uuid4()) + + +def test_row_only_transcribe_context_rolls_up_to_call_import(): org_id = uuid4() import_id = uuid4() - row_id = uuid4() ctx = call_import_row_usage_context( organization_id=org_id, workspace_id=None, call_import_id=import_id, - call_import_row_id=row_id, ) - assert ctx.extra is not None - assert ctx.extra["call_import_row_id"] == str(row_id) + assert ctx.extra == {"call_import_id": str(import_id)} assert ctx.resource_type == "call_import" + assert "call_import_row_id" not in (ctx.extra or {}) def test_call_import_ids_from_evaluation_context(): org_id = uuid4() eval_id = uuid4() import_id = uuid4() - row_id = uuid4() - source_id = uuid4() ctx = call_import_evaluation_usage_context( organization_id=org_id, workspace_id=uuid4(), evaluation_id=eval_id, call_import_id=import_id, - evaluation_row_id=row_id, - call_import_row_id=source_id, ) ids = call_import_ids_from_usage_context(ctx) assert ids["call_import_id"] == import_id assert ids["evaluation_id"] == eval_id - assert ids["evaluation_row_id"] == row_id - assert ids["call_import_row_id"] == source_id + assert ids["evaluation_row_id"] is None + assert ids["call_import_row_id"] is None def test_enrich_usage_context_workspace_noop_when_set(): diff --git a/tests/test_services/test_usage/test_usage_read_cache.py b/tests/test_services/test_usage/test_usage_read_cache.py index dc7884fd..20d1bd61 100644 --- a/tests/test_services/test_usage/test_usage_read_cache.py +++ b/tests/test_services/test_usage/test_usage_read_cache.py @@ -1,69 +1,91 @@ -"""Tests for usage read cache.""" - -from __future__ import annotations - -from datetime import date -from uuid import uuid4 - -import pytest - -from app.core.usage_entitlement import UsagePolicySnapshot -from app.services.usage import read_cache as cache_mod -from app.services.usage.access import UsageAccessResult -from app.services.usage.read_cache import ( - cache_key_for, - get_cached_response, - invalidate_org_usage_read_cache, - set_cached_response, -) -from tests.test_services.test_usage.test_llm_usage import _FakeRedis - - -@pytest.fixture -def fake_redis(monkeypatch): - client = _FakeRedis() - cache_mod._redis = client - monkeypatch.setattr(cache_mod.redis, "from_url", lambda *_args, **_kwargs: client) - yield client - cache_mod._redis = None - - -def _access() -> UsageAccessResult: - policy = UsagePolicySnapshot(extended_history=False, max_history_days=7) - return UsageAccessResult( - display_start=date(2026, 8, 1), - display_end=date(2026, 8, 7), - filter_start=date(2026, 8, 1), - filter_end=date(2026, 8, 8), - enforced_filter_floor=date(2026, 8, 1), - policy=policy, - range_clamped=False, - ) - - -def test_usage_read_cache_round_trip(fake_redis, monkeypatch): - org_id = uuid4() - access = _access() - key = cache_key_for(access, workspace_id=None) - payload = {"start": "2026-08-01", "end": "2026-08-07", "totals": {}} - - set_cached_response(org_id, "summary", key, payload) - cached = get_cached_response(org_id, "summary", key) - - assert cached == payload - - -def test_usage_read_cache_invalidate_org(fake_redis, monkeypatch): - org_id = uuid4() - other_org = uuid4() - access = _access() - key = cache_key_for(access) - - set_cached_response(org_id, "summary", key, {"x": 1}) - set_cached_response(other_org, "summary", key, {"x": 2}) - - deleted = invalidate_org_usage_read_cache(org_id) - - assert deleted >= 1 - assert get_cached_response(org_id, "summary", key) is None - assert get_cached_response(other_org, "summary", key) is not None +"""Tests for usage read cache.""" + +from __future__ import annotations + +from datetime import date +from uuid import uuid4 + +import pytest + +from app.core.usage_entitlement import UsagePolicySnapshot +from app.services.usage import read_cache as cache_mod +from app.services.usage.access import UsageAccessResult +from app.services.usage.read_cache import ( + cache_key_for, + get_cached_response, + invalidate_org_usage_read_cache, + set_cached_response, +) +from tests.test_services.test_usage.test_llm_usage import _FakeRedis + + +@pytest.fixture +def fake_redis(monkeypatch): + client = _FakeRedis() + cache_mod._redis = client + monkeypatch.setattr(cache_mod.redis, "from_url", lambda *_args, **_kwargs: client) + yield client + cache_mod._redis = None + + +def _access() -> UsageAccessResult: + policy = UsagePolicySnapshot(extended_history=False, max_history_days=7) + return UsageAccessResult( + display_start=date(2026, 8, 1), + display_end=date(2026, 8, 7), + filter_start=date(2026, 8, 1), + filter_end=date(2026, 8, 8), + enforced_filter_floor=date(2026, 8, 1), + policy=policy, + range_clamped=False, + ) + + +def test_usage_read_cache_skips_empty_summary(fake_redis, monkeypatch): + org_id = uuid4() + access = _access() + key = cache_key_for(access) + empty = { + "start": "2026-08-15", + "end": "2026-08-15", + "totals": {"prompt_tokens": 0, "completion_tokens": 0, "call_count": 0}, + } + set_cached_response(org_id, "summary", key, empty) + assert get_cached_response(org_id, "summary", key) is None + + +def test_usage_read_cache_round_trip(fake_redis, monkeypatch): + org_id = uuid4() + access = _access() + key = cache_key_for(access, workspace_id=None) + payload = { + "start": "2026-08-01", + "end": "2026-08-07", + "totals": {"prompt_tokens": 10, "completion_tokens": 5, "call_count": 1}, + } + + set_cached_response(org_id, "summary", key, payload) + cached = get_cached_response(org_id, "summary", key) + + assert cached == payload + + +def test_usage_read_cache_invalidate_org(fake_redis, monkeypatch): + org_id = uuid4() + other_org = uuid4() + access = _access() + key = cache_key_for(access) + payload = { + "start": "2026-08-01", + "end": "2026-08-07", + "totals": {"prompt_tokens": 10, "completion_tokens": 5, "call_count": 1}, + } + + set_cached_response(org_id, "summary", key, payload) + set_cached_response(other_org, "summary", key, payload) + + deleted = invalidate_org_usage_read_cache(org_id) + + assert deleted >= 1 + assert get_cached_response(org_id, "summary", key) is None + assert get_cached_response(other_org, "summary", key) is not None From a48dc64f1fa7e3e62a66fdc2becb8b073760eecf Mon Sep 17 00:00:00 2001 From: M Sami Date: Sat, 15 Aug 2026 16:16:23 +0530 Subject: [PATCH 25/55] feat: add enabled models support for AI providers and new endpoint for available models --- app/api/v1/routes/aiproviders.py | 1 + app/api/v1/routes/usage_pricing.py | 16 + .../076_ai_provider_enabled_models.py | 38 ++ app/models/database.py | 2 + app/models/schemas.py | 39 ++ app/services/usage/enabled_models.py | 130 +++++ app/services/usage/fx_rates.py | 77 ++- app/services/usage/pricing_overrides.py | 24 +- frontend/src/App.tsx | 32 +- frontend/src/lib/api.ts | 31 ++ frontend/src/lib/llmModelOptions.ts | 9 + frontend/src/lib/usageCurrency.ts | 30 +- .../AIProviderEnabledModelsStep.tsx | 213 ++++++++ .../src/pages/configurations/Integrations.tsx | 128 ++++- .../pages/usage/ProviderCredentialSelect.tsx | 172 +++++++ frontend/src/pages/usage/SearchableSelect.tsx | 144 ++++-- frontend/src/pages/usage/Usage.tsx | 112 ++--- .../pages/usage/UsageCostBreakdownModal.tsx | 11 +- frontend/src/pages/usage/UsagePage.tsx | 72 +++ frontend/src/pages/usage/UsagePricing.tsx | 462 +++++++++++++----- .../src/pages/usage/pricingModelOptions.ts | 72 +++ .../src/pages/usage/useAnchoredDropdown.ts | 37 ++ frontend/src/types/api.ts | 3 + .../test_usage/test_enabled_models.py | 66 +++ .../test_services/test_usage/test_fx_rates.py | 69 +++ 25 files changed, 1667 insertions(+), 323 deletions(-) create mode 100644 app/migrations/076_ai_provider_enabled_models.py create mode 100644 app/services/usage/enabled_models.py create mode 100644 frontend/src/pages/configurations/AIProviderEnabledModelsStep.tsx create mode 100644 frontend/src/pages/usage/ProviderCredentialSelect.tsx create mode 100644 frontend/src/pages/usage/UsagePage.tsx create mode 100644 frontend/src/pages/usage/pricingModelOptions.ts create mode 100644 frontend/src/pages/usage/useAnchoredDropdown.ts create mode 100644 tests/test_services/test_usage/test_enabled_models.py create mode 100644 tests/test_services/test_usage/test_fx_rates.py diff --git a/app/api/v1/routes/aiproviders.py b/app/api/v1/routes/aiproviders.py index b7f8b9e6..7f8f8e01 100644 --- a/app/api/v1/routes/aiproviders.py +++ b/app/api/v1/routes/aiproviders.py @@ -198,6 +198,7 @@ async def create_aiprovider( else None ), gateway_extra_headers=aiprovider.gateway_extra_headers, + enabled_models=aiprovider.enabled_models, ) db.add(db_aiprovider) db.flush() diff --git a/app/api/v1/routes/usage_pricing.py b/app/api/v1/routes/usage_pricing.py index 0716eef6..c48deaeb 100644 --- a/app/api/v1/routes/usage_pricing.py +++ b/app/api/v1/routes/usage_pricing.py @@ -112,6 +112,22 @@ class UsageRecomputeJobResponse(BaseModel): completed_at: Optional[datetime] = None +class AvailableModelsResponse(BaseModel): + models: List[str] + + +@router.get("/available-models", response_model=AvailableModelsResponse) +def list_pricing_available_models( + organization_id: UUID = Depends(get_organization_id), + db: Session = Depends(get_db), +): + from app.services.usage.enabled_models import org_pricing_eligible_models + + return AvailableModelsResponse( + models=org_pricing_eligible_models(db, organization_id), + ) + + @router.get("", response_model=List[EffectivePricingResponse]) def list_effective_usage_pricing( usage_kind: Optional[str] = Query(None), diff --git a/app/migrations/076_ai_provider_enabled_models.py b/app/migrations/076_ai_provider_enabled_models.py new file mode 100644 index 00000000..c1714f28 --- /dev/null +++ b/app/migrations/076_ai_provider_enabled_models.py @@ -0,0 +1,38 @@ +"""Migration: per-credential enabled model allowlist for integrations.""" + +from sqlalchemy import text +from sqlalchemy.orm import Session + +description = "Add enabled_models JSONB to aiproviders" + + +def _column_exists(db: Session, table: str, column: str) -> bool: + return ( + db.execute( + text( + """ + SELECT 1 FROM information_schema.columns + WHERE table_name = :table_name AND column_name = :column_name + """ + ), + {"table_name": table, "column_name": column}, + ).first() + is not None + ) + + +def upgrade(db: Session) -> None: + if not _column_exists(db, "aiproviders", "enabled_models"): + db.execute( + text( + """ + ALTER TABLE aiproviders + ADD COLUMN enabled_models JSONB NULL + """ + ) + ) + + +def downgrade(db: Session) -> None: + if _column_exists(db, "aiproviders", "enabled_models"): + db.execute(text("ALTER TABLE aiproviders DROP COLUMN enabled_models")) diff --git a/app/models/database.py b/app/models/database.py index 3e886445..92350139 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -702,6 +702,8 @@ class AIProvider(Base): gateway_auth_secret = Column(String, nullable=True) # Arbitrary HTTP headers sent with gateway-routed LiteLLM calls gateway_extra_headers = Column(JSON, nullable=True) + # Non-empty list restricts model pickers; null/empty = all catalog models for provider. + enabled_models = Column(JSON, nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) last_tested_at = Column(DateTime(timezone=True), nullable=True) # When API key was last validated diff --git a/app/models/schemas.py b/app/models/schemas.py index 1e6f0e7f..25241e77 100644 --- a/app/models/schemas.py +++ b/app/models/schemas.py @@ -970,6 +970,13 @@ class AIProviderCreate(BaseModel): None, description="Arbitrary HTTP headers sent with gateway-routed LiteLLM calls.", ) + enabled_models: Optional[List[str]] = Field( + None, + description=( + "Allowlisted model names for this credential. " + "Null or empty means all catalog models for the provider." + ), + ) is_default: Optional[bool] = Field( None, description=( @@ -1053,6 +1060,21 @@ def validate_gateway_auth_secret(cls, v: Optional[str]) -> Optional[str]: def validate_gateway_extra_headers(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, str]]: return _validate_gateway_extra_headers(v) + @field_validator("enabled_models") + @classmethod + def validate_enabled_models_create(cls, v: Optional[List[str]]) -> Optional[List[str]]: + if v is None: + return None + seen: set[str] = set() + out: list[str] = [] + for item in v: + name = str(item).strip() + if not name or name in seen: + continue + seen.add(name) + out.append(name) + return out or None + class AIProviderUpdate(BaseModel): """Schema for updating an AI Provider.""" @@ -1069,6 +1091,7 @@ class AIProviderUpdate(BaseModel): gateway_auth_secret: Optional[str] = None clear_gateway_auth_secret: bool = False gateway_extra_headers: Optional[Dict[str, str]] = None + enabled_models: Optional[List[str]] = None @field_validator("gateway_model") @classmethod @@ -1127,6 +1150,21 @@ def validate_gateway_extra_headers_update( ) -> Optional[Dict[str, str]]: return _validate_gateway_extra_headers(v) + @field_validator("enabled_models") + @classmethod + def validate_enabled_models_update(cls, v: Optional[List[str]]) -> Optional[List[str]]: + if v is None: + return None + seen: set[str] = set() + out: list[str] = [] + for item in v: + name = str(item).strip() + if not name or name in seen: + continue + seen.add(name) + out.append(name) + return out or None + class AIProviderResponse(BaseModel): """Schema for AI Provider response.""" @@ -1145,6 +1183,7 @@ class AIProviderResponse(BaseModel): gateway_auth_secret_env: Optional[str] = None has_gateway_auth_secret: bool = False gateway_extra_headers: Optional[Dict[str, str]] = None + enabled_models: Optional[List[str]] = None gateway_managed: bool = False effective_routing: Literal["inherit", "direct", "gateway", "bifrost", "litellm_proxy"] = "inherit" effective_gateway_interface: Literal["litellm_shim", "native_openai"] = "litellm_shim" diff --git a/app/services/usage/enabled_models.py b/app/services/usage/enabled_models.py new file mode 100644 index 00000000..87d9652e --- /dev/null +++ b/app/services/usage/enabled_models.py @@ -0,0 +1,130 @@ +"""Org- and credential-level enabled model allowlists.""" + +from __future__ import annotations + +from typing import Iterable, List, Optional, Set +from uuid import UUID + +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.models.database import AIProvider, ModelProvider +from app.services.ai.model_config_service import ModelConfigService + + +def normalize_enabled_models(raw: Optional[Iterable[str]]) -> Optional[List[str]]: + if raw is None: + return None + seen: set[str] = set() + normalized: list[str] = [] + for item in raw: + if item is None: + continue + name = str(item).strip() + if not name or name in seen: + continue + seen.add(name) + normalized.append(name) + return normalized or None + + +def catalog_models_for_provider(provider: str) -> List[str]: + service = ModelConfigService() + try: + provider_enum = ModelProvider(provider.lower()) + except ValueError: + return [] + options = service.get_model_options_by_provider(provider_enum) + models: list[str] = [] + for key in ("llm", "stt", "tts", "s2s"): + models.extend(options.get(key) or []) + return sorted({m for m in models if m}) + + +def effective_enabled_models_for_credential(credential: AIProvider) -> Optional[List[str]]: + """Return explicit allowlist, or None meaning unrestricted (full provider catalog).""" + return normalize_enabled_models(credential.enabled_models) + + +def filter_models_by_credential( + credential: Optional[AIProvider], + catalog_models: List[str], +) -> List[str]: + if credential is None: + return catalog_models + allowlist = effective_enabled_models_for_credential(credential) + if not allowlist: + return catalog_models + allowed = set(allowlist) + filtered = [m for m in catalog_models if m in allowed] + gateway = (credential.gateway_model or "").strip() + if gateway and gateway not in filtered: + filtered = [gateway, *filtered] + return filtered + + +def _usage_models_for_org(db: Session, organization_id: UUID) -> Set[str]: + rows = db.execute( + text( + """ + SELECT DISTINCT model + FROM llm_usage_daily + WHERE organization_id = CAST(:organization_id AS uuid) + AND model IS NOT NULL + AND model <> '' + """ + ), + {"organization_id": str(organization_id)}, + ).scalars().all() + return {str(row).strip() for row in rows if row} + + +def _override_models_for_org(db: Session, organization_id: UUID) -> Set[str]: + rows = db.execute( + text( + """ + SELECT DISTINCT model + FROM org_model_pricing_overrides + WHERE organization_id = CAST(:organization_id AS uuid) + """ + ), + {"organization_id": str(organization_id)}, + ).scalars().all() + return {str(row).strip() for row in rows if row} + + +def org_pricing_eligible_models(db: Session, organization_id: UUID) -> List[str]: + """Models org admins may set pricing overrides for.""" + models: set[str] = set() + providers = ( + db.query(AIProvider) + .filter( + AIProvider.organization_id == organization_id, + AIProvider.is_active.is_(True), + ) + .all() + ) + any_explicit_allowlist = False + for provider in providers: + gateway = (provider.gateway_model or "").strip() + if gateway: + models.add(gateway) + allowlist = effective_enabled_models_for_credential(provider) + if allowlist: + any_explicit_allowlist = True + models.update(allowlist) + else: + models.update(catalog_models_for_provider(provider.provider)) + + if not any_explicit_allowlist and not models: + for provider in providers: + models.update(catalog_models_for_provider(provider.provider)) + + models.update(_usage_models_for_org(db, organization_id)) + models.update(_override_models_for_org(db, organization_id)) + return sorted(models) + + +def org_union_enabled_models(db: Session, organization_id: UUID) -> List[str]: + """All models enabled on any active integration credential.""" + return org_pricing_eligible_models(db, organization_id) diff --git a/app/services/usage/fx_rates.py b/app/services/usage/fx_rates.py index a99cc68b..927026b1 100644 --- a/app/services/usage/fx_rates.py +++ b/app/services/usage/fx_rates.py @@ -14,7 +14,8 @@ _REDIS_KEY = "usage:fx:USD_INR" _FRANKFURTER_URL = "https://api.frankfurter.dev/v2/rate/USD/INR" -_DEFAULT_RATE = 83.0 +_DEFAULT_RATE = 95.0 +_CACHE_TTL_SECONDS = 25 * 3600 _redis: redis.Redis | None = None @@ -41,40 +42,84 @@ def _read_cached() -> Optional[dict[str, Any]]: if not raw: return None payload = json.loads(raw) + if payload.get("source") != "frankfurter": + return None if not isinstance(payload.get("rate"), (int, float)): return None return payload - except (redis.RedisError, json.JSONDecodeError): + except (redis.RedisError, json.JSONDecodeError) as exc: + logger.warning("USD/INR FX cache read failed: {}", exc) return None +def _write_cached(payload: dict[str, Any]) -> None: + try: + _client().set(_REDIS_KEY, json.dumps(payload), ex=_CACHE_TTL_SECONDS) + except redis.RedisError as exc: + logger.warning("USD/INR FX cache write failed: {}", exc) + + +def _parse_frankfurter_payload(data: Any) -> tuple[float, datetime]: + if not isinstance(data, dict): + raise ValueError(f"Frankfurter response is not an object: {type(data).__name__}") + + base = data.get("base") + quote = data.get("quote") + if base != "USD" or quote != "INR": + raise ValueError(f"Unexpected Frankfurter pair: {base}/{quote}") + + rate_raw = data.get("rate") + if not isinstance(rate_raw, (int, float)): + raise ValueError(f"Frankfurter rate missing or invalid: {rate_raw!r}") + + rate = float(rate_raw) + if rate <= 0: + raise ValueError(f"Frankfurter rate must be positive: {rate}") + + date_raw = data.get("date") + if not isinstance(date_raw, str) or not date_raw.strip(): + raise ValueError(f"Frankfurter date missing or invalid: {date_raw!r}") + + as_of = datetime.fromisoformat(date_raw).replace(tzinfo=timezone.utc) + return rate, as_of + + +def _fallback_payload(reason: str, *, response_body: Any = None) -> dict[str, Any]: + logger.error( + "USD/INR FX using hardcoded fallback rate {:.2f} — INR costs may be wrong. reason={} response={}", + _DEFAULT_RATE, + reason, + response_body, + ) + return _cache_payload(_DEFAULT_RATE, datetime.now(timezone.utc), "default") + + def get_usd_inr_rate() -> dict[str, Any]: cached = _read_cached() if cached is not None: return cached - return _cache_payload(_DEFAULT_RATE, datetime.now(timezone.utc), "default") + return refresh_usd_inr_rate() def refresh_usd_inr_rate() -> dict[str, Any]: - rate = _DEFAULT_RATE - as_of = datetime.now(timezone.utc) - source = "default" try: with httpx.Client(timeout=15.0) as client: response = client.get(_FRANKFURTER_URL) response.raise_for_status() data = response.json() - fetched = float(data["rate"]) - if fetched > 0: - rate = fetched - source = "frankfurter" - as_of = datetime.fromisoformat(data["date"]).replace(tzinfo=timezone.utc) except Exception as exc: - logger.warning("USD/INR FX refresh failed, using fallback: {}", exc) + return _fallback_payload(f"Frankfurter request failed: {exc}") - payload = _cache_payload(rate, as_of, source) try: - _client().set(_REDIS_KEY, json.dumps(payload), ex=25 * 3600) - except redis.RedisError as exc: - logger.warning("USD/INR FX cache write failed: {}", exc) + rate, as_of = _parse_frankfurter_payload(data) + except Exception as exc: + return _fallback_payload(f"Frankfurter response parse failed: {exc}", response_body=data) + + payload = _cache_payload(rate, as_of, "frankfurter") + _write_cached(payload) + logger.info( + "USD/INR FX refreshed from Frankfurter: rate={} as_of={}", + rate, + as_of.date().isoformat(), + ) return payload diff --git a/app/services/usage/pricing_overrides.py b/app/services/usage/pricing_overrides.py index 7f55ef01..1214023b 100644 --- a/app/services/usage/pricing_overrides.py +++ b/app/services/usage/pricing_overrides.py @@ -54,9 +54,21 @@ def _known_models(db: Session) -> Set[str]: return models -def validate_model_name(db: Session, model: str) -> None: - if model not in _known_models(db): - raise HTTPException(status_code=400, detail=f"Unknown model: {model}") +def validate_model_name( + db: Session, + model: str, + *, + organization_id: Optional[UUID] = None, +) -> None: + from app.services.usage.enabled_models import org_pricing_eligible_models + + if organization_id is not None: + eligible = set(org_pricing_eligible_models(db, organization_id)) + if model in eligible: + return + if model in _known_models(db): + return + raise HTTPException(status_code=400, detail=f"Unknown model: {model}") def _validate_usage_kind(usage_kind: str) -> str: @@ -185,7 +197,7 @@ def get_effective_rate( usage_kind: str, as_of: date, ) -> Dict[str, Any]: - validate_model_name(db, model) + validate_model_name(db, model, organization_id=organization_id) kind = _validate_usage_kind(usage_kind) resolver = PricingResolver(db) effective = resolver.resolve_rate( @@ -301,7 +313,7 @@ def upsert_override( rates: Dict[str, Any], recompute: bool = True, ) -> Dict[str, Any]: - validate_model_name(db, model) + validate_model_name(db, model, organization_id=organization_id) kind = _validate_usage_kind(usage_kind) if effective_to is not None and effective_to < effective_from: raise HTTPException(status_code=400, detail="effective_to must be >= effective_from") @@ -406,7 +418,7 @@ def delete_override( effective_from: Optional[date] = None, recompute: bool = True, ) -> Dict[str, Any]: - validate_model_name(db, model) + validate_model_name(db, model, organization_id=organization_id) kind = _validate_usage_kind(usage_kind) filters = [ "organization_id = CAST(:organization_id AS uuid)", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bcf4ad73..72664080 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,8 +14,7 @@ import PlatformAdmin from './pages/platform/PlatformAdmin' import Dashboard from './pages/dashboard/Dashboard' // Usage -import Usage from './pages/usage/Usage' -import UsagePricing from './pages/usage/UsagePricing' +import UsagePage, { UsagePricingRedirect } from './pages/usage/UsagePage' // Prompt Partials import PromptPartials from './pages/promptPartials/PromptPartials' @@ -136,24 +135,6 @@ function EnterpriseGate({ feature, children }: { feature: string; children: Reac return <>{children} } -function EnterpriseLicenseGate({ children }: { children: React.ReactNode }) { - const { hasExtendedUsageHistory, isLoaded } = useLicenseStore() - - if (!isLoaded) { - return ( -
-
-
- ) - } - - if (!hasExtendedUsageHistory()) { - return - } - - return <>{children} -} - function App() { return ( @@ -214,15 +195,8 @@ function App() { } /> } /> } /> - } /> - - - - } - /> + } /> + } /> } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ae19a698..aaca0b6c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -3443,6 +3443,37 @@ class ApiClient { return response.data } + async listUsagePricingAvailableModels(): Promise<{ models: string[] }> { + const response = await this.client.get( + '/api/v1/organizations/usage/pricing/available-models', + ) + return response.data + } + + async getUsagePricingEffective( + model: string, + params?: { usage_kind?: string; as_of?: string }, + ): Promise<{ + model: string + usage_kind: string + as_of: string + catalog_rates?: Record | null + effective_rates?: Record | null + effective_source?: string | null + has_override: boolean + override?: { + effective_from: string + effective_to?: string | null + rates: Record + } | null + }> { + const response = await this.client.get( + `/api/v1/organizations/usage/pricing/overrides/${encodeURIComponent(model)}`, + { params }, + ) + return response.data + } + async listUsagePricingOverrides(params?: { model?: string usage_kind?: string diff --git a/frontend/src/lib/llmModelOptions.ts b/frontend/src/lib/llmModelOptions.ts index d5a556e2..2dfc3fc1 100644 --- a/frontend/src/lib/llmModelOptions.ts +++ b/frontend/src/lib/llmModelOptions.ts @@ -63,6 +63,15 @@ export function resolveLLMModelsForCredential( ) { return { mode: 'gateway_direct', model: gatewayModel } } + const allowlist = credential?.enabled_models?.filter((m) => m?.trim()) ?? [] + if (allowlist.length > 0) { + const allowed = new Set(allowlist) + const filtered = catalogModels.filter((m) => allowed.has(m)) + if (gatewayModel && !filtered.includes(gatewayModel)) { + return { mode: 'catalog', models: [gatewayModel, ...filtered] } + } + return { mode: 'catalog', models: filtered } + } return { mode: 'catalog', models: catalogModels } } diff --git a/frontend/src/lib/usageCurrency.ts b/frontend/src/lib/usageCurrency.ts index 2b2122c4..a110721d 100644 --- a/frontend/src/lib/usageCurrency.ts +++ b/frontend/src/lib/usageCurrency.ts @@ -13,6 +13,15 @@ export function setUsageDisplayCurrency(currency: UsageDisplayCurrency): void { window.localStorage.setItem(STORAGE_KEY, currency) } +function formatUsdAmount(amountUsd: number): string { + const abs = Math.abs(amountUsd) + const digits = abs >= 1 ? 2 : 4 + return `$${amountUsd.toLocaleString('en-US', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + })}` +} + export function formatUsageCostUsd( usd: number | null | undefined, currency: UsageDisplayCurrency, @@ -21,17 +30,24 @@ export function formatUsageCostUsd( const amountUsd = Number(usd || 0) if (!amountUsd) return '—' if (currency === 'INR') { - return new Intl.NumberFormat(undefined, { + return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amountUsd * inrRate) } - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 4, - }).format(amountUsd) + return formatUsdAmount(amountUsd) +} + +export function formatFxRateHint( + inrRate: number, + asOf?: string | null, + source?: string | null, +): string { + if (source === 'default') { + return `1 USD ≈ ₹${inrRate.toFixed(2)} (estimate — live FX unavailable)` + } + const dateLabel = asOf ? asOf.slice(0, 10) : 'today' + return `1 USD = ₹${inrRate.toFixed(2)} (Frankfurter, ${dateLabel})` } diff --git a/frontend/src/pages/configurations/AIProviderEnabledModelsStep.tsx b/frontend/src/pages/configurations/AIProviderEnabledModelsStep.tsx new file mode 100644 index 00000000..45ba4b2e --- /dev/null +++ b/frontend/src/pages/configurations/AIProviderEnabledModelsStep.tsx @@ -0,0 +1,213 @@ +import { useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Check } from 'lucide-react' +import { apiClient } from '../../lib/api' +import type { ModelProvider } from '../../types/api' +import { usageTheme } from '../usage/usageTheme' + +type ModelOptions = { + llm?: string[] + stt?: string[] + tts?: string[] + s2s?: string[] +} + +type SectionKey = 'llm' | 'stt' | 'tts' | 's2s' + +const SECTIONS: Array<{ key: SectionKey; label: string }> = [ + { key: 'llm', label: 'LLM' }, + { key: 'stt', label: 'STT' }, + { key: 'tts', label: 'TTS' }, + { key: 's2s', label: 'Speech-to-speech' }, +] + +type Props = { + provider: ModelProvider + enabledModels: string[] + onChange: (models: string[]) => void + gatewayModel?: string +} + +function ModelChip({ + model, + selected, + onToggle, +}: { + model: string + selected: boolean + onToggle: () => void +}) { + return ( + + ) +} + +export default function AIProviderEnabledModelsStep({ + provider, + enabledModels, + onChange, + gatewayModel, +}: Props) { + const [customModel, setCustomModel] = useState('') + + const { data: options, isLoading } = useQuery({ + queryKey: ['model-options', provider], + queryFn: () => apiClient.getModelOptions(provider), + enabled: Boolean(provider), + }) + + const catalog = (options || {}) as ModelOptions + const selected = useMemo(() => new Set(enabledModels), [enabledModels]) + + const toggle = (model: string) => { + const next = new Set(enabledModels) + if (next.has(model)) next.delete(model) + else next.add(model) + onChange(Array.from(next).sort()) + } + + const selectSection = (key: SectionKey) => { + const models = catalog[key] || [] + const next = new Set(enabledModels) + models.forEach((m) => next.add(m)) + onChange(Array.from(next).sort()) + } + + const clearSection = (key: SectionKey) => { + const models = new Set(catalog[key] || []) + onChange(enabledModels.filter((m) => !models.has(m))) + } + + const addCustomModel = () => { + const name = customModel.trim() + if (!name) return + if (!selected.has(name)) { + onChange([...enabledModels, name].sort()) + } + setCustomModel('') + } + + const gateway = gatewayModel?.trim() + + return ( +
+
+

+ Tap models to allow this integration to use them in evals, agents, and pricing. +

+

+ Leave all off to allow the full provider catalog. +

+
+ + {gateway ? ( +
+ Gateway model {gateway} stays available + when configured. +
+ ) : null} + + {isLoading ? ( +

Loading catalog models…

+ ) : ( + SECTIONS.map(({ key, label }) => { + const models = catalog[key] || [] + if (models.length === 0) return null + const sectionSelected = models.filter((m) => selected.has(m)).length + return ( +
+
+
+ {label} + + {sectionSelected}/{models.length} selected + +
+
+ + +
+
+
+ {models.map((model) => ( + toggle(model)} + /> + ))} +
+
+ ) + }) + )} + +
+ +
+ setCustomModel(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + addCustomModel() + } + }} + className={`flex-1 rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none ${usageTheme.focusRing}`} + placeholder="e.g. accounts/fireworks/models/gpt-oss-120b" + /> + +
+
+ +

+ {enabledModels.length > 0 + ? `${enabledModels.length} model${enabledModels.length === 1 ? '' : 's'} enabled` + : 'No restriction — full catalog allowed'} +

+
+ ) +} diff --git a/frontend/src/pages/configurations/Integrations.tsx b/frontend/src/pages/configurations/Integrations.tsx index 4dce71a2..4c2e9ca6 100644 --- a/frontend/src/pages/configurations/Integrations.tsx +++ b/frontend/src/pages/configurations/Integrations.tsx @@ -21,6 +21,7 @@ import { getTelephonyProviderLogo, } from '../../config/providers' import WalkthroughToggleButton from '../../components/walkthrough/WalkthroughToggleButton' +import AIProviderEnabledModelsStep from './AIProviderEnabledModelsStep' type IntegrationType = 'voice_platform' | 'ai_provider' | 'telephony_provider' | null @@ -65,6 +66,8 @@ export default function Integrations() { const [gatewayAuthSecret, setGatewayAuthSecret] = useState('') const [clearGatewayAuthSecret, setClearGatewayAuthSecret] = useState(false) const [gatewayExtraHeadersJson, setGatewayExtraHeadersJson] = useState('') + const [aiProviderWizardStep, setAiProviderWizardStep] = useState<1 | 2>(1) + const [enabledModels, setEnabledModels] = useState([]) const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteAIProviderModal, setShowDeleteAIProviderModal] = useState(false) const [showDeleteTelephonyModal, setShowDeleteTelephonyModal] = useState(false) @@ -363,6 +366,7 @@ export default function Integrations() { setCredentialRoutingMode('inherit'); setGatewayModel(''); setGatewayInterface('inherit'); setGatewayBaseUrl('') setGatewayAuthHeader(''); setGatewayAuthSecretEnv(''); setGatewayAuthSecret(''); setClearGatewayAuthSecret(false) setGatewayExtraHeadersJson('') + setAiProviderWizardStep(1); setEnabledModels([]) setSelectedTelephonyProvider(null); setTelephonyAuthId(''); setTelephonyAuthToken(''); setTelephonyVerifyAppUuid(''); setTelephonyVoiceAppId(''); setTelephonySipDomain('') setEditingTelephonyConfigId(null); setTelephonyName('') } @@ -387,6 +391,8 @@ export default function Integrations() { setGatewayBaseUrl(provider.gateway_base_url || ''); setGatewayAuthHeader(provider.gateway_auth_header || '') setGatewayAuthSecretEnv(provider.gateway_auth_secret_env || ''); setGatewayAuthSecret(''); setClearGatewayAuthSecret(false) setGatewayExtraHeadersJson(formatGatewayExtraHeadersJson(provider.gateway_extra_headers)) + setEnabledModels(provider.enabled_models || []) + setAiProviderWizardStep(1) setShowProviderDropdown(false); setIsEditMode(true); setShowModal(true) } @@ -434,8 +440,29 @@ export default function Integrations() { return } + const resolvedEnabledModels = enabledModels.length > 0 ? enabledModels : null + + if (aiProviderWizardStep === 1) { + if (!isEditMode && !selectedProvider) { + showToast('Please select a provider', 'error') + return + } + if (!isEditMode && aiProviderRequiresApiKey && !apiKey.trim()) { + showToast('Please enter an API key', 'error') + return + } + if (!isEditMode && selectedProvider === ModelProvider.AZURE && !azureEndpointUrl.trim()) { + showToast('Please enter your Azure OpenAI endpoint URL', 'error') + return + } + setAiProviderWizardStep(2) + return + } + if (isEditMode && selectedAIProvider) { - const updateData: Partial = {} + const updateData: Partial = { + enabled_models: resolvedEnabledModels, + } if (apiKey.trim()) updateData.api_key = apiKey if (name !== (selectedAIProvider.name || '')) updateData.name = name || null const trimmedAzureEndpointUrl = azureEndpointUrl.trim() @@ -475,7 +502,6 @@ export default function Integrations() { if (gatewayExtraHeadersJson.trim() !== existingExtraHeadersJson.trim()) { updateData.gateway_extra_headers = parsedGatewayExtraHeaders } - if (Object.keys(updateData).length === 0) { resetForm(); return } updateAIProviderMutation.mutate({ id: selectedAIProvider.id, data: updateData }) } else { if (!selectedProvider) { @@ -503,6 +529,7 @@ export default function Integrations() { gateway_auth_secret_env: gatewayAuthSecretEnv.trim() || undefined, gateway_auth_secret: gatewayAuthSecret.trim() || undefined, gateway_extra_headers: parsedGatewayExtraHeaders || undefined, + enabled_models: resolvedEnabledModels || undefined, }) } } else if (integrationType === 'telephony_provider') { @@ -801,6 +828,14 @@ export default function Integrations() { {provider.gateway_model} )} + {provider.enabled_models && provider.enabled_models.length > 0 && ( + + {provider.enabled_models.length} model{provider.enabled_models.length === 1 ? '' : 's'} + + )} {!provider.is_active && Inactive}
@@ -1119,9 +1154,25 @@ export default function Integrations() { {showModal && renderModal(
-
+
-

{isEditMode ? (integrationType === 'ai_provider' ? 'Edit AI Provider' : integrationType === 'telephony_provider' ? 'Edit Telephony Provider' : 'Edit Integration') : 'Add Integration'}

+
+

+ {isEditMode + ? integrationType === 'ai_provider' + ? 'Edit AI Provider' + : integrationType === 'telephony_provider' + ? 'Edit Telephony Provider' + : 'Edit Integration' + : 'Add Integration'} +

+ {integrationType === 'ai_provider' ? ( +

+ Step {aiProviderWizardStep} of 2 —{' '} + {aiProviderWizardStep === 1 ? 'Credentials' : 'Enabled models'} +

+ ) : null} +
@@ -1140,6 +1191,7 @@ export default function Integrations() { setIntegrationType('ai_provider') setSelectedPlatform(null) setSelectedProvider(null) + setAiProviderWizardStep(1) }} className={`p-3 border-2 rounded-lg text-left transition-all ${integrationType === 'ai_provider' ? 'border-primary-500 bg-primary-50' @@ -1237,7 +1289,7 @@ export default function Integrations() { )} - {integrationType === 'ai_provider' && ( + {integrationType === 'ai_provider' && aiProviderWizardStep === 1 && ( <>
@@ -1448,6 +1500,15 @@ export default function Integrations() { )} + {integrationType === 'ai_provider' && aiProviderWizardStep === 2 && (selectedProvider || selectedAIProvider) && ( + + )} + {integrationType === 'telephony_provider' && ( <>
@@ -1546,11 +1607,58 @@ export default function Integrations() {
- + {integrationType === 'ai_provider' && aiProviderWizardStep === 2 ? ( + <> + + + + ) : ( + + )}
diff --git a/frontend/src/pages/usage/ProviderCredentialSelect.tsx b/frontend/src/pages/usage/ProviderCredentialSelect.tsx new file mode 100644 index 00000000..23a6af2e --- /dev/null +++ b/frontend/src/pages/usage/ProviderCredentialSelect.tsx @@ -0,0 +1,172 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react' +import { createPortal } from 'react-dom' +import { Brain, ChevronDown, Info } from 'lucide-react' +import { getProviderLabel, getProviderLogo } from '../../config/providers' +import type { AIProvider, ModelProvider } from '../../types/api' +import { usageTheme } from './usageTheme' +import { credentialDisplayLabel } from './pricingModelOptions' +import { useAnchoredDropdown } from './useAnchoredDropdown' +function FieldHint({ title, children }: { title: string; children: ReactNode }) { + return ( + + + + + {title} + + {children} + + + ) +} + +function ProviderLogo({ + provider, + size = 'sm', +}: { + provider: ModelProvider + size?: 'sm' | 'md' +}) { + const logo = getProviderLogo(provider) + const label = getProviderLabel(provider) + + if (logo) { + if (size === 'md') { + return ( + + {label} + + ) + } + return {label} + } + + if (size === 'md') { + return ( + + + + ) + } + + return +} + +type Props = { + label: string + hint?: string + value: string + credentials: AIProvider[] + onChange: (credentialId: string) => void + disabled?: boolean + placeholder?: string +} + +export default function ProviderCredentialSelect({ + label, + hint, + value, + credentials, + onChange, + disabled, + placeholder = 'Select integration…', +}: Props) { + const [open, setOpen] = useState(false) + const rootRef = useRef(null) + const anchorRef = useRef(null) + const panelRef = useRef(null) + const coords = useAnchoredDropdown(open && !disabled, anchorRef) + const selected = credentials.find((row) => row.id === value) + + useEffect(() => { + const onDoc = (e: MouseEvent) => { + const target = e.target as Node + if (rootRef.current?.contains(target) || panelRef.current?.contains(target)) return + setOpen(false) + } + document.addEventListener('mousedown', onDoc) + return () => document.removeEventListener('mousedown', onDoc) + }, []) + return ( +
+
+ {label} + {hint ? {hint} : null} +
+ + + {open && !disabled && coords && typeof document !== 'undefined' + ? createPortal( +
+ {credentials.length === 0 ? ( +

No active integrations

+ ) : ( + credentials.map((credential) => { + const isSelected = credential.id === value + const name = credential.name?.trim() + return ( + + ) + }) + )} +
, + document.body, + ) + : null}
+ ) +} diff --git a/frontend/src/pages/usage/SearchableSelect.tsx b/frontend/src/pages/usage/SearchableSelect.tsx index 61da046a..c5f5b1a5 100644 --- a/frontend/src/pages/usage/SearchableSelect.tsx +++ b/frontend/src/pages/usage/SearchableSelect.tsx @@ -1,7 +1,8 @@ -import { useEffect, useMemo, useRef, useState } from 'react' -import { ChevronDown, Search, X } from 'lucide-react' +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' +import { createPortal } from 'react-dom' +import { ChevronDown, Info, Search, X } from 'lucide-react' import { usageTheme } from './usageTheme' - +import { useAnchoredDropdown } from './useAnchoredDropdown' export type SearchableOption = { id: string; label: string } type SearchableSelectProps = { @@ -12,6 +13,27 @@ type SearchableSelectProps = { onChange: (id: string) => void disabled?: boolean emptyMessage?: string + hint?: string +} + +function FieldHint({ title, children }: { title: string; children: ReactNode }) { + return ( + + + + + {title} + + {children} + + + ) } export default function SearchableSelect({ @@ -22,11 +44,14 @@ export default function SearchableSelect({ onChange, disabled, emptyMessage = 'No matches', + hint, }: SearchableSelectProps) { const [open, setOpen] = useState(false) const [search, setSearch] = useState('') const rootRef = useRef(null) - + const anchorRef = useRef(null) + const panelRef = useRef(null) + const coords = useAnchoredDropdown(open && !disabled, anchorRef) const selected = options.find( (o) => o.id === value || o.id.toLowerCase() === value.toLowerCase(), ) @@ -39,7 +64,9 @@ export default function SearchableSelect({ useEffect(() => { const onDoc = (e: MouseEvent) => { - if (!rootRef.current?.contains(e.target as Node)) setOpen(false) + const target = e.target as Node + if (rootRef.current?.contains(target) || panelRef.current?.contains(target)) return + setOpen(false) } document.addEventListener('mousedown', onDoc) return () => document.removeEventListener('mousedown', onDoc) @@ -47,17 +74,20 @@ export default function SearchableSelect({ return (
- {label} +
+ {label} + {hint ? {hint} : null} +
- - )) - )} - -
- ) : null} -
+ {open && !disabled && coords && typeof document !== 'undefined' + ? createPortal( +
+
+ + setSearch(e.target.value)} + placeholder="Search…" + className="min-w-0 flex-1 text-sm outline-none" + autoFocus + /> +
+
    + {filtered.length === 0 ? ( +
  • {emptyMessage}
  • + ) : ( + filtered.map((opt) => ( +
  • + +
  • + )) + )} +
+
, + document.body, + ) + : null}
) } diff --git a/frontend/src/pages/usage/Usage.tsx b/frontend/src/pages/usage/Usage.tsx index 469df3c3..1ce1a505 100644 --- a/frontend/src/pages/usage/Usage.tsx +++ b/frontend/src/pages/usage/Usage.tsx @@ -1,16 +1,16 @@ import { useEffect, useMemo, useState } from 'react' import { keepPreviousData, useQuery } from '@tanstack/react-query' -import { Link, useSearchParams } from 'react-router-dom' +import { useSearchParams } from 'react-router-dom' import { Card, CardBody, Spinner } from '@heroui/react' -import { Activity, ChevronRight, CircleDollarSign } from 'lucide-react' +import { ChevronRight, CircleDollarSign } from 'lucide-react' import { apiClient } from '../../lib/api' -import { useIsAdmin } from '../../hooks/useRole' import UsageFiltersBar from './UsageFiltersBar' import UsageDrillPath from './UsageDrillPath' import UsageCostBreakdownModal from './UsageCostBreakdownModal' import { defaultUsageDateRange, isRangeWithinMaxDays, rangeForDays } from './UsageDateRangePicker' import { getUsageTimezone } from './usageTimezone' import { + formatFxRateHint, formatUsageCostUsd, getUsageDisplayCurrency, setUsageDisplayCurrency, @@ -556,7 +556,6 @@ export default function Usage() { const model = searchParams.get('model') || '' const usageKind = (searchParams.get('usage_kind') as Kind) || '' const productSection = searchParams.get('product_section') || '' - const isAdmin = useIsAdmin() const { usagePolicy, isLoaded: licenseLoaded, fetchLicense } = useLicenseStore() const showOssUsageNotice = licenseLoaded && !usagePolicy.extended_history const maxHistoryDays = !licenseLoaded @@ -573,8 +572,10 @@ export default function Usage() { queryKey: ['org-usage', 'fx-rate'], queryFn: () => apiClient.getOrgUsageFxRate(), staleTime: 60 * 60 * 1000, + refetchOnMount: 'always', }) - const inrRate = fxRate?.rate ?? 83 + const inrRate = fxRate?.rate ?? 95 + const fxRateHint = formatFxRateHint(inrRate, fxRate?.as_of, fxRate?.source) const formatCostUsd = (usd?: number | null) => formatUsageCostUsd(usd, displayCurrency, inrRate) @@ -1097,69 +1098,48 @@ export default function Usage() { } return ( -
-
-
-

- - Usage -

-

- Cards show usage for {scopeSubtitle}. - Drill down: workspaces → call imports or product areas → evaluations / models. -

-
-
-
-
+
+
+ {(['USD', 'INR'] as const).map((currency) => ( + - ))} -
- {showCostBreakdown ? ( - - ) : null} - {isAdmin && licenseLoaded && usagePolicy.extended_history ? ( - - Pricing overrides - - ) : null} -
- {summary?.last_updated_at ? ( -

- Updated {new Date(summary.last_updated_at).toLocaleString()} -

- ) : null} + {currency} + + ))}
+ {showCostBreakdown ? ( + + ) : null} + {summary?.last_updated_at ? ( +

+ Updated {new Date(summary.last_updated_at).toLocaleString()} +

+ ) : null}
{summaryError ? ( diff --git a/frontend/src/pages/usage/UsageCostBreakdownModal.tsx b/frontend/src/pages/usage/UsageCostBreakdownModal.tsx index 645729ba..2eafa3b1 100644 --- a/frontend/src/pages/usage/UsageCostBreakdownModal.tsx +++ b/frontend/src/pages/usage/UsageCostBreakdownModal.tsx @@ -1,4 +1,5 @@ import { X } from 'lucide-react' +import { formatUsageCostUsd } from '../../lib/usageCurrency' type UsageCosts = { input_cost_usd: number @@ -21,14 +22,8 @@ type Props = { } function defaultFormatCostUsd(usd?: number | null): string { - const amount = Number(usd || 0) - if (!amount) return '$0.00' - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 4, - }).format(amount) + const formatted = formatUsageCostUsd(usd, 'USD', 1) + return formatted === '—' ? '$0.00' : formatted } const LINE_ITEMS: Array<{ key: keyof UsageCosts; label: string }> = [ diff --git a/frontend/src/pages/usage/UsagePage.tsx b/frontend/src/pages/usage/UsagePage.tsx new file mode 100644 index 00000000..9acddfba --- /dev/null +++ b/frontend/src/pages/usage/UsagePage.tsx @@ -0,0 +1,72 @@ +import { useEffect } from 'react' +import { Activity, DollarSign } from 'lucide-react' +import { Navigate, useSearchParams } from 'react-router-dom' +import { useIsAdmin } from '../../hooks/useRole' +import { useLicenseStore } from '../../store/licenseStore' +import Usage from './Usage' +import UsagePricing from './UsagePricing' + +type UsageTab = 'overview' | 'pricing' + +export default function UsagePage() { + const [searchParams, setSearchParams] = useSearchParams() + const isAdmin = useIsAdmin() + const licenseLoaded = useLicenseStore((s) => s.isLoaded) + const hasExtendedHistory = useLicenseStore((s) => s.hasExtendedUsageHistory()) + const canManagePricing = isAdmin && licenseLoaded && hasExtendedHistory + + const tabParam = searchParams.get('tab') + const activeTab: UsageTab = + tabParam === 'pricing' && canManagePricing ? 'pricing' : 'overview' + + useEffect(() => { + if (tabParam === 'pricing' && !canManagePricing) { + setSearchParams({}, { replace: true }) + } + }, [tabParam, canManagePricing, setSearchParams]) + + const setActiveTab = (tab: UsageTab) => { + setSearchParams(tab === 'pricing' ? { tab: 'pricing' } : {}, { replace: true }) + } + + const tabs: Array<{ id: UsageTab; label: string; icon: typeof Activity; hidden?: boolean }> = [ + { id: 'overview', label: 'Overview', icon: Activity }, + { id: 'pricing', label: 'Pricing overrides', icon: DollarSign, hidden: !canManagePricing }, + ] + + return ( +
+

Usage

+ + {canManagePricing ? ( +
+ +
+ ) : null} + + {activeTab === 'overview' ? : } +
+ ) +} + +export function UsagePricingRedirect() { + return +} diff --git a/frontend/src/pages/usage/UsagePricing.tsx b/frontend/src/pages/usage/UsagePricing.tsx index 92d159f3..6796751b 100644 --- a/frontend/src/pages/usage/UsagePricing.tsx +++ b/frontend/src/pages/usage/UsagePricing.tsx @@ -1,22 +1,22 @@ -import { useMemo, useState, type ReactNode } from 'react' +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Card, CardBody, Spinner } from '@heroui/react' -import { Link } from 'react-router-dom' -import { ArrowLeft, DollarSign, Info, Plus, Trash2 } from 'lucide-react' +import { Plus, Trash2, Info } from 'lucide-react' import { apiClient } from '../../lib/api' import { useIsAdmin } from '../../hooks/useRole' import { useToast } from '../../hooks/useToast' import Button from '../../components/Button' import SearchableSelect from './SearchableSelect' +import ProviderCredentialSelect from './ProviderCredentialSelect' import { usageTheme } from './usageTheme' import { - formatUsageCostUsd, - getUsageDisplayCurrency, - setUsageDisplayCurrency, - type UsageDisplayCurrency, -} from '../../lib/usageCurrency' + buildPricingModelOptions, + credentialDisplayLabel, + type PricingUsageKind, +} from './pricingModelOptions' +import type { AIProvider } from '../../types/api' -type UsageKind = 'llm' | 'stt' | 'tts' +type UsageKind = PricingUsageKind type PricingRatesUsd = { input_per_1m?: number | null @@ -51,36 +51,137 @@ const EMPTY_RATES: PricingRatesUsd = { tts_per_1m_characters: undefined, } -const RATE_FIELDS: Array<{ key: RateFieldKey; label: string; kinds: UsageKind[] }> = [ - { key: 'input_per_1m', label: 'Input / 1M tokens (USD)', kinds: ['llm'] }, - { key: 'output_per_1m', label: 'Output / 1M tokens (USD)', kinds: ['llm'] }, - { key: 'cache_read_per_1m', label: 'Cache read / 1M (USD)', kinds: ['llm'] }, - { key: 'cache_write_per_1m', label: 'Cache write / 1M (USD)', kinds: ['llm'] }, - { key: 'reasoning_per_1m', label: 'Reasoning / 1M (USD)', kinds: ['llm'] }, - { key: 'audio_per_minute', label: 'Audio / minute (USD)', kinds: ['llm', 'stt'] }, - { key: 'tts_per_1m_characters', label: 'TTS / 1M characters (USD)', kinds: ['tts'] }, +const RATE_FIELDS: Array<{ + key: RateFieldKey + label: string + kinds: UsageKind[] + hint: string +}> = [ + { + key: 'input_per_1m', + label: 'Input / 1M tokens ($)', + kinds: ['llm'], + hint: 'USD per 1 million prompt or input tokens. Used when costing LLM calls for this model.', + }, + { + key: 'output_per_1m', + label: 'Output / 1M tokens ($)', + kinds: ['llm'], + hint: 'USD per 1 million completion or output tokens returned by the model.', + }, + { + key: 'cache_read_per_1m', + label: 'Cache read / 1M ($)', + kinds: ['llm'], + hint: 'USD per 1 million tokens read from prompt cache, when the provider bills cached input separately.', + }, + { + key: 'cache_write_per_1m', + label: 'Cache write / 1M ($)', + kinds: ['llm'], + hint: 'USD per 1 million tokens written to prompt cache on the first request.', + }, + { + key: 'reasoning_per_1m', + label: 'Reasoning / 1M ($)', + kinds: ['llm'], + hint: 'USD per 1 million reasoning or thinking tokens (e.g. o-series models).', + }, + { + key: 'audio_per_minute', + label: 'Audio / minute ($)', + kinds: ['llm', 'stt'], + hint: 'USD per minute of audio processed — STT transcription or multimodal audio input on LLM calls.', + }, + { + key: 'tts_per_1m_characters', + label: 'TTS / 1M characters ($)', + kinds: ['tts'], + hint: 'USD per 1 million characters sent to the TTS model for synthesis.', + }, ] +const FIELD_HINTS = { + provider: + 'AI company from Integrations (e.g. OpenAI, Anthropic). Pick the credential whose models you want to price.', + model: + 'Model name for the selected provider and usage kind. Only enabled models for that integration are listed.', + usageKind: + 'Choose LLM, STT, or TTS first — this filters which models appear. Rate fields below match the kind.', + effectiveFrom: + 'First calendar day this override applies. Saving recalculates historical usage for this model from this date.', + ratesSection: + 'Enter USD rates only. Leave a field empty to inherit the platform catalog. At least one rate is required to save.', +} as const + const CONTROL_CLASS = `h-10 w-full rounded-lg border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm outline-none transition-colors hover:border-gray-300 ${usageTheme.focusRing}` +function FieldHint({ title, children }: { title: string; children: ReactNode }) { + return ( + + + + + {title} + + {children} + + + ) +} + function FormField({ label, + hint, children, className = '', }: { label: string + hint?: string children: ReactNode className?: string }) { return (
- {label} +
+ {label} + {hint ? ( + {hint} + ) : null} +
{children}
) } +function formatRateUsd(value?: number | null): string { + if (value == null || Number.isNaN(value)) return '—' + return `$${value.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 4, + })}` +} + +function ratesFromApi(raw?: Record | null): PricingRatesUsd { + if (!raw) return { ...EMPTY_RATES } + return { + input_per_1m: raw.input_per_1m ?? undefined, + output_per_1m: raw.output_per_1m ?? undefined, + cache_read_per_1m: raw.cache_read_per_1m ?? undefined, + cache_write_per_1m: raw.cache_write_per_1m ?? undefined, + reasoning_per_1m: raw.reasoning_per_1m ?? undefined, + audio_per_minute: raw.audio_per_minute ?? undefined, + tts_per_1m_characters: raw.tts_per_1m_characters ?? undefined, + } +} + function todayIso(): string { return new Date().toISOString().slice(0, 10) } @@ -90,26 +191,45 @@ export default function UsagePricing() { const queryClient = useQueryClient() const { showToast, ToastContainer } = useToast() - const [displayCurrency, setDisplayCurrency] = useState(() => - getUsageDisplayCurrency(), - ) - const { data: fxRate } = useQuery({ - queryKey: ['org-usage', 'fx-rate'], - queryFn: () => apiClient.getOrgUsageFxRate(), - staleTime: 60 * 60 * 1000, - }) - const inrRate = fxRate?.rate ?? 83 - const formatDisplayRate = (value?: number | null) => - formatUsageCostUsd(value, displayCurrency, inrRate) - + const [credentialId, setCredentialId] = useState('') const [model, setModel] = useState('') const [usageKind, setUsageKind] = useState('llm') const [effectiveFrom, setEffectiveFrom] = useState(todayIso()) const [rates, setRates] = useState(EMPTY_RATES) + const [ratePrefillSource, setRatePrefillSource] = useState<'catalog' | 'override' | null>(null) + const selectionRef = useRef({ model: '', usageKind: '' as UsageKind }) + + const { data: aiProviders = [] } = useQuery({ + queryKey: ['ai-providers'], + queryFn: () => apiClient.listAIProviders(), + enabled: isAdmin, + }) + + const activeCredentials = useMemo( + () => + (aiProviders as AIProvider[]) + .filter((row) => row.is_active) + .sort((a, b) => credentialDisplayLabel(a).localeCompare(credentialDisplayLabel(b))), + [aiProviders], + ) + + const selectedCredential = useMemo( + () => activeCredentials.find((row) => row.id === credentialId), + [activeCredentials, credentialId], + ) + + const { data: providerCatalog } = useQuery({ + queryKey: ['model-options', selectedCredential?.provider], + queryFn: () => apiClient.getModelOptions(selectedCredential!.provider), + enabled: Boolean(selectedCredential?.provider), + }) - const { data: filters } = useQuery({ - queryKey: ['org-usage', 'filters'], - queryFn: () => apiClient.getOrgUsageFilters(), + const { data: availableModels = [] } = useQuery({ + queryKey: ['usage-pricing', 'available-models'], + queryFn: async () => { + const payload = await apiClient.listUsagePricingAvailableModels() + return payload.models || [] + }, enabled: isAdmin, }) @@ -119,13 +239,86 @@ export default function UsagePricing() { enabled: isAdmin, }) - const modelOptions = useMemo(() => { - const fromUsage = filters?.models || [] - const fromOverrides = overrides.map((row) => row.model) - return Array.from(new Set([...fromUsage, ...fromOverrides])) - .sort() - .map((name) => ({ id: name, label: name })) - }, [filters?.models, overrides]) + const { data: effectivePricing, isFetching: effectiveLoading } = useQuery({ + queryKey: ['usage-pricing', 'effective', model, usageKind, effectiveFrom], + queryFn: () => + apiClient.getUsagePricingEffective(model, { + usage_kind: usageKind, + as_of: effectiveFrom, + }), + enabled: isAdmin && Boolean(model), + }) + + const eligibleModelSet = useMemo( + () => new Set(availableModels || []), + [availableModels], + ) + + const modelOptions = useMemo( + () => + buildPricingModelOptions({ + credential: selectedCredential, + catalog: providerCatalog, + kind: usageKind, + eligibleModels: eligibleModelSet, + overrideModels: overrides + .filter((row) => row.usage_kind === usageKind) + .map((row) => row.model), + }), + [selectedCredential, providerCatalog, usageKind, eligibleModelSet, overrides], + ) + + const resetModelAndRates = () => { + setModel('') + setRates(EMPTY_RATES) + setRatePrefillSource(null) + selectionRef.current = { model: '', usageKind: 'llm' } + } + + useEffect(() => { + if (credentialId || activeCredentials.length !== 1) return + setCredentialId(activeCredentials[0].id) + }, [activeCredentials, credentialId]) + + useEffect(() => { + if (!model) { + setRates(EMPTY_RATES) + setRatePrefillSource(null) + selectionRef.current = { model: '', usageKind: usageKind } + return + } + if (!effectivePricing) return + + const selectionChanged = + selectionRef.current.model !== model || + selectionRef.current.usageKind !== usageKind + + const nextRates = ratesFromApi(effectivePricing.effective_rates) + setRates(nextRates) + setRatePrefillSource( + effectivePricing.has_override + ? 'override' + : effectivePricing.effective_rates + ? 'catalog' + : null, + ) + + if (selectionChanged && effectivePricing.override?.effective_from) { + setEffectiveFrom(effectivePricing.override.effective_from) + } + + selectionRef.current = { model, usageKind } + }, [model, usageKind, effectivePricing]) + + const handleCredentialChange = (nextId: string) => { + setCredentialId(nextId) + resetModelAndRates() + } + + const handleUsageKindChange = (nextKind: UsageKind) => { + setUsageKind(nextKind) + resetModelAndRates() + } const visibleRateFields = useMemo( () => RATE_FIELDS.filter((field) => field.kinds.includes(usageKind)), @@ -151,6 +344,7 @@ export default function UsagePricing() { queryClient.invalidateQueries({ queryKey: ['usage-pricing'] }) queryClient.invalidateQueries({ queryKey: ['org-usage'] }) setRates(EMPTY_RATES) + setRatePrefillSource(null) showToast( data.recompute_enqueued ? 'Override saved; cost recompute started' @@ -188,104 +382,62 @@ export default function UsagePricing() { } return ( -
+
-
- - - Back to usage - -
-
- -
-
-
-

Pricing overrides

-
- {(['USD', 'INR'] as const).map((currency) => ( - - ))} -
-
- -
- Set custom USD rates for this organization. Empty fields inherit the platform - catalog. Saving updates costs for matching usage and starts a scoped recompute. -
-
-
-

- Per-model rates for this org. Leave blank to use the default catalog. -

-
-
-
- - +

Add or update override

- Saving triggers a scoped cost recompute for this model. + Per-model rates for this org. Leave blank to use the default catalog. Saving triggers + a scoped cost recompute.

- - + - + +
+ +
+
-
-

- Rate overrides (USD) + {credentialId && modelOptions.length === 0 ? ( +

+ No {usageKind.toUpperCase()} models are available for this integration. Enable models + in Integrations → AI provider → step 2, or pick another usage kind. +

+ ) : null} + + {activeCredentials.length === 0 ? ( +

+ Add an AI provider in Integrations before setting pricing overrides.

+ ) : null} + + {model ? ( +
+ {effectiveLoading ? ( +
+ + Loading current rates… +
+ ) : ratePrefillSource ? ( +

+ {ratePrefillSource === 'override' + ? 'Prefilled from your org override — these are the rates usage is billed at today. Edit and save to update.' + : 'Prefilled from platform catalog — these are the default rates in effect. Edit and save to create an org override.'} +

+ ) : ( +

+ No catalog rate found for this model on the selected date. Enter rates manually. +

+ )} +
+

+ Rate overrides ($) +

+ {FIELD_HINTS.ratesSection} +
- {visibleRateFields.map(({ key, label }) => ( - + {visibleRateFields.map(({ key, label, hint }) => ( + { @@ -321,6 +511,11 @@ export default function UsagePricing() { ))}
+ ) : ( +

+ Select a provider, usage kind, and model to configure rate overrides. +

+ )}
) : null} + {isCustomProvider && !gateway && enabledModels.length === 0 && !isLoading ? ( +
+ Add custom model IDs below, or set a Gateway model on + the previous step for a single pinned Bifrost model. +
+ ) : null} + {isLoading ? (

Loading catalog models…

) : ( @@ -175,38 +205,63 @@ export default function AIProviderEnabledModelsStep({ }) )} -
- -
- setCustomModel(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - addCustomModel() + {customModels.length > 0 ? ( +
+
+ Enabled models + + {customModels.filter((m) => selected.has(m)).length}/{customModels.length} selected + +
+
+ {customModels.map((model) => ( + toggle(model)} + /> + ))} +
+
+ ) : null} + + {showManualAdd ? ( +
+ +
+ setCustomModel(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + addCustomModel() + } + }} + className={`flex-1 rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none ${usageTheme.focusRing}`} + placeholder={ + isCustomProvider + ? 'e.g. openai/gpt-4o or production-gpt4' + : 'e.g. accounts/fireworks/models/gpt-oss-120b' } - }} - className={`flex-1 rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none ${usageTheme.focusRing}`} - placeholder="e.g. accounts/fireworks/models/gpt-oss-120b" - /> - + /> + +
-
+ ) : null}

{enabledModels.length > 0 ? `${enabledModels.length} model${enabledModels.length === 1 ? '' : 's'} enabled` - : 'No restriction — full catalog allowed'} + : isCustomProvider + ? 'No models enabled yet — add model IDs above or set a gateway model on step 1' + : 'No restriction — full catalog allowed'}

) diff --git a/frontend/src/pages/configurations/Integrations.tsx b/frontend/src/pages/configurations/Integrations.tsx index 4c2e9ca6..3e0373ec 100644 --- a/frontend/src/pages/configurations/Integrations.tsx +++ b/frontend/src/pages/configurations/Integrations.tsx @@ -617,9 +617,14 @@ export default function Integrations() { aiIntegrationProviders.length > 0 || hasTelephony + const activeAIProvider = + selectedProvider || + (selectedAIProvider?.provider as ModelProvider | undefined) || + null const showGatewayModelField = integrationType === 'ai_provider' && - (credentialRoutingMode === 'gateway' || + (activeAIProvider === ModelProvider.CUSTOM || + credentialRoutingMode === 'gateway' || (credentialRoutingMode === 'inherit' && llmGatewaySettings?.effective_routing && llmGatewaySettings.effective_routing !== 'direct')) diff --git a/tests/test_api/test_gateway_managed_credentials.py b/tests/test_api/test_gateway_managed_credentials.py index 3b59c58f..f420e972 100644 --- a/tests/test_api/test_gateway_managed_credentials.py +++ b/tests/test_api/test_gateway_managed_credentials.py @@ -196,7 +196,31 @@ def test_update_gateway_managed_to_direct_requires_api_key( assert update_response.status_code == 400 -def test_create_aiprovider_rejects_invalid_gateway_base_url(authenticated_client): +def test_list_aiproviders_gateway_without_base_url_returns_200( + authenticated_client, db_session, org_id +): + """Integrations page must not 500 when a gateway credential lacks base_url.""" + _set_platform_gateway_passthrough(False) + settings.LLM_GATEWAY_ENABLED = False + settings.LLM_GATEWAY_BASE_URL = None + + row = AIProvider( + organization_id=org_id, + provider="custom", + api_key="enc-key", + name="Misconfigured gateway", + routing_mode="gateway", + gateway_model="gpt-oss-120b", + is_active=True, + ) + db_session.add(row) + db_session.commit() + + response = authenticated_client.get("/api/v1/aiproviders") + assert response.status_code == 200 + body = response.json() + assert len(body) == 1 + assert body[0]["effective_routing"] == "gateway" _set_platform_gateway_passthrough(False) settings.LLM_GATEWAY_ENABLED = True settings.LLM_GATEWAY_BASE_URL = "http://localhost:8080" diff --git a/tests/test_services/test_ai/test_llm_gateway.py b/tests/test_services/test_ai/test_llm_gateway.py index 3e24de64..1d72295a 100644 --- a/tests/test_services/test_ai/test_llm_gateway.py +++ b/tests/test_services/test_ai/test_llm_gateway.py @@ -392,6 +392,23 @@ def test_credential_gateway_raises_when_no_base_url(): resolve_effective_routing(org_id, db, ctx) +def test_effective_routing_label_gateway_without_base_url_does_not_raise(): + _set_platform_gateway(enabled=False) + org_id, db = _org_db({"enabled": False}) + provider = SimpleNamespace( + provider="custom", + routing_mode="gateway", + gateway_interface="native_openai", + gateway_base_url=None, + gateway_model="gpt-oss-120b", + gateway_auth_header=None, + gateway_auth_secret_env=None, + gateway_auth_secret=None, + gateway_extra_headers=None, + ) + assert get_credential_effective_routing_label(org_id, db, provider) == "gateway" + + def test_effective_routing_label_uses_credential_gateway_base_url(): _set_platform_gateway(enabled=False) org_id, db = _org_db({"enabled": False}) diff --git a/tests/test_services/test_cron/test_job_dispatch.py b/tests/test_services/test_cron/test_job_dispatch.py index ff31ac36..1f938ba0 100644 --- a/tests/test_services/test_cron/test_job_dispatch.py +++ b/tests/test_services/test_cron/test_job_dispatch.py @@ -27,22 +27,33 @@ def test_advance_cron_job_marks_completed_when_max_runs_reached(): assert job.next_run_at is None -def test_enqueue_usage_flush_routes_to_usage_task(monkeypatch): +def test_enqueue_unknown_job_type_returns_unknown(): job = MagicMock() job.job_type = "usage_flush" job.id = uuid4() + meta = enqueue_cron_job(job) + + assert meta["task"] == "unknown" + assert meta["job_type"] == "usage_flush" + + +def test_enqueue_evaluator_run_routes_to_evaluator_task(monkeypatch): + job = MagicMock() + job.job_type = "evaluator_run" + job.id = uuid4() + delayed = MagicMock() - delayed.id = "task-123" + delayed.id = "task-456" task = MagicMock() task.delay.return_value = delayed monkeypatch.setattr( - "app.workers.tasks.flush_usage_counters.flush_usage_counters_task", + "app.workers.tasks.run_cron_evaluator_job.run_cron_evaluator_job_task", task, ) meta = enqueue_cron_job(job) - assert meta["task"] == "flush_usage_counters" - assert meta["celery_task_id"] == "task-123" - task.delay.assert_called_once_with() + assert meta["task"] == "run_cron_evaluator_job" + assert meta["celery_task_id"] == "task-456" + task.delay.assert_called_once_with(str(job.id)) diff --git a/tests/test_services/test_usage/test_enabled_models.py b/tests/test_services/test_usage/test_enabled_models.py index 88d2737a..79610d30 100644 --- a/tests/test_services/test_usage/test_enabled_models.py +++ b/tests/test_services/test_usage/test_enabled_models.py @@ -35,6 +35,20 @@ def test_filter_models_unrestricted_when_allowlist_empty(): assert filter_models_by_credential(cred, ["a", "b"]) == ["a", "b"] +def test_filter_models_by_credential_uses_allowlist_when_catalog_empty(): + cred = _Credential(enabled_models=["openai/gpt-4o", "production-gpt4"], provider="custom") + assert filter_models_by_credential(cred, []) == ["openai/gpt-4o", "production-gpt4"] + + +def test_filter_models_by_credential_empty_catalog_includes_gateway_model(): + cred = _Credential( + enabled_models=["openai/gpt-4o"], + gateway_model="pinned-model", + provider="custom", + ) + assert filter_models_by_credential(cred, []) == ["pinned-model", "openai/gpt-4o"] + + def test_org_pricing_eligible_models_includes_usage_and_overrides(monkeypatch): org_id = uuid4() diff --git a/tests/test_workers/test_beat_schedule.py b/tests/test_workers/test_beat_schedule.py new file mode 100644 index 00000000..88c84aa7 --- /dev/null +++ b/tests/test_workers/test_beat_schedule.py @@ -0,0 +1,33 @@ +"""Tests for Celery Beat platform schedule.""" + +from app.workers.config import _platform_beat_schedule, _usage_flush_beat_seconds + + +def test_usage_flush_beat_seconds_defaults_to_120(): + assert _usage_flush_beat_seconds() >= 30.0 + + +def test_usage_flush_beat_seconds_respects_env(monkeypatch): + monkeypatch.setenv("USAGE_FLUSH_BEAT_SECONDS", "90") + assert _usage_flush_beat_seconds() == 90.0 + + +def test_usage_flush_beat_seconds_clamps_minimum(monkeypatch): + monkeypatch.setenv("USAGE_FLUSH_BEAT_SECONDS", "5") + assert _usage_flush_beat_seconds() == 30.0 + + +def test_platform_beat_schedule_has_four_entries(): + schedule = _platform_beat_schedule() + assert set(schedule.keys()) == { + "flush-usage-counters", + "evaluate-alerts", + "refresh-fx-rates", + "prune-oss-usage-history", + } + + +def test_flush_schedule_uses_env_interval(monkeypatch): + monkeypatch.setenv("USAGE_FLUSH_BEAT_SECONDS", "180") + schedule = _platform_beat_schedule() + assert schedule["flush-usage-counters"]["schedule"] == 180.0 diff --git a/tests/test_workers/test_usage_queue_routing.py b/tests/test_workers/test_usage_queue_routing.py index c3a5776a..d8751354 100644 --- a/tests/test_workers/test_usage_queue_routing.py +++ b/tests/test_workers/test_usage_queue_routing.py @@ -1,6 +1,6 @@ """Tests for usage pricing Celery queue routing.""" -from app.workers.config import USAGE_WORKER_QUEUE, celery_app +from app.workers.config import PLATFORM_WORKER_QUEUE, USAGE_WORKER_QUEUE, celery_app def test_flush_usage_counters_routes_to_usage_queue(): @@ -18,6 +18,16 @@ def test_cron_dispatcher_routes_to_default_worker(): assert routes["dispatch_cron_jobs"]["queue"] == "celery" -def test_beat_schedule_removed(): +def test_beat_schedule_includes_platform_tasks(): schedule = getattr(celery_app.conf, "beat_schedule", None) or {} - assert "flush-llm-usage-counters" not in schedule + assert schedule["flush-usage-counters"]["task"] == "flush_usage_counters" + assert schedule["evaluate-alerts"]["task"] == "evaluate_alerts" + assert schedule["refresh-fx-rates"]["task"] == "refresh_fx_rates" + assert schedule["prune-oss-usage-history"]["task"] == "prune_oss_usage_history" + + +def test_platform_tasks_route_to_platform_queue(): + routes = celery_app.conf.task_routes + assert routes["evaluate_alerts"]["queue"] == PLATFORM_WORKER_QUEUE + assert routes["refresh_fx_rates"]["queue"] == PLATFORM_WORKER_QUEUE + assert routes["prune_oss_usage_history"]["queue"] == PLATFORM_WORKER_QUEUE From 02e8c0a258e76da4ff9885f6f1e319785c5e9bc2 Mon Sep 17 00:00:00 2001 From: M Sami Date: Mon, 17 Aug 2026 18:16:17 +0530 Subject: [PATCH 28/55] fix(migrations): widen source column in model_pricing_rates to VARCHAR(255) and add migration function --- app/migrations/072_usage_pricing_phase1.py | 21 +++++- .../src/pages/configurations/Integrations.tsx | 70 +++++++++++++------ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/app/migrations/072_usage_pricing_phase1.py b/app/migrations/072_usage_pricing_phase1.py index 68254deb..3ae097e7 100644 --- a/app/migrations/072_usage_pricing_phase1.py +++ b/app/migrations/072_usage_pricing_phase1.py @@ -77,7 +77,7 @@ def _ensure_model_pricing_rates(db: Session) -> None: effective_from DATE NOT NULL DEFAULT CURRENT_DATE, effective_to DATE, currency VARCHAR(8) NOT NULL DEFAULT 'USD', - source VARCHAR(32) NOT NULL DEFAULT 'catalog', + source VARCHAR(255) NOT NULL DEFAULT 'catalog', input_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, output_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, cache_read_micro_usd_per_million BIGINT NOT NULL DEFAULT 0, @@ -117,12 +117,28 @@ def _ensure_model_pricing_rates(db: Session) -> None: text( """ ALTER TABLE model_pricing_rates - ADD COLUMN source VARCHAR(32) NOT NULL DEFAULT 'catalog' + ADD COLUMN source VARCHAR(255) NOT NULL DEFAULT 'catalog' """ ) ) +def _widen_source_column(db: Session) -> None: + if not _table_exists(db, "model_pricing_rates"): + return + if not _column_exists(db, "model_pricing_rates", "source"): + return + db.execute( + text( + """ + ALTER TABLE model_pricing_rates + ALTER COLUMN source TYPE VARCHAR(255) + """ + ) + ) + print("Widened model_pricing_rates.source to VARCHAR(255)") + + def _add_buffer_cost_columns(db: Session) -> None: if not _table_exists(db, "usage_pending_buffer"): return @@ -161,6 +177,7 @@ def _strip_org_extras(db: Session) -> None: def upgrade(db: Session): _ensure_model_pricing_rates(db) + _widen_source_column(db) _add_buffer_cost_columns(db) _strip_org_extras(db) db.commit() diff --git a/frontend/src/pages/configurations/Integrations.tsx b/frontend/src/pages/configurations/Integrations.tsx index 3e0373ec..0dae3165 100644 --- a/frontend/src/pages/configurations/Integrations.tsx +++ b/frontend/src/pages/configurations/Integrations.tsx @@ -194,6 +194,23 @@ export default function Integrations() { ? llmGatewayType : llmGatewaySettings?.platform_gateway_type || 'bifrost' + const activeAIProvider = + selectedProvider || + (selectedAIProvider?.provider as ModelProvider | undefined) || + null + const isCustomAIProvider = + integrationType === 'ai_provider' && + String(activeAIProvider || '').toLowerCase() === ModelProvider.CUSTOM + const aiProviderUsesModelsStep = integrationType === 'ai_provider' && !isCustomAIProvider + const showGatewayModelField = + integrationType === 'ai_provider' && + (isCustomAIProvider || + credentialRoutingMode === 'gateway' || + (credentialRoutingMode === 'inherit' && + llmGatewaySettings?.effective_routing && + llmGatewaySettings.effective_routing !== 'direct')) + const aiProviderRequiresApiKey = credentialRoutingMode === 'direct' + const showLlmGatewayConfigOptions = llmGatewayMode !== 'disabled' useEffect(() => { @@ -455,8 +472,10 @@ export default function Integrations() { showToast('Please enter your Azure OpenAI endpoint URL', 'error') return } - setAiProviderWizardStep(2) - return + if (!isCustomAIProvider) { + setAiProviderWizardStep(2) + return + } } if (isEditMode && selectedAIProvider) { @@ -617,20 +636,6 @@ export default function Integrations() { aiIntegrationProviders.length > 0 || hasTelephony - const activeAIProvider = - selectedProvider || - (selectedAIProvider?.provider as ModelProvider | undefined) || - null - const showGatewayModelField = - integrationType === 'ai_provider' && - (activeAIProvider === ModelProvider.CUSTOM || - credentialRoutingMode === 'gateway' || - (credentialRoutingMode === 'inherit' && - llmGatewaySettings?.effective_routing && - llmGatewaySettings.effective_routing !== 'direct')) - - const aiProviderRequiresApiKey = credentialRoutingMode === 'direct' - const getPlatformInfo = (platformId: IntegrationPlatform) => { return platforms.find(p => p.id === platformId) } @@ -1159,7 +1164,7 @@ export default function Integrations() { {showModal && renderModal(
-
+

@@ -1171,11 +1176,13 @@ export default function Integrations() { : 'Edit Integration' : 'Add Integration'}

- {integrationType === 'ai_provider' ? ( + {aiProviderUsesModelsStep ? (

Step {aiProviderWizardStep} of 2 —{' '} {aiProviderWizardStep === 1 ? 'Credentials' : 'Enabled models'}

+ ) : isCustomAIProvider ? ( +

Custom Bifrost model integration

) : null}
@@ -1361,16 +1368,20 @@ export default function Integrations() { {showGatewayModelField && ( <>
- + setGatewayModel(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500" - placeholder="e.g., production-gpt4 or openai/gpt-4o" + placeholder={isCustomAIProvider ? 'e.g. openai/gpt-4o or production-gpt4' : 'e.g., production-gpt4 or openai/gpt-4o'} />

- Bifrost custom model ID sent when routing via gateway. Leave blank to use the workload-selected model. + {isCustomAIProvider + ? 'Bifrost model ID for this integration. Each custom credential pins one model.' + : 'Bifrost custom model ID sent when routing via gateway. Leave blank to use the workload-selected model.'}

@@ -1505,7 +1516,7 @@ export default function Integrations() { )} - {integrationType === 'ai_provider' && aiProviderWizardStep === 2 && (selectedProvider || selectedAIProvider) && ( + {aiProviderUsesModelsStep && aiProviderWizardStep === 2 && (selectedProvider || selectedAIProvider) && ( - {integrationType === 'ai_provider' && aiProviderWizardStep === 2 ? ( + {aiProviderUsesModelsStep && aiProviderWizardStep === 2 ? ( <> ) : (
)} diff --git a/frontend/src/pages/usage/Usage.tsx b/frontend/src/pages/usage/Usage.tsx index 1ce1a505..1080ee63 100644 --- a/frontend/src/pages/usage/Usage.tsx +++ b/frontend/src/pages/usage/Usage.tsx @@ -1170,42 +1170,35 @@ export default function Usage() { -
- -
-
- {showAudio ? ( - - ) : null} - {showTts ? ( - - ) : null} - {(totals?.cache_read_tokens || 0) > 0 ? ( - - ) : null} - {(totals?.cache_creation_tokens || 0) > 0 ? ( - - ) : null} - {(totals?.reasoning_tokens || 0) > 0 ? ( - - ) : null} -
-
+ {showAudio ? ( + + ) : null} + {showTts ? ( + + ) : null} + {(totals?.cache_read_tokens || 0) > 0 ? ( + + ) : null} + {(totals?.cache_creation_tokens || 0) > 0 ? ( -
+ ) : null} + {(totals?.reasoning_tokens || 0) > 0 ? ( + + ) : null} +
@@ -1429,7 +1422,7 @@ function StatCard({ return (

{label}

diff --git a/frontend/src/pages/usage/UsageFiltersBar.tsx b/frontend/src/pages/usage/UsageFiltersBar.tsx index 83e7e1c1..67dcef0d 100644 --- a/frontend/src/pages/usage/UsageFiltersBar.tsx +++ b/frontend/src/pages/usage/UsageFiltersBar.tsx @@ -109,19 +109,19 @@ export default function UsageFiltersBar({ const activeChips = useMemo((): ActiveChip[] => { const chips: ActiveChip[] = [] - const wsLabel = workspaces.find((w) => w.id === workspaceId)?.name - if (workspaceId && wsLabel) { + if (workspaceId) { + const wsLabel = workspaces.find((w) => w.id === workspaceId)?.name chips.push({ key: 'workspace', - label: wsLabel, + label: wsLabel || 'Workspace', onClear: () => onWorkspaceChange(''), }) } - const importLabel = callImports.find((c) => c.id === callImportId)?.label - if (callImportId && importLabel) { + if (callImportId) { + const importLabel = callImports.find((c) => c.id === callImportId)?.label chips.push({ key: 'call_import', - label: importLabel, + label: importLabel || 'Call import', onClear: () => onCallImportChange(''), }) } @@ -134,22 +134,20 @@ export default function UsageFiltersBar({ } if (tagId) { const tagLabel = tags.find((t) => t.id === tagId)?.label - if (tagLabel) { - chips.push({ - key: 'tag', - label: tagLabel, - onClear: () => onTagChange(''), - }) - } + chips.push({ + key: 'tag', + label: tagLabel || 'Tag', + onClear: () => onTagChange(''), + }) } - const sourceLabel = - sourceOptions.find((e) => e.id === sourceSelectValue)?.label || - evaluations.find((e) => e.id === evaluationId)?.label || - resources.find((r) => r.id === evaluationId)?.label - if ((evaluationId || productSection) && sourceLabel) { + if (evaluationId || productSection) { + const sourceLabel = + sourceOptions.find((e) => e.id === sourceSelectValue)?.label || + evaluations.find((e) => e.id === evaluationId)?.label || + resources.find((r) => r.id === evaluationId)?.label chips.push({ key: 'evaluation', - label: sourceLabel, + label: sourceLabel || 'Source', onClear: () => onEvaluationChange(''), }) } @@ -193,6 +191,16 @@ export default function UsageFiltersBar({ onModelChange, ]) + const hasActiveScope = Boolean( + workspaceId || + callImportId || + dataset || + tagId || + evaluationId || + model || + usageKind || + productSection, + ) const hasScopeFilters = activeChips.length > 0 const kindHint = (kind: Kind): string | undefined => { @@ -229,7 +237,7 @@ export default function UsageFiltersBar({ Updating options… ) : null} - {hasScopeFilters ? ( + {hasActiveScope ? ( @@ -536,7 +568,7 @@ export default function UsagePricing() { isLoading={saveMutation.isPending} onClick={() => saveMutation.mutate()} > - Save override + {editingOverrideId ? 'Update override' : 'Save override'}
@@ -577,7 +609,7 @@ export default function UsagePricing() { Output Audio TTS - + @@ -604,15 +636,25 @@ export default function UsagePricing() { {formatRateUsd(row.rates.tts_per_1m_characters)} - +
+ + +
))} diff --git a/tests/test_services/test_usage/test_llm_usage.py b/tests/test_services/test_usage/test_llm_usage.py index 79c89c40..d08887bd 100644 --- a/tests/test_services/test_usage/test_llm_usage.py +++ b/tests/test_services/test_usage/test_llm_usage.py @@ -24,6 +24,26 @@ from app.services.usage.normalize import UsageSnapshot +def _stub_stamp_cost_params(*_args, **_kwargs) -> Dict[str, Any]: + return { + "input_cost_micro_usd": 0, + "output_cost_micro_usd": 0, + "cache_read_cost_micro_usd": 0, + "cache_creation_cost_micro_usd": 0, + "reasoning_cost_micro_usd": 0, + "audio_cost_micro_usd": 0, + "tts_cost_micro_usd": 0, + "total_cost_micro_usd": 0, + "pricing_rate_source": None, + "pricing_rate_id": None, + } + + +@pytest.fixture(autouse=True) +def _stub_usage_cost_stamp(monkeypatch): + monkeypatch.setattr(usage_mod, "_stamp_cost_params", _stub_stamp_cost_params) + + class _FakePipeline: def __init__(self, client: "_FakeRedis"): self._client = client @@ -202,7 +222,7 @@ def org_ctx(): return org_id, workspace_id, ctx -def test_record_increments_pending_and_counts_zero_token_calls(fake_redis, org_ctx): +def test_record_increments_pending_and_ignores_zero_token_calls(fake_redis, org_ctx): org_id, _workspace_id, ctx = org_ctx with llm_usage_context(ctx): usage_mod.record_llm_usage( @@ -227,7 +247,7 @@ def test_record_increments_pending_and_counts_zero_token_calls(fake_redis, org_c calls = sum(int(v) for k, v in fields.items() if k.endswith("|call_count")) assert prompt == 10 assert completion == 5 - assert calls == 2 + assert calls == 1 assert any(k.rsplit("|", 1)[0].endswith("|llm") for k in fields) @@ -499,10 +519,7 @@ def test_concurrent_flush_does_not_double_count(fake_redis, org_ctx, monkeypatch db_b = MagicMock() db_b.execute.return_value = MagicMock(rowcount=1) monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda *_args, **_kwargs: 0) - monkeypatch.setattr( - "app.services.usage.pricing.apply_cost_to_bucket", - lambda *args, **kwargs: False, - ) + monkeypatch.setattr(usage_mod, "_stamp_cost_params", _stub_stamp_cost_params) first = usage_mod.flush_usage_to_catalog(db_a, org_id) second = usage_mod.flush_usage_to_catalog(db_b, org_id) @@ -549,10 +566,7 @@ def test_flush_redis_processes_multiple_batches_in_one_run( db = MagicMock() db.execute.return_value = MagicMock(rowcount=1) monkeypatch.setattr(usage_mod, "_flush_pending_buffer", lambda *_args, **_kwargs: 0) - monkeypatch.setattr( - "app.services.usage.pricing.apply_cost_to_bucket", - lambda *args, **kwargs: False, - ) + monkeypatch.setattr(usage_mod, "_stamp_cost_params", _stub_stamp_cost_params) flushed = usage_mod.flush_usage_to_catalog(db, org_id) @@ -656,10 +670,6 @@ def test_upsert_bucket_sql_uses_valid_empty_jsonb_literal(): def test_upsert_bucket_matches_legacy_resource_context_key(monkeypatch): """Per-row context must merge into an existing evaluation-level bucket.""" - monkeypatch.setattr( - "app.services.usage.pricing.apply_cost_to_bucket", - lambda *args, **kwargs: False, - ) org_id = uuid4() evaluation_id = uuid4() bucket = { @@ -689,7 +699,7 @@ def test_upsert_bucket_matches_legacy_resource_context_key(monkeypatch): assert "context->>'resource_type'" in legacy_sql -def test_upsert_bucket_applies_cost_after_update(monkeypatch): +def test_upsert_bucket_increments_cost_on_update(monkeypatch): org_id = uuid4() bucket = { "workspace_id": uuid4(), @@ -699,18 +709,46 @@ def test_upsert_bucket_applies_cost_after_update(monkeypatch): "usage_date": date(2026, 8, 12), "usage_kind": "llm", } - deltas = {"prompt_tokens": 10, "completion_tokens": 5} + deltas = {"prompt_tokens": 10, "completion_tokens": 5, "call_count": 1} db = MagicMock() db.execute.return_value = MagicMock(rowcount=1) - called: list[bool] = [] monkeypatch.setattr( - "app.services.usage.pricing.apply_cost_to_bucket", - lambda *args, **kwargs: called.append(True) or True, + usage_mod, + "_stamp_cost_params", + lambda *args, **kwargs: { + "input_cost_micro_usd": 100, + "output_cost_micro_usd": 200, + "cache_read_cost_micro_usd": 0, + "cache_creation_cost_micro_usd": 0, + "reasoning_cost_micro_usd": 0, + "audio_cost_micro_usd": 0, + "tts_cost_micro_usd": 0, + "total_cost_micro_usd": 300, + "pricing_rate_source": "catalog", + "pricing_rate_id": str(uuid4()), + }, ) usage_mod._upsert_bucket(db, org_id, bucket, deltas) - assert called + update_sql = str(db.execute.call_args[0][0]) + assert "input_cost_micro_usd = input_cost_micro_usd + :input_cost_micro_usd" in update_sql + assert "total_cost_micro_usd = total_cost_micro_usd + :total_cost_micro_usd" in update_sql + + +def test_record_llm_usage_skips_zero_token_snapshot(fake_redis, monkeypatch): + org_id = uuid4() + monkeypatch.setattr(usage_mod, "_client", lambda: fake_redis) + usage_mod.record_llm_usage( + "gpt-test", + UsageSnapshot(prompt_tokens=0, completion_tokens=0), + organization_id=org_id, + ctx=LLMUsageContext( + organization_id=org_id, + product_section=LLMUsageProductSection.CHAT, + ), + ) + assert not fake_redis.hgetall(usage_mod._pending_hash_key(org_id)) def test_orphan_recovery_runs_at_most_once_per_interval(fake_redis, monkeypatch): diff --git a/tests/test_services/test_usage/test_pricing.py b/tests/test_services/test_usage/test_pricing.py index 6d01dcaa..eaf63c35 100644 --- a/tests/test_services/test_usage/test_pricing.py +++ b/tests/test_services/test_usage/test_pricing.py @@ -193,9 +193,11 @@ def test_resolve_rate_ignores_stale_negative_cache(monkeypatch): override_result.mappings.return_value.first.return_value = None catalog_result = MagicMock() catalog_result.mappings.return_value.first.return_value = row + cache_table_exists = MagicMock() + cache_table_exists.scalar.return_value = "model_pricing_rates" db = MagicMock() - db.execute.side_effect = [override_result, catalog_result] + db.execute.side_effect = [override_result, cache_table_exists, catalog_result] resolver = PricingResolver(db) card = resolver.resolve_rate( From 49871cbfc22e3afb902d954f29fb729125c6ab99 Mon Sep 17 00:00:00 2001 From: M Sami Date: Tue, 18 Aug 2026 00:09:12 +0530 Subject: [PATCH 31/55] fix(usage): implement _cost_fields_for_pending_deltas function to streamline cost calculations for pending usage deltas --- app/services/ai/llm_service.py | 16 -------- app/services/usage/llm_usage.py | 68 ++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/app/services/ai/llm_service.py b/app/services/ai/llm_service.py index a2ea3801..95e64488 100644 --- a/app/services/ai/llm_service.py +++ b/app/services/ai/llm_service.py @@ -490,22 +490,6 @@ def generate_response( credential=credential_ctx, ) - # #region agent log - try: - import json as _json, time as _time - _msg_types = [] - for _m in messages: - _c = _m.get("content") - if isinstance(_c, list): - _msg_types.extend( - p.get("type") for p in _c if isinstance(p, dict) - ) - with open("debug-bfc313.log", "a", encoding="utf-8") as _f: - _f.write(_json.dumps({"sessionId": "bfc313", "runId": "post-fix", "hypothesisId": "C", "location": "llm_service.py:generate_response", "message": "pre-completion routing", "data": {"effective_routing": effective_routing, "credential_mode": getattr(credential_ctx, "routing_mode", None), "credential_id": str(credential_id) if credential_id else None, "model": model_str, "has_api_base": "api_base" in call_kwargs, "custom_llm_provider": call_kwargs.get("custom_llm_provider"), "content_part_types": _msg_types}, "timestamp": int(_time.time() * 1000)}) + "\n") - except Exception: - pass - # #endregion - try: response = litellm.completion(**call_kwargs) except Exception as e: diff --git a/app/services/usage/llm_usage.py b/app/services/usage/llm_usage.py index dea0c400..bbeac232 100644 --- a/app/services/usage/llm_usage.py +++ b/app/services/usage/llm_usage.py @@ -312,6 +312,63 @@ def _deltas_from_tts(characters: int) -> Dict[str, int]: } +def _cost_fields_for_pending_deltas( + organization_id: UUID, + bucket: Dict[str, Any], + deltas: Dict[str, int], + *, + db: Optional[Session] = None, +) -> Dict[str, Any]: + if not _has_billable_usage_deltas(deltas): + return {} + try: + from app.services.usage.pricing import cost_fields_from_deltas + except Exception as exc: + logger.debug("usage pending cost stamp skipped: {}", exc) + return {} + + usage_date = bucket["usage_date"] + if isinstance(usage_date, str): + usage_date = date.fromisoformat(usage_date) + usage_kind = bucket.get("usage_kind") or USAGE_KIND_LLM + + if db is not None: + try: + return cost_fields_from_deltas( + deltas, + organization_id=organization_id, + model=bucket["model"], + usage_kind=usage_kind, + usage_date=usage_date, + db=db, + ) + except Exception as exc: + logger.debug("usage pending cost stamp failed: {}", exc) + return {} + + try: + from app.database import SessionLocal + except Exception as exc: + logger.debug("usage pending cost stamp skipped: {}", exc) + return {} + + session = SessionLocal() + try: + return cost_fields_from_deltas( + deltas, + organization_id=organization_id, + model=bucket["model"], + usage_kind=usage_kind, + usage_date=usage_date, + db=session, + ) + except Exception as exc: + logger.debug("usage pending cost stamp failed: {}", exc) + return {} + finally: + session.close() + + def _buffer_to_postgres( organization_id: UUID, bucket: Dict[str, Any], @@ -347,16 +404,9 @@ def _buffer_to_postgres( } db = SessionLocal() try: - from app.services.usage.pricing import cost_fields_from_deltas - params.update( - cost_fields_from_deltas( - deltas, - organization_id=organization_id, - model=bucket["model"], - usage_kind=usage_kind, - usage_date=usage_date, - db=db, + _cost_fields_for_pending_deltas( + organization_id, bucket, deltas, db=db ) ) db.execute( From a33972d61db06b3ea6aa5b11f053e9c41da4039d Mon Sep 17 00:00:00 2001 From: M Sami Date: Tue, 18 Aug 2026 19:12:13 +0530 Subject: [PATCH 32/55] feat(docs): add usage tracking feature documentation and update related references in the documentation --- docs-fumadocs/content/docs/intro.mdx | 2 + .../content/docs/monitoring/meta.json | 1 + .../content/docs/monitoring/usage.mdx | 198 ++++++++++++++++++ .../content/docs/reference/cli-commands.mdx | 47 +++++ .../content/feature-contributors.json | 17 ++ docs-fumadocs/public/search-index.json | 139 +++++++----- 6 files changed, 357 insertions(+), 47 deletions(-) create mode 100644 docs-fumadocs/content/docs/monitoring/usage.mdx diff --git a/docs-fumadocs/content/docs/intro.mdx b/docs-fumadocs/content/docs/intro.mdx index 36d2e905..f95f6f49 100644 --- a/docs-fumadocs/content/docs/intro.mdx +++ b/docs-fumadocs/content/docs/intro.mdx @@ -33,6 +33,7 @@ EfficientAI gives you an end-to-end loop for voice AI quality: - **Metric-driven evaluation**: built-in and custom metrics with categorization labels, surface targeting, and org-wide or workspace scope. - **Cloud storage**: store audio in Amazon S3, S3-compatible services, or Google Cloud Storage via the Data Sources UI. - **Prompt optimization workflows**: run optimization loops, compare candidates, accept winners, and push selected prompts to providers. +- **Usage tracking**: org-wide LLM, STT, and TTS consumption with cost estimates and drill-down by workspace and product area. ## Platform model @@ -48,6 +49,7 @@ This structure keeps testing reproducible while still reflecting real-world voic ## Key guides +- [Usage](/docs/monitoring/usage/) — LLM/STT/TTS consumption, cost estimates, and drill-down by workspace and product area - [Workspaces](/docs/getting-started/workspaces/) — project isolation within your organization, plus workspace roles (Viewer / Editor / Workspace Admin) and how they interact with org roles - [Cloud Storage](/docs/getting-started/cloud-storage/) — S3 and GCS configuration - [Metrics](/docs/products/metrics/) — custom rubrics, surfaces, and evaluation scope diff --git a/docs-fumadocs/content/docs/monitoring/meta.json b/docs-fumadocs/content/docs/monitoring/meta.json index 2ace091c..e0a482ad 100644 --- a/docs-fumadocs/content/docs/monitoring/meta.json +++ b/docs-fumadocs/content/docs/monitoring/meta.json @@ -1,6 +1,7 @@ { "title": "Monitoring", "pages": [ + "usage", "calls", "alerting", "cron-jobs" diff --git a/docs-fumadocs/content/docs/monitoring/usage.mdx b/docs-fumadocs/content/docs/monitoring/usage.mdx new file mode 100644 index 00000000..1c2cb701 --- /dev/null +++ b/docs-fumadocs/content/docs/monitoring/usage.mdx @@ -0,0 +1,198 @@ +--- +id: usage +title: Usage +sidebar_position: 1 +--- + +# Usage + +**Usage** is org-scoped analytics for LLM, STT, and TTS consumption with estimated costs. It helps you see how much each workspace, product area, call import, and model is consuming — and what that usage likely costs. + +Usage is **not** a quota or billing portal. Optional Flexprice event metering is a separate system and is not shown in the Usage UI. + +Models and providers tracked here come from your enabled [integrations](/docs/getting-started/integrations/). Usage is attributed per [workspace](/docs/getting-started/workspaces/) when the underlying workflow is workspace-scoped. + +--- + +## Opening the Usage page + +In the sidebar, go to **Usage → Overview** (`/usage`). + +The page has two tabs when your org is licensed for enterprise features and you are an org admin: + +| Tab | URL | Who can access | +|-----|-----|----------------| +| **Overview** | `/usage` | All org members | +| **Pricing overrides** | `/usage?tab=pricing` | Org admins with enterprise license | + +--- + +## Summary metrics + +The top of the Overview tab shows rollup cards for the selected date range and filters: + +| Metric | Description | +|--------|-------------| +| **Input tokens** | Prompt or input tokens sent to LLM providers | +| **Output tokens** | Completion or output tokens returned by LLMs | +| **Total tokens** | Sum of input and output tokens | +| **LLM calls** | Number of LLM API calls | +| **STT audio** | Speech-to-text audio duration (shown when non-zero) | +| **TTS characters** | Text-to-speech characters synthesized (shown when non-zero) | +| **Cache read** | Tokens read from provider prompt cache (shown when non-zero) | +| **Cache write** | Tokens written to provider prompt cache (shown when non-zero) | +| **Reasoning** | Reasoning tokens billed separately by some providers (shown when non-zero) | +| **Estimated cost** | Total estimated cost for the filtered range | + +Click **Cost breakdown** to open a modal with line items: + +- Input, output, cache read, cache write, reasoning +- Audio (STT), TTS +- Total estimated cost + +If any usage in the range has no matching catalog rate, the breakdown shows an **unpriced usage** warning. Those rows still appear in token/volume metrics but do not contribute to cost totals. + +### Currency display + +Toggle between **USD** and **INR** in the filter bar. INR amounts use a live USD→INR rate from Frankfurter when available, with a fallback estimate when the FX service is unreachable. + +--- + +## Filters + +Filters narrow the summary cards and drill-down table. All filter state is stored in the URL, so you can bookmark or share a specific view. + +| Filter | Description | +|--------|-------------| +| **Date range** | Start and end dates, interpreted in your browser's IANA timezone | +| **Workspace** | Limit to one workspace | +| **Call import** | Limit to one call import batch | +| **Dataset** | Filter by dataset name on call import rows | +| **Tag** | Filter by call import tag | +| **Evaluation run** | Filter by evaluation resource | +| **Usage kind** | `LLM`, `STT`, or `TTS` | +| **Model** | Provider model identifier | +| **Source / product section** | Product area (playground, evaluators, call imports, etc.) | + +--- + +## Drill-down navigation + +Click rows in the breakdown table to drill deeper. Breadcrumbs at the top show your current path; click a breadcrumb to go back up. + +```mermaid +flowchart TD + Org[Organization] --> Workspace[Workspace] + Workspace --> Composite[Call imports and product areas] + Composite --> CallImport[Call import batch] + CallImport --> EvalRun[Evaluation run] + EvalRun --> Model[Model] + Model --> Kind[Usage kind] + Composite --> ProductSection[Product section] + ProductSection --> Model +``` + +At the organization level, the table groups by **workspace**. Inside a workspace you see a composite view: + +- **Call import batches** — CSV uploads or manual audio recordings +- **Product areas** — usage from other parts of the platform (not tied to a single call import) + +From a call import batch you can drill into evaluation runs, then model, then usage kind. From a product area you drill into model, then usage kind. + +Each drill level returns at most **100 rows**. If more exist, results are truncated at that level. + +### Product sections + +| Section | What it tracks | +|---------|----------------| +| **Call imports** | Call import batch processing | +| **Call import evaluations** | Evaluations run on imported calls | +| **Playground** | Text playground and experiments | +| **Voice playground** | Voice agent playground — LLM, STT, and TTS | +| **Chat** | Chat conversations | +| **Telephony** | Telephony and live calls | +| **Evaluators** | Evaluator definitions and runs | +| **Metrics** | Metrics and scoring | +| **Judge alignment** | Judge alignment workflows | +| **Prompt optimization** | Prompt optimization jobs | +| **Personas** | Persona generation | +| **Agents** | Agent configuration | +| **Prompt partials** | Prompt partials | +| **Conversation evaluations** | Conversation evaluations | +| **Test agent** | Test agent sessions | +| **Other** | Usage not attributed to a named product area | + +--- + +## Data freshness and history + +### Freshness + +Usage counters are buffered in Redis and flushed to Postgres by the `worker-usage` service on a Celery Beat schedule (default: every **2 minutes**). The UI reads Postgres only. + +The page shows **Updated** with a `last_updated_at` timestamp when available. Expect roughly **2 minutes** of lag between new API usage and what appears on this page. + +For fresh data in self-hosted deployments, ensure `beat`, `worker-usage`, and the default `worker` are running (or use `eai start-all`). + +### History limits + +| Deployment | History window | Pricing overrides tab | +|------------|----------------|----------------------| +| **OSS** (no license) | Last **7 days** | Hidden | +| **Enterprise** (`EFFICIENTAI_LICENSE`) | Unlimited | Org admins only | + +On OSS deployments, an amber banner explains the 7-day cap and points to `EFFICIENTAI_LICENSE`. If you pick a wider date range, it is automatically clamped to the allowed window. + +See [Configuration](/docs/reference/configuration/) for license setup. + +--- + +## Pricing overrides + +Enterprise org admins can open **Pricing overrides** (`/usage?tab=pricing`) to set per-model rates that override the built-in catalog for cost estimation. + +### Override fields by usage kind + +| Usage kind | Rate fields (USD) | +|------------|-------------------| +| **LLM** | Input / 1M tokens, output / 1M tokens, cache read / 1M, cache write / 1M, reasoning / 1M, audio / minute | +| **STT** | Audio / minute | +| **TTS** | Characters / 1M | + +For each override you set: + +- **Provider credential** — models come from enabled integrations +- **Usage kind** — LLM, STT, or TTS +- **Model** — provider model identifier +- **Effective from** — date the override starts applying +- **Rates** — USD values for the fields above + +You can prefill rates from the catalog or an existing override. Saving creates or updates the override; deleting removes it. + +### How overrides affect costs + +Overrides apply to **new usage** recorded on or after `effective_from`. Costs already stamped on daily rollup rows are **not** changed automatically. + +To backfill historical costs after a catalog or override change, use the CLI or API recompute workflow. The Usage UI does not expose recompute jobs today — see [CLI Commands](/docs/reference/cli-commands/#usage-pricing). + +--- + +## Operations + +Self-hosted operators manage pricing catalogs and cost backfills outside the UI. + +| Requirement | Purpose | +|-------------|---------| +| `beat` | Schedules usage flush, FX refresh, OSS history prune | +| `worker-usage` | Flushes Redis counters and runs cost recompute jobs | +| Default `worker` | Evaluator cron dispatch (indirectly drives much platform usage) | + +Common tuning variables (see `env.example`): + +| Variable | Default | Purpose | +|----------|---------|---------| +| `USAGE_FLUSH_BEAT_SECONDS` | `120` | Flush interval (~2 min UI lag) | +| `USAGE_READ_CACHE_TTL_SECONDS` | `90` | Redis cache TTL for summary/breakdown/filters | +| `USAGE_FLUSH_MAX_BATCHES_PER_RUN` | `30` | Batches per flush tick | + +For seeding rates, diffing catalogs, and recomputing stored costs, see [CLI Commands — Usage pricing](/docs/reference/cli-commands/#usage-pricing). diff --git a/docs-fumadocs/content/docs/reference/cli-commands.mdx b/docs-fumadocs/content/docs/reference/cli-commands.mdx index 6e09c83a..c0ce1658 100644 --- a/docs-fumadocs/content/docs/reference/cli-commands.mdx +++ b/docs-fumadocs/content/docs/reference/cli-commands.mdx @@ -107,3 +107,50 @@ eai migrate --verbose ``` **Note**: Migrations run automatically on application startup. You only need to run them manually if you want to apply migrations before starting the server. + +## Usage pricing + +Manage model pricing rates and backfill stored usage costs on `llm_usage_daily` rollups. Requires `beat`, `worker-usage`, and the default `worker` (or `eai start-all`). + +```bash +# Upsert model_pricing_rates from app/config/models.json +eai usage seed-rates --config config.yml + +# Compare models.json pricing vs Postgres +eai usage diff-rates --config config.yml + +# Backfill costs in-process (all orgs; use after migrate or catalog change) +eai usage recompute --config config.yml --sync + +# Async recompute via usage queue (requires --organization-id) +eai usage recompute --config config.yml --organization-id + +# Optional scopes: --model, --usage-kind, --start-date, --end-date + +# Optional: fetch LiteLLM prices into pricing_catalog.json +eai usage sync-litellm --local +eai usage sync-litellm --local --write-models +``` + +**After migrations or catalog changes:** + +```bash +eai migrate +eai usage seed-rates --config config.yml +eai usage recompute --config config.yml --sync +``` + +**Flush / Usage UI tuning** — set in `.env` (see `env.example`): + +| Variable | Default | Purpose | +|----------|---------|---------| +| `USAGE_FLUSH_BUCKET_BATCH_SIZE` | `500` | Buckets per DB transaction | +| `USAGE_FLUSH_MAX_BATCHES_PER_RUN` | `30` | Batches per flush tick (≤ **15,000** buckets/run) | +| `USAGE_FLUSH_BEAT_SECONDS` | `120` | Celery Beat flush interval (~2 min lag vs Redis) | +| `USAGE_FLUSH_LOCK_TTL_SECONDS` | `300` | Per-org flush lock TTL | +| `USAGE_READ_CACHE_TTL_SECONDS` | `90` | Redis cache TTL for usage summary/breakdown/filters | +| `CRON_DISPATCH_INTERVAL_SECONDS` | `30` | Evaluator cron dispatcher tick (default worker) | + +The Usage UI reads Postgres only (summary/breakdown/filters). Redis counters flush on the Celery Beat schedule (~2 min eventual consistency). If Redis backlog grows, lower `USAGE_FLUSH_BEAT_SECONDS` or raise `USAGE_FLUSH_MAX_BATCHES_PER_RUN`. + +See [Usage](/docs/monitoring/usage/) for the end-user guide. diff --git a/docs-fumadocs/content/feature-contributors.json b/docs-fumadocs/content/feature-contributors.json index 28ca6227..e7990cc8 100644 --- a/docs-fumadocs/content/feature-contributors.json +++ b/docs-fumadocs/content/feature-contributors.json @@ -287,6 +287,23 @@ ], "lastReviewed": "2026-06-15" }, + { + "featureId": "monitoring/usage", + "docPath": "docs-fumadocs/content/docs/monitoring/usage.mdx", + "historyPath": "docs-fumadocs/content/docs/monitoring/usage.mdx", + "owners": [ + "aadhar-EAI", + "Tejas Narayan" + ], + "contributors": [ + { + "name": "aadhar-EAI", + "email": "aadhar@efficientai.cloud", + "commits": 1 + } + ], + "lastReviewed": "2026-08-18" + }, { "featureId": "monitoring/cron-jobs", "docPath": "docs-fumadocs/content/docs/monitoring/cron-jobs.mdx", diff --git a/docs-fumadocs/public/search-index.json b/docs-fumadocs/public/search-index.json index e2d517c7..2d7daadc 100644 --- a/docs-fumadocs/public/search-index.json +++ b/docs-fumadocs/public/search-index.json @@ -1,19 +1,28 @@ { - "generatedAt": "2026-06-13T19:35:24.170Z", - "count": 30, + "generatedAt": "2026-08-18T13:39:50.745Z", + "count": 35, "records": [ { "id": "advanced/architecture", - "url": "/docs/advanced/architecture", + "url": "/docs/advanced/architecture/", "title": "Architecture", "breadcrumbs": [ "Advanced" ], "content": "Architecture Simple Overview EfficientAI is built like a modern web application. The Brain (API Server) : Controls everything. The Worker : Does the heavy lifting in the background, like processing audio files so the website stays fast. The Interface (Frontend) : The website you see and click on. The Memory (Database) : Where we store all your agents, test results, and user data. Technical Deep Dive EfficientAI is built as a modular, containerized application designed for scalability and extensibility. (See original Architecture documentation below) System Components The platform consists of four primary components: 1. API Server (FastAPI) : The central control plane. 2. Worker (Celery) : Handles asynchronous background tasks (transcription, evaluation). 3. Frontend (React/Vite) : The user interface. 4. Data Stores : PostgreSQL (State) and Redis (Queue/Cache). Core Services 1. API Server ( ) Built with FastAPI, it provides REST endpoints for: Resource Management : CRUD for Agents, Personas, Scenarios. Orchestration : Real time control of test conversations. Analysis : Serving evaluation results and dashboards. 2. Asynchronous Workers ( ) Powered by Celery and Redis, the workers handle long running operations: Transcription : Processing audio files (using Whisper, Deepgram, etc.). Evaluation : Running metric calculations (WER/CER) on completed conversations. 3. Test Agent Service ( ) This is the heart of the testing engine. It: Manages the state of the conversation. Generates accurate system prompts for the Persona. Handles the latency sensitive loop of: ." }, + { + "id": "advanced/call-import-sharding", + "url": "/docs/advanced/call-import-sharding/", + "title": "Call Import Sharding", + "breadcrumbs": [ + "Advanced" + ], + "content": "Call Import Sharding For large batches (10k+ rows), call import row data can be spread across multiple PostgreSQL data shards with a catalog database for metadata, routing, and parent counters. Architecture Catalog DB — , , shard slice registry, dispatch metadata Data shards — , (partitioned by consistent hash on ) Scatter/gather reads — API and workers query each shard and merge results Fair dispatch — import/eval workers respect per shard pending scans and Redis progress keys Enable sharding in under . See and for profiles. Live telephony / evaluator results use a separate payload sharding path ( , ) keyed by workspace — see in the repo. Operations Connection pools When , use smaller per process pools ( 3–5, 5–10) so API + workers × shards stay under Postgres . PgBouncer (optional) Point , , and each shard at PgBouncer ( ) in transaction pooling mode. Keep SQLAlchemy enabled. Observability Row Celery tasks log on the import worker hot path. Watch Redis eval/import progress keys ( , ) alongside catalog parent counters. Rebalance Dry run registry updates: Use only after pausing the import. The rebalance tool copies rows to the target shard, updates the catalog registry, then removes copies from the source shard. Troubleshooting stalled evaluations If evaluation runs stall after recordings import (rows show import but no diarization/scoring): 1. Restart API and after deploying fixes. 2. Retry evaluation from the UI (use Overwrite existing transcripts if diarization previously failed). 3. Ensure the imports worker consumes ." + }, { "id": "advanced/database", - "url": "/docs/advanced/database", + "url": "/docs/advanced/database/", "title": "Database", "breadcrumbs": [ "Advanced" @@ -22,7 +31,7 @@ }, { "id": "advanced/development", - "url": "/docs/advanced/development", + "url": "/docs/advanced/development/", "title": "Development & Troubleshooting", "breadcrumbs": [ "Advanced" @@ -31,25 +40,25 @@ }, { "id": "getting-started/authentication", - "url": "/docs/getting-started/authentication", + "url": "/docs/getting-started/authentication/", "title": "Authentication", "breadcrumbs": [ "Getting Started" ], - "content": "🔐 Authentication EfficientAI ships with a pluggable authentication system that scales from a single operator OSS install to an enterprise deployment behind your existing identity provider. You pick the providers you want in (or via in ) and the API/frontend adapt automatically. Deployment models | Model | Providers | License needed | | | | | | OSS self hosted (default) | , | None | | Enterprise SSO (BYO IdP) | , | | — the header, always available, for programmatic access (CI pipelines, SDKs, scripts). — email + password, verified against the local users table, returns an app signed HS256 Bearer token. Enabled by default. — license gated. Verifies a Bearer JWT issued by your OIDC compliant IdP (Okta, Azure AD / Entra ID, Google Workspace, AWS Cognito, Auth0, Ping, JumpCloud, OneLogin, …) against the issuer's JWKS. :::info Why no bundled IdP? In practice every enterprise already runs one. Shipping our own Keycloak alongside the app just added another thing for you to operate and lock down. talks to whatever you already have. ::: Self hosted (OSS) This is the default after or . No license, no IdP, no external dependencies. The equivalent environment variables (for Docker Compose / ): First time bootstrap 1. Start the stack. 2. Open and click Create account on the login screen. The first user you create becomes the admin of a fresh organization. 3. Mint an API key from Profile → API Keys (or via ) for programmatic access. Password login (email + password) When is enabled, the login screen shows a Sign in and (if ) a Create account tab. Signing up provisions a new user and a new organization, and makes that user the of it. If you leave the organization name blank, the server derives one from the email's local part. Once signed in, the SPA holds a short lived Bearer token and silently re authenticates before it expires. You can change the token lifetime with — the default is 12 hours. Linking a password to an API key only account If you bootstrapped with , the backend provisions a placeholder user behind that key (its email ends in ). You can upgrade this identity to a real email + password login so you can sign in interactively with the same user. Do it from Profile → Sign in Password while signed in via the API key — the page detects the placeholder email and prompts you to pick a real one and a password. After saving, the same user can log in either with the original API key (for machines) or with email + password (for humans). Rules the UI enforces: If the user already has a password, the form asks for the current one before accepting a new one. You can only set the email from that screen while it's still the placeholder address; \"real\" users change their email from the main profile edit flow. Hardening before you expose it to the internet Turn off self service signup once your team is in: Rotate to invalidate existing sessions. Put the app behind a reverse proxy (Nginx, Caddy, Cloudflare) that terminates TLS and enforces HSTS. Restrict to the exact domain(s) serving the SPA. Team management: invitations & organizations EfficientAI is multi tenant from the ground up. Every piece of data is scoped to an organization , a user can be a member of more than one organization, and each membership has a role that controls what they can do. Roles | Role | Can do | | | | | | Read only access to everything in the org. | | | Everything a reader can + create/update/delete most resources. | | | Everything a writer can + manage users, invitations, roles, API keys, and org settings. | The role is stored per membership, so the same user can be an in one org and a in another. Inviting a teammate Admins invite teammates from Settings → Team . An invitation captures an email and a role and stays valid for 7 days. From the same page, admins can also: See the current members of the org and change their role (with a guard so you can't demote the last admin). Remove a member from the organization. Revoke a pending invitation. Delivery. The backend cre" + "content": "🔐 Authentication EfficientAI ships with a pluggable authentication system that scales from a single operator OSS install to an enterprise deployment behind your existing identity provider. You pick the providers you want in (or via in ) and the API/frontend adapt automatically. Deployment models | Model | Providers | License needed | | | | | | OSS self hosted (default) | , | None | | Enterprise SSO (BYO IdP) | , | | — the header, always available, for programmatic access (CI pipelines, SDKs, scripts). — email + password, verified against the local users table, returns an app signed HS256 Bearer token. Enabled by default. — license gated. Verifies a Bearer JWT issued by your OIDC compliant IdP (Okta, Azure AD / Entra ID, Google Workspace, AWS Cognito, Auth0, Ping, JumpCloud, OneLogin, …) against the issuer's JWKS. :::info Why no bundled IdP? In practice every enterprise already runs one. Shipping our own Keycloak alongside the app just added another thing for you to operate and lock down. talks to whatever you already have. ::: Self hosted (OSS) This is the default after or . No license, no IdP, no external dependencies. The equivalent environment variables (for Docker Compose / ): First time bootstrap 1. Start the stack. 2. Open and click Create account on the login screen. The first user you create becomes the admin of a fresh organization. 3. Mint an API key from Profile → API Keys (or via ) for programmatic access. Password login (email + password) When is enabled, the login screen shows a Sign in and (if ) a Create account tab. Signing up provisions a new user and a new organization, and makes that user the of it. If you leave the organization name blank, the server derives one from the email's local part. Once signed in, the SPA holds a short lived access token (15 minutes by default) plus a refresh token. The client silently refreshes the access token before it expires. You can change lifetimes with and . Logout revokes the refresh token and blacklists the current access token server side. Linking a password to an API key only account If you bootstrapped with , the backend provisions a placeholder user behind that key (its email ends in ). You can upgrade this identity to a real email + password login so you can sign in interactively with the same user. Do it from Profile → Sign in Password while signed in via the API key — the page detects the placeholder email and prompts you to pick a real one and a password. After saving, the same user can log in either with the original API key (for machines) or with email + password (for humans). Rules the UI enforces: If the user already has a password, the form asks for the current one before accepting a new one. You can only set the email from that screen while it's still the placeholder address; \"real\" users change their email from the main profile edit flow. Hardening before you expose it to the internet Turn off self service signup once your team is in: Rotate to invalidate existing sessions. Put the app behind a reverse proxy (Nginx, Caddy, Cloudflare) that terminates TLS and enforces HSTS. The bundled FastAPI server sends baseline security headers on all responses ( , , , , and by default). If you terminate traffic at an external reverse proxy, keep those headers (or stricter CSP ) enabled there too. After reviewing CSP violation reports, set to enforce the policy. Pin third party observability container images to fixed tags and rebuild external reverse proxy images on patched runtimes. If a scanner reports a Go stdlib CVE in a binary this repo does not build, identify the flagged container or proxy artifact and upgrade it separately. Restrict to the exact domain(s) serving the SPA. Set in production so , , and are not served on the public hostname. Keep so and return 404 to anonymous public clients (including vulnerability scanners hitting your ALB hostname). AWS ALB target health checks connect directly from VPC addresses (no ); include your VPC/LB CIDRs in . Full migrat" }, { "id": "getting-started/cloud-storage", - "url": "/docs/getting-started/cloud-storage", + "url": "/docs/getting-started/cloud-storage/", "title": "Cloud Storage", "breadcrumbs": [ "Getting Started" ], - "content": "Cloud Storage Overview EfficientAI can store audio files and recordings in the cloud using one active blob backend at a time: Amazon S3 (or any S3 compatible service such as MinIO, DigitalOcean Spaces, Cloudflare R2) Google Cloud Storage (GCS) Cloud storage is useful for: Storing large audio files outside your application server Scaling storage independently from compute Integrating with existing AWS or GCP infrastructure Browsing, uploading, and managing audio from the Data Sources page in the UI Select the backend with in ( or ), then enable and configure the matching block below. Storage block | Option | Description | | | | | | Local fallback directory for uploads when cloud storage is disabled | | | Maximum upload size in megabytes | | | Active cloud backend: or | | | File extensions accepted for audio uploads | Amazon S3 and S3 compatible storage Configuration Set and enable the block: | Option | Required | Description | | | | | | | Yes | Set to to enable S3 storage | | | Yes | Name of your S3 bucket | | | Yes | AWS region (e.g., , ) | | | Yes | AWS Access Key ID | | | Yes | AWS Secret Access Key | | | No | Custom endpoint for S3 compatible services | | | No | Folder prefix for uploaded files (default: ) | S3 compatible examples MinIO DigitalOcean Spaces Cloudflare R2 Google Cloud Storage Configuration Set and enable the block: | Option | Required | Description | | | | | | | Yes | Set to to enable GCS storage | | | Yes | Name of your GCS bucket | | | Yes | GCP project ID | | | No | Path to a service account JSON key file | | | No | Folder prefix for uploaded files (default: ) | Authentication EfficientAI resolves GCS credentials in this order: 1. in (relative paths resolve from the working directory) 2. environment variable 3. Application Default Credentials (ADC) on GCE, GKE, or Cloud Run The client is included in the standard EfficientAI install ( ); no extra Python package step is required for GCS. GCP setup 1. Create a GCS bucket in your project (Console or ). 2. Create a service account with object read/write access (e.g., on the bucket, or a tighter custom role). 3. Download a JSON key and set , or mount the key and set . 4. Set , , and fill in and . Object layout under matches S3 (organization scoped paths), so you can migrate between providers without changing application logic. Data Sources UI Once cloud storage is configured and connected, open Configuration → Data Sources to: Browse folders and audio files in your bucket Upload new audio files Preview playback in the browser Delete files Test the connection (labels show Amazon S3 or Google Cloud Storage based on ) Verifying connection Restart the application after changing storage settings: Use Test Connection on the Data Sources page, or upload a test file and confirm it appears under the configured in your bucket. Troubleshooting | Issue | Solution | | | | | (S3) | Check IAM permissions and bucket policy | | (S3) | Verify bucket name and region | | (S3) | Check for S3 compatible services | | (S3) | Verify and | | GCS | Confirm service account has on the bucket | | GCS bucket not found | Verify , , and that the bucket exists | | GCS auth failure | Set or ; on GCP VMs you can use ADC | | Wrong provider in UI | Ensure matches the enabled block ( vs ) |" + "content": "Cloud Storage Overview EfficientAI can store audio files and recordings in the cloud using one active blob backend at a time: Amazon S3 (or any S3 compatible service such as MinIO, DigitalOcean Spaces, Cloudflare R2) Google Cloud Storage (GCS) Azure Blob Storage Cloud storage is useful for: Storing large audio files outside your application server Scaling storage independently from compute Integrating with existing AWS, GCP, or Azure infrastructure Browsing, uploading, and managing audio from the Data Sources page in the UI Select the backend with in ( , , or ), then enable and configure the matching block below. Storage block | Option | Description | | | | | | Local fallback directory for uploads when cloud storage is disabled | | | Maximum upload size in megabytes | | | Active cloud backend: , , or | | | File extensions accepted for audio uploads | Amazon S3 and S3 compatible storage Configuration Set and enable the block: | Option | Required | Description | | | | | | | Yes | Set to to enable S3 storage | | | Yes | Name of your S3 bucket | | | Yes | AWS region (e.g., , ) | | | Yes | AWS Access Key ID | | | Yes | AWS Secret Access Key | | | No | Custom endpoint for S3 compatible services | | | No | Folder prefix for uploaded files (default: ) | S3 compatible examples MinIO DigitalOcean Spaces Cloudflare R2 Google Cloud Storage Configuration Set and enable the block: | Option | Required | Description | | | | | | | Yes | Set to to enable GCS storage | | | Yes | Name of your GCS bucket | | | Yes | GCP project ID | | | No | Path to a service account JSON key file | | | No | Override service account email for signed URL generation when ADC does not expose it | | | No | Folder prefix for uploaded files (default: ) | Authentication EfficientAI resolves GCS credentials in this order: 1. in (relative paths resolve from the working directory) 2. environment variable 3. Application Default Credentials (ADC) on GCE, GKE, or Cloud Run Uploads and server side downloads work with ADC alone (including GKE Workload Identity ). Signed URLs for browser playback require either a service account JSON key with a private key, or Workload Identity plus IAM signBlob (see below). The client is included in the standard EfficientAI install ( ); no extra Python package step is required for GCS. GCP setup 1. Create a GCS bucket in your project (Console or ). 2. Create a service account with object read/write access (e.g., on the bucket, or a tighter custom role). 3. Authenticate the app using one of: GKE Workload Identity (recommended): bind your Kubernetes service account to the GCP service account. Enable the and grant the GCP service account on itself so EfficientAI can sign playback URLs without a JSON key. Service account JSON key: download a key and set , or mount the key and set . 4. Set , , and fill in and . Example IAM binding for Workload Identity signed URLs: If ADC does not expose the service account email (some federation setups), set to the workload GCP service account email. Object layout under matches S3 (organization scoped paths), so you can migrate between providers without changing application logic. Azure Blob Storage Configuration Set and enable the block: | Option | Required | Description | | | | | | | Yes | Set to to enable Azure Blob Storage | | | Yes | Name of your Azure storage container | | | Yes | Azure storage account name ( not required if is set) | | | Yes | Storage account access key ( not required if is set) | | | No | Full Azure storage connection string (overrides + ) | | | No | Folder prefix for uploaded files (default: ) | Authentication EfficientAI resolves Azure credentials in this order: 1. in 2. + Managed Identity is not supported in this release; use a connection string or account key. SAS URLs (temporary download links in the UI) require an account key. Connection strings include the key automatically. Azure setup 1. Create a Storage Account in the Azure Portal (or via ). 2. Create a container (e.g., ) under tha" }, { "id": "getting-started/installation", - "url": "/docs/getting-started/installation", + "url": "/docs/getting-started/installation/", "title": "Installation", "breadcrumbs": [ "Getting Started" @@ -58,41 +67,50 @@ }, { "id": "getting-started/integrations", - "url": "/docs/getting-started/integrations", + "url": "/docs/getting-started/integrations/", "title": "Integrations", "breadcrumbs": [ "Getting Started" ], - "content": "Integrations Integrations in EfficientAI are not limited to external voice agent platforms. You can connect integrations across three layers of the stack: 1. Voice agent platforms (agent/runtime side) 2. AI providers (LLM/STT/TTS model side) 3. Telephony providers (PSTN/number/routing side) This lets you test and evaluate the complete call path from model behavior to phone network delivery. Integrations UI 1) Voice platform integrations (agent side) Voice platform integrations connect EfficientAI to externally hosted voice agents.
diff --git a/scripts/setup_flexprice_meters.py b/scripts/setup_flexprice_meters.py index 6d42a011..0531d2a5 100644 --- a/scripts/setup_flexprice_meters.py +++ b/scripts/setup_flexprice_meters.py @@ -463,6 +463,29 @@ def _delete_meter(client: httpx.Client, meter_id: str) -> None: resp.raise_for_status() +def _archive_meter(client: httpx.Client, meter_id: str) -> dict[str, Any]: + """Remove or archive a meter; archive is the fallback when delete is blocked.""" + for method, suffix in (("DELETE", ""), ("POST", "/archive")): + url = f"{_base_url()}/meters/{meter_id}{suffix}" + if method == "DELETE": + resp = client.delete(url, headers=_headers()) + else: + resp = client.post(url, headers=_headers()) + if resp.status_code in (200, 204, 404): + return {"meter_id": meter_id, "method": method, "status": resp.status_code} + resp.raise_for_status() + return {"meter_id": meter_id, "status": resp.status_code} + + +PLAYGROUND_DEPRECATED_EVENTS = frozenset( + { + "playground.call_evaluated", + "playground.web_call_started", + "playground.websocket_session_started", + } +) + + def _list_prices(client: httpx.Client) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] offset = 0 @@ -640,6 +663,108 @@ def restore_missing_plan_usage_prices(client: httpx.Client) -> dict[str, Any]: return result +def repair_playground_meters( + client: httpx.Client, + sdk: Flexprice, + *, + dry_run: bool = False, +) -> dict[str, Any]: + """Archive legacy playground meters; keep SUM(billable_minutes) evaluation_completed only.""" + result: dict[str, Any] = { + "kept_meter_id": None, + "archived_meters": [], + "removed_features": [], + "skipped": [], + "errors": [], + "dry_run": dry_run, + } + + meters_by_event = _list_meters_by_event(client, active_only=True) + all_prices = _list_prices(client) + canonical = _pick_canonical_meter( + meters_by_event.get(AGENT_PLAYGROUND_PRIMARY_EVENT, []), + all_prices, + agg_type="SUM", + agg_field="billable_minutes", + ) + if canonical is None: + result["errors"].append( + "No published playground.evaluation_completed meter with SUM(billable_minutes)" + ) + return result + + result["kept_meter_id"] = canonical["id"] + to_archive: list[dict[str, Any]] = [] + for event_name in PLAYGROUND_DEPRECATED_EVENTS: + to_archive.extend(meters_by_event.get(event_name, [])) + for meter in meters_by_event.get(AGENT_PLAYGROUND_PRIMARY_EVENT, []): + if meter["id"] != canonical["id"]: + to_archive.append(meter) + + seen_meter_ids: set[str] = set() + for meter in to_archive: + meter_id = meter["id"] + if meter_id in seen_meter_ids: + continue + seen_meter_ids.add(meter_id) + entry = { + "id": meter_id, + "event_name": meter.get("event_name"), + "name": meter.get("name"), + } + if dry_run: + result["archived_meters"].append({**entry, "dry_run": True}) + continue + try: + archived = _archive_meter(client, meter_id) + result["archived_meters"].append({**entry, **archived}) + except Exception as exc: + result["errors"].append(f"meter {meter_id}: {exc}") + + agent_playground_features = [ + feature + for feature in _list_all_features(client) + if feature.get("lookup_key") == "agent_playground" + ] + for feature in agent_playground_features: + if feature.get("meter_id") == canonical["id"]: + result["skipped"].append(feature["id"]) + continue + entry = { + "id": feature["id"], + "meter_id": feature.get("meter_id"), + "name": feature.get("name"), + } + if dry_run: + result["removed_features"].append({**entry, "dry_run": True}) + continue + try: + sdk.features.delete_feature(id=feature["id"]) + result["removed_features"].append(entry) + except Exception as exc: + result["errors"].append(f"feature {feature['id']}: {exc}") + + if not dry_run and len(agent_playground_features) == len(result["removed_features"]): + spec = next(item for item in LICENSE_FEATURES if item["lookup_key"] == "agent_playground") + try: + created = sdk.features.create_feature( + name=spec["name"], + type_="metered", + lookup_key=spec["lookup_key"], + description=spec["description"], + unit_singular=spec.get("unit_singular"), + unit_plural=spec.get("unit_plural"), + meter_id=canonical["id"], + ) + result["recreated_feature_id"] = created.id + except Exception as exc: + message = str(exc).lower() + if "already exist" not in message and "duplicate" not in message: + result["errors"].append(f"recreate agent_playground feature: {exc}") + + return result + + def repair_call_import_batch_meter(client: httpx.Client) -> dict[str, Any]: """Ensure one SUM(quantity) meter for call_import.batch_created and remove duplicates.""" result: dict[str, Any] = { @@ -1064,6 +1189,11 @@ def main() -> int: action="store_true", help="Remove duplicate batch_created meters and migrate pricing to SUM(quantity).", ) + parser.add_argument( + "--repair-playground", + action="store_true", + help="Archive legacy playground meters (call_evaluated, duplicates) and fix agent_playground feature.", + ) parser.add_argument( "--restore-plan-usage-prices", action="store_true", @@ -1099,6 +1229,20 @@ def main() -> int: summary: dict[str, Any] = {} with httpx.Client(timeout=60.0) as http_client: + if args.repair_playground: + with Flexprice( + server_url=settings.FLEXPRICE_API_HOST, + api_key_auth=settings.FLEXPRICE_API_KEY, + ) as sdk: + summary["playground_repair"] = repair_playground_meters( + http_client, + sdk, + dry_run=args.dry_run, + ) + if args.dry_run: + print(json.dumps(summary, indent=2)) + failed = summary.get("playground_repair", {}).get("errors") or [] + return 1 if failed else 0 if args.repair_call_imports: summary["call_import_batch_repair"] = repair_call_import_batch_meter(http_client) if args.restore_plan_usage_prices: @@ -1136,7 +1280,10 @@ def main() -> int: failed_meters = bootstrap_summary["meters"]["failed"] failed_features = bootstrap_summary["features"]["failed"] repair_failed = (summary.get("feature_repair") or {}).get("failed") or [] - return 1 if (failed_meters or failed_features or repair_failed) else 0 + playground_failed = (summary.get("playground_repair") or {}).get("errors") or [] + if args.repair_playground and not args.repair_features and not args.repair_call_imports: + return 1 if playground_failed else 0 + return 1 if (failed_meters or failed_features or repair_failed or playground_failed) else 0 if __name__ == "__main__": diff --git a/src/efficientai/services/sarvam/stt.py b/src/efficientai/services/sarvam/stt.py index 2b775812..eedb493c 100644 --- a/src/efficientai/services/sarvam/stt.py +++ b/src/efficientai/services/sarvam/stt.py @@ -145,6 +145,7 @@ def __init__( self._websocket_context = None self._socket_client = None self._receive_task = None + self._disconnected_logged = False def language_to_service_language(self, language: Language) -> str: """Convert efficientai Language enum to Sarvam's language code. @@ -244,7 +245,17 @@ async def run_stt(self, audio: bytes): Frame: None (transcription results come via WebSocket callbacks). """ if not self._socket_client: - logger.warning("WebSocket not connected, cannot process audio") + if not self._disconnected_logged: + self._disconnected_logged = True + logger.error( + "Sarvam STT WebSocket not connected; dropping audio until reconnect" + ) + await self.push_error( + ErrorFrame( + "Sarvam speech-to-text is unavailable. Check your Sarvam API key " + "and network connection, then try the call again." + ) + ) yield None return @@ -329,15 +340,18 @@ def _message_handler(message): # Start receive task using EfficientAI's task management self._receive_task = self.create_task(self._receive_task_handler()) + self._disconnected_logged = False logger.info("Connected to Sarvam successfully") except ApiError as e: logger.error(f"Sarvam API error: {e}") + self._disconnected_logged = True await self.push_error(ErrorFrame(f"Sarvam API error: {e}")) except Exception as e: logger.error(f"Failed to connect to Sarvam: {e}") self._socket_client = None self._websocket_context = None + self._disconnected_logged = True await self.push_error(ErrorFrame(f"Failed to connect to Sarvam: {e}")) async def _disconnect(self): @@ -356,6 +370,7 @@ async def _disconnect(self): logger.debug("Disconnected from Sarvam WebSocket") self._socket_client = None self._websocket_context = None + self._disconnected_logged = False async def _receive_task_handler(self): """Handle incoming messages from Sarvam WebSocket. diff --git a/tests/test_services/test_flexprice_service.py b/tests/test_services/test_flexprice_service.py index f1d3006d..912d2b61 100644 --- a/tests/test_services/test_flexprice_service.py +++ b/tests/test_services/test_flexprice_service.py @@ -444,6 +444,23 @@ def test_record_playground_call_evaluated_does_not_ingest(mock_flexprice): mock_flexprice.assert_not_called() +@patch("flexprice.Flexprice") +def test_record_event_skips_deprecated_playground_call_evaluated(mock_flexprice): + settings.FLEXPRICE_ENABLED = True + settings.FLEXPRICE_API_KEY = "test-key" + + assert ( + svc.record_event( + svc.PLAYGROUND_CALL_EVALUATED, + uuid4(), + "attempt-1", + properties={"workspace_id": uuid4(), "call_short_id": "123456"}, + ) + is False + ) + mock_flexprice.assert_not_called() + + @patch("flexprice.Flexprice") def test_record_playground_web_call_started_does_not_ingest(mock_flexprice): settings.FLEXPRICE_ENABLED = True @@ -499,6 +516,45 @@ def test_record_playground_evaluation_completed_includes_feature(mock_flexprice) assert payload["properties"]["billable_minutes"] == "1" +def test_event_properties_ui_surface_is_audit_only(): + props = svc._event_properties( + uuid4(), + svc.FEATURE_AGENT_PLAYGROUND, + quantity=1, + ui_surface=svc.UI_SURFACE_AGENTS_TALK, + ) + assert props["feature"] == "agent_playground" + assert props["ui_surface"] == "agents_talk" + assert "ui_surface" not in svc._billing_properties(uuid4(), svc.FEATURE_AGENT_PLAYGROUND, quantity=1) + + +def test_event_properties_omits_blank_ui_surface(): + props = svc._event_properties(uuid4(), svc.FEATURE_EVALUATORS, quantity=1, ui_surface=" ") + assert "ui_surface" not in props + + +@patch("flexprice.Flexprice") +def test_record_playground_evaluation_completed_includes_ui_surface(mock_flexprice): + settings.FLEXPRICE_ENABLED = True + settings.FLEXPRICE_API_KEY = "test-key" + + mock_client = MagicMock() + mock_flexprice.return_value.__enter__.return_value = mock_client + + svc.record_playground_evaluation_completed( + uuid4(), + "attempt-1", + evaluator_result_id=uuid4(), + workspace_id=uuid4(), + call_short_id="999", + duration_seconds=30, + ui_surface=svc.UI_SURFACE_AGENT_PLAYGROUND, + ) + + props = mock_client.events.ingest_event.call_args.kwargs["properties"] + assert props["ui_surface"] == "agent_playground" + + @patch("flexprice.Flexprice") def test_record_test_agent_conversation_started_does_not_ingest(mock_flexprice): settings.FLEXPRICE_ENABLED = True diff --git a/tests/test_services/test_playground_post_call_processing.py b/tests/test_services/test_playground_post_call_processing.py index c3b7376d..1a26ed43 100644 --- a/tests/test_services/test_playground_post_call_processing.py +++ b/tests/test_services/test_playground_post_call_processing.py @@ -14,10 +14,48 @@ ) from app.services.playground.post_call_processing import ( claim_playground_evaluator_result_slot, + merge_playground_call_data, record_playground_post_call_usage_once, ) +def test_merge_playground_call_data_preserves_ui_surface(): + merged = merge_playground_call_data( + {"ui_surface": "agents_talk", "external_usage_recorded": True}, + {"call_status": "ended", "duration_seconds": 30}, + ) + assert merged["ui_surface"] == "agents_talk" + assert merged["external_usage_recorded"] is True + assert merged["call_status"] == "ended" + + +def test_record_playground_post_call_usage_once_preserves_ui_surface( + db_session, org_id, default_workspace, playground_agent, monkeypatch +): + recording = _make_playground_recording( + db_session, + org_id=org_id, + workspace_id=default_workspace.id, + agent_id=playground_agent.id, + call_data={"ui_surface": "agent_playground", "call_status": "ended"}, + ) + + monkeypatch.setattr( + "app.services.usage.external_agent_usage.apply_playground_provider_usage_from_call_data", + lambda **_kwargs: None, + ) + + proceed, updated = record_playground_post_call_usage_once( + db_session, + recording.id, + provider_platform="retell", + call_metrics={"call_status": "ended", "duration_seconds": 42}, + ) + assert proceed is True + assert updated["ui_surface"] == "agent_playground" + assert updated["external_usage_recorded"] is True + + @pytest.fixture def playground_agent(db_session, org_id, default_workspace): agent = Agent( From 88fe59dfe05390e1b8d625e4aa2b5dce9244b04d Mon Sep 17 00:00:00 2001 From: M Sami Date: Wed, 2 Sep 2026 15:44:37 +0530 Subject: [PATCH 53/55] feat(api): add traces support to evaluator results and playground routes, enhancing telemetry capabilities --- .codebase-memory/.gitattributes | 3 + .codebase-memory/graph.db.zst | Bin 0 -> 10703053 bytes app/api/v1/api.py | 208 +- app/api/v1/routes/evaluator_results.py | 28 +- app/api/v1/routes/playground.py | 3807 ++--- app/api/v1/routes/synthetic_traces.py | 379 + app/api/v1/routes/vobiz_telephony.py | 1567 ++- app/core/security_headers_middleware.py | 121 +- app/migrations/078_synthetic_call_traces.py | 170 + app/models/database.py | 6131 ++++---- app/models/schemas.py | 10901 +++++++-------- app/models/synthetic_trace_schemas.py | 148 + .../evaluators/evaluator_phone_run_service.py | 352 +- app/services/synthetic_traces/__init__.py | 1 + app/services/synthetic_traces/otlp_ingest.py | 152 + app/services/synthetic_traces/otlp_mapper.py | 1034 ++ .../synthetic_traces/trace_service.py | 1116 ++ .../telephony/call_recording_lifecycle.py | 1210 +- app/services/telephony/vobiz_client.py | 663 +- .../voice_agent/sut_latency_observer.py | 90 + app/services/voice_agent/voice_bundle.py | 2334 ++-- app/utils/call_recordings.py | 86 +- .../tasks/finalize_telephony_recording.py | 2 + app/workers/tasks/initiate_vobiz_outbound.py | 164 +- docs/examples/pipecat.env.example | 31 + .../examples/pipecat_inbound_phone_tracing.py | 69 + .../pipecat_multi_agent_webrtc_tracing.py | 419 + .../pipecat_multi_provider_webrtc_tracing.py | 180 + .../pipecat_upstream_webrtc_tracing.py | 150 + .../pipecat_upstream_websocket_tracing.py | 152 + docs/synthetic-call-traces-pipecat.md | 272 + docs/synthetic-call-traces-tdd-confluence.md | 730 + frontend/src/App.tsx | 504 +- .../src/components/GenericVoiceWSClient.tsx | 2542 ++-- frontend/src/components/Layout.tsx | 2 + .../CustomWebSocketCallDetails.tsx | 835 +- .../SyntheticCallTracePanel.tsx | 750 + frontend/src/lib/api.ts | 11569 ++++++++-------- .../components/EvaluatorPhoneOutboundForm.tsx | 226 +- .../components/EvaluatorSmartRunModal.tsx | 369 +- .../results/EvaluatorResultDetail.tsx | 21 +- .../src/pages/test-insights/TestInsights.tsx | 613 + pyproject.toml | 358 +- scripts/pipecat_trace_dev_bot.py | 207 + scripts/test_local_pipecat_trace.py | 163 + scripts/test_ws_echo.py | 246 +- src/efficientai/integrations/__init__.py | 1 + .../efficientai_traces/__init__.py | 49 + .../efficientai_traces/correlation.py | 149 + .../efficientai_traces/handshake.py | 71 + .../efficientai_traces/pipecat_upstream.py | 206 + .../integrations/efficientai_traces/setup.py | 334 + src/efficientai/utils/tracing/setup.py | 13 + .../test_security_headers_middleware.py | 305 +- tests/test_efficientai_traces_correlation.py | 79 + .../test_telephony/test_vobiz.py | 471 +- tests/test_synthetic_trace_ingest_service.py | 364 + tests/test_synthetic_trace_otlp_ingest.py | 37 + tests/test_synthetic_trace_otlp_mapper.py | 443 + 59 files changed, 31358 insertions(+), 22239 deletions(-) create mode 100644 .codebase-memory/.gitattributes create mode 100644 .codebase-memory/graph.db.zst create mode 100644 app/api/v1/routes/synthetic_traces.py create mode 100644 app/migrations/078_synthetic_call_traces.py create mode 100644 app/models/synthetic_trace_schemas.py create mode 100644 app/services/synthetic_traces/__init__.py create mode 100644 app/services/synthetic_traces/otlp_ingest.py create mode 100644 app/services/synthetic_traces/otlp_mapper.py create mode 100644 app/services/synthetic_traces/trace_service.py create mode 100644 app/services/voice_agent/sut_latency_observer.py create mode 100644 docs/examples/pipecat.env.example create mode 100644 docs/examples/pipecat_inbound_phone_tracing.py create mode 100644 docs/examples/pipecat_multi_agent_webrtc_tracing.py create mode 100644 docs/examples/pipecat_multi_provider_webrtc_tracing.py create mode 100644 docs/examples/pipecat_upstream_webrtc_tracing.py create mode 100644 docs/examples/pipecat_upstream_websocket_tracing.py create mode 100644 docs/synthetic-call-traces-pipecat.md create mode 100644 docs/synthetic-call-traces-tdd-confluence.md create mode 100644 frontend/src/components/call-recordings/SyntheticCallTracePanel.tsx create mode 100644 frontend/src/pages/test-insights/TestInsights.tsx create mode 100644 scripts/pipecat_trace_dev_bot.py create mode 100644 scripts/test_local_pipecat_trace.py create mode 100644 src/efficientai/integrations/__init__.py create mode 100644 src/efficientai/integrations/efficientai_traces/__init__.py create mode 100644 src/efficientai/integrations/efficientai_traces/correlation.py create mode 100644 src/efficientai/integrations/efficientai_traces/handshake.py create mode 100644 src/efficientai/integrations/efficientai_traces/pipecat_upstream.py create mode 100644 src/efficientai/integrations/efficientai_traces/setup.py create mode 100644 tests/test_efficientai_traces_correlation.py create mode 100644 tests/test_synthetic_trace_ingest_service.py create mode 100644 tests/test_synthetic_trace_otlp_ingest.py create mode 100644 tests/test_synthetic_trace_otlp_mapper.py diff --git a/.codebase-memory/.gitattributes b/.codebase-memory/.gitattributes new file mode 100644 index 00000000..c12d9ab6 --- /dev/null +++ b/.codebase-memory/.gitattributes @@ -0,0 +1,3 @@ +# Auto-generated by codebase-memory-mcp +# Prevent merge conflicts on compressed artifact +graph.db.zst binary merge=ours diff --git a/.codebase-memory/graph.db.zst b/.codebase-memory/graph.db.zst new file mode 100644 index 0000000000000000000000000000000000000000..72eb823c282799343b932f621ccfad674d58ff29 GIT binary patch literal 10703053 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%0D!WGvf}Uk)D!MlvHCBLl_=jC725 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 u!BhD5cxtr zWd$;if3csxN~?J+6hx3qjT3U8)&&;gUXUE^RCVVE$?)5+yY3CY0TQifO!AHk&bYD6pc~2hj(;! zN$^V}zfk2vo3UsXdeRkAmR+f^iNABn*~bF|5__`Fx%_c_Hz}g+K;C@&D8Jd zRD)dBBV-y!HaxPKMpXvsepqeYJi9>wxJbey{yhpD8|8Jk} zxpRY;0IPn#gyZdp5vVoYyySi1Z?w#1k-6_iaVb0I0lpXT4J*xQP;5Fkf>`!5Q#V0t>gC)N1lpl^`HYVAb0{w0S!R}`P&+-_7X8#GvT|uW(~luMYly?rTxilY zaB{t?)*q1T3D8?Po>frZ*bJx!@o+C}=6uCiSkmDZ7cDK2qf_@j3D^azqp7F&!-qMl zYaP>7Xr-WbjTUqjY-YP{1xk*j6P)K5J*@E$rwtUeI*8C}0;)DmE1BojG4jMZk2j5m zAP%GoE%KH`Qo2^C+I6H!Er~ukE*F?3k@6ffrUXKkGM*S0EA%S0U%IY z86d!AhVn;{?Q_bD>Lk)$Kq=Ri1EJ5e@?eN7LLmRm?KTcgA3#*~X+Ln+X@X&O%>V4E zOLI$f%fw}@4X zZ`aG%LdFx|@Hd%#e{EJJSp;E8dYq%R$ev!49k(%NaL5oNhk}t?cJhN2RWjWM7vD~61KXzfUg+(NICQP(t`j<|lFN=0e44VmWuso%@&!QdNzty16FAsbS zQ_x(*Me1c-(D&Ad%m*gGGXh3zOJMQ6UO@iklJA^*gNsfE7JM_mNWsC79F9VQrrXU{ z(Xg;~M%>ZAO~+ibyU6c-KI7s*t|vr6!w7+&)YJrgrH)bgS>L`_CX$)pg&+Z%=qe<64VuG z0EI*-ktmPYHhB<+=B+PyMXU%AdUI|wC{P}H4b#Ig{^Py}WoJc`#~zuJDhI3prC^9c zLc#|9w}3>*Wz(ZnJfc&(B5&p)AD56sujmUeUVaGu+7{DE2=@_WIC&#&76mnY zm<4=fwJ0!#W_=LFfkH<}Vk=guj`P-9;lRMIngq>GtZ8(ggv44X6sl#6#*oRggAifj zy@l2(rOzQnTdHUj@TR4hwhqu!>;-gqRll%76#*EJ{7!%{<~To$bC(;c{|%#;J%)(^DSx>?hLBOx z0W_|1u50V&+(Y-F|KRpciudMsOo8H1!+xcpzdP=!jy>6&3hi^j955xNfc`LEBeHS~ zw@|lGoWk&`I_%iD3IsCk`y~}cRsclSKiO_*))^`MRJlA@8T|G%>|9fQ)KETfVp<4*nbla(b2iKvz{>P z5w)c1iYyf%zd15ID<6R3=~k5ZmgZfqzR4u<^j8p*LJD~zwH9zzol0k?WpT>bgmPT4 z1W;Cy?5IEUqxB&7NB-@>SKCbs@z~(xIKhc{CoAc9cfoxhSQ( z*OMMsuVE_RET%=hwm*WGD8W9JODwB}t6d+TTGFXWC%E00`($ z*Zi_}RQblics3C@uRu8vS5AZSQe;g8$mY;gw=K8Aiu1qR2Lbt8R#!&{{d>Kxp*mS- z!Ai-8vIjy@A2?j%&Om?@h2M0w+26+v!XeSaE~47xj$e_t7+#Sc{~IAucT~)zz1WsC z0*8EbwteVD)ZPUq3^g&V5V36`pj-l6{wod8(YERz#r`m=z0XTF-qdh-xf26tS$3V|U3gG;j z-I)}-sF^XO-~Ue=8tm-dOdj?vBxnjrJF3>NABs-;VDC80FBPnSqCQICKwO7pTfVkw z5QJEcl6(KreuJbgYe?5z19HH%_2m6 za}Ow&BmO{N<=Yz|BIpwD1m|mP&CCvgh&XN3O9T*!@LaxYeFGkPa;08(00VVBYUxEp4d#cQk|juEaXfs!&3Iz}>}+nErGsntQ2N zJJR0ia2gd5$PVt{+>AU8;KnpejRmxGVz&FDpbb74%8{R2hY1+q6V*kt6gMXp2Nezk zIl7a|V0y`vzC3g6QoREf`1kb>0NBI-KU7TtYCnlfNDvTZ9yiEt_wlUm27ZWz)l$?T z?tw=43Uyu2GefrL`UhLNyRr-~hy}}jPew7a@_~^bYX)P$8syQ^ojxI=x>+b=ZrMGN z0YwFe1bGBB7Zol=TwPr^y5zbdq+q6qOMxc^2QQ~r6R$*GMioI+BvK*#n!mz)eeh)j z!vS^%tO5~@*o6q8SXSIoWK-;5tc`O<;IZsT(|30WhutTF`3(3EX3o6RyXlW;b6=HsmH ztV)T1vW=2N0dxUU3Sd-#*3!n3&hi8EGsiGFG;1{ZG{ashF9);+&<3G(tvIrR0t=c2 z8WvJ5Y_zaz(OpDw0esxvT$N?Vt6+2O!Qpg$@mRW8#)Ko5S^oDqEKpTYHrl3WE}D#8B7Kc8F(^^ zYd+Qxt@WA69B3v=e@Gii51`-BR<)4&gSv&S#eSO6WQL;|%Uj9K=q~$4zTHpaP4IOZ zPvxcpuzfZ&Y)@@7+R(IhaKqi?+=6d1-pss}Kwt<0KdMZnqjsRNgT^Ep(yE+lvPPB~ ztPLYJly1={^9pF$daAbkJ1-ux5fB`%V zjA-CDFsA_$4zM|Z#sLlwxIM7u0SpkxfWQm{PC0&#HaVzt_|Tz9$JUPO4mLXkh=44D z9uc^SKuUsJ0yzmL@HpcU$KwtUsUCI@EnHKemWr{Q+K$j2S*oC{g18FWQ~-q}isT8BMM$=ixFwNGGC|2wNt}{6N`gzEOPnlG zw8YDT8Ww=DU}bV-0)$B)CL)^9Xp*K$1}89_7;zHF$%rTP6ERPKP?k`FKmi0rDoQj; zlPHCvNL@f*faQV+Qj$`1NP#0Ih(QYshG38f0~1r&rf``8X$q2x5Gr+4sH(74`BG(4 zm7|p+RtQ-UUa7B8xsrE91r{Dy0%4)bG9$~FEI%!;mLgi}v_NgKw}9E=<8sC&0xod4 z5YQz?mn&T&bwS~!h8Hwmf?uFt=DvIYqX3vXFlS+ugmDz+OpFvUO>$8=GIAI)m@^QQ zL54BXF>+&;G=OTLfd(AN)MUtz0hWQ5Aya0<+}a$LIVLk{X5g6FqSHZlM~8JL?o6LE zCD2gOs6jJ8nrIrjG*fC6PyBc+_Xo@$91t2nOM#eyf&{`7C@83fpeBM831SD? zgRl&OGzbNu8idFYS}N2m)T7XuLK6%n4cRaxXJ~?*nB4<*Z0wp2p&bfxh|VDkckZ2! zyFhm%5J?eHL1YM#2)q|~MDXhHOeC@sSxJs^2UIvPA*qa{79ITQV9f(O59CRcCfNwU2LUMZf!kZxwQf*} zbzLC@YQfeDGR^|TA}hg?+X7e)V6oErhPJf7pR=sy#Ph$d)ttgEPuH_^mhpizfGvBX3#-eH zr5}LaW|>74O(0qmkcB?&L0xcI9nP**E6K{kq54}n)(W}(ffzCj$Qj@((ZFRWyDz#;)EhyI&M(9pVW{VK`?h+{^SixmPlY%Bg`bx3 zS)QgXy>A3E3Ud<7w2{C@!WyBl%vApR-p)usL(E^FFQ?leO`E?y8ft`H`Rjv_3mP&S z5<3_kjcw8>Y3S!SoXPrUZ5%X2vNg^|=|+!+Y@*?(Ynk@ask1(g$X}nE-7s(XhRgKS ztKlpy*Dl`BAMb{&pK-a#E)@-xh=x;9T1I1%V9B=f*VoG(4W~EGWZ&)@mw#AFU3nw4^ox&V(b74wywD2?eGKH7XRU5Hq=9`fB3R@E z(8WWDCA&3V+zyWbWiiQ*&DXgx>M3ICK z5`ajsQ_(3aQ;?>Au914iIfd;u@v~(_5tn@2=N}i6V-|1IYRj}8a?`%>@WLbQ*>E9{i zZ*xq?ZaSMzrDt)gxN7>BA++uI$5KxoI0e9o&gi9dp4doOvA@%%K>6QR-)UI1ep`KG zL3+^uIb@20sS2iyOhw1&e(aYpT6MIbPg}S~Yih38^2_#T()M(Lp0wnl(PTlfuFI+*BgJN*`3Zr6y52BV0J5Hz zXaD20H$U7N+ftcT1J*!07ye9u0^8~tT0|*p2?YMO`eyx1wuY$b zP_=R2xDfOP9_UZC{M^t7=o#Py-*gOJL+8-vT(*$=qcP5YbqW2~3Atb$Ev&bIVb;rf z0m>eui@Ck`5c%tCgZo(TkiS0Mg#j-*SuZv_vcG*rXZGb17mwAPXxtoqNB2_EuYSwM zc>GM_LUp*Fz#g~SX>T%gBvHbNZ$;yV?Q5Oxb)V7gE~8gj^(0+MU(z$=ug~Vdh2%zZk8~ovNH@|k=)Xgc;}&rVc=Kzl zY{y9Yuxs_MzQxtE^eTNz_v8L?eOK#G+DALIf4TGMK6+da(tq>_I*{I@_i?ef7P?bM zE6y2dIgJsY*3qT0wRxI00+<033=$fgV5&-OgG^Pat&=GaEf=jZTCAMb9j#AB%ipn@ z2S>V=`%(kfxrV|W3QyY4p}@wWkYlNdmKG!zq-TGcZ^QGX=ZWn**#NshM8CWzZ}&t` z??F!o1hH;>AWHKbw-Uy32*KL&dqQY>^J`CvO0>eTazB=h>WZK#kBNOtW1GMP0Q0mw zJAET2T8`FpBqn@XN9%bG!U)PSR;f``_Kep|u-4{j8teZZ+pAfzOA1L$f}Gl>F?xDa z0QKqVy`@ClrG(m_+D|%mE_zY|MNjW5#Tbu!V0r%+G1)P)6gqy#I2$DE5tzgjy=(SI zfx^FEFVG?Bdy2DjE@;uRr$m}?|9ym}U>K;HOk}f#(nD`u-K^v}s<*B#mV{MowjF=y zt*e`={Pk5q^w!k{@#9~1^Dr}ieN4GjZ(W6Lh~Bz3B7m-C{QGEh$`oGZudjxP-nuH` zA}Wz$Fmm}eiO4YFh~By~RS=Tst*f35%&FeGIz>d|{~}}Lug@tj(OcIwFB83W&9kDX zH@x;^p>4;;{Pju5se0>bD8-v!qhnSml=IgY%-P<$Mj^kjx2|L-zFpr+VcT2Rv;m0A zw_CP}Y4ZG)zrG0hiQc+0ISSwW^)=;Hy>*S$B@|Bc*ViGp>a8oX>aOS7i2Eo~D1`IZ zcOk#(tt(puw~rgouzKrSNQy~@EE37!CGwa=J6kG+O5uw9^;yWXdh5z=`Da}=?5!&e z4=@a)=&h^V)D1uVTRi(G{C_+fasPy8IhG2)R&QPJEUf()hr`O6N2nBk3V##a>!ZrI zdh6Q7<~fG#8vjZ!jqfla?4|Jy75=+k%U|DyoU6C4U{T*kAa5s^$l}5cZYk&b*9C5?w-e`+iH4^p3ZR* zDfLF~BA1an&#^Pjx3!Mkwm*v1lyTYOHlZ!WTNrL(MpVOu=QKu4k~YoYo+y2uTcCh6 zu?SYFTL?K-sVy2kOFx?Gw%-&;$XKPeFg*8bQmj(vkT`CJ>5Z0MAVKaCH!d7^p5rMl z756Dmr)up5$e@13i|Yf3MyOQLrfgA~=A2Sv00sb{ z1G5kS02mB%8i@!dLs3@JJ`{ifBSx@rcwj(0G$b%IARG>aLcw4l3Zi!Djr{ZR|3HKM$k1B)7 zJ2n~N)^gGvh}-lOi}&sQlMa=LYveEZLji)`@8378B^)yWRr{LVMI8F-!B$v`7giaP z254zpPEzw0CK4EbkT#&tZFzXUL0%cGFxC}g=6`(_hF#$$bLn02N4x~AZKozQtT3*u=(c(ia8Z2Z9jMmr z@9A3wVwPaiJgBu*j&zEJbT$^nC_2`(E_?~3j6(3qY}By>$#V%6KzMg**(lZ&z#V9a zXCJBEepnU8X^``-KP-=e zQU>V3>dXcQgP0o=&jbsLU#1Hb8@H8(!|etO@&!hQn?40DWEEeRg2&(V`>ExeBQ$v= z;Ge*c)9XTuNR>h|7$`smxPQKRLY9hkWyk1UzwPJ>f-qb%6XDGp&9?}ap}k!oV{AZ; zD~IQz9uue|q8oAU$ORxX17P$kB}9)QPpZhRr(iL4O{?|7zAQvoc!+4=RW?v)4Is4% zY^}d#X5%Lq71~9LF)-*%-o&DpLRo0Q(5Qrk2q6ZbDexizKf=2(H&hjZO9$@yYcv6+ zDkMb1h*j7i5bMxMLJ_|i(*giLEEqN#Qj9_y=R`Me0F0fsove|Mq|@|4!)>18lnT~< ztl%*MPs)3O4k)50W|5qN%bpg<;BXSlsKQMe+r{6s7!pAPWfLoSZArJ#Egy#^`F8T- zTQtB3t^SYhSO!WTnI2xhFX<4HDJ`-{-3{prhGQBSJF`nB`tP&E2s18zNwKhWPc}&b zey76$AxIZMZJ8eh#vA?m2?rE>bt>Wj1x)~dBWA4& zXV-?>vXJ5%gu*~b#b$Ga!-q~;?W3Z2dvyr-3wdm049(`8Y!QVH>tX-&={ z<3RIchZ(v^bI?;U+1$779*j@pS8f{Jqjz9X3AcJd)b*u~&dXX_@5}hn+>rsR?~iSD z$sMA?4p^8q%BCxHP|l3yF(kgh0B>T@Rl=^1SNc#`xe;0@Bi}tpHuKJsz1(Ck>DI@u zH&E9$RMFu{4dk5vB1?^N&q~A2(miYJAzo}@)UF~d5P1s-(gHJorE>S^pT{n$d-+Pm#UwW#TWEE(nr zR_E+V4@gR@EG4pa!iDt<1pgJts+ANVse1~Hh)E*yBfgz@+;i$ox`0@=$r*BZw62X? zu!EcqJk!A&9=~KPO1oeAM_JUmW4`$&ak3hr?wFP9ENlHErdc;97;Cq>B+0?LTb8r0 z%$^qVx@ncFVxD*+OcvHY&#ei5TcHZi`tqe9(WS85C&(-Zv}8rOEGVqsl$BxEuucP(=hVeAf6lfs98>O_=cWt%vHQEE{QYLNkjU@YD3>PGx--UWlTTF-nc zp6Uv2Tz%>@-|3>6(LXitxj?K9O3wojZ@WLJ5(R=jM$%T{O+0cc580jJ(b68WS0v!c zOI|pA?tLb=*j=&NKJ!NOF&N7eSu$DGu%!~pXNDCV;dVF0GuZEC)O$mh*uFF*?vLJN zWTtfVrBK~$I`-*G_$^%s5=*VxQ?L;E|K*{Grbv!Y8*0WVO;(;e83KP<{kJ8d{O4B+ z&zQjuBOC$n$te;W}u_kVOofEPQ*qIMf{l8-e>S=T0_-Kz#7~@cNtC4mbT~e!+ z5{9Tw{YAi(BSDs5MMiZ|QkLjtsw2mR;Ouo2uMlcXT%l6HA)n1yw{pIsxT^+Uy_Vmz zO;KV)tu9~8xq^bdwG7*6LfB*)pj!B?%CIF^%Lxsj5X~~36q+t~5H{r`4P9#pPjQ*8 zE0J*1d=C`-Xrj|h;kub}G&d6~)$NXJ21iI%v|5jjTfK$?J8 zNhTMFW~~ct4YUtnAX>7mT3Ay*VLiY0v0OiZ3w{+W+!5c7ho(b3al2X)r+_=f2j!=# zNaAQYWnxXUpIcf>Q&DX#>^b{Z;E%ynmG2#;l|d9p2YO|CdQjTfg(pdX^1LC#QMD~I%i&M{4lR^9O?OgKUq@OBuWj+t_h zcl91S$)6%9j67GqfR991zvA zLLieVKZdO(Z~9}+_u3B>YDc1Kofe$bJ24bYsdse)2c7TX)|k%OG%q3rXkWL3*XcF% z_KNY9XFw#&H@1*|-t&dvj*(Lj0*|S`uZ9#bcNrwG`4ligA;2L%&7o-gjUTN!uZh&^ z5G*5qvd2%$zISHb5gC=DywEx*nYbr3gFF_WnUrp?81G~Hq2^7oD1k(eBT1qA{mgeX zyp4)1cNB>KGLbf3D2iOPnzxjJ!YZvpdJm5|mkSjb^5U96N%^Dn>SObdz>IBuZElqC zbD4OCdouFL&GSYx+NvZ0Q<|TY!AbqNLRxo}%i}Q|^y}-79IeRzuxm_aUolW~GuX(N zfGOVG_7lKbXO;rn#>Hz$?#of^n~&w_i+_x-%NOgLYoXHe89Bv8ciLq)cEI&S8Kq($>G$YKniS8mqg+W>M!SIJLMR2h<5PG zD5+NsQ$YALK4cQUmwTKVID}7{VR#H&BR7x==?%B`vP|Md^a0>o#%_?M(+_v7s~Kbi zB1}m$YOi!!|EBT#Tqiai| zte`mP-c8GXjjfUY3leX={P?E=Y}qusH@{*RZox5puOPf+*jFmiWgHA(dGFKEu!7?a z0fg_A-{yI6xr_y7%Q*cq@mZ^({@ zoY4GAexEx%Gml81@IH6K0Ia;4=$mfqLr{8Sn3_RztGBW-meCUk%s}dOJ)kM3)vDd^ zto{R5w1mGh^t9x$u_s^_PB~#>#kmN+7Lr*&Es6MWLu}yA0l{+xu8c5Y?zM;wH$;80 z&3=KK&rTtuF;XlKD8T75K{FZ-}kidjm4Vw)bJ8H%TR)0;l4&jUuTftcIC6s@#2Hh$Qp#rJlG|W_8x9 z1R^U9L;v0At{MsuNa}CI=hPY$P$P*j_r?*PxEzki435YXM{xp;peT%$qAyPwobeDwYD~jAJ`cnq-0n0h=lywL^uj--J%7 zIKighJxwJg-`yO{D~EZ4=HestS^H5CNc37Q2m8%;%qH%$7hjZk9fR7L0F1okVe$UhM_rl z#3xMR-{j>?w<2bF&JN6|$yEZJ@A6v4}M3EMnd*LpEj zUQi4y$b{w8Lw2Ig_&x57up4Nz1AeFd+jzly^*#?ex7Kv(&F!2qUS}FB7My5k#ZBL; zNFw>G)M&iY1-HV;Y`o}orI$Br>yd{>`3tfO`YHsB=8xteo5r=1R6ka94IB(y1I1Xv4701U&8*O(FubYOD_tIRaa zlO{8=j+ol=YOOZtJ-ffH?s1s&k*$53@<}w6GMg&7`+m@X;S_Tac$}iW7TI4+zzM)! z3#!qtFwggI6y!O8y5D?7l0U3U@1;14%iu$HbrZ8{XE@^9X!QO9Nl4+at@<@$gnFsO z8f$^I6rC^J!C|*D?@_B)QXP@1&=fa|d!^(;Vk@YSHiFU$n+IV&88i}Ov>1zy*@<$v zze1)#aLQFS8pBXTwL~pso1|Y|l2XSBwJkXsYX;-{KsfMbfESDmvor%q6~3%do`;zA zf;2Q)PL6Gf3qKY}CV{t6aYoy%OyFyGs&3f==Eyc*1x2?p#wLhTw%OC(v7AkRm|>HV zQV*u@U>a10Hq%Ii*+hFBkOsgSQRby-2HW7*K1^656Ki7rL;D9NHk6%XP74DdO>H05ZI^w-n9=Ewnma$>MQfuvuJ=p0 zI~{(4UuV>Irv~ z+VshSG58q;u-yK@;6OWe%f{JAMv}3Ecm+43N+%U+Lj1ApozE$THkgGU#s!oUR}O;M z+|$XGtZu<+t<-w5R8BO(Nx~3$Y~XJ4_(&M4<4qXWOTC|{4=^KYkrk2}b2Z7vv-yr~ zh>CLGdFos@{yfC(^jyHF%Bwq!aPM<-DM~fCDns7$mf>!1X+$qzyqmv{mmP0XP7mXD zNDJHiwUphfT=y!tfmqm+_>MVI6DZ+Mfgy<-Ppx=dUC9?MmpEgk`SnTb=Utw2vc+D# z!WbP4-p47y03eqN2n}0eHs`?J9AIASDGwK1RGO^hYvj`70%3563V6NX%zRMQ7PC=V zT`pK1$1L;yd2&hH*H@QM9**K|2XE$TFaiZl8+)mYX9DLimsPDn-~Sg#S? zU%96PckgPg!H>&6F@jHmE)6P}BP+QybN&B{9xo=xd5 zT;_Yx9o5)NGtTpE##G&U7z=su{`n2<|g;`$0v~U=nOsHG&^A4s;qbi3%qaP&)<@=rBip zgigopk7i8KW=A;4A8M63CaUqR23esp#bE(^gSJ^&e=IJc_zcZNu+;awTOKxpuRXCE<7K_Az*!*-Zk%p|QEWn~ zkJ$n=ZW&6s%j_6!IIu~`-&Rc4Jw;Wy{0nFi*V<{DzxEYzEIX{Kb;3ZJ5t@kzDBkNU zrU^cz`pHJz{10ImYY-elZA!nJ3>we|D{{DX-N~SiH^7#D?~YSWiYYJ$)!v5aP*5R` zu9me(ils!!6@tQHjoETEN@V;bl7{lbjN8=t_p2~<6}0Om!VDS#f(7bQMer<;J|QP# zfGNwk3gNuIe}L#3%O!Z5y-M=6J1$TBlkkUHpYFdrc>$dM=VU63Foznc`f!^7v4$41h&EAX3xGzRd6`B%f7M)l^cC(h6jBH~W z0I!8ss4_#n_}rz~u7f7$$I_7t+WUpjx9)B3e+td2neKB8qzw%oPtKmuYdFTI(9} ze6b{0<^7%J3W;Xx;1?#Cl>n;AHpRE&o7zS?C>aherOC#sQ&%u=zw61O;9KzF03c}o5R zga@s+A}rz^D|oEzZCQcVP{~D8v(U?2w}S*vk+=%QknBdo$AA4?0RY(N0RQy==Ku== z0~Z4UhRzBB0~Z2rZ)9b2ObY|R83ZGtz5e600RS-sFI*)7n%@O+JP;7Dr2~W@C<<_) zLs7_t3KddwjMO|y9%*Zyq6yi%GKaOLVHko=me-BjPZh1CJjzM57&@XUcoAEs6tV{s z2j&K`W@a86g*iJVuBQ?Ag}*vw%}lmAxVAvOIykiob#`k>TqUmiQuns7g}^#v_edcm zLW6|PhCU2g85$yzBPu}z0MUY?3q>-D-V{ARaEPD>0s|04FzR5G#3%tHvjo)yR}u&v z%^jIIB6FmqfKR~;1p_FcLQ+Kffy4*t>!o~|yxBJ`X->kDR3xR7rjVYIHZ1^LU}FIR z3$BzJm4YbsQ36}4TxznUU5Xjd46-mFYB1NpL<2Gnl$iLKVlcU3qH02H3T@KU)a5|Z zff5H<2Wd{TlNP6Z~oB~n!=Y^}))D)d4CTbp%xmHR;O2)r%`O zS5jDoSUa%nU}#t_Fs$MlSKkx7y*x_fm#N&8)|aYuw+MBi2O&Eu!gw? zWo=VnqX2?}7Yavgcx)@!)UY|V1-5~<&1uVW)8ID54UpT|Tjtx8Hy5rA&J!*S+#9zS z_acr>T$G%l91poWa$Iwfb4ca}&7IMGpu<8pR3}$=qOMHcl3kyj47)dW)4RkwA9s81 zyznORmf&IW;ygMpOCFKloSq3iGI~lC_$tJx;HJWq1w;#>7V=o=+LPQk)BeR6Xbf2}AONfbL;)B8AX5em z8ERyRlc6e*F;Fy+Phgm!rJxf*A6y%}4ssbZBE%zHL5PO1sSvQxM&X>oE5k;^AclPm z!44}An;ecF%7|9PErR_THt0Qg4 zyaUUQAs%!*PVgwhW2whq4;nq-^sowD3{)v_9k?)*oGdBmi;;LGJ!4v{Th!0sCavkDj$Pp1dA}ffXA<|R?u!xN!bBbIU zX*3eVh#w`OoX|Nr;iQa{r6+w)WITEEk8!+CRego(y+`}uwWU= zBA2B^767s=&~iZw3@vT6NNp+HvSW*#Em~Y2xfJ07h|5@)%q~&7ycY^yI=yu9a`Xl7 z%jcIgUmSqR0Okf5Ibf(^7{iE$;S=W1Anc%xCBhS%E02l`oI#wF2PE{X2F8Nn!yTyjf1s=<%11`-C_-7jk3~g zQ+6tQmF3EcWzVt{vU1tHY+qI|o0#2@oy>a3(peE%6j>M98rdCL(d?0|lWdhNm+Y7< znyj1boNS(~pX{LQYlpN7v`gA2?NkeEy|iT7SnH;((^}AC&=zVLwUAm%EvNRXC81rR zjiCkAhH8gsm1v`Av1q?&%V^hV;b`k<_h=E&YPAKn2)0RErmfVLy4~7_ZOyi8+qf;> zmTw!lMciuGN^Uc^^ESlx#J0tj##YA`$kxcV$(G7?%T~-bfNh(toGqU10ow$&J}NYN zG^#Y}G)gs^HQF^gHkvltHcB^IVf1g5aCC8$!)WE`=BRaa#Hfk`+ui>Ra^mg=0%BnG z*cJ<`$+nnST_&NHCyb4mk&&J4vI+a8)-l>HD~rv{(sq2dUuqqz?HFw>Elu8Q@qVdw zOkuC7)ogoBuHDw^w(Ultj^DN$8ynnqYipBpbF1aJE%){|x8>mA>b5j)zi?bF)9pGr zIlEmqH+Q$|=;-pcv~Is}T`l9;+3DTgRlviuke9c&^Yr#ht>X(juR_hY^DNYUJ8!S< z+kO}7zwO6YW7vLuHQ@I1%dq|a{sgxa!+zmf4%~u(_23rFP{)K@Ffg!i3knJz4h|ab zLqbBteOOqiXlPi=iCgjTaB(XlA~tSm;(p=8T4q#KbX;82up1foOReJ#yRor;+>MSk zc|cGSW9xKTe8D`sdZerB_-?Fa!XE5UT#TB%FHcUS&6wb zEiFCl%*)G7Ow3x=+?$!1HRs;+;GwQ+YStfib#t?xca@W~rg!y&*SlK%>z2;@g|}6< z(r#y6z#4#`+7a#*(oGqdx97mu{}c&l@L*6l}YRpo(N3r5+jUIY$=KT!U?SM zi4sRCOej!s0%-+`_cKKj3l-89D;8K|7aPT=(u<9P#S)B-q6LE(8-bbX0|I9XqUk@SytGKYCmN**`+Dg80Z1-9di9LUs@!T0}%44B142Y!%r=gm4`h z62U4P*++Nj8(@XqWqWb)9I(X+|{Y&2#nq}gcBSXdLlo|?S|%>@DWnlyZD z_8K*PZT6Zqes2W$bF<&D!7#vn(*_XEe&Z%Xoc-oapqv2&3D|Pr49?kd;t&?F=!L}*fcI+(bxx;9{)`O>U&(@R2v5x^7`0PG< zHXN|~?0Njdhl8K}rw=5c{pWzlKz=$Pumk;(6tn{YnjC}xWdeIppi1l>B*;>`6S%a> z*+H1wt=-s6&w3c$uHBl}cYV7zt?uaNv~JgRFW|cW2lh7L9&lYD_dtOHLBUj+)JNJtCb zS1BoJNh)QqU_k>VB_=m%$$hEUxXDdoaCP7&HMqu3ZW0p{l$+!>HC^Q=7s`PHDHrOU z+!wyeQLfa}lq&@VMdeCGxi5T`AGlOf*QrZ2b**x#9$c+lDyr+1D>c2!RQIZ?3d_B+ zvdVI=s8ra!y1LSGuduKnxK~+OTkaJMM{uED%Y9+3tIP!fLf6)o7Z=x8ga}|=U145c zVqag^Sb`e?42&!+?5Z~Al8K3xxm$w!!uu_3AqiKt31eesWMpR(CR}4Qx2&uz%`G!C zi`}xbGc~si4QPba8QYa&mTab9Z!fcy)Doc6NGqcNOsPuCWF8Eidnap59g7b1zh=(C5Bc za9`NMRp#5<^Sf~2kB{#fV{rTW`T*TNKfera-``)L+lK*zpxXxsNa((0a9=pXRUQls zEEE(p92~sHhb|!@A)-rISg2@dc$E`f#lyozR}m4h(S1vFUpVn9Gb$=xV@J1faZ!WY z$jJEUzBM*po5k)0D5ttZZbb>?z6eW&Q zm{8Cet8hu2K>9@ScFHT<&lE{5R7g8z7H*3b3tYG@K9yj&o-EuKUh0%zv|td!MT5>* zh702b(+n2|jHVhf9CXSw+!`{RY`8UMJb}8NG~5@4>XdKPi0X`SxS%$qa=4&3rgXTV zpbvJqpg5>{xSl!O7oK*?Jlq~SszTi!JFI-TJ$O)mxIKDYiMpPl?h9je%0PT%1^EGY z#zI_CAX-CQQBep)T+tv~MqE)L?hA8u%0-4mI6|bWGe+VXDdN=`IdD+v$T8iaql(84 zs~$Y4p*^mAgkt^hkp<)jEDA&`h$sXhLAHhn;UY35LJ=ZeMv8bHG4h4v2v`y%VbxfP z&qyM+#Ahgx&}7M2RgH-^VM6A_n>3jK#YtIJ?a33fC{WI%L_wP(C5_6IxC#|DD^=tx zR@Sgwp^ya&o0cqX)p(0T(c;!+%Ue~Ai+ADTaMdq<)WzefYF@m^f%!5Q1`M5;Fm+?Z z*a0x+jtl^JWys{2DWiABOoaf>SU?lNUK%tP)TH5Cqoz-QHGXUa_-n%kgPS&hZ`@=U z;7uT$0R-c~86+SMp&SAV=g2WEV9p_(0ubxaNwiZ(@s6EEJa-uL;Au1^%wjXOSr1#S=BRHs zebhOt@4o-ty$iUmfdT~%7%(7o;J}cO5Yf<35fKrBq9P*%2oe|>9VJMh;OGcRfr1wa zDd~NsNl6Q&Ql$k8mXwqjEd5Juz+jG<{);zg;KalK(j;S?%eeoSdc} z>gj2grG5$u>cLM%MNNN7>R))4seU>&SN-(C99#XAii-Nv>GbNSRaIF(W%Vy?HOp9^ zMWw=@b#|c16&Hik{jP_?`bNpv!vz&Hz zriO+lr=_iZ9REw5V+tRu&1@gbbKA#jbL{pp1qyH-P_I0cW+0B_jYx4dJkpyFFeb6uXi_Bcs;zmygUoPr}r;)jxYRP zg_-a7EX;m;v)p&#!XFsXc)!0n@cl3h0s;#DV1|FGb4>Vyfpg&x z3eJW(Y4wDZB`7b=nl0O+a$CN+Wp+n_QN=jP(R&!5)2kgFCIn< z1~DFniv}_t#tSAIKMCVsxQUi$JdGF*YCH`YPBxy#j3>~ar138dMawrHv_=$;2el!U z<3Vps>3C4k2Rj}V2NjP8%`pvqC>{U8RkYk=hm{W=)E_;rfP`X+`p63M0~P{AYltWW zIwta=LAHu~s1U9rLn6?!kv~GD3&|fT;-%z6jrT$sP&MNZ6L`7$@=uYjQ=^Pyq>h3jY; z05NuD#@x}dnI{7P-kB#uCJ)VrmeC8~(U_@_=Fyz7u;!rx*rR1NzXr_(0e(#yzBa!` z&A)ITEwlMHYy8}N8v*{_d>b|x2KY8@0OR}`H~+#JXc^9%KmrC3q+>ZB2hQM}j}wQm zfR95!VV#GP^Dk^j%XGfZoI^WbrvSt|Ux!X&o`=&-(q%B3N7i{TUD{W~~y|ks$n!%PXs2gmlQq8ob zOLfzhGN>JFX#=M%WnzNb(x&~wwfwZ99Mn-8%4zE9Nor3)?U!1|5B5}4O|_?_>Z++) zp6;p#53F_-71h<2PWKB>Yni&Qs;aQ|m6cW2zM@jKw)ASh@Kr6tSXdCUvbJEDOG_)l z)`GBKY8`iNt*tLEF0cI+VZYQm2HRg?U!1|5_Vvna__EIV(#6&P|&@* zUW>YSkE&t!?ua|?-d!pP-n$zj$$NJ#D0=Und8NX;SNLM#-4U4Cdv`SfF1-6@9KLst zy3_aWWPSYJ9a7NWyC;39VtDtd&MM%$$0e#6-o5d&8u;!$qbm6BO*yKA?@sokO8D-A zMXH7G4#=cx`0i3vs)z61wWW&q?onc@iSN#NrmFbvi)^Zk?_MOQ%J}Ybc&d%>e&na> z`0i1Isv6$Ct~zgccR0lS`0fZ$%pBg`%Eb)%?tC@o$ag>DF?V=(2tj7acbAdOlkdLa z$xQj~Wme|OcSq1=wtRQLG4tiSOQM-E-~CB|IrH87+{_-{eJ{?e`R)kj%$x6CNoVGK z_q;pvhj$M%V6MJ<;huSZ_b@**{qB(n%=NoV2hG-ZFU-!@cZVCDJKvqbOoMoL2v$18 zyFZDbMZ7yz3EK1Bmr~Fp-koj*P2%18TF@ol-3tb7;@#0?&?nw~tp<(a-RnT<6z|Ts zgI4kGXFuo_?|vW%{rT<+h|nzFy}=Q>#k-p=p4c-W*GX$yKj!6LEjzC42|R68Kj|ey!!(-w2pW0 zb3^ZV_aZnnk9T+F&^_K=E`Sbwcg{Mrk9RkKhyL;IQav<~cVFv6i@v)XAUepq;|ZdL zyt@{b9`f#ETbjtbXZTPTdG}4aHuCOd*ZRo2Z-S_iygNLiPV(+vCu$|{&ZweZ^6rbD zHIsLbb5S>W_qQ0elXr)sQ9pTiN*gtlcR!G$j`HqTJZdTL?)0Oc^6qeun##Lt5vi-Z zyV8-`%DZbOsjs{{8j~8!yCXWOv%LE&rPlK9U{&fZ?|z1*=JM_hw$xqT-9nez%e(J^ zslU8C*O(g2yPK7%!@N7>nOe-dBdMv!yt@~h8i04F8lWcg?tF9VGVg9lr#AEMNqFir z?_Ov?9l*Os?y1qdyM_bmH1E#&r&jasZ-MGH?=Asb3-Im>Mr$_jt~p!Vy!(>X>gL_& zI<0Ts{Y->)fOk(o!V2fz5lvX*y!%!PD*^9b_i2^$?pH6YbKc!fhP8lq$6&)s=iLwB zu-18ZxgAzJ@17WHz4PvQqE?{2lky64@u6Dyy0cZ6c? z^X_0%tOvaNofWH}cZbqq{qychU95oKz3qz?fp?b@V-57~cK}!gy*rs1YXa|HhsHYS z-R0I;vF{$~#+rS1J2+PDyYCTT-M)K;0<5F&erCt&+g;{k{q3G7$hx;X;fJfc-NQg! z3v72Y5?2G;9SJGv=zXO}Nl8jl=>nw%3zn3Wlo%*2Ej3VLz<^1qL4zhGCME|;O-)Wp z4jeowIXOWoJv~J!K|x6=MMX_1Nl86Wnwpv(J$T^WWImy1L53 z!h*A2YSx+76ow&8wY0XT5Fl_(x3(foxwyKf5h8p|yI!~}&Fjm{3+(IbE9?slOzbNx zOv0C#*w~i@2@|GcFFck;Mn+aPVS-s%*_oLc+Syqe8d{u|rZypdsqvXSK}~IKf-JL5 z*IsxnjRG453T)b8~}xdy9L6d#P||a&K{Qb8m8TbZ>KWc2l|+rc0}{ ztE;=Sv$MmqySvky;=M3lT3%jW1wB0r6)NoMS$L@y&MxTf?e$GByzqrIzCJ#_ziHnK z_oey$`T1e^`}+g@!+;@h3i!f+X&EwP5DW|)6ci*J94sUxG%PGUG&Dp!JWNDHR7^}< zR8(YKTx?`ybZl(Y;OMwPgNDZq8#n|$zSPGv$UqJpA_E~IB_ks{NOpLs9nK~tB_%7T zlP^4(Mp;Qoc{#0o;mR~iOH0hl%gjs6mx_7j;W9JxZ0g~KJ)~)BYI1JA)ZaY&!1+R) zXukQuU#eHr=;~M1W~b*}J59USx`5TMRs)@Wy&eGJ*G?f3Acm@HAzVZVB2O0~qG*JR z@I+9;rz4E2Y9vS`Fj3;@gb4*GP9UW~@v2CXv_gfn8h?6;6$@H?Dpl23cncOwFIq6D zs=08{0LBZZY8=B~I1I1O@S_-BRg)pZiN=hl8Z@A6)QGxaLkjf96b{e_0gCRZM1U*k zy*q-IUZWmoWkbLq02BZK4Gt#Yi2w)~5)G(BCh%|M`vWjCe&c`!IOqtV0d%8A>ak>v zXw%Wq_zeY{a87DhAF9?7EgcPw-%wx!=cEApY@^ibk^OG8oQw=~eT~GL?u7s>dx7E+ z;wuU?K;5)yebg-@N?pWArdx}9RKAo<5w;oX$y{L8z%vv_M6W=wNx%(=Gy*&u>FDu@ z_@xo~v)U)e3}ClO0bs$MGL%q1oftR;tipCKqyyr|hh-yLS{xj`Afv#vhxw;wciW-| z*gk1+Qq(bY#Oqchqv;5{R|1>}IgJ_d`jQke6spGu%11g{Jc5$oArypo@=cGl9McfyE2IP*Atu~(@gyoqMK;n?Tu!(LhyVd^T- zKALjUFZ8ocIO7NBHm5K4T2Y2|z>Gg!`%L|iON)lUVC+3T!)Pto4J(UT-wXvN^9%eD z1gqg~2_@&NDlwVIV`eI_P?UFw-CQbtO1& z&^Z?vh)JW`gK7eDKoAtk$jc2r(L7Bovc@`gauKMTK3!;sgmBdc_MWv0%U}@1OBsS~ z@&#M_w;?%-#Knt&id=x`1Qj@_x&m-CGzHJsfYF?)CZ(moN2S#;Lq>gJ z|M1W}v;sLc%2kZ+`i;tt`#@I6W&^2;LvSvFIR#}jxw#@ddQn&+BTur3cnh_!@8q`u z*QJAYM)sm*zI4Yy0jR|Z= z1g=KuL4_e=i7%`$F7*Wn#6=DY5^e455}r9ssd9mrGzKD&Eh3&v6P%djW0BF7IW&sc zDx$C81N#~WomFkf96K0O*El1gI6?X5$y7lAo9sd#?i<-ekf#78u~ACrrUOGv+PVu6riqb}0Fp{9)suv= zS1PBuol_PFghsMw#(Vr=4xIp=9h$=a;nzqwc1~7=_S2;YsqPbu5&LwV3Fn5DLT;|R zfs&vRLqu4%azHCuz<{E?B3@0tip4mSH`o<-Ae?y#PIi`H^htHg0)0e9^-?$xG)b-- zuc>SA4Wsjjw=uw+1s94E;OsaXtVgIgu(^_Eg4|Ef+!qX zWWj;5;8Aid$Q0rp&`z2VZ6h*<*tx^GMlm`LVxXE8RVH6K8PIK4;H?W(0e}|yiZ$bsz5r1+Ihi#HH|R@8uQa@3pyI}p zEx;7~9hZcFUroosD=en~F$6omsRlAlgp$<{+Zsb81)j>pnVl^zn*c3#FBr)(ML1(* z?g~FD9MLVE0|{h_h)3HD7bN>IFPBkW3mVdHeuP6`5bX`REL4R#;GBVcNtbV#lQayJ z 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μTJy=aRF^Icfh^Nnx#Pwdns;#bt;>}Cox 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%0D0zM6l#^uO3Yo) z%RuLH;UZ%A8=A0$r7huhmi{_H7raa}7B1=mrV~r4GI7N0A1W|0RN%?H2%Z>VEy*}rTs(v=lp~G8T+^UQ~LY*llmF$Q^ z811_4UF`tuqU=EH-|K|y80xO-K^ptY1>fG?+y&bO+S%CG1=a=C1=I!81=0o41<(c0 z1<$OXp*#s$R%#0A3z!Ue$%0003810?|eeErP)#{2a9HTq-utoYOS z_V;S`i}l|0@$`iAm-4gm;qdqHQtxo?)a~Hy5bPi8CF&3980aMDFy>d~b>wp6tKzre zz~L_7AKvNR^4!|n*V^FO?AWu{gVw^;f7E=_rqZ6#=Fq^;vd*5(yUd}>waXaF2gv}( z|Hl@_EyWYW48tSB4Z!rkOTR3>JiU;-ox5kccey*bJhvpac(qitd9#ADzOkRMjIW`t zm#t8&s;j7~{HhPB9H=3ud!~k^m!yZIm7;c`QJ^fJ0iD&HznjOJ*qQQ}(wEVe)|LO1 z%9GoY%#s(85s&nbQI0;1dW=ttLW(Sj2#8aMK86*9^@HexH-cD!Ux1>2lYW$af_%k$ z-g?Y=w0Qt`<#x(-sdTDyn{s_|mTY|G-wKD^kwB_@nisE z$6?c8$6x4P=v|^*s9TC!oLP-nnOC<~p;eVsgHw`HZc%hlM^7$JHBGck&`bSGbxK7^ z9!LyFCq@iKLPUE*t3u2{t^k8Te7``YKYl*#J_J2~JeE6vIyN~9I2AW3HY+tnG*&Zk zGFmZ!FsUztE`KeDESoDoDpo08C=@2wC6^?(Bf=uJA7IPJx6uA?-5~~rO5O5D>4p|ML48RM{E(R-s2P+0`WH&Q4HaIpieStg$AA27N z2!RU)Iemdo1Pp-@1axmPeS!l64FiKM0t^EY2nd8ia$#;`XkTAqUtcbJeS0Elb6;m^ zbRs%*a&=`aB58AUa%E*{b7N>_WMg4&Z*FO0Z*(F$A|fmzXK8dIEFvO0A|fmzXk{Wg zB4#)_F*ar~Wn^YEH)S(2WjSU!IWS^2FfwB>IAviXEFvO(fdK&u2~`_aRS^gX5m0-5 zdwqc=tPvJDnxv?bVec^n2?C)MLdp=KaRi4OkL+}}130H~xpzDAHL746veAEnGLHJ1 z`@hjNCp6yljp1!DGK$bN;tLf%U?;^;TAK-_Gmn$@2bFivV{VMSj%EUIYJ}TARHJY~ z#>2^Gys-sn8K>!sJCqkwzHfwmBmfuUjbIWn<_N8EIFg>Qr#X(LZMS5Epy`NllE*Zn zDd)W$Wwta|;rOI$bCe6qv>UAeHI+C9+Cyoq*C?B2w^xAN0^cl#102E;Wf(MJsBzkr zgmIuELz*3LigO~>VAE(_$9{o+EsOy@vD8Lw=r5bzqp2;p>X(1b*KxQ2Zoxu?9y72e zcz*|8XML;m?zJtqI%XjKJ^;{kH-4%5g7-Y_m%G9ETe|(_JPg^BPymeh7pjLTU=9cH zlTcuAFIgiMNbnsPRR4IbCm8rA;Uk?WF9w6+XCBB72J*oza5)hM;p;uEI$_FgZ6+fN zqOkX?g^dp){xA#*xd==LnBc!_7;JSmP=or`)nQ%&0=z~3FbL+9D;^O88%Q5gNeusU zC9-j1y1t^4h$*J;3oZF826FEzp*Cm2_+$u-y+q+no?%>tuf5l zrU9Cv3y$fHU*Eh;pwK&qeR3nd{#cWvqQW=|{F&B<>~48Y=2wdcnRo&(Y(Wv3m^0?& zYKUI=tH+w;`b|LuEy_<0mRyDxI2kl`eZ)Z-P-!wJiT-ja6X2s8p3YTO zCL2kApK{_W6ITUsGg{`69ClWEVN)*SO28%>)7N0_VA-R-@53+?QXhr*F+&>ef2nY= znVAtF(ueJxHJUlFurY1^7z6_d&9)f?hGeHEx&+Q-sw8$yWpo} zmb~D;bal(7Lo`}ArcVZt#T5;LFrf{rYBb7_SG^BEnqs?JFp_5boD4!KO{=z8I__*2 zOI}j&Y~XZ?-O-ag7D_<;^Sz?TatQ>x%;Db+1)J(v4lzpvk8thM(2|L_0I zoqO+3|NA{=)P0$|cRwSCNo5VE1T_Sy1l-5Re0eu~?}o6Xr?=Dl=qqrTVotd+*w9b6tC^W~}e9v8aikJzF;xL$1D#s~LC z>Vf(XQ=j}o^nv0E*6 zs~tiM$bDDuM1(tp^)`ygdMP7vgmHu5yc8YX5$?}Z@jWBq#-0V=L|HkjO~Qo=%HB(i zYqa%BD3#(o)^G_|&O`a0r&~29+~1rxYJ$!)`JSC{g?0WY0PgIE{1xBxZWone`yD8j z#2sEM3YUjZ@;%E`@%WI;2XfyV_sjG>zs%qJiSU&R{?T_v8&^%iwUgopY2&KHn1<#v zRYa#${2P4FJF1%9_tgMvzL(8w^S8X0_jUT7Z{8mt^X1)qquv|U`#e^>EB;T%d5t(f zX7M6fe7a%Io1^m{KO5ikZa1ZZ2l<#UZ|C>4XuEW=V367P;RZdVx2Av~2I$DhJ40{65V}lO| zE^43i1@WhapALWw2nqt@|A+`38B!8afoU;_K`Ox{McydJ(Ty;Sum}V1ba;MQghS0oPhr5?woZvG7%wl)-R>L5T?|X;4}j+hC~znG$9NWk{M* z%&It1xM9Lm%GnEOO>kVH(lj~BRD0LBthvCJf%Jmu5-O3lAXV?S^j2hruSu>o~j5I0B}5SfFtNILJ|^ql(6$;hbKr(m~jFV zC)|L-3=|Qd#Gpcv6|7Ncq*Re201I?e(pork!R&=5l{6TfRn)AwxT1sw3M@mi^oT(y z!#e|(8I;g4rv+!rmMuENPWt47%q%X!}q+VjkUYo4%&{7`P%&YR7hxOsP~A7x)o4v*=4@x7)hTP{U{g}?7OJQ zXv(O|^x+X)@~FQF-?W5%%J+ zu`ip}sG7R-xFjAs7T+^lG|tJX?;wYeed{UdsW&+kRQH-Oqci!Q*`je#Tsu)6hLlu) z<5N?;hXxHQs-DyLT%$&3G8&ClRnzIzhK;_BgEGZInc|>UP*y#~m3!4^EUc{U%<6Mm zT77Mcj=9yZe9uui$`wuG>P}r<{fh5-dX?F{yn4@9&{w~jUS+mqvM0e@u-?=692VA# zVy;*ZcBQgIuAsBe;CsH&d%P;WR;Aaf{QT)izU5es<@oG()~C>tLbP7PT9vmraC9rE5Zb@e z(mEX9vpgr2qtV`@R;9K^wbtd>*rd`folZO4oRmuK$+ot> zt5%M+|M>OV;jmNKUYB*VeW&Lz=IC#5P;gz=0=2`q!^y-B<$K<4I}o;ttzxTju2EcH z$_eFq+1y-DMx#+Wx-Q1|TvylMpmrs=;sQwKgyky_5s0)sk1^ z9Ta_%36qEO!02cCo|`0nnmo{nwCd=cA2@j}-!lkEFAU*H1R<24j!>N8wIoDE`L_fq zKlfy6XaZc4hOg3TPmoqo`6k~p3d)}vUYJ4^tIDH6acy{>m-(J|J139>eax44<&ROY zyj03fmUlY~T2pvDw|pIM!4-0OtP5Q)xL97^N5w^c`5748q@;(d0tE9?D^y{g#qi3T zmY6@o;wt@??>UP*=9yw}mwru#A2Qz@&fqfrwCLmbp0&75PaJM>&3rVLR;Roh-*b%B zZg)GR8$CUB)CW%TshKWfea}bmQGLXPLL~Tz zRL3R3N%c*a0T*S|IbfnveH~J*hV_$3thxA}mzL^Z5iS;MHYTPz%xJ8+cvI66{vB&B zCpgHd&O+8B2`U8j^$X6YrsaO8ue3Eln)nH)l4>!J2q+|-}7!4%v#lR zPE1HBC*##I=jN(j@=X-SYw;R<-0h%v_^+11H&GUBR3o+EqgIW@_q+vf)jeGBk;heM zySlvKqmozM{Q645$K8%=RA9B1?_&`P33Y683WIz6Cal(q!N*|j#dj0j<2TBB z2ZwunYODrY!bjnfgL^%* zdf4~udL&lPe(h#oo;-2i(4n_>hJ<4CZrsD;rKqwLX_qa(r? zNLi4KBxPktDbWmL7#<-Kuv9EUQRKo@#R#P-%aMu`3P|fy929hhRMb#Pi&P6!LTD^e z@zl6dF+XR)W~eYFnnb9JwcQLFT4(HFY)AaWK#6aIw8? zEIX#~C?S9){DJ=F&;Ah&*IVixJ;VeM`k>-N$dBy+LqNg+xe`PN0bd0e832O;4IF?3 zfg}l>DL}OVkdP=s5@5h>h)n}o8gSwO^?@h|bRu99L7fED36vB(RRClG7YkM(@<5R& zBk&O=judgh4j33g;v>m3gC!+VD6wG)2^v7KK^q5F9pEtOfyoaK%9GDAi5qQb!(6vJk4`FzC(!&rRX#8OFWr!~>06+&2VnGPOPyqvh5OPC&5pe_& zCKU0*2xyowVW1o_0TNqD^oS87CW;b8mJnk?s0p4ENH}3706xZGpJ+m{A_|!l-lHoc z(+f;5)5r2X8=XMoH#RkWDRFo_e&W#S8FcziNaQCDpPoK(7|TVW;xMRBoiQUSN=j<# zf6<`vpHx)8MvY%p^_|w})P{|3V`_~~tm?I zb#-}p{omKuXC{+iV1LMWY?ZQQud!9imc53(Vc#hx_Mvht$4cec*e6;pf0K;tZ!wv_ ztn6Us9=UZU3lR{ZBWy|MvE+*8jX-vDojH{m&d!t6#?V zJh!z@;hexf)WRjTUmC|>mj7?N{aJBwKT8e`O74fv&3z^v-T$hq`(}L4^D{XArcd3! z{gZspOIPW2=_GD6vl4R`Fszk<;*T_@SU`P))Akt6{mfo^j>RVPz z?Vv~ly=ApjCQS6w17njWI_Tu3ov)GSL__(WL3ok$FqU5)zj>lJH#$8%A(2KAULL=J zqHp>LGw3b8XA+`lq7tO&=94KCXq-SrKT)WWLKS@k#RmOUp=q?Dv-qB?IjT8UxT1g6 z9Mv3Au%rZKv7kj0O@T&j(aILM=%x!@^wtYr^p)?K%A2#>QH%WOW4^pE80eTu+N}y0 zeYNrqh0!^#0LAE-IK=W(($}iCs?9PMjjL*-aaC;$OBzT9GWseSSJg)2s@fTt^pb|R zOiPpgZK0VpIBmE=AKmc8;f(&`f<`~PWW8|EQNHIKULlm8As3D1qOn}mi!Y5f)8QS8 z(rh~b)o3aYYxEr7^KSQce03M!GY(E2WWl0# z=D5bo&g@dMG&>g|6 zJ1I26XupI8Py+&QOp-IXps{}+R`cbj!n>(LSO9qhKhFFh4!R*^rAqvpq*&7-;*gV` zu`0a|ny7sS#$bDJsRGBrDaKhSi!2W@nU1x%t6GA+Gsk!OVA5KW*z1;tvYkoH37~4M z24p8qNNrM|{2kJWy1}|sHQPjvR`iMn@Leu z>dgGtX6nDN!K=9FBW!=`p~Y}dt}$x~x!Xf>@mm_1PC!)*>fLa+?XgY4Vj2Q2$H81l z7$Z7x+``%Qoi;uy$~|1LVR_3i%io+VUET8dxH361b26#0O! zgNY(>^X`OE)AQaN;=dZh_7$fcQD6aAnV?0U=Fnl0oSe5i9=H+PRZ;8c5mEY{lf(=t zxG*lH%q=^^y!(zpqn{NdHoHkadD#L1MH||59$wZW9V3 zT*aYW^to20l~$MT=WqQT2YEm38l5Lb>ftY5C7S+qkwZyg;` z!NBLA+$ld$`X3m>&|`MZT$EU9$c&^A4kdz?*+g1#<75Q@ptm-o3B-cH#wObaT|^$nJ~B+Lgv5(HD9u+wlBnF47xLGC$dZLb7uHwmSn*viz|w3auIx4%^c&BPWB!L%~dnQRW-%{?}L z@(4x90abFKQeXe$0Vqys#x)MJShJNd7sqD7rzhKG{xTK^IdF?&9z$~v4;3kYz9kVX z((T|5g3xrA0k&nO-Fv0t2G~|i$O#VHt!!gf_Gs*onfTHzd7jx}dw>Go&0aUL&c}W% zxFcT-zN+ey3CW1ckqPYjFy|KO*k6=3$B?qawZS6{F7XwmI4gEcr# z3R`)OB|_f=MdW=~$E9~wjQ^gJhRDmSx`VW`OfMlNeDdMAfI)!y6^fGx`JlWM ze_99NLSxyCet*z9%9NjWB>K1$s%p!e7JQD&K~~+ci~LnpNupI13*UY1q=L8G4Js7R zw?zW_l&b$+C|Tie;UG}|N8BnS9+!)erEqdLg2TW;V|0yRfh&WNr|fP9SBBhWj`Y*% zzO|_othqK8n!l8Kg-xuke;@WuE>Le6=CW(Z0wR}3DtQ>J6H2RuF3&q0@0PcxTas`= zFC2G|!Hq(*GZnBPk@zzi|Obi{&L#Vcb1Kz0O0(+9O2mXkm z*uEN?Lac-|6RwMhAd{*)BJJtL8CC)XHk4LO;*gc1PQLzU@!htkURq~&Zav;&Er7^j zT!E<8!!y@`R_>&lOY*38t7a|OgzDe32uA?Cdu{MgTt!<26LN1}l)SIi zr_%fD)nOn{)jLR$=c}*{DPm;ViZNe<=)ux#+{vGpqwyEAnM~LtrugVz}D6FizuYv8CsX>u%wx0 z>lNl+y>dG(S_))f^WfHD`RMkU@$Y2qp99?2)2aI-asH2Py`u_OH*H2Y^fhAdPI9#o zz7#+4uQg}ccVNd)4wL`kK0nwWV7`gzd{p?(m{Ii81&cH~{n3kB!7f;8E+leo&^J+F zwcW(JtOdNb2s@?RSiPFUrH$|WHi6@Rb%zkKikPq1Fm*FOCt+9u<9P%FWW^9WMn}G7 z9L!GU%6!8GvHO_z_fm; z)Su{ph~Mi5t(RXo$_s*cT|$CsQ&u9-<4{-(Ic%KNYKD(6QSipyJ%GTatCj14`FE(d z9*&etjq&M!hw6)S(p|W?X$TPr~tT6X)V_OSOP(R9eE+kgl(z+ z%U!v(HL+cI>RFJKa#G&4b7=6iX8~;fQgk}si5flwGBu?dp{g+*Vc(L|uIJ#VU#%6- zvsJH-9AWl-`B6P^U|Rtj7s!U4;ch=EQVXb7bZ{XCCc}r}!!?is%$)Q?CxH9hI|YW9 z>Y*LETP#3wH=VqC+Igq;;>%ds@G#q*xtX09k}NBc|}jB z@!dm8c>p2w@?!QUo*Zu|7uM2IrEVT|xheBOS5CWhAT;uG4WP|Dib{U%A^kCNVEch5 zeJ;_#A?bVybE(O`f`o3#WggkV{U8I)4}YvyTD)Yv)v6X`ahbOscVJWO@)Ey3Qv@a} z-(s4cW#}kF%HC~}9~%`id;EKJ9YO2~Y*1jDcGqM3M-<{lu53}J{o2&gvG|` zsf-b?&y|B~#Tsi0cqp>Qw#*e#{M69WG}grisLt!F$0#!2Zj_3SY0)H z-*WLNXB^o^!B-P71Tb<`N7fX%MbW~GdU(RgU#P^ah9iRK6o6y4i!QkBQ1t$s>UEbRye@Y@U9%2nYSe!a zE%(*GyCWb};zmcQLV~5VgH@t@1FvJitCr7Rp3Mhu*5x=JY9aVEdl;2@nt11=W%`r4 zl*GnQ9QV3xXKH1XPG5J*#EU=&!&Oe6=-^DjKdjvjQy!wqX(>no8#qBtNBuA+eYf@YB$E{8lY7Ap zI229nsfhz`@vO)daH1mjXtWJ7&fWWZ${f zT`3w&X({$N3>?fK7gk5SJ6EbqQKPo8fU0vLU!sWAUgFv99o+OJ5)lamETT|yn-Vy1 zwfFVZfu6_Zu1w)B%PsUVD+mNJH&A9O9Rg(2L=o0+S`8M4Kl-}-A|9`ygu2q0Qm~eZ z(f1VTnNo^!@#VnEkoGnagkvz17+4^esM&k3nbd?YbxGkza6kS4TGg7(U@E063xa`Y zy^Mh(7;-I=c)NvJD{FzZfpZF=<;sVeM>_VUJE=bS$eY#QaPo(c+$k~{gH-@+>wP+%tKGZ(w@YN+`|x2Sr*RN^Vzl#!oU7+ z8)tEFy54gePX~v}ge_n-eybQ&c4WVs)M@wQx+~bQ%vtwyxrdE`7;az?B_~ZkdR@b| zRaJ^?e4xqQ;{GHo(NP9cr2tj5<64Fhid$t3m=L+090v^5!N+52)N9`37FUJT?P35l zWj94K?^s!wma+_K#f*+aKnY-FZw^G^y28C)I_!Y{LR$zHJ&m*=#@%h}(vDJqW_e(1 z2|6hJpmdM}+Ejj9&BK!%jEXb>XCCgQ0VvoNTW3Pw zxT7sf{&+zBeh?#l(Aw?mPpa2khUr@_F%AoJpyId)XT#wY_b`Kv$X(J|iK*ryFQshO zbtw{xL0mhbyNnEt=7Rs;-x14onjXk(uWS8#Fr=s!~31uurt+wN0AA1)wir!adc~XY#b* z&E=h|nA>Q627+;|MKFRerxgKpMO+zFk)E&T=-7nYA9CW|NDj9PWAXjnJh*2Ua&_EDjJDAFP z<^kLn{}6q-X3x5@QB{^$FN=h-Ol{hjESXx48j1P}mWfczT6+Rty9_X5euP#m$wYsX z%~0!$v)i&9zI8Q@d)K^o;Ka>Q{g&Y3yH1xwa0Vdm+tfLdy0oVbH0>`#&#B?0ddyoiAlmx=kjLJSBKH0MHKoB~1#ClJIf`67v_#bLD zhsPiZh5S6O%_)Yy-nnptw(GlQtvWEE%U3~?;A40GrxEoA-I5qb59t{7ZEL34zF~ZM z|8Nd=wDDgi#|jKR?)w8jBSZ)4sahn?=T)WuX>8XFyb&hzvyl)L^acGw>OoO6pCduS zg4b<9MR<&cDhgG`^K$mw{bEq26=<1C51jkOyG{e>q?T2`J986W!LW=StWkHO8_sKsO z*u1T%X8P-5JWwLce1xc80CefGLnfa{%a5qIbfxy)mIioyd4#SQ3OK+%5yJtDN9-9(Or@W=D*bpTt2bLbmAK-N-H%Hn{7}i21v>P`Ev99iw3bJ25Oy zkJX}dsVGF)0iVI7?Sev83lyeq_<87Jr=+ASgjM7Ow+^1d1p|aW8H&h0+A!@_hiE)F zU|#)9L`0X{Clm4RJMO!x4n2F-=qMAO;GRtRW^KJP(FI6n>%1 zt!D`AJ``j}d&dGq?!fqgA>{ngv9U!B7BVLmfRo?5-)y80&RKs=vcTK|Xw_<1TVBgU zb0%)eOQGUFXONQTdSBb8^a4EJ$ye&P@}@TTi2iGzf1IX|Fx7OZ{*@a-(=U zLB5MzLFPSOEaTOUlO3>EY1#%+)IT9fN)RMydXVeM2_CQ$-cDNuS4x$XI zMFfrJEPrV>R2S4WBtG;*!b~nq_ ztIq(u;+}d{32BwWAM@`0)(prJFllk<0+8TN65vYHM>(+E9h4P`DMcA6|UU!DZWFc5&L$4l*fP|)9fR2);$;*LYI~!cu zqc*K@srOg|%czFfkGJD?ai1CxFOxW~RYUeUf(G%`$RacRZNM*;`srtK;oXf`5;|zj zgdlpG0ei#AbMHJc@hE=bxf?73viyQ6Y&714g{Y$oQymVCJ>_q}nTAvKn`JbLZJtR(25Yn7YPXZkGmgA6@bV^cnb#K zx5+db1xmk6ODN^1L0rKAhglR20$_1S5c6+ln}kBS;Z2JIL@Ga47$%W@H(kx=y(G~H zzG2DjVk%>A8EE8qgRo=T7uMr2j!zls+Ouc>`o;xvsXSMVRW4h$k)k}ovCf^2P7%{P zxOkzECk-@gq=WS_h|L8FayT{=_yXzw%Ge1xWZP`O2T_^yhS1}2T@2aLIqyPLH7rz2 zM>0CWC*pxlI<85gtsfcurz+BFUQbh(?UD7`8s>F1CiJZZb{F#A`q&DCaGj`6BxAJl zJiU_o#D2kkrKN_kGWNxVpfnwScF7C+@PHDVDm4fLk7)qahY8Wh=Qz>-!uvS*J@L!W=hr@lR6)}=duuzsDznE z%)*S*W2VG%=Dc&>3|8oAsfw=A3L&n?W@VZKWrYVM8y#)e?Qx?d6i9*J^mUHOVc>yE zpVq%5A7rDQUpTfm!V*~W^7kl8aWUG452j$lSYvp5qC~@DsY-0iHgV9E36LpY+^M9B zTMsKPEUxNx^^Z}Dlx0vUH--mZF$fFKhmHT#wtbg}RdNcD5P*=OOJC%F{Oh=EL*(cy ziv<`D4?G_+0c@OT++kSd#MH{|O^1nQtF6 zIjA+^pNn7;OOE?+&<67VFGK|3#e*&4uL^@@!(BRuSJwHLz^uM3`DqRP=1)yHK|J4Z|KW z387{lir{j~5&;L+8b&42rBw`rzHD~9K%}Aumj6#>N_G5P-dX8URg-3{QPFEjr4s4f zFqEo&93$cGwQC^U*sc`DSOMpC#5D>A+Q%IpNT77w3VU%;?UzhviUI1ig(mBG@@DEj z{yQ`$n7{hBGbDhmnEhiR1h-a<+K6Q z`@A2SDezuhbbqITx3IC>><_$yCgEq^gQ8<%*ptZ6kecL!A&JS`zQx42gk?->0KHaN zCC}OuHWfY0TDWKi0^S-1*4>kUQ42P znM}Y}eNydVF7uA-!}s|B8hEidOepIsz4ojTRA~gD_Gsv6F&?19xy7OMlW!LCTv+P< zR?-R+oP5OofRKUJDh$>y6~aBFY})>N!9ahO25uD$kfUA8IQc zDLl^=f#VkCo1w#w}D=_Sg^rwidypQ>Z>9lN;qb=k5KI}!xkNOccc zxwkMOtaNK&5>RC*`0zd7)SXj5Xm-Q&KR@Bsu6;hODeX9z3JRLX*r|#O>B{`(6VPd& zPJ&-*Lpt-={A8i`a_c&sanVBVw{2ub*r0RCBmp}LA<~yPgfKTEUCK}-qDC?Fo6LcfLRkzu?sX<4lV4(6%MmBp|OABR4Hz(yVUn*G~-y#XPyfHg5LmnDm zBs4YAGpY#BAR6=N3_MCPO20xz6ek+{ZN&HiX=miyHKLK(@Cr5ghH9Q;OoR@h$YZS5 zVJmS`b>nTLXx|bD)Ht5g)R-oDlwtvAEPvlg%sF!|Q(xpF!IzSj(Zg6uX|KLDhLe!& zk^+9sjF_VZnEzEJu0{b$mD!yP-j*+ma{8`tcR?W&<>oNN^xG#LK<72qIQUsaTRD<1 z4)joANst~btXb>rco_Am7~ysf2*Fv&6>o;yX28&^X9t0pBd~2>n(2b^EZy$~4Xx6` zeS0NxQBl7Q0Tlpyg&Ib8(HFYeh^3=Mg5V zj~t?T%0Az?5!m$E%d#9Ih&eS4X(@w{Uz_|S_oTneQGyk6!^&&|3{_L*S~KxlP;+J3 zia+u706!xRvqR=u02f8tg60@B@nyb9uArMuFEU~1V0{<$8`Jd~ZX|ml6y*3!Z>+w@ z>yP~3q>VRZsKs2>pM@c1?Oa0n69K^k2dHgtnjL~^`?~4u8UzVYAta20tAxkY(*;bv0!C2*7oLKiMS+t-U z%dsQQA!m=NTKRTus7SJ8L$uR<5+qcExK+Sg$|5I6jQ@Nd7*<&N&3L21^d0~OiECZ( zp5l0U8Eo4c^2_$zu$ov0R^JSn$Z9CqnVsRf7RemPc##{bt#x?q33N#{#sCLY8S6gd z`9+*jf2BPBIyMdHf!9y6@>H$7O|`?I;F2}St2@Getv@kd{6_UsamoVjoNkaXNg9O) zd};Ko31GEGqzG+ygSH$9Y(=7klT_9X<3iE~k+3k9b}>mdaz=xp*Z!HN#~H2+f@cAD z@^5|6sK5Y!&wxrk1-n{&5F7`)8uPD%!~$e9JN^peCfRHwW2${&XHEI*rd4V-jzL9V z5D7+yD`sOGQw?YSI4U}e;wQ_ns;(?0V~c~k4Ljw@lM+B{c1$~%%{q=Ti5+GW@w&BK zU{MAl8vGQU(t%#u6>B6wWGh=54^|}JP>}uf2UTr-3SyJO53)#}+og8#^ zC#&1^flX5lb(-jAP#2fHgfpmGt@mA1MB{Ud4)NwSPNh@@J_;wDRHgrIGm2|&BaR+D zgK|20u&iuvUeeMiA*7|!N>3&0gu{xHXhI?>kYPeg9nFmyoRFEyu%(BtX7;8>^x1Hp zUYq?WRDU>cmdhl2&AJD7(Qq!aF46rS8uqdPJDE~z@|ylOO&W4W7rV8n30_FN9PT|J z!mf?cBoETp{rFr5AdqSoAzU)Dos7q)6;jY--N#Z$u|7ZHKM!3Xnqj^41=&ezyYxry z{{r0on+%iA=!rIRBPG-!qB$xFnAF@P34s|aLFF;gX`yKG(NP)Xb=8;4W6S2Qv_o&Q z=T^E2RatNyp>PGw$gcb!ULY2*=X-3))}17ndXHR(O;}IF<=;_!;<{>;-DrPRuY)7T z5!`@{lqe|$QQk|LQ6K~O&?i}sI)jyUW34@LDB9<;a4PJt`|xs&85P4hwe+@0rL1#Z zN4#oT-VBphEhAlZG?J?4>t8T}cPA_%K#vdRg83 z5@lV}k%g(hoSjEhhc5$9PS)GtPN|YO12di_E1K2|5KfS#BbSh@Dkw2te+yjs_%`Qc z8Si_A0|!O#^mhso(JX3wfJ&&i8__;~f^iFvJWKY2V24UpcJl)uXmXw8A;53*G-3b( z|KN6J@vm(6E0zfE*e5s_G@U$HZ?b9c&V_n&2cPJ`#rTCrD;p>y^#Z1rV zrC2v%kt2&TR>iWe8%j6pggd%}zz1^t!Yg4pvqjh`yF7t$PeiX)X+M`d95pVdwmXmB zK8M{$DuR$?WN^mK>Dh~%w4Cz2e1b(VxW9-hAO4Fiey#3W)yn*ry+NDa{zpDi zP3z$zf<3e7>30(}N_N(mm5c-yNOCJ-irq z7tTu{xQ}0EW24rX(jql{PWEPmep*63s=*`HmrljlgO7I;rKKC(y%uY8FSSXV57qDw z3!axJb9Z?kMP;j@70JZ%e`ej-UDl_-fRgd2krG%4A%J8wP!p z5glAX*Fl!Pv^tff9uP_BM%^t#^ayhcgjHNybQp9(rLo&S2EX67MT@SKniE&;U>Ji} zc_K1f0)Vj@4RI(=L(Ch*iQnw~W!g@ThZEVddgVdPw3Bq8jQWi3r?(4z@k`~@gkAZ4 zYLa32^B~3YCjEI1bqD*(?a`+Ln^@iYjaG-<7S%gbS-99~Q1s3er4O8}=o_m;#DO#D^Fz zB2XiBFGH0b9jOq7Dv%Ih-v&-#?#-V|bwD?V9xy*60)exb?`nG}|A!&CCV$S5#47 zUI+JKAA=NKX5YCGIqfg@II1d!HtRQ7OSQZ9MVdl;;bp#7C?;NO<_8c$4b! zz5pXYIDgumVos{tj{Kff@Aiv9sa9!j_SDz&WR|t-sbQUG!OkVNbZAznetYGY@nr{4 zGf%r;+2r1F8i8vEiV(|t!Tz_)$_m}9BJ6Edz~bpVcmqMPVtgX%)swgiG5R#_0ao_$ zvsgLMwNEMI$y(PlPtWI7ymNzlk!p50a z3$|2$31%#*aTsJ?vG~O!Y_8IfZ1r`-s7a+gDp&nEF`UxmRmZXt{l-zTosCrFm*bXs znA+C6x3*J*rWrHmlo50{S%Qb7sqXW=Xztb}f?d*EP>?WkN>-#TH-spu5h{^=S|wK! zszzPb{Bnwk7JERW=*G8X$#|Sa>+uX z7e};!y;#;qmlXg1^9Mxb0lFQzLQ4=3Nbww2s%wkgz_Wx<1$cfW+I{|JeA?;y?v9rw z@;37SmQE={p=={*S2!z$vq(r?@1&%T95JHfNrvGTU4hix0GGUgo(~dA*DJSQ$ee!npIQF zoLqLZSi{WB>{lo9(TSedgx{LoDb}#l*`c;>zOFmVaEJE5FFC4uUIl z^_#BT)%CW9y|Pyq@s;1N&L^c#rPQgEIt9xwSeFY6>w#kV-Rg*AIk}xWO-z=RWW7aM zF6&|_@)Je(Gs|yQw=SVQ@GN%}Tf-{GdQ|i!wEUuVoLIwYX}wA3*L7Irmqib1k-J)SBDq|Hl7SXj_r!U5osIvddU|{L-Rp_d{7yR?znF%-=q&j1%hz{eYNqxhb1B%D`#MgnoqjpF zYm$B)8f*A3|Me=s4A|=e-Ok2>`4#Nct>M8u*p<~>?Z=n~N=+8_XT)~fNN<#kE^zpA z5RWvFd`Uqa!5u-K!=4&GH-2saF$r=aR5;{`eiWUBXfp~F@sAKd(vd`D#1y44OFm6j zPH`lrPfjJAP*$LVMDx8c4mj+*5z)A?4A(mx~3)u{1I%a3gj%QISlxWPfEj73`zZE*zq_nNz z7Ur!6I5KfSEwr@gBKKv^0~djG2h?#zb?mg*#dn5ye^SFdFuf9cC8kaEZ`%E z0#klyS>OxC9tcbb71q+reP_%Hc z!UQbXZDDJHg%)(UK!S@^7v&2{FFU*x!tfNs4>7DDLjp7KFNk3JXn=;PszEFOK4Pqh z;W0+fhNl~Pa0r7#AQ`SQd1i`oFw)_~4skpnd(iR$?Mx46XrR>SfS{pC1~mYQ88N2J z2D}@Va9AP(Rpy16s~oy?VB0~E2csUce2_Y0;s+WfHAWz4Xp(yDtGBmzh1$z0`%bUP zm_2Uj3qHOpbB#KUu2HAaH7ZNJI`#Ekdt&Y9cU9aPX1X%Bb}j)A+W@aiabV!pY-`wV zN)?o{3SMcpb_cJ92Cr~cwyS#0BP1+j7GBM))~g}w9u2Qvhlf|PjW`EIyyn2{aW~;B zEZCOFCSJ*0s^|NBpH*64{}j!h6t90GjKOXhW!_?ISVqfjuB;7r!Py($ch~Q>jGV`_I2`%El5+Ue*6<=b zRMb|JbL2KNklb%;m`0K>Yv^!38|=Ho0|(%_;r8$<4)*ooeoF#a=nvO9*yK(EaW~d5 zU;#~TrGtHqxYgEhUv#>I{TbrPzUY((%dre#i+FWw_<#^S*!O6}?GO;bY2sx9Y;rS? z_>K=Gu3~Fgfh1n^V+03VK#9L?4ewP6qnrl-Xi6sj2ZY(gS?pB_8(>{0uCOLbWAF zl7MCM!#x3eg0I~fVVm3q1x`K`Yq$a_b1S)=%Yv>cKH>=S<;M5|D*j??7=t1Hv-Of; z2JBnKrAXK&l+$To#b0c|CifNJN%Doo5tU>u(c*AS*e#9}>$J2c?90W|IAFUtS_fWS zZ|!uF^u+_WZhF8bHxi87#2>J47>C;$){BC$g(Jp=b_mch-X}s986O;heam>+6YjMK z^`e7%(LoZ}*EQpKP*^twXJZYQ(3W(@PwXfvc+~_nZX+d)-<-gjsc|F}pghMZu*r>F z!K30SY*g5-Hl9?$jq6>7ecw0;D{$QJN?05h!gAQgf>*P!$`;&D3+u}9ofY)H<~ZEe zODwRi3vRQei;nZOP{(gvh;qI45*OCB<&xv&a{Er7e?*e`~{5B`zKC)@n{8p#)Cu0q}tXv4*kQ9-|Rz5bLv9S)YxywEo^!>l%&9eG?~lP+LuM>$}BVTz|#x>iR;l zm)Ea;ef=s}3-;lhVPStqtQGrdV`INcMpo9AeIFDkeu~)4?9;?4Qhe*#*|%;LD!z21 zp?#f}_N!`YYwg-!v0}xyvbAh~CvI!oK342>I;T#!wQk?-?d>DN!F@K4i~E`diyzpc z#pl~{a&zt6XPZtc*F}5WNEe?drlb3Db#)&Zjee<}-ACMacRvuX#rttm>G#Xa(`#L% z-cO46_I`Cwr=Rb|i*M!Q`&KITi-`OBKA&If_x1k!0|fll9T+Gm`1z{b=1bLty6#}h z{MBk({Z+#DxRHe4sIV>kf!5ag{hm+5_PCRVf9>$_Besa24sQFtQ)1$`)2;q|TT$`D zX!X8`H~^YO#0?^DYZmvmZVJ#&7gNM&^P*Ko+b+^vTbo6@t)0-f>3l+Yd$Z`%TjQco z>~R*QKW9<87mtgGGg{=Lbkk^)i^y0vYqUCxHrky<#7(25W&^f`ET+9;Q{|?K@c|qs zIaiKMkWP|IbaZ%j;K`wfS`fhA#$P%8B-;V|W< z)(HnEpr@S@IZ!%4jfny>5hQi9d|`QoS>;5NrW9uc?g&#-UJqi$4F9W_}Fdb$nmN*85j7u376eySlNrB`n2rZe`swJ(JSxaNXYWs3~ z;&#H@54auY$AYWHliUw-Q|A6$1V9JZEk+Bl+jn|+B&0ieIeNyurYX;hFfWX#H+@b& zw!bH!P9T`Tr9o1bSUi@VFwT%NCxy^ zz>ov}dce>F0tDa^K=MJwhgU%C2h9Ra7Vr#+0RRf*3lJC@&=N$4193vC2#Fs+0m0o6 zAtFwQ07h_BB!nV8B!DN7VMI!SR|Rbr3>{fK;x90P0Sy>jkbxbNqXtnl@Ug*7k_(b7 zQ8Hu+4kn3B>Nq&+An8N}CssT_@__^(yoE4A1fCI)o``uOB>@nM4Nz*7&=$poC{+`# zQrZ)aDC9ulMhb>gs;B^=;zEU~Do$3qSP^ms#?odflO>6kvK6FQK-%JE%a|6HxIo6m zsLPQqJ6`zVB?t^-VnCWf@JkFZO=!px#)X8Lu%{K~!VYh%wNnjq(y$}N8a~X2-D&G4 zVn*yC;=w^UE8$)fWz&nY=|$OeevfoN+xk^h>{&3FV01RWNE)BGHN5GdKzOSi*Z84W zfUIP_Q;auzqm29-jZ7Ox>zP*@e&dH?-Rw`Juktuv=jUXOPm8s4F(;Ep50#S}>Bnd^ zlfI^gj&Eb;(nfxFyo;^j0Wyyly3ym&wuYsGf7L-8J_IO#d|8eR z__YO^N}lQu?+=fGAi5<#1;ESXv4F(AP5y4#z@M3uKinD)Lc+IOze@+&noqta*02z# zzY;!i`M?UV6Ki-0L5}h}Tf;-7tV(ap?$Lz*+8T}^D|w==;UZ6gM@tVwf_&$1VhvA_ zdyGaS=?O2WjbwQh6i9te1^KvL*HbN+I->OYrVDbL7sO!9&+QDVydsh()Q4%H=BXQO z8C`j3l6+M9Iwkom%jYf0W?KG^Nk-f9(G|Tf0;0lscu}iJ+v5JXUs@%6!t+&L{Z{ z3WZ198YbaL&&&rx$tk>63Hi9o&iqc4>>gG4I1|vkoRr+wO!GFeZbDB?xl||Q<3_4^ zITUg;x#rVS$nFYHVhS6THV>sjcA4BfRF&+$dFIwmEBOr02VHsNJXq8Yi;;iY8qShi zcvwqT%K5Do<~$V(`M8lTyx`W&x9K*k+%0t8u!Wpd=cQaQi~2t} zUW!a6^j2HLWpbgnF_W#{nnPAH^iw)y-`pW9{nzFp!(L2KS|$7w^ld(5?Ngk|ivIaS zMpMG@g`1|O`A-mPQXqbcSW|@v@rjJu<3<{NSA`TQ794Z(R5)vE|9xx~t5(*j`i3EXgI2Z^B#>_G~TDvxF z9;?mY{>uY4owkkgU|yjVDWP#eV3y(VKA;#+0CfSLT!;%kU(E$~%p&rfgV~GzriO-U zLUB&8Q4O-iA7?ys<8GvNR|dSzQtH9=-DngOu@_U|D*6&Z<0ohD3&I2e{7K9Xi3R3| zz*kS{@mEt58k1DkUG$|uHI8BeS3D9gu(a`8M87Zwu|~HR1am}0%Iy*q>=l~$rNXZ; zqh#jPB>7HCVAwO~i-AD@F(Pia-H?z>@$V8{T4r`h#x^@Jzl^$D~<{Fe!y5^>C`KBu` zdzg)z4?moZ7`{N7i{A0m5ebG)G?YpyKh+b$dnWXEiheMgf1l{av=L0#h$E?MGkt*5 zC^?Bdkfy%}Mkm85SoaV+_SO`z{hVnbKwr-da4H^OeGs}1tY1V`$OVnltRj|}O0m$j zhF$Qnb5Oo>2k6Iyj>uyugeU9ch2r}eopAlcwsG=I41W`Wi7hMk$3Chbw2V`0SxI~B z)8r%N+s1pVbUM=Gd*x+IZ~EZeJKasinNnC}-ax%Bdtoe87MG(8Zc0kbzzWvs>uvVH zUS46!O(5LE;hneuT;B@VPDo9w?$0K~}a5eny7P1{8m4&5{nk z?SwRSIt2d(S;eY4Z3ZEKA)Lh7Ws|W~FNds|z9=RIdsO6=MCRkPa_cO*Z=@(C6l7_7 zd9Y5<7o=>_^sP!jM_5k*kI?}3!A5q*2~5PARm3{QX+=)8h`l>CC5Qf zpUnCtY{swW5V8b_As7Cg{4UKm;%-MOhUlb~5@lTlv9831BxxDyCSbLbS`(jk26yoo zS2x{cKAccmH=`EKp{@Ozi?zPdNp^$`DH@M=Tt64HJ}zJgz_GCHA3-7zVEP{%#G3#p z)m?nW7%>fCBQo@BCK{3+pyQ7QX)P4%6~u_O1vXNMu|zRhf_ML1Nt5xx+36J6wQw_C z7hWE2Na4__$Ykswn#PH>Odh>%a{4f8OJ3w(#o;5*yloucG7Ej)Q>6my< zuI%H{12hS6KAf=B0fh=0ce_)}nMtT~V*-<_#-fBuT|DQXFEBg1?#j^!5YkW3s>ay` zQ8P!@N7Rk-i*V*OKSo%yDTpnRo#?IRgBga%jtVAmHyFCShk{Kl;;L>|%>pgAT+AOQ zfId#MZSzvr4+K>DR&!LEg@GF2`FLTCW>po!JY^;$8jhl~8B}rjY$9fuMU!;RGRu;t z^NLlqp+s<5vvA?4)x7;DBQm9`N3{@EU%y<9i1(xT0>-cn0t%3kq>Prv!EY`7`c%G8=cO05F8vM1gWPVpc{esJHxfl2yO4R)oJU@0rgC zyzPEnD3IZWqUL^aLrbAq@Xx2xF;Bh-HiImUa}j>V#P6X+M2vwurZcX!sp0b0kTPuu z1HmfyYeX1WMLA_wgn{I&yt`H>nJ~#zm^Ni=3zeEjyTlKVQqD0Om^5E#`J~ml#Nxy{ zr`3EsU$r*UlGNwmNsW{GWvQnI37U;Api#j2_R3DJG5R;8SFL7ohYVfJZvB>a(GF^# z&)Ce3+2rO8OK&HL?!zt+Qi1j8Bhp8;WX08i08}@7`o@O;IM&^8r};ScKLl;kK!!KC z8k>h78CT#RALP5FRhdQb0N-|Wd%K5ac5LmZ2z&Q5)y?EyJ#`{P_c+m1L%)mFg1K$M zu!L*;u_Ic-+hfk{&E#pa08+~(keI1;^Fsp{gby7Ou@!iwVeDfKvSv1f^W)7y%UvXZ zK}R$NN;4rAhs{FEyOS+tc@I|ybhOPIWnaxHlEH`c=AbIP1y*JBJhtpFTgI)Fj>x@h zT1*SK2mMQn{{i|0@af&N!%S}y{8qh<%-NVjWbF~CR_ z-tq)0-iX8lFC$O#lP`5XMrJ^r04LlS7SCr|F#LIPVlbs(=iq?Ymw33C&`Ih=;hFiqlGA^>hbSrA<~xm3HxXvG#DP~8zB+D?_(wGMD#$Fff??9DQ6QpJv@=( zjI14j8@!c~DYz}Nup;U?uYV z(4BU%GQLkh@|BerYuf=1G0?w~XYb$zX|@oNJNR6@=PX5fT&snJj~|Lv1?LY=cRAX!*pdWqTeK7(% zNGvEg4E&qKLfGACoFx|Gro(#|twD|M9pILCi0hj?3fo!rb`FFiF+LGtJ3&(2zVx}M zTA{O1tCqkckj)w3dkD1klyiU$N0m*1GZhI8VJg>Xn5>(GOi%lw{g|JTRbbatJs6nU zLGEn~(TS0%VUV_?x)f0RZ0*BFlbUQGQK7$4q4FB-)`(X3E_1KSS|H;Vn;wJ?j@Ki( zC9Y%#P)?LQ!wLjxggSR^iS=TkNUDG*3I@p!$sR+DmG>>wrmJ9}>9u^yCWE8Rr;0JS z2YqV#$7s;4*bqqw7<`fmzwCXaW?Qo_z8(3dy%)UE%2yTC#cwdWt>GB}l^R}v^7+$G(eaa zE+u|gT7XB5bv+aaMM?9hZ8)oAn~|HybUg`$PeNArBrwgZc;RqY5Jo$KD-~!CYKSQD zxG6krj$Rd#M0#)63nR;R;ju_Z)cvFb-N0jAh0cly5Y?6(Yoa}BZ_g?!QGQa2o=Z6{ z{iG5F?I7nJBCT?fta(tpqcoPOF$Y9$W6lUIWX*BuY(C9-9x#b>s&r^!D80*08|o;R z1_(4A3OZjBqwi2a43isK4+WU!|B2I;m~tFnuZ}nmOy3-v2>$$~=FBR0#vE^b~RmunCp$7r-rTGl3c4llo_)djKqpb+DXXChN1B;q zl58hB;gv#kVnT)1KJ{kb&`Z(t<*peHn%0{rvnBkK74s2pl>K2_9vHr3|dUczfMONQu0;oPZS-qAM*>3^3dp4+{sP$4wf{zgkPoN1z`zExAx> zeASxFy}vA_?%L+plowh?5jxe{M>j;0YN-VlSZh3S1B|K<{GKJ@Mu^5~@1*Rg`+)Yz zzFLh+!EyW=WzLByC(fQc0-ONHgKK|tF%6kgG4B*rzN8V~7^gZ?7GMfpt;j@hhDIA1 zBhZmJ9K8T%UVKLu1vd*;M3mGHRYfx^C#9vJiwk&ss*gjS7XJw5VFjPQgz9E|jec`J+|Q!imt=j16kWrAjnXk1=NR z@sy>E%Oz@&=c-T}M$I**hF^kNJ|A*3C@}fpHg68acB_88XihgO_#w~0mSo8})R@*) z4aH?6o$L-NMN&=>dy{`Je5@8i<-arzBl!(k7fR1o2BCyZWH17hO_dQu9O)(-#h}LN z1z9N$aG>n4t8D|VUY0VfoF?dUgLW4@|C{cu3SXij24BkOo5M;wW8nXs5N25~-lrzF zQ=~m%7qXEX0q7m@{G-YKO4tUNVy}Qc1$W9#Lq@!$;=xsi7-b_2J%rz6BWT)yNSU)Z zyaV4E19;alwESJn+KcDt?TDeS%8RI9JdqCE)Ihl^%M!DJYBrW700-);uq@?t2kM3t zfUu?vzN<@2Vz9F9q68fS@$=@yLk=cBHcf%!FTeHIs9%Y&;ZdPh=Ve$e))(y31 zG7v6O=ULL_f?@*f|5^ilTV(!UR85toX;bGRa33}@$=*b`v2j`*PTQf%#mIP*wO+fx zS5ko{&Ea~p1-U9q$~uh@lO9Y{3YtXeP=M@HB2he&zjCe5#z{y=49NI%ODDr|j&ayopO{g7xJ)w-P)j;j(Q6Z!;=Vih5H&NtY7MxrZ)mWGXD{75Fcpos>AOK&% zkMZd+zJmL?=N9u7oLEct&45<9%0hSnJI#15){)9lEtSxtS^^x84GOl}>xyV3Ef5NL z+P;h3&&Wi-wRk~H4TvoaFCGtDFokE88S4|Gai)i6O$}}2>_Wv}4^abDgxx%=gAi&1 zs>tUw;$d<{OTDBx@|)mnaII1ST(O~aC=2>E0qst?pe9~PKC_M7FS$i#yS_M2a}mM0 z(n=+ziU{J+@Y(h_6G(TrGy`xqj6Cx(ck_$(;G`G>Q~0qz>6Ev4r*iNFC-S{`w|Z#e(fF@JqUcz$B}LtUcnY3z9j1v7 z3U~J}hXG*%v}Adu!K8CDkb#Q~#;Mr!Kg`*l8cr;31}i`-Z%zv)-ykfuX-GEZ3;+P) zr@PGqPQ^+z6&wjoZIa0}-5ppp+R{Ws(bri{5>SeT?(e-DNwinQ-qxGtkh;Ie`Q;s* z#}#RUOC~g(r1R-L$HU_5Qr7mu7S13W!T7KTKh!}App1M}X!n1oo=p%!X&yqcVYxVf zsaT2DLQ~GvAakd!U6?;2j#9e7zeO_n2Ya5~5=v@=h)xS`PYlw6=s+J;U}xOX=`DMK zR>;5d4I(=GIF)U5{>rvVO=?qk)9|nK_?m+|~x1YS6i>6z-YZ z3jcy=s{Y?3-w_9;0`n8I_(~vVZndGo@&t1%lmg%m4+c7N3pBfS=zSS&x$%4RH*SG_c*i*zCW@hyYoj;um+yP1K zA>s}7vKo@E1M8KLeB;Pktt37%lR?^Yz*Nl>J87dnBIN8giz-<$Gns%jex>W_YD0=X z1oKG4thtFS8wkQuEBQt+7^W)0STX?&jJgZ0a`#Y&f7R=|;619${kzq7r8W1+D|c7V z@sX+h7nCy0K(T&A47r~Q(kk-7Y%<7nWn^hH@;NXFqPUu#!#{`ubQ9I z1$v`(`5j2CT6;D&_oig>mNZSsL1aeX%S?GB7nN(4WaA?+HQAz}ASMr54zPDp{iwJ+ zJHx*~SHDR8R6Yf!TH^5c*jf5e!&h2Rne76A;X14W-J^xa8UZfk90E?pbRc-l|5!4%*bw0L1Jwqp zHpa&g ze(%TN5)LvBzBkwY6uoTm8H~wJg&OM{5IWy?1p4(A8UJ1TQ2*wxFZc~UoK*BC*&x9l zt=R`^{jH1sR`p2CyW>-31sg1QK~B+u#}E?0u96w(rd#>3`5i#zJmIQaoN}yq2_j(1 zwM9H~d84b5eNper8K~k0b`hfQcm$fXFM`$Nnyx8elm`z>R!35R{2Z!I!K$aE<)HqM zwW0e+74#WfYQ9%=WaGbN{FAbe@I77KIv>odw(g=ymU0m%nhwfs)P902f)@uUyaR(k z)-DJ+2n@0)thtIom?WPJDS_ML0iUE8_-no=jWr!h|%65GVn}~pvL893x&==IGPnZV_Ml#$EVRM7>IV7s* z{)97Cw<8$xHcXM$EY=jt$#K*Mo-BLv?=}06IiFw+41*`F&4NPr zt^=#n$wgf|0S0gP!|O~&--6Ggb@xQy6C^3ar*|IEntUeT0v|IvryHmRBa@y}%@SOP z3g$I6=^ae$geRxr3jknTTu}Hs=}Mp_K8Zz`=Z$rsg#JbZ&cu;2^Yiw2-qe;NG^vx5hk>!}HKI#UMqVE~+8jJ{*^2Hq^hyl~n5JFVR11PJKAco}|gvwR_A((%! zoPWF20)*z$}>%xvkcB8Be!z{m3Gr`Uf$R^y7^JJ8S_cdukN#Z z`AKeb@9?$#$_0*~TI_v%YVj%5f!vh-2oyG(UYD#qi8jrQ7867grYB`xE6#K^h}ti5 zr+6FC!D)WmyYg*NIFx_xKWQfZGC)b#PjN9NccWnZh8YFFwhfp90REpr-d2bNcHngO z%=-2)L1FSDI$*j-U)Y=fsNiVr{puY zTDK_|2g4+|2@;aA((Nl7da~aq4x=FW)S?z}lekB}99tUt;qa;TyMh)xZjM^QD@YYO*=%RYoXASd5fK|s{^m04aUcw-!#ABVsC zX6%}aoJn!?F)Qc1bep_swUV-mZke5`uN4b1O?RNmSA;dSWmoGKjxq>$PqL{DojKpP zL-&&*xeMejK)cG~tISo3LyXqM?Ww0I}Cj^MM>B~SA4f-sYrMj?IxkiF|1 zi*ueC(@cF489eLiV;2Ty%=do4nG5r7gq&p&>)Vdv(5p+N&tWdIuw9!Fe#?zs@C{-+ z(l3E)!5>j{Mc_!!h zIC#~BE~fkIW+NXw3u#G1@`Xu8tlZ?|Cv?W7o!v=)n~8Pb3V@rS;ln?G0>3is#N1rQ zQQTTN+(qA1nF$)kSKxdSAoZyu^^LC)?oNj+16ojpYyt3Z#CJrSsiPchZUVPEOpiu* zL~MbRR(u{DHZZV(qrjAROKrqo#T`arhrwZ7J~uHDj6loz5>~@m@_8vJ(8r0aH&avr zAI7{D?7(@~zyo)bIsakZ*j2env7!U{F7?5sHGy-@8E#b?#)S~^=IQ$tBJMZg2GVXd ztu(tMr17ku>)kNw%$BHLHKeh1fX>Zz?VsHF*?SlR9->WX5KD)~0f#~}dG6OD#nYU) zXMMUleX*JefGEl`%m_Ip^*oq!*2e!XVvb7@J+Sc!zC&nL{{U!@T{Ae;@Nc|+9_a_|OU-=z0tqiEi|Z5)!! zr9SpT4UFIFwHV9%{VX=r=Y8y!T$UD&+wu+qMnA+b>;H7}l?%&xcK=ZzQ-c&OqXV&E zv~o1U1c4)MC7vX5v3FbI9R;}ceT5di!gWCU0{PzYZ&ux- znBPv5T9)$JU#9HG9EJ{v*NBhp-b{-bLIRx1SlGyr&fNoy9gi0PTa?3~lJJxdGNG79 zqe=wJ6U}VK44%XjQ)Z4wB%Ak8)!0F~%?i3;o;Zc`8-HXXwE8xPicwIcydRd|dO05T zDD}G7A0qrU6W}0-zr_5Y3*#@V+5^#aI9EQf0^mYGHP?tAdUuf{pvo~859+A!q>1E* z>q*@gg=bv7Msq~LxCDfq+)s1#3Zns_7w-xiC?AK622uu*UEC*=o7kuy==xtVr{xr8 zlLSX`CO7~7f12W2NA{>!j}f8C_dp_gn)W%Gy(uQ8g%q)2d2n}7ebK59zr=vgBdmfDOS z1*ZZDA%OGt@-3wcqwM_aAh|`-=rT~sW*;H&*9v1dG5XlR1W<)0c`JjGYo7vhz#-QG z>)3-)rKg368EABBgibr;#>dFM){#KXAz05xp5nRk)%}%k+VUA@0Svl*^G@2Yz=V^$ zEHgDS9`5~}5w$hu`z3Q->HU)=dnyu$vA1DZqG zJ(R9kAGJ}m1SCb=?U{VB?2!*5C;K{t8XBz4qnuyOf%3?!1s2!}6kGe_SkHs`F*=j# zttQi=S?YPEv_W;8ts@LKALrrl!|Dg)+$46b90cVM{RUIZ94U$S+*uq0uk}v)PXF;a z?TI`JqGdU+%O-Lr1cN*y_=qlz&cK!+Hb|{*&^8-M7%ufCc|*&N(R`eKD+2e3^=t)T35W(?SQa5EqW)r+`~yqj z?96@=2iN*iIx5fL$V2#dV)PjpM%|8pak(OtVb`1Wt zXg~S#1rX12ahE2nr&;E9t8pM#_X)#v=Q=n32p=1{wXiS|0|2wZ`vKW^^-iFChZ*5x z12Enmf4iAkDii!MZ85JI9!s1&Mjo5Ho+0InarQIh8Qh);6X8(*pc_Z8AtI@s=T8zjZ!|y7 zB25s_V&kV3IoQsc@PZ0FF!B$#U?u!fSk9ht;k|Wsa$IhCiY%Z2f6zCEvoPX^4bMGvL~lzSs4r%dh#i@e1p%5(s7nwU!_K~{Y46< zzKotE>h9lfK4KEEIK-N>XSeJ?nSyi-i;EE-9n=G!Tt_E67rU*lziP@E=h7F?=emC^!ec`FW+wNJRc0bFYZ|U`U!^V(R~Q_rO%yn+`idAoe={gxrF&0H- z!$^m^IvJ1O3_+vv8*{=8byJM|`?8H!PuilaWD|*2lvjmX!aBw%KE(a9MiOJw;*Um- zKlaZum_65q!QWbmxQJ^*8-%|PgS)?O90{S4W^iGVhJ}bW*2owJMquEwGqI_0bH_!~ zhD2mq4X-iZ(sz0e^FQ!7iOW&B?jSad za$2Cm7$awqxxp^ht^a^;76%GckI(RWf?`uOVbCvuZdLry6jUl*ZnhZ91t()CTvGT> z5;c?^Eu^T++LUFRM5=-(LJ0Jz*=Znd)iR4{dB_mrV|p0H_J@3Q3j^{UieN~ z`I!+{Jk(IuXO>s!8lhvhl~OK;#C_L)&EV4aO-mUJC5H`2D9bju%J8e`mfuUcRTq9z z%w7E|ObosGk5X&75H=mhsi4Mp!o~C{m~S`qJq{l zcwO%byC*rsuFqG#gnsz+QI(Lj`gB7bAo-SxJ+75=bH?*@{|uVHOxT$!|62&i!T4LF zrMBB(r}k_j@L5J$YGw9#12fR1u7i^;T*xv3_Il%PU!!M#!4WA2^?=$vr9+hwo{&dJ zy(2{$Q*S{_!FqY&1z4SKp8#W(1<(|;-*!f?aMrIZ)boTH9(zC(gb4ouZMClLjM*4> zIFxJDP$q5vnZ9KpAp|&jP@&5RqYQ$a{?Dc?Ngx?oFX7z{+_N}<(nL#3zWUc!`l6ojaRcuy*nQP;+dNk5y5U>`$rTOpf}Z9+J!G1R_;i!Vt;}OLy@mLbd$;ZQ>6EGH z>ivTPP|(_x8|=KCuS?jPI`*~Ds`h0z>w5ygTc>aD93DzmuJ*m`-Qjt|FYY!TJi+Pj z1lEqf&7mrNEL?NgWbhZs2MU>HP~; z1@&ja*8BrtxKxN`2?K^eXOpO-@9p|PLPaKpcbtF-G3c7xa%8L5wQ;er&yblmg}WIK z$^c~A2iiLe5q+)?VFnmr(pF3FK=STNi$J8ajZX{70qHR@B#{*EphH0QR3y|Ag*r9X z^~@rlx<*O+i#>e^N@YF^GQ)yzz2J*}M#iRL0RnX@+a*Me8RyeEPsFAUZ=b>s9@rwF zoj^3$7k@-LZxmj8tUVVN@UQ(7Ftd7$p;3d?_`A{hn1>S(M7TUhMoxj~C|iApJo0U; zHhcBMj*}$cdok8iK1I;n_hBKtIx%l=2OM|+(mj@#%!t<9#OTbocZcW|x`m6%j@4@x zDDfrAyEy`(Mu45P+1YR$)N|rXNCfy9iWd059 zob-r@(W7T3Tiq_^0DLSI_5!W^RO5e)qocM7W9qNT$33ay^irVnFBMEe7AvfQ&s5^SFxLmbhSKB~t+gyUB4fS(+=#}t8p=yN=C(cysoB)IZi z$uQUh0bUEyGSgxYaxNW0QR75~L-ZmyK-wH|YGJ?e(Zit9iYp{OXsX1B+5BBkiyg)1 zdt33D>2Z(tBq^VTEj{nb1JDoQ=kaVv+4kq2NB7WEWo|01_X`ro>T_O%xMDQDcZhzh z8g89&zRy1!S=T-!NH?7gv0&yz*BTNubNvQ)sj~1KdUS1eb`ONaPt1t)m-l+8pZG75hJ1OoK|{5LgA`F^EnC#tz? ziWbg#+$P9PPhqCQOO?_(I7i@ESkwHMM4{1S%@RA;TXQ4Qr-qCi0+BEEcV5u*z~}rA8L4EH;)Hg=7`c zLSRddOYVkxEOXS-wSX7f7Z|b%$1kZvvJ04!usyN6u_#%Ovb!8f<(m#Lv!F8|nwn-L zO^Djtj&==>jjgTCZS76TV{6&fC~VR*y5HF-w#pnFn2diZF>Ref2z%WeSx=nnz{V1Uo^4*~cF zI3r-L05t_@F#yN`Fc4q@f(A&Kj`wKuR{}T+Xi;E>AWIflBE&gliijB^9xyO%faw5L z5#R%49ykNRmCah0m?%g8NME%Wtu)Le^8n! z17bxGX@X#JMr(*QBG`)fLIWFtcZ3)c$Rq$rvqK56C9c#EG11e+juXKriq_Z!xlux4 zW78rJJ=4Xr=RrgJvoyV?eG6GlOYPkqUORqoO|Ly38`~?}+T1jj+e6==-d@QgipQTV zpU36L@gBw=U)SpPz2Gzp_c*4QODyKIH8tCG8gZOI6c_iLdVZT+G%5E{zF8>rZ|3ye z&(UeRZ`NtH?)&&2M&GWYeA;5uY^8)eDsB1m`}4S-*3e_{$rnJ*S_bm_eVea zL;v_9-$UcP${fX<}Ni zLRd};0lnij2jv}0Ij2B0aNglN?)qr9nVI;GXQ1<^#Vk!g>&!9uj)i#&=$;|88x}kB z8$|QWQG5^WTBuzMwQKdz9418j%rM`vI~}5bW|fErnsext|6I&;dgZ^MIY*9*)9m_U zwt}|M7TOjOn!7Z+4$WW?=pu|+%Mk$QEF(m--jSj?E80S%EmQ(MWHisezx#(se`i_-$V=a%6~HEJHFeNi7sNAhZi)> zdRJ9fG=xQW>Q3DzPP5dBewy9*6Ad+IaSZhFXvPc&w{lQ4Flu(=d-z7tUP)53Q?CI& z`j{G({VF-8j?yoNbAZ5($`J*4nw-t~k0)37P7yk61_z z>Y04UGiE$dl#co{Da}f~+LWGpGAgPM6EeMxyyM~Uk60o5a8*@5HbrEK?|fnSh2a;5 zX)v{De9L!SEP5U*FRRbH(toVshShkN?-)x5pU8%`@ld{pM!oW%@m;=$S$gHatl!fj zYJJ?6-uf^uu7~-KZ|EBzbkywerCplX^>AKZZzF%{uct#IacCURmrVW!;{tm)3>o&@ zDW;QRZzC^_J(5h5{gm&xOqcynjvODS)GPlfABC(#$KxU#pD9z=nP*M)Ktb>%;$z9TDH>U2xg7@f!~2zEYhfs8I?!qoNn1x-zyI-QfJ z4wRojbtwkwjNT+eiRygBC{i8f8BBE>SqfF3BJ55L)Rm*)F3@gthW$?7fbB^#m1MB0 zWAPnNTe-Ii@dvT9urlUEqigXU_nr={dXw+?idxjS2U@+2Z!Aizo|nbO2kOdy#@-`? zAk6AtAtZxZ{V2k5tG6R?6>*VJywS@_@H+OU2`amG^*Jp@ufF4ijy(O&ou%L={v6hQE){VT2O4eb%W2rl3wouknxR9fBy`bzl zU{KL|kGx|qs#*W}Qr4^F7}QnSi?R%A+6?`T@0f-iJ;;PkVP}*Hox;v2n54B%R_AQG z(Hc~?-Xlz#4Q@B6ZyhRzM^|zj)Rq61ead&7g9_DyI;d;Jw(s#h1_;-^m{K3_4o=L@ z#zV8KaWYfR&g?;D}|KM(fgo@YG>gtlnOQY)bJ0;Y; zPPc>_i65+#l~+=stqA%*QxpIYKme(DBub+gNaD0pqYsdvZi7K#U3;GlQp3QUV}E;l zZ}l3LGKyyKNG9C0R*>ik16|1A4U*2b8_^!arpJ&in<*mRhM+9MWH>-Gs^<-e>@O$&wl$Sc{BJ&ei^`DYtZm_nOjKD| zK^UPa`AwD~dKhhBMO@N@j6$D0?`}W7^YeFULG4@ zdT9i%De|H(cL#zFjqmYXX^9h_T}o+VbruYlL8(E%2RLMwl)hCr^J8hZn7nhfRW)@H z4LMOdDs6#tJ;a_}RgLuX@_YjD#C{ZzsGxB{$Pnwu%+x*Y@=`~IA~%cJS6y1iKrI7o zk##CCoiS4dauqnk;a|mQZNM3@Y%y{^t^0YjQdrSIi#ma}j}lywZ~)7)}AE#7a7 z?WNvDxM&@`C0av}XUvi4ZIL~Zd?(>+?Q8>zctRYIc4)hE=CN(K-OgA>SGpj8fDq)3 z1l!V2vPCwr4iv@2O4cl9Oc+BvMpr_3{ppxnL~5Ao{}?tZGBpBB^n_2GLWXRYwXdiy z97F)(Voq$pJ_hxAmnW!HQVD=~BGgn2q*B9ZUkKW2n-eDlQgX9ePmxgG|Q zZhFXQ8&h8;4=e=l`aI|n^wv3ZbCWCXL)C)1QG-gU$U@mk9kar%MCl{5wF?(V-dkdH ztr%4-o@nw_bFgk!xDFEgBk^Agan?@XBeD?ss$&r?)t2AZlv8XSG610$z3)9|D<)`Y zimIuS`~lSai}MH7Y!(Ha^>AOIZr1K0#ld6*I>8`lLxVM_H@S{jjkS6v zlIG}N@SH)>rhpTj=7zFDc3Fvo`rQ_D7~_;YZbUTDYp-qo%hlFD&?^-&EF&Om3S7(^ z#Cz~Z>7W&7u46K$e*dU1h<0CoLqDyYIisJ>dEjLG+{JdnQ1w_e#F6BcDOB3N-W&3o zrqSTnD}tp(ej$=F4h>?r`sAx_0(ayMc|$0z`WV?8f{v+sBCWn5#7$6vd&?tHij~b{ zAZz;=lsgaCbcucqObc&e>HDG$v6d(gZ?eL+U+^35p`g;`G@Lg@U{MRkS8EH22reI1+hyYyhO1 z9t;Al;50S^d|0~SAl)asQV5NuthQwDA{%=d!)?g((m<3PWV43|=x-vo&1tkG2!rX^ zI0`DLCA&~iW0MP`_u+N{!=VtvFC$h?QRIWTe)zzhiz5wab`1tlAw6zFBF#dtTn3wF z0UhzI3bn3ecXqW)J*g7cWalI0$i;K5Xm-f~!dq!&e-)<)wCAY0$6GG1gHYA0T{(zB zg`V4$?t85^!+(ms_MKu=4P=^;rXTnMw-F5l6D*i{Jr6`NQ6LFUP;A+OTjpSfY+#R| z?r-1PfYuy{lBhp}7(#y`iK_oTPT})ZY%sH>gVGXJNdj~4iP5TouYq563Na~rbT1C) z^W0sq&^v}(!u!Iua$?F`nR0)V7o^owBKhgU0;_`WgrRsR1m9(H`|?E={8(A5;+wv~ z8fj-+UV;QEE<5_ZiiDf^p?YP?1FWk_E_?h1gX|Cv7!3jwN#B z2ZPI`0mzJ9@Tnw3S3YU|tP!4BQZA;1@kJ$(I)dGXtht1N5R#8hAh6NjO~$syZ{S&} zP3Uf1IO3ev@v$G7_tw;FE#p?kk_z?~vtI?>(&taX;ZhF`J(NY8dhlT(De+SeH|W7u zE=o}=D(7tv^i~eK2KpKhUB=`z#d(lz?yx22VQqDk+MEaaUZ6mtyHi>0P-h{w+AH%@ zT^!)h_C8EeiIiGom$UkJ6j^n1XG0dD!zx=d*&?^^8DOCncI1BJm;q7^?ew6JXIoo6 zPtUgEJ0&q5V_teYwKVI}|7p9|5jH`mu%l)hj5ULQC~`{O2STIZ-L(7Qbo3*vk$)*B(k7xWb)wVu^-H}$ zdh#0eB}E+~dP9dbD=CgJoGuDSMoorzb-#*m|A}iA6xr?uvTMu=8}JVB1vZyI=wO01 zr7-4_q)BS9<7R`@bms1d;s^mHQZCR&@NIZOPw)7Am8w}YPnO!-e&*FSbyrC9qNG!k zo0iUiFU#Pa=3>gwMZb1&JaYRkY77EB@$krdrsO7?`@|Bk^*u5cdP6NvqKwh9-EsHKosu9POoan9F8e2VkGeFNk=wLj6(*NYUH=aRVxab zi0p17490N`d?>#)zb^5B%bnY!coYHgj&NTutOn8^d=rp?nD%fLLxTvlhoq4vq`!uo zcm~AG>aXn(7m0r>LFw1$RY1Fkw<+i}vs#q8iMP*}zo2je7Qcyj9{Q=>xm-#hI(I!;(ql}qR6i#arCOVzJ}a;URLtKD-@yh zei%MZgxs~Cq6TP97P!ZfqFEHh;XH2x`@})7(Hw?nj8|nS4kY5fqTS(zJBq`y*Hr%# z)4B>8#;L(~rDi0Z8a|{3Al|9r41GBur`iY$#Ny%J$=|3ka6GuVaJFR>loWx1-qMbf z$+Y4h_rwaFQJ5OLK(Ew-+BuHTi*404NMc{zT!nzV!;uL+Jb8z(3eF)f@9=b}0Vp0* zO#7(*7Kfr0PCyrjLn%E&cyZ{9Fp-;yD4c{vRN^_hJF5ejCD82_G|riPaA1meOUyj7 zwGS>7DHEw+{5V`rg`q`(iWi5F2dOCkwLfRe#Tp~b$6e^C+ka5U5nL)X#gXS!a?lQ1 zoA-So69O?yL@^1_YEoL_#s+AY4Mp6|O?xgE-uQm!aVIA0LwSX~NLnAR0p{9nZGEsh zn!`bZ;$c>fpnNGT*@5;N?d>Z8pL@bhiNjs6wUh9^yuDsj=v}S)VhcMG2TP+N4%^pZ zG{Z4wUmZ}oK?UQ)oq9zPE3 z)<@UWCyt#oj9bH{A#w_Biz|jWSR`vkX%1yTi)pX+Z8vA9V^}?u>KElnd@vRb8%+Pr-~>4Q6j%IOJe2}VK^w>y==M=0#BrsCJ0>K+%0Jhs7$ zjmcu}>-4e@I+hU<85)hn*muO^l&xEvf7^{+@8}F@fI7(P6RBe(nzc!dByi$;A(ym< zSdsM=a#NTKXC6z|dNWbdmdTL(8n9(zpZ@dTh$LlJik`CV(#y>BhO3oAgIyHP$ZzFu zOFgW)M5Vx&HGog3W)$lY?1pd4S<7fVr^LtnKuD*=A^sSuPKhu7pZWl9?+bIVye>s` z9k8$EvipG@SlMG1>;$}far0W1F+J#zJJQbdFuQfruIT|}4|GJ(HenOz9)r6O&cl{e z!A9`OFZjZ$f;#&h-Dy1pzEa`iWs5sl>K&O$V=$kN9N5AYYW26>9e(O@4)35YESD>k zB&`=NWsVkg&HC7227rejB-v2|(Z~#8kBC=CQgQO0S=K1c#U0){$gJKm0oP#`uj_my zu0v1_CXCjUq918xji^L`~WUf{2P5A_Bu(-E%T`+5aE=FZb zqkBGADg0_}^`k8R@>D-U6rjSXAGI}s7i;fN-V+2L9BaXVPN=~rPMQu&(}Q4v{|4tH z-6FzD`eOH3((si6lV#8okaR~tB#LRZJ&Dewfjsq^Zh;`?xyhl{q$WH$T`p0}4IfjE z5efMhOv%h52C=%7Ob8)nTp;ZGl6j+qb3v?5{4g<24pT7q3Y<<>Zr2L)l$IkOQX|~d zC4I2fEa^Mci#)EQ1gpyXv24l4LV&nkNVVH=cB3+|;CpNVY&mPP z>kgfkU~4$4X%w?tH|$H^lHj+Wt@1sYGHlNKb`2NA{$H-hQo?Z*?XL~)9&W>3aJX*U z25EW>gYb#|vjC-f&-%B=E=j4>pxtMNeNK^AC3Q(uy%yCrIHFjkdTN44Aajt?+;vIj zFih6<8f6YteVJVBw3>Hw$B47u#YVG=r|$5?{0gXITkdY@Sr?;wd-_-a1ZX3Nfmh#( zbF6Z-M9!xYjVw99_9B4#>6#?{mi7mK~1d(`9S!PalQIaU2(?1VA z25hkcA?C}H+du>RM%)4-^N(LOrD0lu!jREOb>X4q*6x3&e%vkC_SPi5Tlmr*EehVk z)X`qXV0djlN8Nn9VtWr!()Ljy8SnNG=^c1cK!hR~?)<1g!O;)z?bV+-gmD#XEqb-A z4*G=x4?`RSJR}WJ_j1HZT`5Uxegas+> zDF6Pt4S1AtgNyJ zfPsR6#Syv_l^al$C>T@ai>IRf2YJ!~n3D7|aA#{FI^lIfsCJFlnCTtrAY6+{)j<0faL!)(z;f zdjNGYZXzYBai01BP1J{V{iOMMEV1aqbQy`IPTr)%bPwDg`4FdAvI2xCU>WJ&68NZPO)|>)JgFysRXbUUmaAxoTsdm>CFrkSQ2h zOx|zaB$N|TofS(jO~R4m(s7PhQMcXr^YGF-i~5pgx#{ zjtbyQE(@s4OdC50bzI{~%1)>;<34jwd|=+i2}l6Vii0vjvgUK`&v$E(D2w9zbP=EF zruSGdpIAOc+VvLb&cqS?n zB(!oc*`S{+D}Md#dqbdO=mSQGywax?C7Y(S76Pw23Y1o3)=BZy(!IHMhZfk%qqy?A zF%gy8)C^Q9K;$NOG3*DITdm4B5(ry0)f!tlwK}v$!=@aNb(oh}`rtZz1}T34JB;!& znOxUW;f;a}>;OoodJE=P@wlKd5#IiF^QhvLFw|eFo{>qP^1Z@jBRiQ&QV(=hDR{m! zKG#y78d+e7@&T{0Q(u7eIrb3yR%33Bz(Udw6a_3u^-?#&AQmt;fwJ)BIAPZgYLy5* zg;o|xsJOr~D*_Wf$moiP`-5_x)M5W@0Y7d~BHlq8#e>6$QNO`3;dn>7;4tat#w-b9 z+MaqD1JO5U3zVeQ`P$N3GxP$tEDiLhD*Fgv7oX@Qc+%Ry$8ZKX112sz_aBh!21lrp zr`Bp%Op)HaHK5wlFLH-k_%&BT^g-RMAR*7nXIX@%*0_8bs{Yy!%76>#b-6&QWO}T6 zxz)9(Fhw3Wk}N1#cxRWHm&c9>_xB9Xs{P&qN5n?4!9s=GdMAmcg4>_GvFt7%O}AY_rGIRpF*@H4>A06zo#%LKlE zYhDhtKEM8!o!{EG2-u^Q-7Q3c?Q3=a|53(O>ddNRSJvKPguRtjtW5p%^?ZAcdp4ous3rJt@i5Y-XYx zO^T1?LOU|0+Mbl&ctKMVj%Jg@7-D3MB*qXUEV8qaiPI}%uy7g(s6T8)ZzHy%r6LtU z=4hkEqU9MM|5@*-qHmR43@iWjwUp~;t*xc|IbW|{cd*^Mxs5i{ugMa%^F~MIo;o)t+s}X3-=mANf6hnQc`w!X>t9x}Hhx#uk$3rj`*gA4T9q-~SmWET ztkOlk{A+FTo65>6``usTvqsgMHST9zIn{sKuWb*RYG!-7ZP)v`)uQ}Z_iahHn6K2S zY86gCSI3XbusufkHKRMd`&6uTSGa%u<%@g9I?niP&d`z2NIH~!rjFEOmvRIZeehxk zrT{rOIl;Wlmki`Ea)M(>h}B?nBok#)YU!kW64e}0Nh?t$eJD!$P!tNrhoS(Xom4uW znp~pC!$tZ~AV@l!PLLnz#}_D`{umKL29fmXPn<{k!;;b_wR9*)(~>qGBmIe8B#L6> z6MgiD<%~`grIADxluhKM2-@_I5umKN6j@1$qHLm)PE?aX{TY&?Oez`=D1mD9`~X4h zlmwEQDC1uAR^(p9D^gL5&e0A}JR9lz&;?L*~ zS*rQN-??A*{xSKu_}e6O{4+IoZe0l7{MD;}{A--*v7aM5i~n1l@j83og@66m3BQ#u zovhQHwV&yXZhz@0l}g>6|GHhD{QGAw%H8hu$9-+z$&RW=*t0e1mVEMmeAur!x|G~A zRg%g-{&c=yL&yG}RH?S_#d`7Oh^Iq2t;C8(~z8Uc8N0bUkR00=}O0yU^&4;W|- zw84rpHsFXI7*K^^pJxFr+|U6qe1M4;f(RoA8I2%<0up?2f-Cd^kX&HX2#x|kLY6!h zl%NGFh(Qz77{nS82ZqQ&kR3FD2OsFc9eR)k8$o=qf*-U12nGlOrVSgku>m1uF$6*k zp$kL^qX-KyLLiL5a)cg8*aQ;7kOUt}xUmFFECCKo_(BtcI3e={98jP@6bK*%GDyJ) zQ&czsDXtuve2T1l`WLPg&$o2;)V5v3V`7x3|0t34#Y5oG1!0%O3I)C zGeqDF6F`Fm(jbN!maIVu8=laH6*sWFAq+T7APxYK!xH4sf;m7spws~nbx>l5&K*8+ zhf3h#40%994_EAA3VTT5!;U`S@Ph#UP>Dat0T5&eL=!<&U=TP6Q3gV&QHV4QfpUlk zAOaAGpowsRB7mR>A&c0-2t+U<6OCAdBMR{d6-X?JL=j1}K@x131QRInsYGN6bqP*f zVhfm{LnhLoi7;%U51a@@C-UHlCVZknQAQ|GMDa#(BaHwU;l_;{I0CJrDFW&b)g;=Q zl@4#@A{D9VwPsfob z{;6!st@V}6crvw;EjxU&t)ngZbzQ2ndZ+PG2p4;dka63#u2QN#r)nmdla*enKc&`d zs;fNr(pKt}vSx&nHcotC)&@R89gqR1SH=(m>Q8Fg36j#M_7E0M0|9Yb35e55yyy>0 z2T1Di2?1b204!r9nVQWHOp?ya{s?hC$)ov^B)N0}@^n6GzW@MGLBxj_nD~MmMow^e z5z0UsyWw<>d|Em~y!dD~hR6gW^92S0F!&;bMOu;oz#{;#iAvf72F7V#HULKG06$zh ziAyJG5=ah6oG(Ctu>cH?yjTDPA(&b^DMUd1NiH2m`n1N=EMz2#k|;`|C|rOVNs6HO z1Js|5b|bdYi&VtXSM;L)GfMUKVC$`~lBrAjPUHV7Y;0A($vDQRPGjR{tRa2tr~BNl z^B0_SpKI}P$*pg4i}qDlS)IMNBLBSTf@|BtW~SONJ2nN|m#x?8d*0P% z&5|b{%J=12Rpae_m5t56$lmt!Njh1rDmMNd##23mFLsl9&?JBo*49 z1nLiM6%~yK)K0evR7Q4dARec_=RcrH= zx?;$pTbEQd_5J*#Qc|*Zcgp1ZaW=`#y!`5#{N+13UtOGB+xfiqvnsWc|C(}fp0#&- zFs^+wq%u+}Il}oSrJHgODV>mQCd#CUm15>Bs7Sy@GX}=ByuchrPH>D7GJzUQGCVxp z55-Wo>cpVp3n49? z%@@7mQbY)*1Wis(FfU1jgeuSiFEpuDI-dkW0rlq$Pof-6O`0FfqPG#V=qut#@kX#k zywROIj9)9AeEdjVb?sEkw^n1vTiEoRh|0V zlC`QPx6%35)<}QatQGUCVyYsaItN`mKmH+w@GV^bI$HnO%_wcHUfFAhzjAak`=^BS zD`ATaPve#272V(V%hs1}tp94THOBQjlmoDly1( z@xkeMG(TKAj3t>YTt1o(C%J5tBqSV7%LLLS5KAWoq-mL)NIjieWBPPHX-97)ONS;E z4NWm|G)aq=j>i+z3QMAzP3Mzh`godh+LLJNlp;LLFr(H^(}aT;7$VJ2G{cOvnJzPZ zQp(h%w18rYK$`SPGW)}_(SSHCX-E3Rlt3{Bls225DPe&H7AcSd3{Qth2QVn`0g{(1 zIuO!?mU4HIa(59tZ=@ECAaC^AZlqk>h`|;;cHW3tEtZo`_1IT+e_qS#JgaVhe|%Rf z>p_-~9hht@Hn$ zabc6cZ1uN~4By7bSW@-W-Kr0+l>gyj{SW{1wm%B#o=;M zvEzHI*8lI4wK(H0r_N~0R$p6Pgz+-oIjQ8X!}=zhd&Xx;HY{DF9uNmo%mDQ1j|VVg z+<*a1jCddv05vqR&r6U(2U2_hv*ZO*H1Y9)5C~GQxOiF!$P-g`qkYjv&MkXzwSDiB ztM^OWi*o%NCwxk(3RzpF*6wGE>*D@f*RSZiY5U(+H!JgN4>Ih?ts`Gfmi*V|Slfc? zGb&!XzO8;AzxL~@$3Fbqu2nei)sxwXhuy0xfmJ};zjItJT&?qiGU&G@_G0!nD4O`Mw4qgPx&Wt@zyKfHX6%q~5rIJ`pq%r&B-)bVe6RDWya;pfehj;!4LOWTgZ0 ztn7=JMf)Olt+5xq)r?6dAA4o|N7dp|%dnf`eT$Ry1L!*({Yeg5U%FFJo-W%8KD6fPYZ79rKLxorjF1i>) z3^1&4g9%_P43HSCkPal#lUZvcxDiJ$x{6ffR`jA3F{=if<79V^8!NeMz29cPtG8BN zTARAKjrN3+eM(l3DPDKRc=`MO9`l`QGOF2Bbaj6Txut4yDM)!i)nZf8{oUN#F@tUw zRu(fC!woDj43ITgs0x>=2{Hze5F`?K5ylZBMTT$z0v}1GK}0rG(^2v^+#th^RuUTq zNDNx2iWj60-L$5K!;(Oaq&qZmSUez4D@h+6oljbcpvXd_2T8i(lX$nVB7H#pX{u>S zE*ln4k%S~BP)tE~taLz$c1%(Fti+9{l{Dwmyedc?P~uJc^d`8xlro6aWDPQrPU7^ZlQNUb2BevqjSkKyrlf$TOfoe>Tpw=S;fBL_ zzy>ZvlT3|}4u~aE%@LKvQR8Siavbs9MvX1Ujsh6W?X6SEE;e@A7OUITWZ5YDWU)&4fAL(k>uPp+b@iD@EygnwWinAF6J;_{W@aoJbOfE7dnVOZ< z0kOm@8q}W2>C6;U2gK==7AsQP;pvss;|dp$#?#SwRuV^JMMA>Sa5PdS;`Vv?rzvX{_4n#0^3`3<}tIAuU3mPxPd$^o*O8_N11M7#$Fd*hWw-T#<|Rts6IW zZc2Ent`v2BRTsbcwSMds$|(QY*7l>C@J`0)9If@=KHKV4eXzyl-duP%*+%*JyH+W6 zW^TPt>Xw)4__u0xb3wIo&Dztwl?`R%#xgp|@x9jGRl=$!JZo~jee)bn>`kX5IXeI; zK5-)k`Dh@<#~Lt%0V2JLvm-_alo}L&EDT6t0ToS=2E`veFc*M9k5CuMCDN1v;&fD! zC$@{)h(&6n7je|0tEfdBeKGnJTyfuf*x>Hu373oO?7j8b)QV2E_mqEIPM+4-;46$9 z6}#S@(0x~3?>Y6_o_5+LXhR&`1mmDKwx3X2%*ijGW-mBBX#BFo9c05utDZ<6cnkqN*@* zf`bF05j5e%l~3YyWKyD`X-5hufjk`|QRDfsVx*}0f!l=a|U`~ zq9sYk6f;vaGfC1K^ASKmKp+$b#NslsWS{~uiuE27fB}s`0Q~C?p&MZ_p49wI?l>S^izK|OK~&eTK(TuAhC2&n0J{;xGgSU-%-Sl zuN}B#TO%soGgq$sXA^qsR#pK8D7Lgc6Q9<$Uk{#rokfXHM7UpH7V;sN;4f4rx>q z3f@#`+Rr#7w3b~7(QyFiW8z>5emLUR8wJ13rBvWwP@3@W#`_#l62vcR*C9zl_+i3? z?3+tjB0R~IerZYM8`sBn-m$L_IEl6}U>P|aE!{z>!T=swy@r9}HIMuTD)}zf_s>?f z+6-8fSZ|Ej!u8~BpAC3yj~-V;a8L@-8eZTXuXaRbVWpW#kj)0mf~A%Q2}^ z)aW|vnYQzcSWgmLIE>P2$`b3bb!w~BG3<-m4Bz=VNlvPbYaV54xN9bVc?VqUC5jKnbZ-0_{~}UsDVJuPBU9A$@3fA@Nn4$ zl73W%sQ6c1L!jJbsUlc_6RPJ2QSl5mamkV&$B6Ci^=GQH5ScCFT;t{EiJo_f`%aBLUh6N)nnMWK(JrWQo zzxFcVF(Q|UYdt$Nubad4D1y;-aWW2$=ZjthzoeR#lrv5Vsi>7GcBs=57UK2X{><}X z*QT1O+%(ws!hg_XPn~1_fFGJH(XH&hgbKR(Sp7$Y$7tNTk|5$?;Eb*Udf)1#OpmAp z7-A*>=xyhQjnW4X|cG>7za!Q^e0NV>VelMB~U*Rh?P!~jInM8*eu4L z-wy6#jDHwg?)A%2$F`p^H33Bs%owi%-b+GGKtARUQx4quaQnRNfSEDAG!HXuzl#30 z_;_=tcf{4mZrgCf7%P6uLgG0uZ+})!rGxX(+4ybdX5HtN(cA&vpWq3$U z*AeDR(R3v_6VG%d@02T+8{$hU=*&sZtCRVRN<^%#7yQDByTAv&*+)6Odq`iXHs1pYr-eWd=wCNnNxoPX+5mEz80@bs+W=X)_9v z%k=X~#R=4uiiNyBj?f95K;J-hF;|hehIy)J3Lup!oxl|UB)CSTdMHl~Zb&9;kQK0H zYOmkY7^NJWTSIKgXdz-@;_H&OZqjDS5smu2qdL6bXjfotc zF;aZ+T38Zx1B`jmst5KVz|aXhvG?&3T(pSlw0-`&EGI=2sSHo~oL@VVfx#}n(EtW! zi)>8i2p`Ae=+9hdVS*07JR)ixk8w;T58G@R zO`AC2I&Hy{O(p$GFu44W)sxV(u$z;)X?+chLiADX9cIpm1LJ*oz#+qgabRoqdN8{N zs_?ajQgq3Ao)4Wa(pqX*%x>;ogBjPlm}1R0z1Ku)^e5Xp zr-lz@H$?Nr0+=Am085J1`WU4A2 zopg0I5(%I^rKF@TgHaubseL*_ot6y7?q><=8n-{bw(ZQ8cRd1?U$^^Sx0^(%#KaS; z@9;-Sw5Jh(L-CAQ(7cgP8f>l-D)lp}iz50N6s6CRPdLs#UetfUHK14#lNu@KN(B{Z z$Pep(+||h=J0kgGssqK5jOH@Y%Fn-00Q^)a{+6 zc`&1|F13J=FDoS#=sU+Aj(KLO%i{{u{ebu_km7UjaU$lug;RKNZQZvJ?Rh&Fp`K%% zA|ozHC7Unk)-_tIU%5Ri@!#bu)~&<-cDj?|V($@-pTH1i=CwNO{M))7XItN2l zD(GH$RjDrC|oG?A-C6SlQBKdL@1Z@QYTnTRSg zIhmXIY6`PT9kI-J7|m*<>gltQYrKF=%1#pp=U<3)k~4DE!t<^E42+8^;(8dHmA8}$S|G-vK3cf>2c z-8s77nbklFgkJ;KTH8_%lX4gswGtIoCS>6}#llG#+}bdfVrK0@!8<4|vekG;pEMBe zj48scu@5x|sqzZRXQ%@K$^GQ3;E@tEyEzzJ@}WF`3EA4y!T8f6@;-?gcT{49w~oj4 zs+7tWPv!`>N=x10{u&4#1=$GaHp*ZS#XZS|XwESjkZ#~H5O`lyYvj^!Ua$gt3@|$% zjcH`-E}R;A!>DwacQj(24%gVCqNPCCSq8z(s9W&(l?Vt;>H*6iu;9w+fh~D!EUtGI z0dJxT74=`@rT&w86QHo}>F{1w7&{G)CYNDLJylbNVV82{y8ytSH_dP;K1#4sG83`#Fh5hUgVD``hKpO%QJ#`jCjLW4iHUrv= z^%jQ>qZ>COv1aP}0qj%-8OwDf%9mL~a1mlASed;wI3@-DOQamdXdz+n3~&Lg#;89` zh9}I_w|~le zG6?u#p5o>)rv72L4^(*%*>0ftBjqIJF*I?QnF?+c(fwiVdTcErWk?)0Z%4`l`oj<* zh?po1+~=BRFz?N@e=)Ao%zEK{;um-kxtzSJ=5E zzn+lp+2RP(`awt@iLurAYSpz8Xkw=6YVUb35Q-dr8ix7EeLJs zisjzMd$|!-HJCEirXl-Umz!0w9whI{k!m19-r1)IiL zgVUHbfls3*Y>_Lbc%(g;e5By#oq1-B;i(1+t@3Yt?lsX@gPgchp<-oNXT=H92}^a2 z)u$Fbt`z3>_nKdin02T%S{H1Naf>w?yD*cHl% z7OZ`y2V%H-k6n2~k^!ra;?+pB;?dUgIy6%3sHF0+0dj4y=o|T3$iEc@h+NrAmm`r< zOZTRxE|yuz7*gb(&}2F?9CWM;JklI*j#a}U|PVa1x9;rf`q!as7 zLIZLrlE|o^rIKk^bCMSl)tR>}Yv zjiNl`tV$UPBcZh{U4y>Q%785G`uqO*)dJO8%s42Db6{Bq&4j40$PmCwLnvb#D+%qU z+Py{vRCX`Is_ff@)Av;c+yq5!b#LjMfk(D7D`zP2*L5ZMDtrMlx}f# z1*8sFGRb8D8Mmr8RoY$@IqAe1hY5+4Uy6#5a^g1;8KKZqnIv)I#|1Pwu)GNA)2%`d z!>AKb)uj2NllD4SdGMM(h{{^(afJw0=kUsnv6|oRv8ri!cqxH8AkJgef5GDy zzWG!O6=ENwE@72VIKgAr5j^&X!KjN$93EiQC8FaNqL0U_()Njo<#7oqztN1VaHO=x zkw@5i>^lA=Riq%iEI`*v{6k&5>9N^@RvRM`@+nHs<+CuK#VkA!lZprECOUa-PVD*M zZX3=8;J%r>H#m*Os?j1f>g{@e4F?!4%>FC)OXFb>0bID?fCxxyeSV13js+6wAil|E0!$4&mTccdoJiZo|8H$Mtia3sU49yLwN`jt_*mEp1BMj}AIN>!0REh&d zE1HrJsY}74P<2a3%Zzn!45hP<%8U=gEDlhOsgi3w97j%Gk)g6PtgjiJMH_>vQ?oFW z5ySGvp8{3mOS8fePbO8&D3l#jWc!xBku9)ciYq3TvUIFcFM&$@{!s;LloTmLvf6Uc!|`fc5pKcxtJ%s6#77NC6WMBl7+;*) z+mM-ZdeS8k^4)<>b)1wqMH(Ut5VKgkrM6dw$PZw_PL45GFyDbVMe2ab`o@NBXwNhy za@HadDaWeAb7)l*Qs2zLoq}(&Wr?LyVe4xYuYU|SZWzZm)zHUCH8I>@KSFj9GWJMc zpc$3L?zibMyU1YC7ui<AU%wC|I@bxt4M1(s&vt0o;SK+lVZe$R>49HuaFP{0W# z_e5`g2h(CiM!E547Q*tR=<|(wvCa%&o2#irP8S22ABe*GFE=4I=wzM)f~Lu~NT>)$ ze<%$(Yy`SafO*d=fDl1CxL75*ZFz$r1O=BM6a(jN^bO!DaUqNTe-phk&tqjbc!Xum z9d~n2SaDj6@=FmaG;oFSbnZ)WMqNvd9IFT`cp1u+Abc%y2wQ1bQH&}NtS5x~WSHXg z8?_~H1JgKtdPZ@BnpBa4fY2g}U$`|{-Mw8R^J%qAoISS|+G-TKS}8(~$%tfGH~91$ zk_q1cZ>ILq4urdAa@o3B2;8-yqVaCf6uW6JMP_Wj}U)Tfzzqp6!R< z@w?#p<7VjT?nO#v)?G7MAw=_Kt?Wjo(R*ZJ?A&<84JXk%O}obmPAas$Bj&l#W3vts zm6ah9{Ugo#kgZ~fDCHm%+6e@z6>2AR`fs;?KOrG8w0USu5k+4a97_Ds_r=i6$&wr@ zcd2Y4RSWdUU0Sm5MKee&hM3rq(?&fFiy=uq9$w)uVMyN+Xj;)KNY-Ot%wjwjbnSiI z3NsQxYlXE#NK$cgeW#h%@o#yhhvZq;hk#2G&}OvaZX*Fo39G!WOkT{?5o+LlyyArs z6wM+tqM{HBb%#qluVG6QMg{Bu4FXFFizQ7qPR8P zO2ISbgw_UGRb2klCMK1AXOIl5hA5F(8J6}~h=NtQ9Acrs*fA8SKO4q(hNY{B{NPvF zH8{akLa*FZid^ig*`lNw6NKbCd^vX&gc6J$ob?iY03liYE9A82B$#L55q8!trAS`I zO4-FZIEsCeiHtn!Cx+9tP}o_mFo5Rwbx_))s{AOw1!Q=A)h8jn(Yx&bE*r;8L1Eqs< zP(m>jLva=a%wd@Ba~P)k92T&!T@J|Hz8KxV{#A4^6~?ZvM~JTzwfe8N{|zPvkqk_> zNB&^J|4E^06q^8*0G9wf>AYgINoThLZ~)+bAx)%P#7#=OH)^w-x=%@4%URwWE@_sX zbfs*Q(rmXy+Ztd7xV#oH8wj{GdOfQ1=?2UaYQW{fA&CSgy)!1g-K@4%N$J{_CSAH< zOf$!HPlPp1YT*(M7`VW;^}sxEWlobi-6{l=BDlZ=32v}$eR{eDph)DPuGMfDHR)`I zb&I+f+E%G4b(4%ZV=-cFE1V$h>q~8G92&RDVG8zIat;sAqr*?pwsswhVRe}JC_8*B zp6d=%<3ALQxUOT-h=*y7k6>Cmo+xQ+9=<)Ohj+;D_VCB}aM`7e-;#Z}?6Kqg@QvgC zFplp4#5rDUTLW>uAlC6gY(K;iVhTGxE5tm`qHP_TL-zsE0;2muR`ejdGP;E=)`Og0Ksg ziQDkP^u#x`Rq%yTTnC0}ii5SSVOZ-;VwkFUg)vN5oJeMB>sn=)#kJoVKkozJ_Wz#$`kyQAiZ7VWvB1tAGvnxK7(J?Rp5Do3N`N+txQc<1;S1 z=JmD*?-_jb4)%lUUTt|m=#g!M`2K4l5T2#`{f=zQ8`1-Qh>8jb{ONQ=IKrEfMI;5JHGyn5ik&0oli=RiX){#WjJvys21|Ul;9aJv(Z+*=&Oc=g##M`vy`~TX)BI)|!RinTlcmY(ZnK*OA${=$bdBFd7N^E$$7=DlP)a!x3?A2UL6XAhoKtkD@KPO*sDHI6nKzA<1Pc3BZlW5)ixxF> zG-ll}@dD;4)xA(}Bx!WLRSqdgi-*zdi6l)|6L3+EG%8y7x+FcJ(>t^Ews9)-p%sH@ zq1hQ}5XUu!vyrYm=z`#(TX(*onNlHVU%=@4ze^C62L0C<3EVP&u3;a|=8hOtHgmqw z&gl|Juu(&lqF^IFp`aNmO=Ac(l$JmX6_B7GV~!*Q2axWwCl}v!OAz5eRsEjH>lmHC zpK&`&7X<8U9NiF#`YHU844h$4rjy`IhU-G{v7~`xcOj{om)Ua_2`0GGN;o8o1@wz*Bc3k-d!rAHwno6K~OWc zF|!Fd=E@PmJ$%UM6VzMX^oLyFm+2Q>pJ!`yY^f}0)X)udoYv7iHq$DottTlE-J-$u zMb*#1DJJXg2Qk@OkV}JRDGy@OdLdVTOY0zpgDHqTU3cLrRl1?{k6_!SBHX;c5wsBc zN8EG6k>BDGhRG0`ha6W*NNodXKd+4d?~OQo7DI+{Z06M>U4@D3Z2S+5-EXSgRZKmE zhb;wc%~`??&p1rQpBLyMCLxeZrz^7M;q+2J)0aJFr!CB?)6t)L^v97XdOwbs ziH5N2k%$*r9hvcqYji=VzJFcg(|_@5;*66i!sy}1+%P1ut<@Aul!OrUn}rqLeT33j-dn?R23YQwXX@Z{D7;G>>@6pOFAF4 zIG|w^z`M|N(=1h*+TN*>%PN9e^;~6A$d*VaN}_VdNECJEDg;a=4v^V~1jTu;a>j*Y zwAYAgqD^6C9N4Bnmz7;mCbJ#~6#)9I_p58{h9M#4fJVm#Q$p`11eqq_e3^s5&|3Tm z5P&r`zeABVto~@2myUHxr*JmMkEhsfPDr0(#0@Bh{u{y(_{q|FwIyx#f&NWl$oJCF zjV!$Nch4(i0Od$0)S=trAXyS2ssoIRH3Di9)AG9qOPO85$A(IL{~5Wx;tZbJXFj@I z8W?Rqw@m{xQyyCw0qg_-?dAt_>c&Smv!K*S+r08@4Z=OMfk*b%u!#Jif}Aq(f9Q-3 z^sPP73RXhJG2{?gZ@9j5#)B~ufFS$qt_f-r{i9G!5iy$`cmp?t*alb+vDT3BS##Am zN~D%rSmYW2Y(jrqPZ1vEIDjb}zIsnDwZp^w9k$&N;$et6n#>XQ$p;v zdSr*-(+sIDOkeJ~v;hE+{$`VbWAl#{9>V1FgrK>%8PNm0z$w9cVpiv-NIJtczU&Bw z7zolLOHd55pCI52{m%f7h$Omz_kDZtFK2CLeItdUwE3^weU1ie9-+a>?}I8d83SC zg)i`F!kZSvb_@-?fedP?iNMnlbq)QSCTtAPgnEYeShB{t51HXMb$Y|;=l(epHc+u3 z(ZEn>wIT^Igfawz>2JPgCiaS|V5dpD`C^ViL>31jm`7_d$c1?k&QeU;u|;1*Rf~#$ z4=10j+swVQ4{{(g-fn=yH>Io%rh=E;3rk-*Sao`z$Ylhpg4E;WE1fk@INq z7^sayhH}kC(yPs~3^LB0Ag_EEn>$n}tOEhOCj!a}I$%>!UeTlt0f7fGA?1L_D8M8j zz#?We48XyFEQ1T1>@$|E=Ix_K5Y>oSH1u|J<>powbvZt)U6PwcRD+YoabE=dgkagaYUV;{~k+vt6{U|INpYv2_@2b?-H!jn1a+HY+c8H+Uaplv-!e zv1Z6BH)O6E*4oTY_xHNIU87@6{Y5CViKG&0DdE!6k)o#{*V+n4^x%jXjnsmenJm7b zrl+Ecq!rV$7L4RV!ALHYW&OzbFxHQAkUgyB^X^ARBUk3;9CI_v_WJNet`kbz&i(B{ zm-kJ{mUiFCyzPXrHEs?6@%(3L?byv8`&r2RY^9B7v%6-7Ik+FycVBv@jq@p6D*|7re!T~ZfCnxBXAW{I4!er&g2};bgWS&UOsw)d- zQ83c9sEt$?$6J;Q)!{56E5<@AS}xUpOwx_9CzaPl(dE^ryP!o)Z>L4AWxw)OWKokO zZSKb3Le1=H^J!*JWcIxII2I*0qfJIQpKX-;!mblC&)3WhHtEhxtz(lhSIgMjN-w>v zLfbqm#|WW(I~}usP3Lq!q2!+8A!50#Ubub>}5tWZf=K{-(YB83zxDIBQU zNRrAbvZjw9f03+M)l>I=1OcI-7$;JpYRUWsC@6+U70Zc8Q$;NcO-@86>qV+6iLUDY zW^trytq1~Q@^pF_#a+Klxz2k*4s z-0Zc++!Rh4>4nUfwMV(HoR8guj#)mWjW=FNRY^rn(3Ns9$eyqOFHE%1coP*#U1KssmKq*xu-4(NBp2&t)E-lczESJhcFsr2qwOn8p72Xo#FN-dS zE{A-YC+c+9GqX@vF`cE4#10@1`uaJ6TGe3}Iy4-kWkVQz%_!W|ploWZJjc`(}>JF^-TK zGV^VR)3%oGHKvrGV^$wBb8}vyWS-61YteCS{N!}V%@|kOxIQ8knj(@Y95;T`5<%3; zu_zg7!+FbEEJ4Bw3*h)m<^RMM8YWz@#S94;Mo3B?67M5P_Uds%yf*FCP6Sx$Dgad@IS_mp!YGl8wBg){RJlss#a-JC?AqsHbjFQD<5<){-cyx{wRCSXe%iDi)T7<2MkMURES6 z38q@ldZBhKu_9@~NYm7j?y^A^4$cbV1Bl9kmfb+rs%p0mAi;t}`{BS{x zBN{Mbcwoi=f*=9`0C0dB5b@)PCN#hcHh6#o0uKfhV1SR7lM!IRh81;SnSe_Ppe$@) z$i}4egDrjlf+HrF!b}n_2!aMWsDT`KaD5PlAjBjL!3hPJf)=hYf-NY33r^@l6vU8& zGDLxf0;u5#Z+Lc)p@$eAm{AfVMvNf>&1v;^UplsSJd1^eg=Muw(A1$Q8Hd^UqM%CJ zP%Fp!4V1&FN`ZKT=S500AO}$@-3SVm>~M zyESvPA+<6_kIb*#Yw~APN;~qC4*$tV8DmkS#uXbo`B#v%S-vD{Es`OdjV~C(rkKJB%B9J5ECGNr5~zw6D8UMd1i@3}BNxDyA{w`k;v_6af4hJY{aiqyEiP|_0-}o2#X~9U7 ze-X)LNfZmq5)@!ziM(?m`qJ5O);}C+;=r)5*vVlg>8?tO7WI<-$;M(~VPRPg6>-jz zJ_{&i;kYq5wc#aJJTlOz0*#G^3S%?l66KxI@{9hEA8Tu4JL_Dl!^d97S7&9m48{hT z|L=9~tNq9tI|yHe?dg4v!N+{*s4+d&(ZK}D(!m51OYB%TY%CqrLzkjHf~XEPF^S$v zAyHCwDUs@;IvdV1}`%;1x844+^8aGlOu+z7Rit` zWci|O=%ENb6hQ=vG$zrE5MCJBLlJwJ;yE%Z(#jCh3=Z1?*w0yatDN6v zjIqUPo0YX+Z45$p(!Ol&PF*wa{&wu2->Q0vRFy<7Q&APwp(_iB%0X4o!cjsyRFKBl z2q8G2kr4(5X5I`ZI%$a@Yn}7r5rhJvOcjSU>xbnd=QCwv_!B~Z{WLZh-P=EBjPZNT z&oORnxm5qf;jF4gB-tg?u>(fPhfp3unKU$HFwHb^B&CY^9#!NK^deb^S~7LXKrf4u z>WZl=<^&}Q9w$C{1c~lp?1}Dy%vow(oy;$?|7&*dvn_3neO}AhWb8#|%UHZJvXnjB zJNooy2w7{)oZr9R?US~&-6nl4Hg3NxI&1EEi;bb{p7!~JkGbUOoGoj$a&t?M4$5Yi zYDrSHC}`CwdTSw<^^-3Mzy%|1o~CSCqRQ8AC`3U;Rs}7J^G{?+Kna=2mlIQ@$k1h< zLq&e2MZJZnHAxj!LDaI)%X&(3fmu`}Ob{kad;K`bGmCTZ<%%1kR`AqllKikbFl}{hD>lYbw&swClVOwY`?8>h}w%-;y=M?^Q zEWbtCSgX)ZSzF_J>%$tKNyk(&dk5cbmh`si&pMCpn9)aJtKKSgbWW%Js$*?3)+Edf za0DNW=x8R8g3gSOl=U2Uq1NO*HeO^S)kUO2DlL%&WO1xm)W%<+`aY^y8?PcM3uuyD zBIt#JVtB~-%L@=`(^@|+KCSih;1gwYlMUm0?{}W) z{INNADQr?kX2>KrXF8c>rZl!IISx63h$Da$(HJQ*1ArsE*}Tc*bdu_td?TUQ=+|#r zA|8{|MRg$SL8gx)X!0IKSohQLVeOtMpEp}d37>VOtgUMYTe4r;Z^p~+WLxH(ysTb_ zTw80$WaVyGwu=n1R=;bbzM&0{ut#hhvK|CRgGCrHGZL>NkbUlX@nO$lM~3_^8%Czl zInvo^i;dl#O&{ZL7G1*EOsV|lXJH4NuTnCg=@DdL28$3((b_$+al7+nOUK-cv14pZ zTeGary%|HVw8{1$WDR+`2AR=VWxFfy>)9?thLT-o_CDvy+?KIr)insGv&K*BtuQjY z?@k-pdHeRUO()xY*TUAAZ~W*a`^)~#Hv6E#gQkl4D5{u`;tEX=D7a#VixP0YO5p(E zAApbng!dp)y~hxZ5D^TSfj|g;bsk5@-jlYmv)A1|8u?DhvB?JgQRe^0{#H33$6FoS zjxXbd@*6f}E&>S6wJ2CTseHAW*10L4nrTTLKU>kXj@l8ql=3 zKm#)ljAc(+B1meHfH3wndsvkBGRSp4?AX^OyXS`miEO6@AeykE;lRp_5+`_*p9Zas z6*A|NwPV$rp}p@Td+n`d(o*jBe)}!X#%)rr(M|SvLJJv_Z2ql{SsR<*!(1P8Cz>znMy~SO3DpV?|0E@H`^T<`(^Gy7{n9A z%LYh`Ykqj?B8Sh&n7~ycXi-#=wW?KD5y+mrSZL+G^la~GPjv1{{YhwFtnVjf_J8o9 z^miL`$tZ;LIXRFpWBkvSC3CZzmf>a8{k1ErYZe*2 z&A3M9dbY0;nX38;Wyx+_Bp}9WjHJNMP$qcJJ<+*G_S3Dwep-8&jPGGTZtrIEeUG0V zM;jq*Y9YHW!uN0Y$;a|J+ivx*O9-7C<0d;crMLTD8JpWNo3csj=r?5>OGjO)v2-jf zt}wEcSV}A{mJZIeSx?2n@f>M=i&MX`leheIgN5bAl9Cp>m|uyEPc0=al!e8{vXfIa zTvTABsameGaNGbAM97liNOiT8HFYG8jb$T$Zv6BvFzYvNV?ECe78V$vSXgXKkz(Sj z>PXZVj1&=B4@1F7F4T&klDE(%y4F3X8WaEqj1=TGB{cXMCr}`5Orl>paF75SBtYgb z5J^|V0TBr4g2tVp5dx~ip`21sa}ShMQg)CTM(%bu*?sS&^&4ZqcwxP8-rDgQb1brr zv)Y~<8FlQYTUY0c&~m%b%9ie5ySE5gGwIYlLe|&a$aOOJtYl`p&X3c!T4y_Y1=W#c zerj1#wW!Vw2r&1Os`b*LR#hXin~wCdenUYoFgDhzI+0{^JoMD&m7pfzQc+MW925^4 z0cg=k@q(5a7clHo)K8pECG$jhsQiFpnsn7<8oxEOYdKhUciKGfS6dE?BCsH94 z7flV98I@|kv#8}_J5}c{Ul&?cT~zeAtTNj(GB-o%w%@Pg zb_^kvkKugHEVHFg|2^m+r7$vN*_xwuJ-vOhbL(bvvll7TcC6)3*xs7$r|#HEj*c>{ z%;9x!UpsC*A+(Gi!+y!vsk4)D%msObpiv?W88S>xx_}}HAv1(%N1mCrVk38tWKVMs zw|pFg@2`vSHupL*<0ozHSt)abb-Ign_+a-y@oVS_wAcn$NjP!v(A1ty9njG z5LVe3cf9S4J`%WL#6nu+SSml9Sd_1SamvQB(8>f&92qJZYGDC=WqzEq{?8nUq!hYDbi}g9OWR@0I`uz3b zwai$&ak8fExn24yU$)QrNqU>h=IYv;&%PPbX42c)`?WuFuf5k=ywrI&lMJ~JP4tAv zYKb7K6S*1Z5Mu>J&_FMkF~ej>7d+sR35F5k5iVARhZuG^f~Jl@9$6cgTGqOvqM!{Q zV(1~p3KBFwkYL6P6D7J}A_*ZggqTNugaAI=54IXfP`@5BW+Xw=fbQbyI1+>sghU!b zW(YuuBV0iExQ-CzMVgvO@e~M0Aft(ul_hBYM;9j)IKspX6QB#w5HdpmJB|?1g=4QD zCLi|tc@KMBd|2zo-cN7rQpS$eHyuWv>|)c|T-)pX-7P;xorE{a$KY+vxi*&-vW0xv z%}No~4a4=63v$9an3+ab!%RKaSiCU!;(6b2gj3`I!4hdS}n(D&6TbXAPf&@Sjb8 zJa2sMuI-GEI#(v|bBrBE*QR6Vo}|=ki}td2(;@%U){VV++i7#lES2=yuFos&b}S=f z$v#dN)B@QO=!zLj~ znfo=I9k*!4re3^>Suf(E7qGiI1PA$S~-Uoc9N%p|kN^^}bHWoOIYZ4y3T+24HO zHbjI6LUMd@Ns|`Iezvc;_V~yKm=VD{kU)u?tWVcFK3P3USsxpr=894D}6n0 ztPX`l2MI|I5*;Mek~}0>klgUG(#%#+R8*eW@dVlNkPIOiV&ew2O9!<#R;q8YLHSra zmX8x5LY58-Jxd3U8&r)JzcyG{>`6&U6)Y^AmzHPhzybjPLIesaMBxAw7+_$Clq^)jAfyQ@a!^GbtPpa=4u-q|3qI51y;1c0W;{_Kn4$*_(2?kkOK*DP=b*r00D)G3PQL70kUw0 zEeL@NR(JsjW4OW$Zdij2<{*JQ!~i2kj~Fpx3~{FlwkNU&wud46^JU1mz0oQoh0NOK z8~L5e$zsc9%k3U*EJ!9Mqb3Ra97#oEWCbcSTAYB0w3&oF`8@bM`7qdzdyjlxe4dld zY0EA%}gb)LqV$wlc0FUFJonjrsq&sv5PviT#5v0=Q<17HD$xt@;HhO%0302DJh~iglo>d5N@wiqHw}Gqvv%?^W)t&ptDF`37V< zP=vR7#(rWmYVjzI>fFDq%Qki2iieNKY<=Qu&^l@X-RuGxixONK!OLzMweNwWplwF8 z5Z-^U7>~yT?`UpV9&NU0u9m8W44IQ zv51*H3;$h-g7q&3d|?+TAA{aX9Q zivg<2W1Vi0nG=@j!ZIMaw6iTD)T?HW@ZKEf9Az~0-Q^69$BO1~36u%CQSiz{k$~D% z?p{coYdXq=`%!NXIQCW>dAZ6p2AK(~8(L@BYC+TPsYBw_hD6aJS~I+%N9RvVem`$q zGYtQ!Bo8xmDs@Pz3IwD!Z{;(*#MrAhcf+!L~R7!C>~Dx4;WMu~~Wogx%K z9N4IPM&!R1v$K?n(4sFbz8H8JRi|5voqA^4!5zrc(R%roi^J7oM~9tP)B zi>7FU_oz{c3du}8xkaK$SRHP2*OCeY00Pr9{1-81?m*&GnAYX_aJ-WPp%{(To#rUQ z3M~}>3BAMMHyA-I_#)LswlYaEwol~I)OPVou6I=lobyfFj)ntcD7SfF z=jx2@k7g4A+)Apa0TxHIrj(GGbQiUjWjY$k9MlS612!$@ zF}j8d@QUwZ_yc&R?vbQE8r9zr={GOr2?5==C$aT9<`&=uJ#$uA*h-UJ| zqqBSNI2E(Sm+*_(S{Rf@a?6x#Boo+){+9wV56Q#K*)^sdYWlY)Rlzk{E@86n14F+9 zP$POx0-I0?=tO%Az7w-87Ay0J^9o$AokF-(+)BWc5vo8_|4rM_#h* zcch$&{o0^1Apl*h`O;fZ71$JX{=rN8&X_SFgrhSo%+Om$!?77$t8BmmDMtH)$dw5( z&|A(V96i~knni8}9UwMJMI^Cy;*+XGH-L&vgwtEP^DMPXAT$5l5j}azXrsLi6tJ37 zR3b8lvbOgn&D~$WjfKWZQFM{}3ER$~w@wrQo(-XzmH=YtA_S@ma{KSU|fV z5aqBj00seQWsF$9?T!N)UQaVc7lKQe5Nqdk6b0xZ!Z}X5`UqrzE-h(J2p0mGuD$oJ zp1<9Nwby!D)NB_I-%MXA$V|zaa?q?1}~&rOy3fANY!fci@B}t z;-4n-H89{qjLtTw{}&h|G8)-H^X0h5*Me zg79!xK+J5Il34fjC5WY-Ey|Kr|?$u}z?hO^DBechOa`rv*zg?Lx5DkG@t z=`qMCSOM#*nh^rC|qBph*o#nw`Lmdc4=qPoWbVK8H)Ce#ya}}qrimQ zp!2gf_psa9F(ftIwVQ(W#aj*4Uunp5Mc6JR3G9_TY?>IIfdc)mywfZhT7S2np@K8+(9F-6MUyT7wPdke+l^vHN`CMXhztz)!x?x9cJww459~u?Y_Iv zH)oA>!P082H$ex__MZ@;~ERZi1xG?5J=>WP#gs(@A#&SUp6d=ob?XjDQqp`uSj|1XQwbyh7V6-HgaR<8)?Y39Z8lXy6_6f}lTnyz~ zu9^m|%6V9x*{4j~_WixVu=2p5P0D?nxeAo70&b)+P%50g6@Xb}@g>`8pHd$NadP8Lj%iZ)DrCp&3W)Z@}}ERl()EHW`cbS&G8 zJnz*_?>rRC*~827`FoSM789SA8N7N4N-pKKk^{#gA+cE@{eY9zy zcZ#KqaZj6cyeF$@nP5^MD;%qhEh?f{3A4<48ZS*(6AI4n$}LyLW%Wge52xE2he)+Vm*Ku z6;<~DMfe-dZ;GbZ?W|HLd^0JTeHd6=dlF7+4pyz8*n*tn-Vg?Z;YCXYwVb#u{$NY z?DG-IYn9PvfM~c~JGVxzxZy%-M{UtjGwvr1!p z+Gcub`~+F0+O=8QG3H0=3NsvHpHH%5hLS9OCeqCg$C+!A&chAfOj%>R#7ssJ`vUe8 zT!BG}W@ppV{A@OhFPqIchrJbp2VuJZV4f8-A( z$ZC{`nK0$m$W5f5T9RFU+@5JkyjvXAuoid5JpAryDTmBq0|K3!xfnf?)n2LTw$ zUfZS3U5&D#_oNz_lXIo$CuI6`_XQ~s(bXWV@4tf!MLcEzc zNOwTH;hMnO^R9BTm1WFqbn1q)1-lf&Y{WRpy<=u+q>T1QGjXe)u9DeE=Sl7*wzAYq z$M`3?>}KL}cUO*d#1%mb3bV3|)>NFjlid7q?RxXB5H}Ogs9vlzsB*yrosnl6B4#hm zoRMg(g>`k%QCFhyWqnsqz?l}PL}7jmh?Pn}C2Li=f^?+%J4y8|$Va+8&F*6*OxU=b zTNR}Mzv{cOI#*iwD5S~J2(7?eXdN%<^HbCnsrX5E0hV!;N-}1cR2MMZ{DvsAF^_ zRs&MA6Bi^NpO6(c2^2^esA}e@ok&hVUQGiaHPSP5gsmcKDm{2LtRX46@r(I4c{=en7 zj7w$>fH*R($b#JF{=%8k>rLC#++5t&&iCVVYMPjTNq>UrA|MGbc7ToNKaIY5&3c^^jm)X+U7f_Q4G z$;F!E*dN%J-=F%hH;t$-bHLzUhN!)r+yVJ=W+pXp-eR8TrJpxnI2T!^+cdVxG2FsM z-gP{ZZyG^og*nKrz0J>vwu)my{T@WNdwiYIUQgo)@X=`a++Ku54SfnFjMww#^^E6B z7a6d6xiIyC#?ipGgk+=<;*s*7xovlkqQNLh--oEpU;Lp&tJZt3z`sCtS!iRlxH#oO z6wBy5i#0>-&}GuDgD#W{+EZ@6BFmKzetKNF*gO=Tje1+(8UHT|?#N2f*2z^k^{b@}1>ty*xy7po z^b@TfM$TX=WkFTFQ6Bm+SLnvH!}x1PjaC{9H!ASNKH`W$W8tDY%|hFO%inzktWk33+47wd>E8eD!mGP<%8{e@ZV@0y z(qlxh&IpuW;_B+Gr)8hWk%@!pm`6C+RnPuONUH;sfph|GEBauUC?lWcOu=UKpcuxS z#aWwn%tOQT8BoclrmfEV-p3|eVvKZiY(k6k1D5S_IPapQHXtGq05JW+Iv^S@6V8VM zu2LK*hkD)b$Ap_BCaxnTUcaAaY636KHU^jvt&D{=JKumBJ-mmxN~VcV3RAr8Lw(9+ zld9O6j{7h{pK3NbXZsHmI450AW8I>a4)i6Poey*&&%|<=2vZ0eUGRaWFo9*F)}9Lc z=-B$H1GlU_)dZTPjqV<0Zx~{Gu)E&6U+It&IYa=^f^1R`6QHDN&=>a3)8shg7MpBx zg)Gmost{D*Z|Rn_T-_!M*r}#y_mK6ssRGF?<;J#5Hm3BSS}E+InNh&-Seb)GWD72s zg8_JnQMx^@7p&(+o7hx@Z2H+$g41M|Lrsv7$9tL(%xh{NR1I*m@sX=tPolmW^8vHY z)Y8=IX?&@;o*xy>SF!JJboZsPDxTQFSvTs)Hyp5t#l`u|_=5S|ApmtH`rc%E(?Xt| zDqH!rIBWc4x`+w7H@KZ(rj(rUjJ=qM%mqASa^{nHm6q(uM4Y+g_Q&%1sL)Ua`dou~ zo}`6++-xHGC>K}#A&?X|TWpy^36^k+HZR102v$6^KpG*>6|Lj=UnUPJ*18v;vUxA#l0W4F5eE!KCOHjf&V}rJ#*cLYVB2J|y*~K8Bb|Mps?qzf?jnvAHEd z_AOz$ZkS2X$(==di4vd>XY5};OUzk+Q}Nn_WKB_+j)*Qto}U@FRt(cO*~(!|SVZWd zaIV)fCHp6Nl4fv4I`(;w%M4JV{*zpJ5&uSi4t-{b1n?3YgORSV$OK_8Y+H`6^m-J$ zY-Jwcg`7w;^pdSaJC(v25*EtZCb5%m-3c0`FCi?%Pnj)YDX0=tFUx6$0ks{*(8dD! z6zwDv@rb!i6GFvwDn9Z%xO;i8#w|x2DgeWBJ}A1F>&!`}lpP@CpfB4!V&K^sCs|le zB{#o?d`-Ce($$|lU#j)u9-dsz>|M7dgaT?X{v?pwV!?D$m>~JF3YY<%0@BRO_SRun%boWw<6*8H?nJi3h_3K+qHW5VoE7Q9J+O z2;vn;>>Xw z@H$RcqS+fuhguwSG|?$^B7zITpd(Cf!J=2PgCib5VqWOy={npUG9EcMOcAR;iNXZ3 zw1RgbVt72?B&tYgrm;MCHqMx@dX%8-`0p5~%7(q944aEgC4DV`&KvX8C1cP@u!bgG z1j!vLX(fAVk_0psh2a`hc0i%2U;)RnD1LL?)x-unLmtLbzoVj2!cr-Y zn$#xK8cWiyOT709gH*gc^7}$16kK|f0Bc1-Q};XbGHw7OGg!z3`&mAP`YHF?0&29L zgDDLuAo4Ajt}=3qoW z*g-I@Fu7I60#XHriKUd-y-L(j8EUmV3WVIAB#vrH@uqsI)V_+jXSv$xCn150KWs{k zG+mFXZHJ(^qkxt3zXyb>b5T$Y3eDE?&DBc@Ul_lVO|lp%0M$z{&W795i630uQGo0# z3t!@yIxEo0$hJe~PW+&|=Z@lShr9rt%B|`p10n|V-y7Ue<^eiFP=g((TGY-oI@+hJ znaVUUn!oT9@4RWTe({imGm}&yHxnLpp~&tak^ebR1Wttfk8gI0py;8VhT`t4BymgM z2ZTy(y4CPAJ^~QIc=fojA)?}Ef6@rrdC_elx;FqK@rw1H2FjI)Eq-txpO;J*w&>pJ z!V}YsuTWBP800cJC7v1sb19+h`1PGS2)3V#cI22aI{V#hjDj z{-D<-Br-k3l`$uSMrs2U@i>4AbfDoAXY1gv%odoPCg7DW49A=vWKL`#{-+3v$ypxF zId(!pFLa)S&?Buz5>=35lqE5`1-W348`TK9qIak%;$clle*7b@!J87dLVuzT$G-kF ziDZmVq#M+cwj=t8PnRP^nuL*xcr-)v)@{RAU*)E7Ujdd zIz&(7m{uRE=`+1aTh%gN^0x6J3ML{l`tQN=N@E#;PA;#PnGdaWITyOc&z7PpVn#V( zfodo_!r%EMoHfbh#TZ{HFX7wCFiqZC9glu}WjTaU-{3LsyyZ#50F&sJcj%qh$9Q@T zChis$tg)OemMWyj6XQv{o!Vyvv#l=ji}qT~!p)#?pl`d!2l%^k9y$!t8jpmZ2OXK` zijbTKLo&DH;{!qw4AS_m`$?jN;sozh2lU5>9)-%)V+7_P_Lqq&AveU&3#k4w`|$yR z%;@v^<1~{gH828mA`Snl+W#z|IuWfn`OAnFxQWlPP^5{z$_mZj}%Te(BEC zYw+X>>A?6L=qecNL?7;8!g0avsbptG=iX2`ka|btK!M1WQ}TT~PeO8FZ&L_JHI*dX zcnH5eC`?GAr+dJV9zHEKWlfHH*2>;x=1BEbx+mwz8dBO4xzy3uni9E^JIw9=QpFW> zYVw-bf-)WRYmF$c`4$3_lDr^930s3-3BW)qlO&jhA@%UT;`mm`4IB@Sl(^FL!z^D( zuEdkK8EK&OH+u1Ek}zzDPV>#A9ss@)xFtT5d(b!o43s=H4!VzC5OHdc1BInmoK58o z17tnjW37DP1P;f7)C1bHTv1{J2^&lXib3EH#FpMuVY#XjOH^H5Z1(crJs>DwfWGSS zq5y+He80;u!j*(Tb$Lvx4*WqZ^-pDh+N1 zq!}()2!MG(XGp^bfOiCyQ2OK+PuA7$gIvHQsBWkSJ-{X&sypO0TE1lW0!{QNz&R~3 z^}&_r;p=YQ7q>BZ>>7X6B?s`v=@Ehuf$UWO1B<%HyQsYrorSpf*rijvF=sYmv~B0W zt`ch?YVjIOUsBX&29D)e8~Qh`RC-=khxbVQSdTJbp`h_1l4KB*>2@QoCkCAiGN_iZ zO1)w_}_Y#ne`kY;kq?R+yxdSS^I2m@1hj#o?3cKBxx7wG_%AQrV$w)-m2@ zb>uFZ2eJUVbXVdceQXG2qYTa*bfQoOI^T2JTF$jFt_vnD6jQz^Ou#{BQDX#z&U#rL z#*(Unw)|A0I}g4)^e0AB8YtzXK0Uc+6#c#E&AGWiK}BkhtlqqtNo5;&?R3!E>R|(r zFvD5=ZMT}{om|xy9?LVMc`g%E%zd8vx!qpySFv^?%wFv3<={MtdKv_5EC@odIoDg~j}n}uUk~61KGLWTZ z@C8`>PGM;qc#*i68zZeiL1(ZREF<r8B##x-~!E&{Y}( z8_aGsQqI7-=F~)SU&PmCNLSIHCXekIt{|etR8p`{Njboq5xpyi#L%dHt1EWyv<@-S zKM&Ctty{&-lxhQb;HEomoHyC4AMPNO3n`s~H71c{`Yl46)dX^v_Jt8SCsYN$XxBjF z^))lHoUMi&Wobc4Jh%yl0eZZp7ofgbALoApY?#nR-$G_jAPVDzZRA_j=?%{+CkN!y zs+qo{G5%5E*n8ys4_vd8Y6K)w~58IKaJ$Ix`hiv74zwB`bZw!#Ig4C&gMglx+MN+ z-cbQJaHxJQTb}T9rA6Fwq z@N41i8%d6~oF!Z|A-N`1B{9xoF{4vytz7Ewx`)1Fj2&!ISt(Y`L4~6D(9Nxoomomn zxD!7@gL1?bB49$B`^ zt0UHuflSHX_f@PzPH+NAx}x-$y6#&-mcER0D0cHBlhetj0oj*V9dZOp>}D&9$2-V+ zDt560t!;(T{LC1_oVFtd>LOj1Td{9nttea3woxE#E0dYz_o4es0X$$MAq0jtE5OrO za?@6uU(G`L&Dk5y%2*Ta`|;!>9~4LsDHtI&8O zIiM_%R%EQe1kdeb`la93y8$jd0jwlU!v53y>aw*c1$yIwht@J*qu7xR%jXPEJn1Sp zn63z$VKjiM6JL>C*>$*%w}H&k&R8Wiqmh=ay*q1*3Cp#2TVzlh>-l#98yJ>+TL#|Z zGMm4BT-O3VEg&CKOBkr;3%%~UK>AXE(%|#430S~{3~~J)9oII-g}G(Cw%2NH*TBFl z4Y<;RgC~X6wn>*NWo~E*OOWT}+XQL#l`8lLewsAW#ZVnmBpl!-z=K(UZ!o_bc)9rQ}+A-8G$I zJ*pcpbBsDrhSG)S;1WkFkyP!qQ#? z7+O+z3!U7g$QcOIPL&3(R7P%7Kc&6f)O3?LTaUb~P__LA8$0{BT4=ZF zT)0kEFyDUPrm1HEbBk{m#?}70r<8C1+Msqx-}vLUojIoh?H0&83%&yf-(rft%38^a zDTWO^Kv~;`(-x=WHodkW<9Wrn8o5oCO241|kE@S{@U1@fXz}*g#)ItJDEsZ6)qA4b zv0py$-wa_cUs60^=>uP#fjpcF+@MXYoXmimDi2pd)&v9dislGI;wa~8-Cezq*P9dq z{w8`F(?Zt7yGcM~n2Hcl$5z@!1i6r-n-Om=cG;RpR8nxl5UPUL)c7I+>zl}! zHgHOB5>!8yR_Jif36tB-j0hAq`%#Ht?yTV%u!M~uNM7(gfNi&AEw zT8Xkyo)J5JcGVOewfT=o&f^+#YPG?6s(BWwfP4azNjNl6hmb1L1Wics8PsllM!1=u zNI!w@qPh1*$xqd$DiTCzW0O;gvuAxd^W+R$FOgJ)>4NnpiZ|6~PE^k<2iuz!FT%#z zP2l;>%E2YI6k4!0BJQX|DuUSi!2cj8AlbTvY*s|;r2xGT1LkCku;9#q;b6fU zH*iP)NvLQ}Tf8{|41o*B+gCJ6WiK-WCQaY0)DBt4vmC!?St8t(wXkIw&W2RWTbxG= zTOQXebDBz!ll=+WPi*(0F3|E|WC0qMps@1Ia68B7B{g<_fGx&F7ZY9!J0Y{PSucfQ zDqVT&qd$IbM|*6{-d%q|!ma4ud9=Z_30g+3<+f}y&H~nAsIJ~FmGW%q_%!V^TyZ+OFrV=nv z+SJF?SpySDUnMipMJFa;SkrgVKlc(Ak@!w8vVD<3iVBW|cznUDJrA#wNJL#K`}}fL zNu+6!A5|Kc{$5UtJck8L;zscWjSM>OB<}cnTP#R3`AZ@Z_V|vm*vFe!6CpdM-AQR` zXj;nLjkhhFdcqliUtbySOVl_@C>?`UgsIR0RBs;^o-0-N)Cz zSRDH!(Y@P{`t!z<6x9180Ho+>MRZW7y#r7hQL0J-Ol_X0wU2TWLgVPFW!PkT07BIJ z5`R7C$HfGFeaiwp%fe>Xpd>qab-3l1Ghqy_AOIUH>931eff=94wx1mC4Y8JVM8* zEyv|a@euBPWiZmPVJ(}F^T1FKNF+-ntG=?k077@U+E=gNpk{X27wWH`s<;c_#sBjG zv@k6rAk*5Oo-Zg?T=rolafZu_AwZzdb~o9H*|R&5$RHKwX7lr`QBGF4cF@417XQiQ>EmvN% zjTSC5ug@>0ro@?rQPpVN1Rw-(Ju=*(LLZOYa46v1#}7nA12G=cSA!wNkvCkS{Rh~) z3;?fgef!55o|5#0#|X}XM4nHQ<38GJt-Y?rT6U- zw%dW6b{p(Y2d>ZUJ?oGOa5Efw@$okIbUv&aWp{(HRbnRVJ&R)krNgcsJ@v~J$Dav` z12Ujm*TLrJHNIx)Tp{q*cJ?!Ta|V}V0y!X-(v&991Z+796d!vg6dd}|A($JJ5q%@o zO?uZdh3qb&S=v|>49D^l7osp4Ndvyj-WvJ2ir(?Hg@wRBoedB?AiS?)IK}XnW_8;JPAhb{8uzM z+U{kx9z|_P%(8QcudT5ta(|WGx0V{6LlYDcjZCIEgJDn5Cco<8U!}VXhN zJ#?(>rAxyp(Wc%g)ih1&yOC`1RNvW*xEi4tc393^Upl6&$73Ty9drbr&Q&n5v2Z^keq^d7*nkYi4^l|?Vru*?YjiBRReQraV z4#RvN5?p|exQRk32HI(}1m#e`oJME)u0jDl=o0UPFmnH9`NKC(6O9@j8wc*C)zEer z*qjq|aT_LboaQ(ttK4{kcz1MW$#g`%ZCaw<49XY?(;MiAau_^$h=%BJ-{i5H8LG_m z4i4VbJ>OzfFxNx%1{gcN9kg0bAGC-9;sL& zGmsF~BL_jHi;Y)V?wCdA=B*ydWT-c%rDDJ6QGd~H*W#^%v^nKB0u{A7q8s?Ab0?Z; z1IRvpi|!-%qYtQ&{^yAg)-xXNXVXVMCW?G${OOCWtfdVdc~PuIBUCpiQp27p!(cJA zL@?sq5Wm#q);DLs7gR>gaggOn19O(Y8_H0S>ORK|E6)Aicm%K;LxQf8)l5*X#VzM? zP@i0?Q3L${_XqsZ0%|eBa8D43Rh$C=08j(~000000002M-Q68{RkU^N4-Eu9P>(d( zMYg?)NGUT*3SlKAXNKUv_9%Z#Y(Pm#Nl7IbG8CZ$5y*;zsmi1gYMfYv0@eoZ2K5FS z_5DVz!XJ=Hq)f0N(PTSS5sE+-D_`k~o{Ys+uzS{A`M$f-#1y00#k8#SV@&;|`c{c4 z!19CRHjL*}fsl<0#Q20<;bd09LIsNzA={Gi5qW~OU>0Dpg5`?PRz(osk10~A4LJx; zJ^EtwV)|mbN~&d~m`fdXoc!{v)%$nkUFsSm_w#y>qqLe^D&fn80?P$hemWDBPz?rV zd>={-$4wBhe84l=SX>k(WT&91dNqWDo|pE>W2aM3-IAl_0wE@{Ai%fM zH)Q$K#HhrmrJPQxW7qt>H}Ci{=h|mWxxJTex@V-*X0Ed~f9{dr{cpv7#{9LX^Vgdx zrJB|3^!(1Nwb`TH(Q`Rv?VVbV>6dYSx_7j3YcxtcW5=3~(#q~3MrT@>&>}Qr0V2K| zE9|A1)DyFpV)aVwVj81VW{gg3N*TM4m`*$Eq>_IswZ^g1o~535WB39@)kW*lBnSLa z1gbhc>6KClI6%fjB8ueFREH~v`)=9I~ zqP&#>ICx|r1Ox*$z8ok|;3(pCDK0`mOi_SOMS&QS3FUjoIp4RRjtSTCN%O>0t68HM z$6RTgI&0s*oYDHK(cdeT*naD)_B;07J=OMqP4C}5Rz2ySoJ!rJu9}U0^N4Yr8Ph7~ z#1&V{d)(E^m}l11=N7DGgjGqSfP)C!R#JHlgnlky;{!knh{bI)Pb#gC{$q*r%rehd z<>%2_y7!biPVD8+SI5bB)moL`e*O8^mkAbEVJxol0*)!HHq2_nbP@6qWPneG69`|N zL=n?~0}LAo5t-tMuX@|BwqL2B*fy&T6?D6rkpUI>) z%d@r#irtf~plbI(v3sD$!|GHXtD{k={3=H^F-a+Qv&8JpQ(`p#IXjSN@&evWbe9P4Q*#XFR+ zp+pBlN`jF1TC7m*9tgedT1;B(+KA`nSxzajwxj$r_w6-aFe!B%24W7txxN-HI%C$Cj%pYD&V#`DfU{gszWD&wU;YMWNgUdiXz*?%h| z?eQ%5vSqq}9Bf!3<^v%DXaTrkT`DXeOcyC#q=ca5v)Mq(22wgZ-p)!L6A=^^o+o7RC<7899$;nQqk!c3Ulv9F`Z7uQn0H_2YNC}7L zG&0fXjAl%tQdSZT8JQr*mkFUFgiSVcL%O*^nX*xdL_;K_iP1R`!7-fxh$k~TzHJDV zA#QL;IypEQ3U&-YWdKD;Cz=`3>4ub(^foJb`iIHi(E=cIy= zZSnYw$iav&RuHl+>_gqbh&iypNehU}xA$NK9*k%~$hLZX8LUA>4HtxL%g1NKfCHEg zup=tvoJc`l5VEcK;Y`6Y1;d%JcNF>A)a#U2lkkSRy$i{>tbFI`s($ZH@BtwcAZb%{%42+c`jI=O}AeS_xoMBXg5tT@y zA)@SrV?z@&8s(gRSQ&zf>rz7GhoBs%Y&3+VBhn$2L?|H}dK-;-NV!8#=B)rxaa~H1 zv|?_ct{FfCDUrengh{yVh`nI&Aq4;{rzh8?q^Cuyfx2b@An1q*tYB(yCK>E{yw^QY zlbs^v6&01x3d)M>QWAmIjZlyPsPH!UC3Z21Rm$_Hz0T;X*RGeccbhrAQAa=Rys~ym zvlgetU&dcK#g^vSXWSWYjn3Qsl$w!Xes-*J8u$HDV@bP}(e7(|uC>ohah&*P zDQm=c+S=)qR?9YO%CVQ2YMfiG&mOC`TkE~Y4hYm_r*uHi@?rTINrZ}wl@%c_uo6T) zi`!BVNVRbfa@+TCJ_WxAVaIhnFa=lM_PUj}rJzqG@v5Hxsm1WbsKs2%8mE>1>#N6+ zT3To1^!NBN<}vFiKb@HNYjeDE@7z13U1!ZwAGxOWO>4(dqj!EiY3CFBC~NNbcRuCD zmVZs9jI&<(tM}APi3ACY+aT7t9xBd=PpN!#;19^+Hu$pgG7S4E@4>#i(h)qVj4DRN zSn;;FE+s%b!C?b+&4>}CgonHPaOnBQ#CM++3u-<*u>0HwA>`q!js>&C?!|J&GUr|D zJ0rakM=N8TJY#KX{+Y^nsjd^}c&oPE@42J)E6){Qy?gcb<{s&nbmn-ul{DTln4B^`2*>8s9PhT7Tu2Vy@%<+w*C?*SOvvXQuY^i~F~-ZXNIUTBo1d zZKLeI<>jZmcjU*9AG)esm6H&@b-z+&UYD)p(Bit35a5Zlfx2eM2%-TcMTj4u3?v9Z z0!9#F02v$+0wio$_<$n@OYj{lmm_ z5R?#v2MEDs2wND!6-2N`(83XBBmoE|NTCEMQNTkKme7I?wD1Hmyo`YdX~?05D7=9P z9AtopAo5UxKBNfo15HXw3XMu7hUc7fa$95mmjCYk&G~Pi{iIjR>#>hl%8GN|@q6uN zi9KEI4L#>N=uMbsQz}7j<)gWDET1jXiQzWv+{WWI8rXwJcY#2?gMmG0_dcMf`S^g{ z1ws{Tvre$bbb+bVrs^SIIBrwHlxN~oE<8y#8Y+MrR~(=firIk5XTpL?XrG}ag4oh+O=7K{pn2IGuc+F4Ly_1 zvjXCF5B%wPpvP_6hTMitxR^Yjm1lL`$5K8T#CTMDm1M8BYPH%NY{CUX9fb^c^{RKV3>@0->!77@z#~tQeIP)3ah4OU&Yn$%?T^ z?f%?q&3)}uPpyH#DBueaAWrn-w;|96m30b9c!7jRfzT0K{P4|)Qso|!a6?KYMW~ws zh;NB1D~2V-s;n2wnACS-I8*I*+cm6jzBYb7Z}f7;&Fj{D%~R4CvEDOJIp@c+lSnR- z8i1t>NvJvY6Vr*hGX3R>uW!E;-UbS0i%rwqjHKl*!|2e<7c1okS*!CZpNd}r^AW0@8$v~0} zq{+!jBpFyD$-vPl(-}dX5fl|Up>$3(IYnk}YDPDt8X6jy8Bq*Ml--Qr%?OrmMlf0F z29|DMO^{3!1d4*dA~D%12A*P|i6%4AKobqTiOEK&9XLYmKq5LdG&eCBO%X^_1fE8j zC>A!N3y?ydedF$!=ia4GcVuGSmp7MzDw|DgOiy=P-K&tL=dKwnM9CCO=c27949i92nr!(CJ~$& zWhN0U5+pN;APr1r62XxuGl`%{1a(H4jR@L^U`dn-IYA*OC@PZ4h-5@CM`V+YW<;o& z8&RqIzrj@pJ=M0+Q*sq1LOWVqDmzG8H8)V#jQt4!iW4L{;EPLzUJj20Ax!PgZ4dS! z<%ql-o{vjv`xW%&5SNbSaF~Yd9SiR8 zbR=;RL59$hDA%0odQYA%f>}1FCAtG z0RiQ@c6M4yB>8fvNW$$DYf3D0ul?%y^Zni9?Xxy_^x4ZO?Yz}b+^v=P-+iT}+Fm

EvE-!6Oa63a_t-qS}aqoy^jn%zHgoNbJH*K4h|ZPj>7DgXAGYInzKDUJ4C zd41gTvLNT>aN=4(M?(mP7T2|NY9+CKq$?d_CYlqY-LdkbgvGOXP9Vym}j?QWN>k0lvh+c7BJ2ovF zsqY9XGDhc&7Ryq4sjkx!&;pZi+p7D45c(LwmmhxvUwSqN)FFyW99&7(5Q^#Ix^|GX zcJzE67F0;jw(>x%&C9s`I|p<6TvvzUSd0tKX)}3E1UyiSj^LCEXE?hcbL&$I7x^{%LZg5}zR6a3g zxW3Sakgh=Y>6k*UykG5<=enLL@%GAef*H(U-?Ejx@?sdPQ|{jX@4k1e_2e^hEVZ3| z{W^BpcjdNbIeC;Ae;#Z1GJegqi*wd6uRVS{`|VlxZS$UYUN5U};#rx4fPsd~4WVrd z)fa#$6efWx?^7PU7sC|07t2_+63cm|)%Km$at&+MQmd)Wb7qQX?_KsSW0bva%(LB} zTWe#?meaoN-LhZnGtE5CE92F&jq_?s|NPY2siSj(sqM09kY-{DgT@;|PPDkL9V4wA zOTGX=Vfm;aRN`$Un2}Y-IUw(42kf55^MR@D!{S>9eA)3@2c-RM6MTU>Iw#JDhjZIb z5e&+LLx=_z*R_K)WA!;vMfaTLA!)&Coq-V~4j~{FxUL-~E$n?jrkELz=i{L!>e;w% zgEHRSd0Y0=aaosvJf4qg|71IrL2rZ5^Gr5YYR}w|Y(yiI&ME8JSXyGN#cjXb`D@Wr zsqdFuXB=aWv1*!LI&Hj~-Q35h>G!wF_-W0X*NbD+rj?OuK59GeALX8%;;2!o#a$zf zG;aI-jJHRAbCe&;9llA1L9T;2m(H1^z#zepLnsCp*R|tW-xE>1&UM^-TPn5nuza|! z2Udu~zCh&xa|n%|T-Oef7LFRIYlhF)^Mpyuksnygk(MJmR^0+xFeVW5Qrv?*KAeZ} zg-Mc?XqG@_19S*kEUs(ENsGysQh^@7gD*%*Mp)kV;&ty?++J;;B#`B2UZ8>km6w0)NoIt9MpKyABJSAP`t+wu! z^lUsH^q|*44~2)qXWeJ1jpf7RRgKAuas60MuiYG*SuT~;5pEIs$p#$RLR``5Vx z+D>^IvlhFWYV6i|cgE6Vn&-#2JJL@tuAcUdb3i9+d!SN9#>5JW3nqea?2Owu z9}jvPgixj0b5glC<=V}5Ms2a@xkgP>t>f1&*RfJtt!B4I zv7DRd;W}0wWO>k=QBVQGPq)f41*{06#dR(53J79d!C5}pPsc$Pw_(;|QU9o~yjg2C z%4%oV_0n16rnTywZ_ZrLJN3j=i$7b+CFfL|@mnq@ms;-VE$&iwkMFf}_fB!GI@_yt zO?H1hy;9dfg$h4bY_hnnB~i45{0Q1{1T3^}i z{(kFzd92pDMzHEdvE($c^(!{4==Sh_d3>w-LJ469ghy!{gzt&c+T_bc!+qO$J@B= zZ`e7ASr^M{tAMNsp@HjKBBYfAND##0XRmyGF0OM=jee@lx}(;lR%w-fcb~JaxJqtg z#FXPmujX~v{p+t^TC6YUclP?Vk7@K#YG3ELC!KimUn7?}&dYU-{%1-1)Hu?Z`?uNC z-{rKuKDmAtu!6(_*R^CxYbGd(6%%Hw4rbm4Vb-@E$nu%jHy{T{tmUqUe z?Tl95Ke>%s=X6dPeZ)P^i+!#cN6-6=^ImT|t#a00HJWeKcGF4Yr5wkvbDc8(?Xi{d zYmG)tM){?Un(ve1If6?H(UMFQ1fyi$=E=Oxvw0wpSCUZ=QY|m7Uz5FBO7YgByx&^R zwVr+IS?lgp+cM%wGv>I{SgZdPR~b3qU*lahmONMOW39YPI`b zCB=5iD{Iy=*BLOLk094;8S9);-VSs{~}&>WGZj6E5$kTLoE}fpT3- zkhEyfN);(M0D=fHYCyvYBG_Pn9q1%S)?-Hf;f04t#>^IM9$_hRrvUuVGCeXqS7(gNeL%6U)14roM2QO$iU_}RR;DIiDASDD%h(H1( z7y=0ZV1fmlfC&m}kOCMOfMEs?*ue}T43UH`pzwtil<-0e$gss1KY#;}JEY(Mh?kU< z6dFQsiTQ|Z)uzN_Z9Utj#6uEeRBcK;Zu6@g-g`SR9jDaBk^@(iEj5JD{;n$~i589^ zUyTY7frk(^P?wC+hx+MOIuPodMkpa8Qx-%7lf_ssCat~JT1{#{Ms3m@udTGUU@e~O zZj!7z=xrZ*o*!xLwNki1P=pZffXv5$AR`cAq6X@cG4rXkBBgOO1EU-+D;y!_h*lCb zUydlF86=RXkERooRbrY}4CiTiol}dQ+y42k8I|M1Z98;yPzMY`f)~9`O28>^cZW|(Q+h}l4&{eLq^9B$oas(K^-d$B4v&r zk^rL#p$Kr`=F<`6q{T@TPMi>;D=RcCj~b{;2F|A>1v9NwUS%{YB7 zPdLx{DJVgJ8>lM?ov(%4R;rbX3ZW#VxPiKYKz&P0faO(T2)A*Y6_g+Vk&lPBrMxY5 z1qSe~VmytbV^XC1c#KeAtgAZ1VyC`rv!0I&{UJ*)IgjXNK=#9)W9M&SxpVBsi}cCH82P% z8{z~U;snfbA`>|QMNUAPm`sNRbx1G>DGNe^K}hgK%1$JBB0&^QhN6Ke5)CBLz!E7N z(ZCT6ydh<%8Hk#JrkSiXf~6501)+)25oMzwa1;cNW-?I?6xG1fC@amt(hM}o(1>C( zlMEce1eNEgB-6%2>YApJAsK)H0000p5&!@w6cmO-;^KfrB$+kZ@e}|AR7_TCRyHb( zgyIotAPfXi6o^3##4rrQ01N|BFi1g3EgJw@UY=%9BiFJE5h03=FvtX5!inkyDLdSK zj@1c|mmh_p!h@~-V#VQIOnoShi4aWp`tPV%(1O59puD5T;)@Z#V>qHYQK@${V5yAT z9uCQRw$NYx$cDBO2%L#H{7TU$PvK`07|dfyyUYa`%OA-2-UIFvZD739Tcb?^X_gp`Bc=_#Fp8rKDMPC7r;fK9lR%{`T!tJ!=@LE1L*WE&Vm+{WY5^z z;l!p_TJz1q0njADeXfMpxQ8QGlSOAzDH0X*4lUtqRK`5B_v%{2m1`#2m>J=qc&Lrj z6B!i0fQAcqQmTh$kpFcUsWj`GJvhXmIwUKiT|12~V!>%Xi9;vmfqH&|$br9fW$Oks9_>JB*L$9dA5MY}tv zuD_(4wbel7{Nb2Z>72IosCMHyS8084xjr1{b))AE3#HgGYG5YCbkI+m(rP#`C7$+= zBjC01@J%&ot{9i7&?8Ak+Pr?D$4*aZq9S@6GPQhCw#$g+&zf87|NXAeO`13;{6%1amxhKqE?v? z8-UAG9;*Po_b}x0W04U+(&A)BE>&e;e^7Y#JV#|fRRZg}DrreIaPix=a`xuElNJc1 zPfl4?Ms5+TBEcc8_@frCD*zu{{oS5EMQ?iaiB%wYU>4%N;t`rOa8jgCGm@eL(1A(9$a!>j`tl745-?;=3)dpWWZv)5jn6>s ziRF+V4Gf6CNg^9g{JA1TR9b@Ad<%853wzpGfH8mw+vZav&qJvn5{(7H#pOT+*0{18 zDheLyI0zHwF6tB!itN+5mFH_f4-C#$2fC7RPSCUhd%@L@=Mo%n-ntsA(`JV{ zw5p9YhZDlJ=BM(yR%6tw%S~oHF-k|4%{c*u8vS_0=UYUc6?RQR+hc4X=-=6dS@t^S zry5Iduk!hT)%k32Gq(VmMAH+FD5QxUs8O1P(NhlNAS2((yQYQ98QPuasBp#3eU9+> zMi!zri(D~BZ9#$6)fJpjRq?Y1$#jgD*18n$7MOw!Fa35qX{Y~vf+DTnV8b`F=Aje_ zcm&3sFUEoq@IOpMVj58y_%DtBT$Q)c&&8ho4#^K|YQPf(g*cKiffcgGF3chj+0I%? z0c9lROh38wIv{B~p@_%pV%(TW$E^FBuWxF4U9jRMGQf#@p!z{ z6LP2YcEVXfJl?iOtwH=KHh>?_Z01P;xceGWXr!t(7AdMB{ZgIIR3IrK1y?pA+_Y~m z3Q6O$_;z@A zdIW(IOh^7eLD$|}zbtoXQ?BZ<<+-*L7SbKHUdl-k;*o(mEDIxao50tU z6>mrtIw)9z`XbLD4wa;8ealSgFZOTVgRjDzh;&*u(oR5SC0t(whc+d(E1c9ulM`D5 zYwF+7A1i@IGjJtz9X+?OC79Iay_Ot6Ye@HGupp45OLe+>UWHpYCK8o*AM6d3-A2&O z3`4S}KO55%d%?;FPVtfVoWrP(_ny%fYaaPQTFT7VLX! zSCF2S+Ufl(6Y8acS@-78rvQe}v>@~#zH3~>h65$Z!+ZJ}%_GQ?v3!*sqFCAGFCh3r z+tcpT=$*JX;|twONzod1gJuyBYd-!Rh$Xb(TdsKxP6%_m>xh%3-a`9fC;(=NJysx{ zn&4=^v@@= zvlD+fNQ%7`z8uC@>?xbDp9hpN^q+#NZvDusF`xSp_ z?N1hG=tR(eyHwR*S~uVScsI%HeuDfk*Ygh!;51y9)c+id+17mwBmO*K(k|hAtbm>0)3ll; z2p9}!uGu|%(d=wSSLCZF0#ta!gAWBt{#U)|84KHn&YZn zWIt(L<^^V{Yj5ILEOmRQ%RdvU^bZnTpq)YK&njhr8jlxH9p1#-<8`l%B!zEumt`nS zWGJ2xA@_suR|XiYM6O6iHBv;O3%rC9Zv5aY&ngLESSt~Qf*TL5jv9sbq1CPL!i~eM zM5O2}(2ff?p5>KyKvmSt!aCi5B@(Zrx}RBjxYUR1E+z$+Ijk~Taixdbd%9J zQmHvAa}dKd9*V~Ze?WNf2m3S%34ZRdpn_YygFfvaG# z*9Nblw>V0WFbM3m*Pc}12cxk;?X{^V;d;3WGO|{rvtX}X)^7J?!L*dR3cI2nBzdk* zqr(C61_ap+C1hHSyK$7EVHnR6$X&8r($L7{yPXX~rjC@cZJ;kmbvaMC@$k3VQjIH~ z^&f`^up*pQIWN?%rYox_+0{CI#8c^TR04)}x~f`u^Q1@bs}W&}I>pry%T?4Z1ct~o zuGX5>B5X$L`{i;ruh|WLFzxluiQXP3q^(jlHV;4Rn%I#AyD6g+t;_!wWZp&kP+ae) z44Jklk30VkJ1dQ=P6u~V0e%T81tLZilLc4dxih6_xMt`pLUPxMpwez3>o!|wT$!At zIeqRo34ro~wkX(&b6@x7A;zJ(t}U86Nu!Gx@kLNs>QvB3<0Oq|3z^Q`)hfbGD7`$)s2bqGhbub%@t5n+PyOAqPnhjx*Ea>6x&G60;rig48(3q^?XyR}$^n7GFl37~_tJXG%0-^4R zD1*+>vWbVx8zbx4xD~|tJyrLDeb)^Dy9xO^7w=xFQhfI^0ci8nO=k?ix&LPFiqZJE)b$3I;XH0y5KP&A>>w^`?l?o+(l_L$m_(57?tZ6;_ z3b~@>m$=b~3fqg8ZVPZ4vs@hR`;JjYJvj1sx_AgxLdfy8iCl+r{0KhF!p`P9SHRu|FYcfBfVTa9m)rJF)+=j zSHooTlm^6FI4@dS(?e62RB^tL)MTK_aMNIME-&1XEu4gGIQwn0I#5a#9;n9f{JknB-^X?t$~v2Nw%D_0_yfo;>(bcwwsWhJ)xu zvZ$F_{*P_8E=c!V+QJp5Eh%)wzh1i*fY~~4(rS*-RkBPON*bev1SV67O#|}ut>&QTOBBO(3Q2Wp z4F*skWcY=Gsg*M&7#I(VM$g%xLoKi}&XT}$)qVoQysKRyYqZKLF030hU2d}IY`G6K z)|A?o7Sw8`lstC}s+T_CV9?F`m85Ii`C3R1hoG&*FGw zsQRe=8l{5ogk9w&z|Z+bXw1>3E{c&l^hZxN6w+5+#H$2-YN>CIO|GM zK2u6aU|R(eVu!Mh!}e~b?4`3i@WMbS!Rnrw`wV6-^vh{rqfiGkBT$CfrA;FLKb zz36|bQt^X!X(({Ul9k_Ugvyx*E?r2U;})Z7#1NM;C^|!fgbUWSx*!dK&x)uH@u9~W zhoxpP&f8}eQ)pEL-Y5?u1VoBuE((Tu&gNd6RTnBghRv1ndgY+Su==}J%F;(SdGR%Nd`?5cU~6G{=;;1m%7`m6VJ z)qYIrvTQ{-e6PpNbKjq+g1#A}1$an&-AMY(6j*vLfKVcP&Lr~lh~ZCJzMDtm zS-yDy)2UJd(iI=nVJ0ezsmi1^DWx$ZTi8vtjWt?UL2fnINrBw-E7 zT?JYR^ zKxy9_s~9PrZ~w=GZ{i7ti)=!|$`9}eyQ2zJpBbq@MWU>*&f+kcp*5+L>c@O1qB$-r z)S*YMf3`Dppn&SpqKTqj(+Lw{RCkkT^3iiWI>kTx@K>-KtEyL^28Ln6BVr4&rI1<@ z((fUUNCF7FC^tc0L!6n-8WE8mpY4QjiU~H`Oxt7b3JN|9gYkz45Uw)WWXoptIj=kl zi?RVsHj!lvk31?u9@ll1j^E2^@$3F2RDv^Bm*f@XyDaYBtuogalHBb^md>f4(dpRK|5)=iqL2`|^ZtdvF`8^8nvhH_U)WuX*0 zr@j<>2lPl|qtRuA`oS;lx?jSoEJ+|cKk91npB+m_c*9Wy4?smPnITRcFqo)Bk~26b zFjzzq2O}p_hr8RP7VeS;0?K0n+1xfwI4M)TlQ1oV>sAD0s_9h{Q`Io}dLt3PCp!|4 zV^MBsa|w1`v**mbdQ4thWXLUF-7C%LUl!p83N~PS6Xb8CB=h+9t(|5g_7^UmnZDa4 z(%0hx(x~^H&6*3Z*%}xsTdnv|3M>JYk-Je0J;D8ckBRcIVFsoM&tH*rk|a;51Lp5J zPqB?o1W5Pwy-4R7=8NO0Gcog(_7dj5qF^F(vEB>l)O(wQj%C?m8cxRyQB9PIGz4iN zB=HC{hY)XhcH)G6JT`fmlLpp2Kox(unBB_XRXUk>$J%tLWsr`#O9E{To~2oG$c5kt zZo1>N696|5gHOL=#r1FS1w4Dm<&UKrz#d+^lwst`nq&lS>pw*o#?UX)r7RRO)6~RS zD1H5x4}R$p@H=%_w$>)j0T&*ghU_CJT~j=lpS^!?mXFJ6;*$PE*`;rgYK+oyj;Eai3-W^!xHS~4LCQJJ$u8SCjbJX zQ|jSa&imrfwd9v(P}`$YL_bo=jhlbM1CL8iZ&?4NS)ToP>2r-sqp-cP%oMwsVu(d* zy&npfL))SYp`$@P5G0fXm0~S&J}NM(x>CPICSr{QSvs zC@l5c)X2X)JOSU8F>7$OsaJ`l4+~jr?=^k|;slnArL*bqY7nC!@E_39AAZgozV%*iI{kYNFhtAGA-r{AIiPDCaoospOI2h2B)DV zu*Ss%+KYYGj#uvLTy$8zap z`!!v7pAvzRZl}KZ#B?)KFZVIQ6@`*-(;7`AVG zUNM4B+x@%ZWk8(AqY_l4{88%b{!uWHAM{l4!W|>^a%p)y0iQez9Ai>=o>O|cB7_7+ zSfV5mOwH%n!Y>C#NRJ5{41>4X1i&P=Ct1lUJ;8XglqF<8NS9WlPq94rpP*4`OIkXVpV3GD9j|Eg;RTIMiDd5f(iUAH*2z`NN-ODg z*8Nk8h3mRBfo~|e;V9{C3PEADszU;y>FZ9u>eN5Vm3rnvccR(UlqEbDySZC2^k$#mH^%)r-2EoR+~Z=_Z)z2=Iu&(k7)g;$q}w*k`eh<%XkThjniW5OnL& z>?4t+mJD^s)xsYFNe7-tLF>&}Y7zbYo%lLH^0W(n=X{R_JTfn#AoHf=rBW`K-hqH}}XUp9-nOOQ5jB7H z=A~Eb3DpV-{?QR?U3QwNZ5VOhpk2ENc%I}QYC=yN=Z&41A^@7j#?n1{x8z=r;Q!a)Vr68 z0b93t4ElVCV+#kkC+gbKpkxTh$NG=cRC*C^(t*4TAM?uY%{jdVHbUl zO7)DHt3fT;_Tn1Dv zNaJ3;%cdo+& zFTs3_RvL&Sb6fC=xpO!3{^!I>m~u54ePx!Zzy%AV%QmMFOj|#5)Ct4nQ+531tHb2pL2 zUkX*S=UN_4KHdJ=Fc#JGb?<zK85E#lm)s@3TQW~_)JgL;&~|y=rpH>P+ul@ zjOGM8&>73}T-O;GaOxWS1mXQfAqE@dx$9Z{(Qy=OpQEUX9?kiSg?JRa07X4Z7#RSf z#KR}<0Z1Q{bvKBL)Ny#`qiq0wge6GyZa`N3{RIGfR?W9JG)ag5hD?XSy86{`I(Enz z*j13SMc4-4poL}u5nPi}XbUWF!Hn)h_gx`2y6#dPh#!cFwCbWUFF7XBHCnf2ZJC&t zRX1#YJ-OeiAWONbLPxU;3n@sKrf&cf^3~TRHdAz!&zPRr6}%SrT!?uoXTzQmV!IOM zYe4|EAM=96u843~GLp&2M|InkS9Ze#tm(#gI=NqKTqdahc3nXlIUvI;>E05kpBjn( zG+iXO!MQEwMLzR!%{nnJa;alCCsYr_0xY^5bvAXk<|Mo?6DTUNvvh$8M>m{%jJAzWLdnxtQeMWh5@O zQCQ*n>Xh>5dB%dU4ZSDnT{d^^mIHmbUnSlgYsT}|y`tAH%5Z&w{gV})c^_J2SVRT$ z4K2JZo1%Bhw$a(v?p5*RKt(9Nm z-4@kE1dIkJ2&8qp9ZFzIq=*-RwozJL4*5UigRqJA5di#X{aKlBLNY>}7q-wZSsuLj z1mYyf3!H9|qfftNI#B;b(zWNF^kB}*DW0vBNs6Qw)}+0pxvtrVtdmQ2PsL&g-F>u6 z=3Ff!Dt_so)`o~VhmU_zRR<6FFb0}M7|kvu(O!_rY`*gH9s2PB`UJfwKQHVXh1k(s z1NGuNQ&QrRzeS>PjmRZSNkvatqC9tvd|tKQ>g~kS^67I&H zhY`U?VqIGna3^!2DW%zRgs7GkdsnmX7@R&}@sZ%&Go-im6XKLJ&Pr@@MqNUfM(Um_ z#-jJJ8eJkHQrXu7tlfDcKv!D9A}=F##J9%JxogHM0w|-$6;BsKW)Du+gcDYN`L!jD zP+&@2cXj>yIiwS5gn#f0g*SsE04+JyL3Oz;OI%mo7={jgPC6Os3u%OB1*;>}=H&A= zteb=u(-U}G+D@<#w5?;^+FqnT?H9>`Eg9*SGy*9%R44OgFjNq0{=Spmu5rs}$^|o| zq*oP>V*u7`ryWs76Zx{vg&ADrL(scqm*GoT%n`{|lZ`+FSV8 zU|*QZEgjE5gnsLyK`%xxs4$EWPdpTg6y$RNv;-gmf{8FkV?nQe<+-*XZetSKT7LI3 zTqeZ{Y%l&&9jhkMZkFJ?=r`FTLLLJ?q-6*4w7mCQbtf)yrhbs(rWl#y3achSE1Jq?5#lL2AVuNK6#OFKi$RK4@vo|zVI{It6Yxe@)yi=J zx@rX}$|4#oeD1fd4N?;z{5#$am6rzDMNSB$*d^5W6Cp&(zr8f8CQykt7yN;W>QWQ< zxj>4SYVw{Y38i5fB{{=L%0WNjW$ME`3dhMl^Y82m>?s1t z9!WYB+z?B@ojg5p>xyz;7sFnv%30tbk7^QvM{ase`cX@xOxdHJ2`fi=J2;5mp`%~Ei=@Gpd13{K4ai(7E7&>-2Onf=YThaLVV4o^wv!=D_$6EgFM zRD8;_2y|7nnAOwFYN+1$Sd+ujFDj(Svw`jGL(y^$s~3MkMIb*G4L#PG(#jir1ZfaEE4YahyrR`6LsbTyMfwN$V`a>9fHa-Y$_jVt&v zPd6E`n{$`*NX}ILOmLjZ0E7~KpdG?Jm8T|p8R`6)05@<5F8*u5d$MjUyq_)e`5QR( zjS*&)O1P?vM8Z2qcy4m=)Kr%{-OBSQiKp}3F9ULUQS=p}Ur{8c{v&h0lOZsp9<(k~ z_{dY$=08GBH&HL$b1$34Fo{8RLb^ytoNzc?K`cnc_}t6zg1@;c@qfT%QT31c&G?p$Ia#5{l%!IRaOo06| z$+(Cr%#w$TC%}MPp(TUE;v9D*fae3~9Wt2~=ON0S+EW>oh6I`E%8XXLhb8>%tb5U) z93;Z%m=fXm{1V&`<{!$@2D*^MxhN#bv#QIvDDre-D)GKh?*Q=R(Bz%Zxk$QP+AulD z8irSgWZMH0&PDRLb%YBkRZ4pj8*i;)cvOLUg!=;W-nar4d$PKEWE|h5Jk3#{%I&k= zGUpe)Zy1(Ua4)MI-X~(Vw1m>82lq|K9?8NxNmJ=b?4%&vRdp(c1_LvC-I}tW&bSOB85fUy)3EG2|^p_Fl^BBTi|wgaWemXvPVoo2xnVO)$U#1 z30xY#3Tv5En+s1wOrm#@(?g0US{3lK4i=1J`OHY2hnR}xNox{zY0Atf%ExOk5IpIH zl4+7tvf{}Tp)xboH6X}%`5btNna@Dw2~MVhLJc$HzzqmG#7#7yB+oSJ*jF^F_pU_1 z&I9ts6v&E!IvxjbzKhNCx$yG(5c>iuOkRM_b<}$SGm)Sb%&aqHq2DAtE1B4-SJ%6} zTLCz#MLNqBpm|RC8TG3}6VYVlpTm4boN6M#45oATzfifvtZazL!8`MN{d1mNbi=oJM6ad2UBAb=m1 zN2uy>PcbffsI(*B*y!h4)|3#CZv-cYJ<+ncqDGSB+I+qnCI?4w)Or;;1aah=!6F7|1Y?k86=9MB}8*NBPOlC7#3W5OGq|2Rft55_cfmv z1h42(;l%)fUXplR{6f^4!9XdKK_lc<|IP})?7{{|kGzBv+i&ezi zM(*Z~c@(!F< z^VQOTLhY@Ph;!bAOw718xE<5%(t!flExZkA${iU`_~Z&!${v`74cHhi$!ji%!UhGg z0hMx}Zw0Hm4h6VpMhiM2zb_1{b=fDASC={RT|rr>;)nyzwGS1{Ojp-AjAEkCbptgs z2lO-`XKK6}{_ZUR;8KEjlomZvMNKRcM8cD#`4M;K3@TIN!28W55r!-YB}px9ZOMg5z9%dK8jOGi}`Uv=Bh?3$hv@Dq7hw{QsIbM_eng-mfH16U6ACX z7%mcPr@(l>6b}tL>jtG2TP|~F8}uk4s$!Ur2=moc+J?S4H$W_!KS{j}7hPeqM=3e$ z4A?NxcgpLIX}EPL_sEQ8kfJh&d%S8%+!@;nCE@x|zLJ@Kx}`FEoj8W7g3;R%qDZ1~A% zzi88-UiR4gWW;(3puBt|pYLT}E|jBHGg>x`4Vy8DXLc@+xaG(vIf5TTZFk zo*&`SLq@g+AAn2iui7uS4o=+2w6nAfr;TX|ryT5SqkE09m&s6WERXxXX(A(bd`uoy z*?XKzB&XbDh_U`cqvSM9#=JGSmlGKQ(OOU=|MGe-$X#E~U5ESO_f`~5$i~34_TOqW z0yRF3Hc77B3u&d<+ccS?S!t{(7f{r!Kxl+oj9JN5R=7^$=O@F<`C9oj!nM}ER6#d_ zoq9i!3TwKe&S*06AR0mQ-vM3=N-BRey)6T6@0zT>DptF5eqLt-%@p%NHIO@Md$feW zxp?gm%0(WNxy`$~ab=6CdFMs;gsV5>ons#0=hZ1ai`6E+Ge0?IlWBIR!rY4BiMzha z^f)+R?c8q}blP|+#%V$J0g~YcQow#4&gfO+e1{>gvOOYj!mQ61GZ#bNXSN`8d#$3jbXS!U0n=AgM0&0>KR{lu|kDhq(gvINbUz zgR zibJBANF)qexPK3B(pa(VFChoPU;W4=^3r6q^Rjpm96PXC>K@7LgeO3c#!cj`oczTh zvb9>J+({Cs5oG!~r+zLDh*__O!SdH+@?)-Mb5pcWmIPJA84Vr6csSM~XjwAkE^QNxeh56G*Hi2NE|u1F{KDL|l9KeP{p#klA1XIxhv6;(F)aIrYq3W^zB) z(XoTPp#gj%`L5CkC)jsh`nE{9&)((Nwo4`6RiTT*)N>{h%MXV#~YECO_L8A zlMAtmOdLnlO_QuRbe01WupSbH#x#t`{TxSA0j*7ZlXt`TIl|LzL>3|>aOhBh8iUBq z$oD9lF{G)7M01qm#A=OI1HfEujL1GTk9?1cH1=xeI6+AW$tWrYQB+LS01>nzVPe@_ zZD-sZon`(u6c;>fZIXS^#{ir^tod{&kr*j*I5dHgOQV!I4<5Gyzu(6qUYj$G6=Py0 zLfh+E!$$GdnVdj$mwr>@zyi%rz&z3bFo?+ox!^XwqHSb5m8FvY{x->g%Vvc~Yni@o zpXr{WT3dGE1IdxsSVjlbAk^J`kyT$FsNAt(%6rjr2tf`UcLsDL`?u6jN%j~o(74pp zrvVc4;zrXn*)`@Fn)3OZb`TRvFgG?x+7;FAt@0@xP$sRSKhn+0xFMY6n4kPxZ&<5Y zhi=1mx+;opZ*>~LUY`n*)h=_Fj5VW7khBHY;lBg`$hXu>L zh6pzfrN*QK>nUJ!EIo?(VWC~rLv7C0QHG>xZre;{Oc}|V52=94NFq{H^7U!d3DWxM z4fW8`Vlqjw2BZVtVpUzWb?z10m|P1^4!varcaOVw`GLdX7smdl7VoZxw_H} z?d#J7H%{nz?W3xS?|2{;WX$#~-iWKmybKIFi9pNTr#x$K(~9?@O_Yg)1w1>BF!LLP z4Ms&=l*RU|HXC$isnR8{K5B2>MIX4ax_q;)OE;qNPfJ|4n-x)l+9r}d_wz3o-Odn+;u&19CVh9#TrdjKooUdd$vl>0pR33$8^W9C8-TjBKluFcIHSU&@ zECK;zfCKk$2|uA3_rJJkE;zYv6%-&saS>2m8C(cx0vO`9UJeWmZ@&WB@^0SqB3fL? zopcdh`=0m~mF0N=>B%1%N=~+V$gnhuW0KfvhBc)+;FI@6?^N*(;^CDRgs9TI2TkhjI-vnjaF(bj9?9`ch_(w4m5+J{=RYs7Azq!VZ?;H zq>Wf!JOLj=v{(rL(I!@Bpf>S#{h(PKv0IhyW7Y zw#sb3P1rm>=0NK=Q|sOFxupUP5ZqY9T);rV!Fy{%=0G3;;fb0G&woMz`57^cNao&e z1<&v(=6(kq25hr!NQqQ9eZ8p=L#czUccoTTrZ5*dl7@%x^+kKWxRmRp04VxE28M5Q zF4(kUU3qk45ez3t3_Hb+1a8czY%cZ$rY6sktyI3)B48<}f|c|p1d->6vGz2XB^v4& zVJN7!!Q%IDr1B1&>-TVh6zK||9xFNL3L(f~znnPsw4tnN^Wi@HIP8g<4!h9vW)YUB zdN_aM8{|s|3qiBAX9@{%>Y&riFG5yE{hBT(v1sI%U*M^jWv;P+NP7^GVL+_u`h9Py z`$Z9H(!F+XR3)=5@Vpc#m|Y=D7FkZqwC3yl5m7`Tv;^o$bxZ&M;r3!@HZL6keH6egLn zEi`;I>GJpt;Gm8Z9r`$1dgT7mbi`>s7T$!3aCe)f+!N_d<`vJ&PCCnQy2+h&tA=*n za`zsVlHo!O+6hQV%jyBubY~#0c*bQuo-P6J%U9`ZNAx#Cl%OVfS?|95IxQYOZLde& zl71vKQUPD&I|~#dgYqMl)3e3Ev}ZYSCKwp&M&!$0Mc9wjeB@h+ff=B(q^2utX{uai z@>sJW^aI@M69W^_N8WMmNAg4R%nhjQ^`YrXSV!dRNNPaU1whs+2Idi!@g`b`UpS!B z6^-qCMY(oG4R;@r;ZrZrMOZzDL6R}vnwYY*L9^EB^3Ec^{7h&VYucM&%*RN%U@W-e z0;5+R|I#qRT9d(feIedJ*3#uD&H@AsBBmF8>?$c%boYqiB?QE6i74R{p+b2ItmUKa zr|xuR2gq!>QzyHrDFUa!45k-Ssp?WZrmAm%wXP{{v4borjq+;-xj&~UYll9+pi(5T z-vJ_n+!;>szR9tWX~sybkO>tsTPY&w3z;f)r|Yes{!&cn3l`xn-k1%)pDi7ZoyZlS zcK2^HyBJB=cdH7)xvv(Ty+Ff^2@n`=7wVGHtL^3|GF8{WUd2IJLMLeUl z?&K;q33skUAEinpf^K%2kW^zvOC?IbTpXFp4feedTeMyiu4eh~|BAAiq+@s3{w?9f z5R2G<|H1Dw+DDCtlJ4VtW%X#$I>d~IQzg42hVls^!qvp4!J+}%zD0|t*Y}YIo9{EO z15v74w7lQJkdg*_^h3*VHG-8@%mrXu9Il4xkx$*12Fvw3c2XCJ@=e)%wwC)84u4+X zW2Am5nqO)Xm7$Jz^-{AItWCqMgcEN!4s6dJ$>g_Yr@}1KP0bcvW%o+IPGM(UBDrhn;EAIG?9LizGqNDQq?P8y^gs3~GMxES?^0PUu8<3Hzs41@UJ{8^!n z;Vnf5#ak;32fz;|*F=b9$+DWW0D_6)qbTb(Jpd^SsPi@(hw*8u@ zp-iR>M#M+zL76E4B!#PN&jWhAL+R%Hqblv(`6gJbN8#;iVz~}xjob&YY^;>@tSDpD zlw#HB|8Vdw%)_y8HS~9+m@KCEMo|gluJ!qCSirq_tx2VaSBF-QQ3FVB4On2PwbelE#$@RG zEjhET8X%;ue0E#!)yO*>B?xLUjPi~fC%ffr^@q@cd9f0N&~kQPo2DKsU6+}l6!$Di zlH7EmvYO@$&hF(uK`yBp@F0vUKAYWH7AD{_r_1!$>=q103#?qFOl13va*3OGN@*PtzP&C-sx4bO%fkzT2Tg}M+zRxohrA&K8XzhU zGzb^F*GS4<(SaM$9kO!mkn!SVVf4xn8BeGYhsY5)6nu;V%Cyl_Rxq5|qzwT;OAFKj z;96l|4FE*}7*GJ<`j!H^RSNkY3x3!7V~q~7t!=b!^Vea!yW6#s0t5RgeCR2~&$?yM z-MwjNQZOcFhoK*Am1bE=N~+C4Q?mi)1bPMh1Yg9(!?KKsIVoYp#W`{uA#>Vs#MN}@ zK*IA*olXsqQxQm9NtA)b3oJNr1RzELlJNYAqH1Uo*GrBTJQG6+&z~q9nEZ(ejTvf& z64#F$YQm{(NQ8wGQK&LW9XTwIN9lB!yv(q!x&^3mGYYC7nZxs-OMj0F5mmVF~DC>01l*!iEPyTMQz1nRhd#&FXbQJnGywowgQd$V@ zywkaJ-*!@;Pv_(B``ojS_A)wswpwS5{tjJhv$AfE3c9JPYKa_{2xQ`IO45lAj}0$6 z9VRrV9y+cikTmge;splg#fK0VAC<^Na#A@Fkt`0Wh!YE_wglSYxj}(Whe^%JWyj?b z#R(!YM`DN!`BYA7swW{iMK&Z;X^L!~y?2Y#*|BUC%D(Lxy^%r6EF*)nR!U#}=jcxv zM>;*aY{q^*)?9z3vi-~x?D;M_9VSmsF?`XG0untS3Lh~rO~?>&fx^dU3YQ=s7?>9y z9~mJ*W?*7`d|+T)K2^xtTw$A(X!Rk9ZspBl(Bih(n>{TzcG4f$NF`rqXYfk4)tPi$ zd498!79QT3wH%fym8&dmSEQuC6J}5ZmK6Cmx^R6m0CMyxeDcUKQD7my)MG(mNrINWz3UbeDTSL zEMH)GPmC)R#RPO(z+XMZ(%6v5rna`ATaN!N@5i!1=KH)t>aMl!p0nNt?dALKHfP7^ zW&YkZHX(cK*7k(-E3CA}=wM|`sdRcjvSwNP>C9~F_MyY#JWpJqC?+*07k3$%@|o3S zOQm+}-PuwLTfeu+ZoL#f2%T#ur}duIwFdhccBNcfV}>84)4tX%!}jJ+_H4SdaPqBF z@;}&0T3W1@S*;{WtZCh9BU(}|@nm}Ehv#vk0g7TmbL#P~BraeoeO&v0zxQgdtKHFB z8oyy>eDC{wW+~aj=}swavKhn5&6?6GpQW(L2rrcVwMN^0>0{~LK5lGa z+WNPc^=~z;u==yvJ|91Z^qYI+CRvVtCLCxo15@5l$2e2BYp0% zl&*b~&$i*~_w2Ueb(cvgY2&Z&9jVV|vr@;_nbpSb+nv_chYwk^jM~O-Y^D&%3M+6} zNLXP7x8tHi3W<1laCkvcOrV@*cw9+HRcT2H$L3>8HkJmGxlVXrHzh610f7YtEG5qk zE2V2BrNSYZh`AS|-S>w^s z@x-IW_2deo0wZgZG1(I9FoA{VMih#Ia^!U58f_6|WKAVdNiL~qLLgC)k&kL*O)~H` ziyuS2N_XvFu@KnNWcz2ya*Aa+7K~s9La;#&bTEJ)gfWCAMkoRaP9(ts zO4z{?)WC!wP5{9ZqyUAM0v1=W0T*N#06+!^q=ALX4UdI|C5KN*=>~ z<;@MqRI;BLo+MW&s^*BCml;pa=46`^7(wK z4S^)dWKEQQUYc4X-9#R#n%XAaL^>gfeuzvsF4bERI=5Ie=+!ny{rZJ`uU9*I2b~fS z!1G@a;Hp%t4Q=ksSYsi{x-YC5{dV4M#Hk(4H2L>uoB9!%Dw)dPde41( z%tFV_K(a-p6h;l$V<&d03n1B9~(BR>Jvl|LkuBAj}r|ct{{jYqH;OesIHbGinwv( zO2P;uT9lk(%cX!Lju_%|T4IQc0Z1T01Q8d+fujf{r>&z?c5j1jGC1#KO?ve=Yh#zs zV!Z4seBW1CvyJuy%gzE8Fi3XZAo19^ zg5+|kYDAy;DLIMJTYXkGD%;1(p|3;c@Z>Ma?)lfvC8!paLOlcgs1)+wbh{ zf0K0D=&y0pSYuD_F0h1=I$I+<`LzWH zD+3D{s_^)5QPd2r;%f5VRyV`QChZApqffb?yS>#hPX`SwrV_|TiD$+OikeBH43U>6g1iJ7Ny?sC?pf>^d*+v8w-lT(Ml0^68QQ~vWph6;IZA3&W zYa=4r(oE%=)uPp{`I{_ud(X&ZWzjy@NYHs=s}-InzOdq&f=ZAZ35uy`NFt6_H;bdy z9CnK}PS~ikyRu%&nh`oDpdpDqA?ZPa0g9Rtous^mpz@4ul~i`2gM>D0(CsUkMr3$h%HlvaJ)GBM0B~V3!V^jUN zgUw=p=C#@U+0?oinZjGy(@42JvwVzNyExxzG=gw2M?&_TF_f@*~ z*HJcZF4p(1_vhow*v0G4eXOtNyo`0v?%KD$I^EsNdTFEXGAr+k-Y8@#8>21nM|mTK zm`XS|RmFou;|&It+akVoVNMa>8x=ZTFOY6j>Ez?9MrP3h)SRZC1bvKo!r1=I%Y zr!*q7;r-E_u5d#xsi3!K08d80#s&anUT3d%~ z`ybbjEMC@{HjUNilg`QJ)~ppvqJdjjCp~TMgO0 zU)lO(c6U}MtMdz?L;2WAcAIOqzv zAG!Ss)DLVtGPpQ^D#-;?QQp(DU5AG#_sKEgtl#S&o8t0{^Um+zh;;1|84J? zh9s&i#>7BrffA9V2T2zcHB)m^Vq=DyadQbFg?UgRC@v=`7Zo{L(D3}J(}8j`Lv+RD z_*)M`3%&;1ifuJ#f49s<25n`ORy?$bDS>cECg{)DQ`N{Lx~L8mdde}nS}K#JVKOpI zJU&F}beLQeBqc~XDUm!;MCo*(+?*ciqTwum$Bx=|*`w6C*6{f&U;12ryU3bvoBtPb zyd0ay_L%XF0gVe5CLxdw3OpbyRHsIX908oF(2PoURHCIRCxnctZ7(vu&)JSM(x3J{ zZf&;bF}6E5UrI(Jp|=&VOn_ufHlPc#Nr6X-D-=~DL{1hqW~doTR4D+Bl+}}v1br%~ z^zs;s)-%T1zq8vYBdiQk$L4d)y~hO>7pTBH5!|3g|B_qj?l^Y+PQy_|J&cn9%E4coqX9l!s%YM z_cEidI>zqy^`wurmrOExC2O%-s>qSU*2vpZ#jJW%Rua_{EKH&~gbgfTU|E63#R^5$ zfKD$ts^}0|Y!-yqzK_l&jji^*wo#__+p{9$q0{ja3p_}qjw(;2$TZ;sBjm-%3(N~U z$Col>th#@&-R5SFj#0L8GH2=ffpmIkf#<=XsG1r%PO9{{HYQN@-doM`XKuA-!Fs>R z=w#!tG~vbA^8fw7<~M5K`mJU4(5gR8U!M7LtBnKe&#jlRiz{)0`u>1(Ey z=`)TFKStYezHQyzr2KdC@-yf+_G7oJWYv1zRytVNkZ?Yv7kF~;f}(16uqqLd;w%K6 zFYd=A3cWEjZ&3pNV15nkv~e64QW)WYOSD81yw+XxEL8#l|Re9^K;Ky>u=Ah zH&&sAEkE;xWjMPi9uAY3D%A%vvB z=>d)mEuLt>f-NE!S)f9N1%h1J!Ids>z;?h19w71o0F(fV6?7p37t}xmI6y)Vn&5*l zf>4DajKByjM+k!nM0mnW6p~;C8ff8V3rNPG#0;l2u*4t+WMN^+fuqIU(Enep@5&3A zGPIYO`Q$bkbFJB&?XR{f`^AS-M%z7aW%E)=`Rna_W2DciDo`&T9=dqBSfp@evNY9- zR_o_x&A#UvtL^QD&yVrbXZ@M|78|o~P&NwPe2u~WZq~G;{3mH61JwoUhli#LMg6!; z_QtdZ>I@!GlqW7BxTs2^s;ape-Pn3RqZ`wcSyffDSX;~+w)%Ww|HpUhV|B7;p;Ooz zv{g3C+8y&d8=a9Oqx0T}54{-=G1@>u;gRVAimOG+6p=>Wu`#tS($=%1q?6qW+gj(^ zTu-m`*}GXWdWZqUbHf!NSC1)PK_AeZwSw*qzH|Hj{3Lt`pSx*WdzGK$Zbl#4=GJzy zdF5+Vwi?-4kr+EXA}}b5$qhM48gMyb0U9)-@O%_kOpO{YMP6WFfHVPvC7l`vH(9oz`r4LMK0LJMt(>x+6QlR3EzC% zF0;^~YZgV9NVbGi#N&e(ozBdT(~Tk*i;@+UX zy^qSDRQhBFYi$lIvzP2kYRH;IqZ?a6A0wV8s&qOtN=`9Gt{>J$R1TV3TpJWCj+wsBr8i)^SveAf74G1sU^Tz?S^@O zhilTm-lMp>F1LSm0aP42pDtHI=~Cg-k-54$Hg|csQ@DgR`r_W0(KPC2KMqiW)8DoE z&sIPyVwlZ#bvtWjI87Hx24yr^7(r_xcplR6BNs?)3=UXDK4uP^M_k7F{HqMN74+Tn z(@wU=9EbZPPQ3maG%Vyt4>`EQ2Se=Pt(;~`UZ`jaP%+^)nM)Afla6s~a^u7<^FA^q zekHyCQkT899ypzdR|M}ObUK1NH#tr9Q6j&fd+XJbnhaNi31?CjTL1z`XByet$EN5k zQoE}WOzaMFLfiZptYRGf5x(rGgRY4xpy4C%_s5qXI{^q90 zKaFR^OM~Sp*zt-c=nZbiLSEL2gMayg<7$2-3QTR!2V4OBE{_&FgR73{b{)|u4X1Dqw~;{ zr*feQLPm+1)`ek3qK_%Xjlr+dssB?NU7X9Y{aIS(9PL`Byi7D6-y{kZBx#8@>VFaA zXl73YEerj&BBJH0_urPWB~JJ~F8V<6;Ze2#ndJm6EqLemwy}Ie*^g&<-Va!w*aoP%pXp2e5ZB!+xsAXIM0WDSTrb?`Zi1&6x10t*oQm{)Aye6RL4a2P@@t* zL*IvAmnkNOI5)*`ku(xrziX1WUw;SLAj1EQz>{2)1{WAnKxC8CFOU`=iO&!4n7yn| zS{TIMAr7&$*Y8DUiG^|co&rUXKD30env_YHG1fLmtj@g_4IGJd53>N4f<~%Co2+-M z#eaeYDg2-W!e%b!C1I!rr2i@GCgMJ+bRtThTM9;rw|9@Q%8Fn<4=41|7eBQ8n$+-p zWk%gQ8PHn)7w@#VDXr>@?k9q+qc9c-rWj#@=+KNb1`->gox&_$NplcPJ*+@S3Bs9->v4aROwd@1XPSsZheNxrI4iZAkSmn}9hb1HET=7Xc|%ywM*d=@mi zy6a#2`M$**XlWF%TH(iHXUo@`@CeQ8^JfondG<7K;0XNs=QkU)G8*kMd>=iABS{jH z&cj2&41<~G#u`7C+-@^I+E#H~bMLo5NhDMfV?8@2R5k9wIZa(O4q?M?b=w71Hl8xU z#pZZMPS;bpq5MLwSg9t~M`+S1*M4D7*u=~nPZoFW4Ypz`%<>ULfU_o6Ks%zN^_K+P zfk$sh=P?G`?_Te3#wlzmRmdJwQPqE3I8SPUGDE3NWX8biNJ;EWGrxoHVt~Dgph-SQ zfT6TofZEBj-Y>?*0f zInOvYF(8?x|7U6fHD%B<&M{^{EvHWP2ZKh>P3#6q-J+pLhRD!K_Zb#am>+@Y9^uT& z{K%{F7Q7{0!H4$dx;sjQ=u+WRSKQ4Eqh;7Il&GZD0`srO+h)?vU4&{X_e$@FY^K|d zVQG-{e~7>Lzf=y>@kMzJ>&FwGGjNa#WEHC-zSD4#>{RiGPXBFMjrqf$Kz@_^&7P1Z zt~<6e&ysifXVz~MyT-vRRKRj70Lh(jF;rN2Z4{LnFpIcbNRfkQkQw+DXLk%GL_ZB! z)J*aA3=uV=`dg2@cw|5d5a7%Q4Yo&(2y|nXCM*b^`dP-2T3<|u00(b9{|%VI%O$K9 z0UIO^ocZvn#)7fGR{)0DXAPK`l5tfT_W3!+@OwO-ydX$xsKi)+9#;--ln?oEbAf{Sm!l28~ z2M-m@Mgn#(vVC z7HpMpE_uhblQaavq}wP1o)r|#jJUR)WRjuc1uk~6r{()+ekVjY7hDQ=<+-1%|(hqVk5PoPwlLzWHA-UZsGBV z(d&jF2#vbZ9##O;A z@pF7za{}BDOaMpi9a!@SGf68;^bfw%$Z&%X_Lf_1Kex`BA<|=GU;PyRmy;Wvw0)hF zMNwNEui~M3Z;&8d@O&F$ZDDF16?R?V>m0Vz+(@uSQbRNl)Rrzi)prlFZ@m114RjUs zJ*C12eLWJ~74qSv4hbUKj4@PZ6X#jV^XR*=<+l-t-`dVz zUd%27oJrI99|c324WSLRWd{irF?n~%9z-?J21oxP#Vz)}L7U`_8^BcA>q1s<*-NMI zF>qeJRaR8cAV>#{hDtRJ4Do#%U-0Uqq*$xBJUwDo!Ihx*Km`Ett=J)miQCiuqO%%fZkJGZ z2()fc1E`74>7Q2;wu9i<8~gNnwyO1KLrxs_$TT=%J>F_KOV(B*lkxWlp)j4tGR=;l z0^2b{ViBE_S8KA9C|^Z(dIsNWcvIJiF&R*556*qKDk9JGdyqzK!ZmwYu^sORj5cs| zf1ZeBPGj`&$WXq>S`b8w@_#QnpMVG==!WxR`RxR8+=yn z{r~7U=KZ(~7!civ`Y}a`1(AN}76@T6o;OY>xd3kK3)xL+aONu?tGh~>a%Fa!g(M>k z@2*&%5$yo%?tp238$qsFV!nq z-Q`D$B=knAH%Mz<;>a?v%Sq%ghk@!1=p2;L4HYEl@dTF(^Ti4oeu>t9clVd-owzW5 z$*p@wKI41o!X^wX3tKdKwJo6kq$6VRl1|zA&O^j7g7nslU3*Jk@d!0m88%Pmivq|{ zjA6EzVByR5BTL>4EX|r2nH7@steM41TcAGm;LOF8V~q&cQSYJrs^1c&cnexz7a z=k@7g_tXL~z!L)u;U?LOjJCEnSQfekdK*c!L8 zsUGlu9%(E6kXC|~f>abxV935p=*liaJjX@^aD4O9G zy&!?9Pd)-X8WgoM68!WN<#1ySL*-ya?#R=K1ShXfG&(`Urx7!TjG2R0$?1sa)a}7n z>=2?%AnH>5F$$BJnT}lw#S}Q^`1Yxbo+VEQyEwWO(k4CO`b#!Ft=?Ey| z{ZV4le2t)Z{E2bNU4E_5+45q6OeWPY=#{BtzsZSLhr674946$0h8#&?Wc!2Q@lIQ- zo#aKfQLn8V!pmB%k#3VGrO$Z6i8$SPNGaCgNI;Q!5z3uh3$a z_qDkMBfa^wp9hY38Oo%a)7Q>qC|+i&lMwv6r(66gq809fmoPI{y|; zy7-{!^u1kB*ncj#@^D7Xh-)z$8Q}Ef#1=7SEAk*7x3_)Y^D62i1W3w27&R8)(F*|M$6`|% zeC}+Lr)k*icL=P1mnTYxB)c|XQZ(5HL(gXIh#+~?jqj-vLGHS|3YMlbj33dLZFXLX z9w{$}3L%cFk#H99(M`TCS@t(Jroyt#VXd_}l*Rw3mm9>UA-yiQ@X;hf?`(IQ`LLUF z2!|h;vvs*ksZN@zTJd!$)vZhN4nEd8yZk}V#HcG9nG@P3*&1_?OzMrn`+1^84NTvS zIa1M-DhnHhCYgxBvdN>@M)hLlklczrHCVt~IqzIW5O{9?sDR)atD(d-k0NsS(A2j| z?~XhX1>|%4qEkMG?VdHE5yNlFz||Ix9&^Lqk+7K0jc!=^k+8bQPNocFvpOS_OA05a ze414>>S+YwWSDRQcc|n7``D!}1`lX}Iy2JeLe8|VFn#)h;F$)_K^1WIBpU!ndrQmT z0c7&mm@TKNRaWl&yoa!9^FV4 zUHBoaWWIE?3Nx>e7Cn~AXi(sAWdxTN(*5i9+STgv1O*D980A?)=Mu>c-3WH<>Eo|z zYRMngalkt>$c41Lx`~7{0s<&L>jWo(!V<_s2_RT+76Rq`I)8=&KRFJ9e8D9brxR=O zcG!WtGIgUlHhJzH2ZO#JnCl3RMewOCeaQ8+Ur6!91 zaerxM++{R{Beb1yn?U$E%@yUeb;4;DY{d%A&e%pBksIaKQ_J{^zUq-|+0w04a2EXv znQ!Ikow#R+Rcf;uP1&R7Y+^A*b-V3<iCM8yRGuk+ zD%ZI7d<5eQkf(nWy&K(d5t~;|v&9W-@Zu59P4RScsWE56uDRU|{QBh=zV(JjSD^h# zY&}YF3!cESIQWYFp(UOE=hhOhfNHb6a>=YdOAer=M1NvO0YTZ3yl&VQ2#3T{7&GIvYbny@r~g5ePcO)`V>zmg-K9mR zU4H5fR6U9_zDzY|Hx5QulAwO5heZ_}BjW&k;^6o&Ob{(uBwm;P*9jbl1TCoIDnV@r zqQm=PfzLE>t>s`!f|8s>PdC{hLD}6?w@HM+ByE|h_^EZ8=b<0LGsNJlOGE|m0<5Xz zjaX0PM=it>3XaaG_1eBun?TyAa0UELn`9obwl9c)VvRgxH zR~0#(B#D_Ca?TL}k!K&F^W3DXqw&too*;B3zkmD`H`DLmsw-m@bV?+>jF)g|FO|M1 zhF1cv=mA3NG0c%ST0$+Ox2Y5f;IANu7~;~D3VU^3EX{poUTeP6En@~TuFx%`rd^0x z!_b)4fX;k69R{hPJimZ1+%ikcwgDCF!w}2JT6b}gArM~K{(JM)R;p>G7sfDuuD*Q@Pr_rxPyjz%1y^bJ_@++k*ZprPtv zJ}J53i4RA-`c3o~IElP%NyGA;`B2FREx?CR(^EblUWGFivY<#JhmmZx?TXz<{A^wN zOy*x#i+9@cl)x)6)6}GZ3mn&R3448>HNO{xJxL=B^kkUkIcsCvlR27cR~>XaYr!F< zHvHU^EmD`%Y1%GN0-?{ogFHBEk3x0%ZkrZTwPMd0wHwftn`HASGt$Ghd`R?Cpj#H| zASU6F4~u&4zv_p8wb!C&dtY0{$|oS^w@SrvxaJ=T(DR)7?xM?pCmVY1a`_K(#Zf!7 zrtI}hEDJ@FWD=i3?2zjjHf8QLr()?*%NI)=dd`jV(Zx}Znw}NQ-d3h4tLwHKdL9%v z93yxzOSB>PtU{I@NkV8?XgetM8*{UL&z%ueOy)!rI=Q9bf62^dMCJDPFkO)Cdtk`1FF6#X>;+7PX}o=dG3^@fcUO0I-kOT@Uulco zawyP21J}AmWW~{F31}^Q-y;DvEEp^UIi$YSE}z>MTuDsZJ~&DPV5?T5$1jY zJhs7Yu5P^RHDSlbz^QrpnauVHfnnPi;7_S$PF8?yaWgr#H7}QfX$odw)%`0Ffe%`I z02&GPb>_qV^fQzQ6^I;2#+(BGimjacNvYjZ|xNm2))5f4@QrgI~vBpsMz zkI4&>1^!13m3W6zPRhIT@q5}KF>wXV6`J{`jplu3&joIzHy>r-Cxm$`G=}-6n_QUt z{RjxCXD1ZqdvY2;7Unpa#-9E-@UnCkxFy|dSvVTX%X3vcHs#8OLq>jSIri2ZusyA$ z7VvDi8-kvtf5fVkZF0yavuEJAk*$ z2z<(n++oMM(MJPpk%s_+GgdCFc`6qcy3AMzY!O+Vr04=ApGjUV7J0O8h~fkSwh(CA zlbXm_YCDv`~oqitLFYC(E!59(c6OjS~#gwR~P z>;NFqkaT+Kq^Pi#p~ew7%bERCb5x6k57}7-*I|Or42gj!_6W6^S}&DA(J;2+`vWv` z??djl-8e?&^G#ZAHiA;V7#uBuHA>*bRU>GjML;KG$b$jm(7OwnROSd;n z)9S_u6dQbZG-R(S_c4qs(T@N@F`||B4HhULX}9TSg<(>exF;w5F3#^CTo({43A zs>8p*f>`$&8jU?b6kOE~rA}^*@kkBk=cwuic$vcGmkmcZS3&(dqcW*>(hwx5C2lu^ zf-K8KA-NwI>$uTg;y+P*w`+A`szel#B(~zIlFz9GrM%0F=$}2<(yQ3Rl1ujJTrip4 zTim9jW1j7!5mGioxmC!`lvgpcTZbyJt^$38_N`0H0fyF!^VGReGz6s%lC#Lh4)I$R zeYr>yM`R?OWP-9Ly-UN@%~`wlG8Z>Qfgvg(u~=9-K$0ax)0;8jQ>GBnKbIr%V5F7uCAr4=W*9pm0HD z_TGoMYLGxN!ifPn=-}kp^9v3vC2lO8ZK+ZTQLJt&r&>J4hoO*cDTKRQi0r(84a<0B zXWVdv@%VBlFCaBb)5@=y+X64mSHh?5e2Y14XSdPs;bMOOFJ0%oHI*Uy5n*a+3!$Nk z%YNxglTb%!Du>*s%-WeIf}=VjbN5UE3={l(D<0YOWKAy^CN!!Qk20RDQBi4t4Zd$| zn9C&zZIPKZGa6vs@2~W$(!dUe?>id|Q?E+Kn~YxX-%PjLCO~HS0w9V)Q-PgP@qK4a z(gc4i9^#dZd#=qlcFonE*3||5i{X}GO*H2eo4e67sTgTAbq3NzfsJ1hn0T?1|NM^|0&aO|?YAU#VpA{x?@B?r~oI)Zzt zLb_XVW1u#Me_dC5#<|5<*A=LK5QK75eB3al_(m^*VVZ;CXYYDJv_0$eP15>S z0pl?uQpW9^h9<}_b*xbVBdyJ?#jF&SLN8{4VN#E;9hJ?gQ;3eI-Lmw}*jgEU_q${T zn8CL^st1vT4W=1fK&prPr2 z;M;qBj;Iuhm%mQ3p~ruy2U2voJ5ecKBe#DxG3aB(AP!xxyl?}0)o<0KAV7w^PE*aD zb#_|#g`{a(uE!LE=U&AIUz0RUOBr5PRXs5K;jxFjdVLvO48Qo(RGLax(X+2t-bY!)2hSTrdd>K)v}s zPQa3j?7^G?LcPY8##(o#)Rb5`r|RDb2Mhs8Q=ndv$Lx1u6g2VWYq}axenUIpPfLNT&x3@ ztPrvKDJ_?v#{a;Q%uFH|4M7@qCZ7!Wt|P4mEIW!S?d4%p0DlY@;2op$Gr+H|&(5rn z=>eYEqW-7v40*Y%aLk(j!X)KSN-&t^H9ak@#W$UeA*5|YoIu#GqtBEu!$_xorWL~x z`+VaWSiHv$?u93Oy)eBe=kdfLXOY+t%f*aF_|=Z!Mu8f-qPR zH(cVq{;(K8{=jY&%hp$Qaiw}INHjNKu}+Zt$)lBzUBDcJfSAX|4>wEw69e*~DZ0?q zy^UB>y{GidYUhk{ckibCULL`YgH4Q|-IYj)^9WJWD1d^sB}qUhELZ9aEud3)#O|j# z@(6E?5!bt^BQ-Gsu(l0ff@60%{!0Bo*85PpdbCW5f*Ikg=gW;*PzM2ir*w5Wq%CJC&(MPWY3Ax_Zy?qyIRyLnK((nqjCobQ(_>2FY?`x z@0iUuP0usH0<#DFB4C8*Q-+8RfJ%x&-5Wh`a4QFu)cf9GXDQ`s=L20a$R~C|_&G$K z5AuzoPAE7Ky%r`qb2$XZ5Y^pbQ9a$s*bTyf9P=s1a&&JlSOY{=ABYbA4bgImpNS#T zL%us#+%*yNQoW&*qLNB;)sRq-5AP^)(4Ks6JtkZcb#p@^8!qLQjy1}f2X^bbU37x) z#t9rq?dN1!PSpbrtaGi>XMpUqy9ci|UH1)7D|a<)lOOmZQtWJ3|Hp7jh*d03T(G%C zN^}ec6Aq;>5hww?^<1#r2D_m|M+dn&nY3>&*r=Z7O?|vzGHFz+m0d+?e3s>AD@LhEIw3 z$@*G9@qcLIeI3OG5)_CVK955;+sOSX zQPXg9t}k&m;CeVZE*9s#&T}cK)fm`DRr7+1yp90nyO{6xD2z;igy*uGbP<5#unO?U z?TQRHqn%AlCO{{B5x~66=*bi;0<)UL^o)N0@Ho{;rHcH4NO@RGS?saOVyR zdWx?$kv-^D!6XO9OX37QmQc+IwV-ROVKBH*Vs+x>f{BfK*fsKP4Av>$BSCP&oxEg% zUZlN;O&0fqI^V8u1qK)nuP=JOS*Py^Pu-Mo|cmn$Mo?P>Orb383G|@SGf$ zv;vXU99#E^5g|O?P|bHeo9xuO?@BNPXy}O*a1EO85Gk0a5VB8WpG+3#y@}{#y)Kg> zoM$e2s&Iv#h`Yx#9&{Qom1FDR;WjV*)mF5Y!o<)mb>@c}bVVx)+I5C{cjR_OQo~Xq zNLN$Gvxqt*O_w}BbWs3bZqA>&(!8Y3;@HjXP2(5)nDmE6VM?;wi|wgR6H@ET&A`ZY z_V_$rk*1~#oKk=41y`Hsx6D%<+h#{2BJfe0cGL1W{W<}3trECllSo%n(^F_uE(xe$ zzOCMEHV|(2u-AoOHWqs*Oq+bgwqRYXn}X2z_KJ||NWO%_jf!CH{#0}cknyg5t^liC zwQi<2s+d;Me|sjBXCJwwttwN2F1HeD8J4xVdLK6{Q{6bL*CxtAZq@Az+*e6f0K%-@ z@m|g^Aydtg&B`8*C)KMYm{O3NC5Sg@cMci-`he_6N;ZS%G`FzjW|rY8rTK0!;Rpdl zvYGjOc}J4{#~?nBaqR2KvW&_?c_kuUdsg{;~I-~upzx`Xf{)3V1&uOAi-x7N z+j)9b=YSyX#J%`N|8bcF*i=V2H*jj0E#Hp*GkLd;|L+MJ}&{WWK%R;^rpQn8ooeyW2WG~bk2}981_Etx6e7ht9Q1UcPlKrr2Qmq74O*gNNudW8iK0kI#d36=mh>;3`j>72 zoE&NnF?W7t_1S3}W2(BeN)d%bL&9wR7L|a|3a2Wn_{KQv)Pq|SB*I;i)ws|!7X6Uz zq?IY)1t#`d2D2reNoP%JgPslVroW#ncSRK&_S}ie4;MD5G@=8TXPcKkCspi?k>M`V z-pyT+fsf1XEOl0{F5IL!`@O{0M-`|*b~ksm>2KBrAxd0BcX#1W6?>c#*YNKy9MM$2 zq#C=$7oDI)7X*RS+RlbvX`U)pp+jLo2$>cZ*-?f)0dF$RO##)XAs z*FK(A;Q)1S2T+4YC5h6IC_mfK^WLbQ!^5&HvLv^h#+FYO$>y{%6*Rfc(=|_7e|?UP z2w932i9qxaitB>$6XJ`dZ1yGH!-?wNDT$VW@{CP00YyVR-h~tCLP}trcxtm4Z8l~A z8{EpAc9Kv5^WqIPhz3f{F|35(6yoPZ>rKNICmRDavzF9GwC?AsP(_ zZtel$d*gvh0GS#bBBExpYWgS07OJe9TTAjt-t1I-!_xj0E*& zNZYW%m&+ENSnIY=ENbv25cXydhUmb!!$zg&imyL96L19-wl$di##JS-GilNqG ztm_*2^6#<}fTpkSS2=H?*gR(oGV#V7feD5dxGEQd&kh>W_9)3v=K#p$EP(a!-Ekg1 z1d(MYcravo=A)>6FTiOZpop;VJQy-X^AQ(+H;Ba_vdG4_f=VPGatLU%7y>?%Z9J-| zKg3Vi2gdOFKnXVl{Zl4T`CZP(27we}gL89L<&(-o1HoqNuejMP%gI=01IVN#U{h}W z@5;?GJCXG@Ku$~o?)}z(#osKK&s-;J#9&8?7Oos-j25?rDI_VxYxYTQQkf(7vU$=& zDNn%?`JWMIC7DXbk9$J%WT0ReI&`qiGGLc*qrD_F%uY%q$c3jl7IRY&g~^8 zb`fA?d?$n|DCuegdICu%A!`=;uc?uc26`v0C}m2E4I3Jh^k_Kf)G}}>zYRT~HHaKn z2#3I(-SJBPTMQNB1R&e#Cnk*k+yZ z35#M*m3X3v@fk24CrP1hUN27_p-{o@E#YA^ksXsd`0@W;4hK{#UFjybb@@~mZz+nP z7lT+0Y3f&19`AOnGP?m4K*EXAQ`lB!>fH#&G-w<%A@>-3m~8TYN5>qgmJRVaFTn8C z4mNEjrY;~{a9R6?#kE&XIk7}B=xRSzx0n-UA9=bf2S3j=X&$*-=R4xZCCkXC>AqfS?n~QAp^~&h`vxwr?s) z1N{e8l?}F@qO`ynEt(f??oA4yfwKUR=Q zYE8l@=aeEc%GlGJA{>kX8X}vgQcTq;XTA|?-bq`6D#|O~TM!pr^Tn`HSaf7ZAs?2Y zm8b<>QQR$YDTERwIEaX(l6u7lUiOq91G`ZQ>Sj+rU)bN0;vx;)7|zMl10tK2*_xv2U&i{f%Et1d(|Iwfb0?e#Gj}1ETtXTeE|GSz5z;&}u^1AA|<5EY_SYlATX3 z#zU1>!CKe^m0N5|i2Wtoxfq(>K)TYtWHl7-tg}Dp1Fcu|1tPtX>@qJ1Nl#A~Eyuo% zSTTob_d7~<764fDv#o{>#oP4v*v;h=qE0CT47%_;D2YODDdLvrex#n}dR;_wsbJ&o zPB&~(7YGg@tG-fA5XcD-*L{c=xENO1%9}BtDAp<*@_k=-b#`(fVk!{|J(^-2(ITHI z|2#Ur(`X#;{rBGGz|@`7cOf8(?AE}?`v@4>Rfz1FXWx^0lFGQtRw0IH2QBYG1m8W{q(pD&?$xXWX?_~a|x+=(hwt# zuRBiPPiJjRO^z&@xo%^^mjPBx+ zml(SOL-mkwiP=Gz4-=fO=AkkpmNibW-fKIkq^jwrD`57hJ) zo_heWNq?XEr0#Z}3Yeam|HvEH8>YaT6`-Hr{PJy!@*KrU8Vc47!lZ|HAh+v!gfby` z@+-V%3HjkiQzj|01P8X>g$c-574GKZSMrLi&R^ldmk)yQs~>sCwD{57RKmyKG23Y# z(#Ay=M^eN^yML@>ERxxfU*TZR;Y$SpM?g2agz|RpVi2(7M^jS?|4~KO*)&+Y4?}Mc zimZZA%SFH1hu+5j3a0#OzuNs%46hsuSqg|TmjOGTj=T9tiXEA++p!fg1AV3y%(RHY zs(FQD>)Nv-gs#q8!N8r%qkHfv3*D(#%7xm7761{()ik!e%&QnBI@w8WFSem-e)z1=YT?1hf{@>+CiLv=fi|^5Z-Q;4arK0m zxIkz>%=tbAxS>71tfdKv${Nh4WY8ZDYc7iI3e>O(0|oJ6AlUtP0xQr)4_)kWL*4r{jhRkn8ug z8VB6LHwc8FxeJvCzX9rl^s`80K%N9xY9(8ib>Px8-Us)5%e>&7{GkL(d`kfy=b!FR z>W!<@d()K>77QmYcP2~>eN$cwCYn((E%CJ+Sn3IV$2nqSF;G8KzNJer}y*=C#LF;hSn{rwKUd;iZjyySW z>71fu{sM$50kuy#iBkVVVfQjg_XY#N5Xdh-G z`btihN3cz=`fq!UKYm-& zjk}|Tsga4&bA5MdD1tKjl0)}Ahm{^zC;p(%g%CJ!ii+t#+McISW2E64!94E8qV)i~ zH=_AKi92HGo02a?_tlVAE_%=h+O_qDV$^SvB*7&|z8@I;Ch_J6ie~aDhpzMpVZ3J^ zgy$<>IH_`uZ#ojr)ZZrD)&k>RpznoP+nk&>fX2V3dE*)r*xVh4=U3nRQ5VT_!lG;^ z-o`gbgCGxu$tz}4T$iKvkI`r_z7G$xb}qg%HTR%b=p_gN?U%5ddQ=ZD3GBKRhI`d{ z7f6M`k2@K`QUd91C(9OgCRoi5z87POxk9u@h-4I_sr^2R8okyIZu=;>*8#7XD%G{M z42%L7>T|ah(ifVXK7}91{^U4;dcHZiVanT+qTD`RMIY&#e>C-1J<)D;h%t;PlB{_n zCeb|iX)9|&swZ~C{|WeUDVbqG`9{+<%~YaFIvb~M__w>JS!%4XRahBNG>t#*Iwgt{ ziE=D}B(rik`gUh(of2J{HADXNorZ0R zb1K4t)MlChBi226w`{h4XwH#fHUsl)PT$H|EnU^+a`m+7H5mah38B{%?~WB65~0vJ z`iAo#c4`i$HZ=`cO6ZnYX^p!|`5k5!^}?sGiX<@?U@hDyn%N`gZF&bHE?_`VU;j=8 z3%nTaB{hmcCv~1ro5X#11U0t024|VL-MSvkkG#O1`!e$p6^a-0F_}2W&_Rr*PCBxH z*{t8Ri8fqyQulJn&P=-^O=d7JuAb8Fqb3uZW+{xJ@w?tkmrh~=Zgmm3zR9Pjm6Yu; z%-u&(fCZR~Ilr0&AZ*3PPvnLsv6HUgtfumNEB8Oa6?8ri93(n2$kgXprD>c4-9?%1 zxj6?F9kFp?0*JG%FekGeaNhak`ER;SMH@t20xwvJN0uOBDm8P-M!hM(cdseg&sNP%mXk7 z^q?TI0k{Or1fv9;>KAAH$T7UH|C`UZoUy_a(58ukL^%O%TK?zw0pCDDVuiU_tC*{M zu{xN*CJ!qhHFU0*8@kkLlQ_OfoQ!W{j%os%H>`Z%(D5ms;#|&7_$E7HpgU>2QgXSw zelcemC#=uj+I5{5V|ZnJ6Y`wvgWWw_D&<(AAQcc#V{iNDY`0%v{`y6t-l;iQ=ikv0u+c}oB)h1yOWi6adS;nGP*9au`LU`Z5R77bMZ zRux7kG$uVB8dz_0jo1Hiipx-K6vO-s3v4+ z`JxB|B0z^)p}2L17AdeLRx)(19eU_etCx@C{Yy3cjO(+Vv`t)Q!Z(nHvZ_Kl$el#v|S%wPY;XowR#(}WVX1X!HX5~+-CzO_AYkyM+ zGu-^%M{UHfVVWS387gt65zNBExd6sQ13z>g!&KYz4zcW{&@P7+S1f^0H7>NoXx-XS*sf4YV z$q_{Nqd7j5c`_-rI_0C2XZkdl9oU7%#T8f(Ra_U57}X2W4Izqxfha@g(t$>|M2s;? zoj09%#`$1>P(Qpnzc@3E5#9sUU|Aspn2QC5=qwr|8mQ56FK8&Ia1j~>*d!UzD9MI$ zR)oe#5gMB$k{~%kWM(w0_M|qeRv+uBEsy-s4dqK&<%@Lx*SA#a|JgIdILjHUttix% zX}lwYv$ny)0=r>F*69#M6dxPFVleDQh>ecp00z?qsoG#=fyEo05i6Kw8!CGz8R>+x z(+NXMwSP_f>v+D8uV0QeMjNG-uTwp5wE9h+DSY()kD_C^dXG(v0cz2Q`*YMkO@72Lu>V zk!AgGsw6tP$^acLup~}c)(=TVv!nBY3WV~}Yu~gJ0!)Ykmc^rU0NfGr-iW9;p zsl|sB+s{$!x8I`|%TDF{{P>5xz2_+P;}+8g_pO`5ci(CoE9R8HE+3a2N?kLS z_%Fm8=4~&gn`Zo?6~d^uv1+J*k)pV+oFA&+U4616qr^&51uRFE>&mIB7~b(P;5|Z=aTA;qr8t&nV-}+%g7<*Ps00O^_=f+p5NO&{t9fc z0vlwR%!E`d9V=i_EYLfg31m9sc(NxcEre0_ zRgNk~sY|VT=t2zD%gRU%o$CdtJFTqLEGixFkm-S zv8)&>o>RAD%9BRiFkqaDWyMe}q3Y4~m;zKPW!%gO1L0)5rfPCiXzgeG_S#-|q`227 zpY5}Moln5MLN}z>E449NDmlZ?Bxelm_$V7{33|JB> z*A+vBRY9aW0`w$LHYs&Nam}}ha~KJ}HxrIjyHSf&oATkubIdkRx;vaU@40uad~c>3 zQP6^=>p6e!w&Wkwb#QxIW=7g*@v149r25ea0Jpb01Nf(7q_3cR2UFd&ivjx9)#gDrsI zV+1EOAqyzXOaTdAAh7}wuux+QI>dnF3=}{^%d<{zBd+$=$M@}3KDGAFN&CfA>MCKa zn(8+nwfM=W)ygUPo^;Z8@$5EpI;)j)zjtfC*3!M}oL~90^<_@?#drs1#~MKZi&1o4 zQhrnu0$HM1*%l8G7URTqNwHQ(h_Yl<=#SMSfDu)>E-6D1$kIaP)JfT{F>N)ZnO##u zni-k48Pk?=*{*StVU5F)da{huyfkClW@bzqH!^K_iD^rjwk4$!MuLN5+U!^Rc5Xal zowvW3@6<5MD=~$3+WIoQbk|Awj?hZ^{?)lzCY~~FD85ZnFd8hbOA42gWdKE7SQ5Z! zsCZN~S~?ddxHsX`k$=|l-prqdvPSvwb^8xbsWq07TPeR!nEB(~LK}DQ@b&)qlb6Ce zxz2ND{NRT1>-}l#op?_TCGC%SjP~guL|F+H^n-5_C*=Lhh{HZAOotR!*MKW|lQJln zhdTjf3?cn}Go^Y;`*lwXyR`3Ld~4-PI-!O4$!cHCFv6EDh5t5Y%q_%{XKpEuziHew zYe?VLuk>aZSlcR=1WvdH*b-E(YonRza@e}5=7hDC@@gEp-Y@pJw>)9JBa;b|;-gGv zM~NbtjA@gw3(E!86D~EhjfQkLu3%x^V1`Nw)eIut4NOoUcw?y>6)xy ze!wX_!U0x^R53Mlt{0jPuOyBf%uw~HWYo~PUXJJz05s)`6iY4lmKjS~>6_W&$D3;z z^L;VjIj_DmMys>dd{SPmp7mL(_pH#?cWs{<(+uI{GCnK&&XME%w&jn%e~0fijkre{ zpN07CE_?1jV|eHQgOSCujC43~DK15QaDhcZqC>#7JHUJhsdUtIKD-}1n4yZ2fNsav zsmU3&uT%SbwfG{9bmEuE5NR{MzgCbQU`(R8tQjgERx@1=Rx%Wdb2cf?p|&G96cR^` zQka;~FTA1(wMnsAr zRSuiZ0ZUMv3*vrI_%zB+nGg|92eqWa*QV7flrQb8z3U76%<^Tg_-WebW7=Q7TeYmy zuAxPXA7De=O?OeJQ;PR+qil%xa4tKU@QK4dMKYXe6qjWsluq7CL#~ZfqgQ(vX{R&( z>~+KYaXua2)H~NHyM5PMY-j-jcBJUKW~hdcbUQ|+64u+qGY&*r!cjPAiBS&Kjf8Ko#*#lzv?YFeDr?Daz_}gy#F{$#(Kh~d~iX) za&*@7Iz_=?eE2l;kgaW#tD3s7bgd|`+OjJFR=nx=<7A7bGBPpSZSwKew3oTtN0o$-D zGU;sS!+rmq)#CcNv^m#{XYIG*+%3oW(nnu}e9D_M->+t@?XF*;wtdpCvcCxL?6X>~ zUq=2Yv>1Y+MGH*GNwNb2y2mJ4q15i37Vb&0r2Wpf@8`p?#`pBm$?M#=$|~QEm22{2 z8(O|-$)4zP~5@7o`CUCxvrcZDjQwyS%g*$C}28bR5u#X zVIPE5fGA)Ub%h`U+#`4&#GV>7HO1*DIWLqk`v9Z}EpiP@dl2!?=8?>M{B1z0(RL@+P zTyb3(C@XTwA_f~SM{E?4RzyWMKMgI}oWnUr28;_)6P5)-6=Rh|hR*c@QxKxF7#FE@&I6E~w98=H*#qz(X7 z+9Vtm4UVEBRk9tJni`y$jMK3>F_HSFQ#{kM8ylM#3xyPM;xUnUfJ&ttE~Cz} zV5t5jM^O$}F4SOz6iWSM+<0p&GslzO3un(5+b3t|@aaPwZT;VT?;#X7k$f};B0}<6 z!aM#UaW3PWY(PD}b-+wd&sO3+M7Ri{!m@}cf`Y}ww~0i;D4q?KGLfFmAAP*SOz*7FKIT2jsK45|rJNseg*9SbrF_ky)pf@xuco}dUjMO-*Ji|( zw#`H$=}gCD*_O&u^NSWHv?$pQ;3CDx3djtd>*WANO|THX)x$;Thf1hN5t5lK+Zzx+ zk#KlQsT7y(jZ?hGhlI0?(k7BmHvhFKVT@1Td-3)j<&*Sr4Dp?^(*9lFe_Q4Y@x)Q) zm-RE|T~BFe=5x+B@6TA%gXSV*3d<78(TZT8(AN`K6I_1qAcPMqArf$divuWmK*$wM zf-NGGGsFrKKy0S4q-FuR2@`!~qk*JFNdTJ}f*2r_1_)Te#H5l16j(&TM9~qd_+bMq z;DG(W76Hx(q(A}38(89LVvvm3JF z+|}g&%aUTfpS(BT{G`OSzo)O=wc2O#LkeSreb)KvwR*-~AC4GjiZiSi;>{mM`TNQ| zVYk@Qmm`FI>i9U{Rz@9EfC*)>tPQJv(C&s6m~lZRoXp87gP9Z3=HW;g3Uic+3p(MO z4T#@xE-0hncxg-V5tqZcpf};0Qll?cyLsPu%jcTU`mY2n1y}(l+cprDdEGl z+WGc`Z!(z^*1WW#Ud{jmfE3rY=?LjE4rNG|`N)jJJ}ExpQk>)Q<6C)Uw-R@ICx4n= zS?!+q?d5tu%<+pF|JXZ92pQRAK!`}l0*mv~ri2FA3kI%hBZ7dA09iIZVV&Y5?u1VP zm58ha*2VFtv)0T=00Auk0}=orC=?EbLc-#BI201hS5pt-RZL@8W87f)+B=^wUd8DZ z@HYguw*(xeAre|uu@%oW87_n?N~QJ0Lqn&-r7bFe^e^gL{Pg+3r-e3gX-9%H{J&z; zd?HyOUPU;#v{o$bFH=9fnMFzxKw3b3udh2yfJLxgxPoXjXdEL9e}RV7E(>3J)rRB; z4Um`&3SM`}9+3m>P^eSWmHmLlgSb{Ora?E|8CQ_<*D0?g5s{p+f~e8W#YAtD%mjKi zbR4$vE1x8@=BuaS-dc&R%CaI zi&;QxBNdJwcA zs5m+?bzd8Mehtl^k)za7dl&~&3V91t0`vm_b)T$9Q6%8*uMv{rtUnk$8P+6HM)9`| z2;-Q!(#}PN>@n6$PCy&F+@jQIQEh%`A3PBYNZ?(^*VxNxE|FWXb2$VQUw%{a zIYmPYYc<;M-lkgl^RYp54=xK!pupNKWFlHJ(nflF!hw>Sh_1;nM@+;0^MH0CL8;Zl zaeG=C_%vd^Pz0yVaxcceAWCp83q0jIF9WwpHi(a?#OF_-82gfRL#1(fpSI|k_tF^O z`zPDdY*yi-25^v%eZ(2+pBz*zM{;kl4tZ>k**Iy+hV85cC%<0s8sU(+>!ryx_<>lN zx&)7DZi0QQDHu7cb`L<0m(PLO`0b)tJnq@l@Ko_{E9dRtcG9WvQJG0!&d}euL@mgM`%fwab z$?jHoR6A@P4>fHo;&^1z+=j+kv4E9(XAHdy>rvJ$NcE(|r0sWloV;X3km87pvq|yb z4EMRsd| z^P2{N*gcnirsyjjT>NCQLpjPcCzCzBV&CIDpgY z2v-!GJ(KvJn692{*BwF8GKPhlWIyp(z~*<&I;2vQTQ1S@e0sv{;m+3)Zz_`{BQ#o~ ztqriJh}YC5&5dI~GN;eJU4~wuXLBcNyL?V;Wwx57?VlNcJgsCfY>+gl$C?6QRrpU? zuEsz%drJSR=f~OYe*cXsqZv*chxUp24FMLwYd1K@G)DP@0%6pSSw||hIrJ->bjMsB zFbbv22%fb8PP+p~emOq*XX993Rr3uSJa4gJU;T}{Q@`gO7`2q~l2&cGx=d#dqELI~ zz!CNQVd}BKf-7j5>*?DNKk*nbJ#|u%aQ_SOB*wO`npP)vUP>88jW! zBq@qK0!_bsMBr-9ivAZoco6vltqFx%)NG8&(gctOpi!kX^HUt0RZGS{Jat#VrWjfl zb0`Ruu$H2QqI_d!u<3xu8Gc?^$?;31Xt&ARuvdG--f-nC-&Z$Ov;&&%>aYj%VN>c~ z-~__?-~t5{{F#;`Iu3lKcAxWi77hasGzY6svWFYP0c;i+(J`~kGg_(f$0F!x9)_N- ziY}o&sW2Jg3Gk;_bvj1r3Y0$L3GCNL9mp)&C{@QIRKfW$!Yg#1sg579>i>AR914eb za6Tga>&djmWC&ua^%bAQki4*4QAOwzBb5)U_7Ld z6YPDDnk`NA#6i3Ls>H&_jP#bkARieZd*BuFLvhNqUfV-(AYp{AOdPAWhV(&o`{Do+ zPuKO(ZgfX~4wcLMyxyk;4!?6(PBXn#OY7w=A^O1Ax?Ptt zPBiwhgXVvjIUtC*caew-+F@`L|A_HB;xjI%4*iXWhe@EQi7ZdpP{@x?_;t+M&@-Ap4W^V0!V;PM;8om~g##tcRp{z%F!5ESXpE8MJ*1tce`Gc#P9?oZ-3T^A2L- zeQivem7u>a@h$#d*Vn=+V^vxrCgjF1rtIHql`l&j=RP29zQ>?w@d93*S2tWYePZgx zW9pRATy2A$q3v~?GzrYTgT)IV&3n$y1|&McBnAP#i*U#@s3CmNLH#bFPD7M>9^A{i z#>0aIlFcy1nU;Z8$w81Y5+So70)9}bpk@y$VcbH5i4AoD0F zUDYt%g|^G5X>)$>M4v*{D-@VhcjW;Ij6$v{iswXu3_>MpiusLn=~+`x+|kcOG%t)3 zh1o!kt+_95xgRfvd?_c(BNVe zDkgySg3r$CCL`<>8DTt6{?tV4#?Po8)kl!*Yp<1ITFT8Yb6a3YczWSP`hyz+x6y1) zwmAe~UChoOjd9F{PMB1Rv;Cfd0gaA@elqurt44LW$5@djm+5J>&xo*m*pG#<*N}dE z>gw;vVJt-ff5DtwW>ZhkWBhQ^E@)kN?`1bnN!aYP8y|4SkP_;&kq_eHw;z@i`X>- z1gKbZ=#^p<8+;M}HV;`4VX*6?#_E>=fWKFIjODW~xw3;Bp7cBQ(T!51U#r0u!&(al zdiu&I09RQMxNcCcSz+M+SW;_qqa)U6BcetO>;8k|)twm>iClZ4hRr!A0cSnC0%8lB zh%+*Se&Vbb3VFyV%%I6W*s!^-6B>n8YZILH7#GD2A`?u3pGd%A1}ziLdNe0kX*Pwv zYE38-WV^49A?w3j*y~1mbUQkBYiX!@q6MOqsetF`2YU5az`4Ozb{~B?vVbe&(+p^= z(zN)Za)QVE-^jq5brigA2_*IN{*d3e12WbNaVxL21Orqo@z(PnFbO#slaPaK9aK z4DIg6RZnWC|87Z{;}FneA#x>MC>KK1MD~+x-c7zWEuaTJn&7{S zuROczbmue=71l>lPm0m^!Y!q_7q$~4^mBV_oBAlbfH}DHDP2L^dU`I6-~$R->jh{4 zI?z=SQm{+%HfDUD4pyPCs6)@tvZ30kmdkK!tNW6`G zh1|)-eP-|A92lkM(5D{v+SSme-{Ook--c$5coBjy)|SwFRNd)>@8@<5%q24Ya)81& zikz=)!4KtlNo(_*T%1CxPO>2Pwrz=08)RQsE1iiCW(JkxBf^LT`g%Kx3xs|5U$i{q zH~4puSonAq+nN+vBy?gT*_zmJc^Y{yA7LBf$;17|RFgtWvVG_q z6@^d!z&94WfzFxB*OLZ2yJj;fbUYiR*`arI?Zcyr-upk&O88;q96OG}hN#ctpOKRS zj{FhPXM-TW13*HQQBlx~j>dsaN>R~6p*Q1Q_IFaOIE-9yS{I`bAS%Se$ak4ZuIbMX z&fSr{E*o){i3DC2nd>Ow9+F8yM@lzG^9Xk3v`VUn>F)ED`C2BeFmSp~T6~1(lrQ<8 zE)2>hBBq>&24ziWDC^#d((e0rXWT$RW+~ptkcdYbw%IDC8Z3h@aDQvN8|_xZYa<;DV1=qcvVFrXKq80*E?4dT2}-0@)_DA7?VpNf z*RB~kM{$8e-EXREcW-uy#{@TKv?#4GwJ!5DfhHXg=CN}V2YUO-j>kWYg460GCGTIa zntWS`;wZy9GFI=Y4EozIQNT0&^f$aZVGMMpKi}e(Qwkh#$EE@0ym>Tmgyxl#&LB3;)G4#veIEMh z+gl9JR&+S`%ngx`NJAPf;}7ihMBOA>K%#m??XC>OT9l9p4*GgbIwQ%AxrMULyg6yaBAjl>oAY%3visz6x!?ib8u44qNJ0CIaGFda`C9o0f z382%5e*vpyYet(1G2UG@cAhn$ih=OgRE%kQcPA#X2UKP{q{`dT3~9Yp>X@FbJRD%= z3iL~6WK)+QbRNF2G$lAef@4`BZ%wrs1H-=@F2ONgA zHh$}lk<~z9eGixDDMWNh&iInMzhxLkDj~+tu${s>Av@{%UqCk-Cv}I^6;g)2^yPDL zJ|661p2F{6dz5}Qhq?+x7lam0R&gI_XEhjLgqYL=Mf~ict|i#z`PP2MOtx`O0`rvvXnH;s0@73?BdK(9m=IGFxGd*6_o8}aZBqCq|N^+Z52$hs>aHGFp& z>Uzna8fZz>(2_oIm)zK_3dlINeq@^p*Bo}22-U48d)sNRn={Mp8E)>K2oody%F{PO zG*a2ama{fVK!~N00q-Q<)|KJ0C+h+w`L7dIX43Ie4C`#B;)fc1GZr*Tb{6Yq zeE0+F^_ZyhKL#0Tt=;(3_x8HL=WNvH*28G55sSE=h1kjmRst~<*hRpfW^OaqJXF+d z`@B@Wxd-QbA0~j>1lG|H(a1ZpEG&2^J;p6UywFQV^s!r1WO)$h0CntuNh0)Vj+8}M$qbnOpftP`a3pk9)( z1$Xm~zP~~}#FhNrRiI$n>`YBSA+8{_p>kpRc4E*~0?a2el`UvoKOF6v1C{f=NpahX zp%_T8B+gu`92k_6%)iuK2t-HO_eMw68ph)M9T~FB=DzpLeteuecZ#B<5GvYBl<)k~ z-Fd~uh&ki4hipbRRT>7}4c_R4M$|I}Q?517QR`EDl!iAONChO*VCK_A&spN*EUL6! zoz#ad1xZotT8n}1V)%w4mG$nlK|u9*Tpl4h3nhAX&F`7aX0k(y=F7mLoJm}cn1_yQ zuvb6Z??T5+Ur3=}9lQdQ(wygHTG(s_DRkHd-Rm0BQIDCXg;I8rM@_IXRd^}NDaM~} z3hPf2Q)hOGn#<#E$4q~e@M&Tb=Q)6demp_4LS_D|7ybAy zRPpjZ1L5MMVa`gJ+0Ce29uYRo@IhM>pOQ+X$t_EUAnh(FOHZ+AbD}I`#VsEOz{jXg z=C%gvi6+4)5lp?%CN*A8mVlILp&rU{mpBSaa|b@n(e(?3I36CACCj8p+&U=-vc7Is ztrS1f(h4mLsF(dUeH(Zc_7jI93G>EMHfl7Gq>(Vg287T{wKcWo9;w&q6oK|n3jWrp zy+EoAt$At+T0hdKF^v+zVwbNk-hd)XrR$3?71*a!)f%vkpNXl|yyCM4-q|#+2cBAG zlDbgnPZ)J)U!Z}r2Q$}>15Z(Zz7>RUt`DKd2V_xy#I6r%2d_Ih35vx6G?TeLB;!mv z%Mfo;E=&mhK^2r}$;MH4O1eH&WBX#oN^qEQ)E%T+vNfab+?9WgE)3Y7JEmBjej``| z9ME@qoBN;mXF%K3_m=v$;is}eN*dsIV0TPN`B$BDy0>gfvA+;LM$~grvRdO6kHd7< zg5%+ojBvjsgKSH>qSgHM7j0z3$9Y?R1VPagshX*T>ol;b)BXHYyj<&N`6+QFS$C}h z>9~|JzcA(lEAsjd<_Oa65_he?!ZSs;o0j{#RD9Cvka61?QZcT*-Ll&W_QO;WY1>Wf z35}Np1H+V0TAfsTyN0iu0%KyrADFT|X>~7B;5m_TgPl(Cz|(|0^fQVM5pfvF&`NP? zEsst{-H-(2q0&94J9Ao)ng_~MS!8nuH0__g0Xn1Gr{z!xr~C9wmIZvZKM^9~VDKKj z1eboE4?i+O1DOGEOn3WrOHt+WPykNSyZjH)s4EF|y{GJJ0uJS9CqsYYX}%ZHVVC7i;n-*1Z=@myTO3& zS@TL5rfLx>GPun%b4bm&O|$-wqWO=U+G}2UP1n&Kr@bo8Qze_`F~6lkck|4{sZC~q zOm-r$uJsfoV5l{4YBrW_=ufVE z*4i?}wOM)1s{~N4k&w*UkxY;jHfxNmhU{S#bpxj#%T4&X^7l)kB^o)6=z%D;#wM<4 zC6z?n&mtsJEG~nXYC;|lCC}5*mU%a>wmVBEhdgS+&!T)`=K-?ZB%%u50FSA}4Q)Hb z|N9GLVb=Km!axZ_9zkL>0YN~rJ8T!&*v24_3qs(g_w?cF9rIZUtRRvdwr4YpnafQ% z#t#QyXNR#XS6FovC5sDQ?TNz~p*gU8b1<*ui=P}yjP?Q-BOr4I5q6b3Cy4Z-MNy&p zxEb42{vb?kbSOe2h-FNv%7}UGR_jWLbHt-g{b<9Pw9o3`h%dWPeo%u9*?)#{V!U~- z;^XKr!`@LvX)os5^CZF@YSwgr%^dM;I@u3)HBVx;A;lQhsBY+DBcY$mvLOZBj|F*( z^Ukj%qFWnMqF14!UD`{mxW*UsyfH_-3?JDl#-+VvjgHV4=4cz65*zykB*K~G8@iKq z<)qByjTVpFvcspW%yS(JxB9l;br#dw^!p!|`g+T_0=wp97W#+JTk8Yis9J~z-> zP{!19;8H^(#x*oG;@8w3Y6XThXWb7Mr-^~TEl>HezLrRly4dK7Qo4P6%N{!m{fQnO zojKhjrB4k8wVvJ?xhh&zc=O&wm?mOAFUP(9;j@$`Rw@}@g596jh9>ed2)27&k;=fo zN{Mn|Go7AtrU}V4`zafLbgQH{WTHpI|E4W(W*|2Bf;m$ekWfLboI~6xDu-!p2mZ zX}CO+;mLj&_}{>6i#$XqW79P^ak8@5Abtq27NExxbKa#|lJmH}%B$#!gah1IAb>$x znN!fcRSHHR-Su*xClIB`Qo)EPOA)#!>|VGQePwtb@M)+7 z!8&}_#4^4zw4pE9HRzFm2P-b>CMh5;e8g7&1zpf1j50J!U_hKxng(323`Ic?>&K~6 z6kCmm7fXW&D^aOtf^Xx+>IJiTF!;)_e!$|cLgtXU(UHKdH6f9oh5gP0V26SY&cg}D4vpBQe7_oD)65;$sKEV{)zNPkGBKV^=RS}6?F#NN-O zCm@uv%_4ZY1L@b)tb}U{Z37$rLcq}jrW!l6M&KxQu>F)G->qX56#WB2l>^u$GXTJ8 zJJyKX3tL;`!ZH0|4L6Ozj_!Iz!wiCkHG!S0J!q!J_m_=W-DK#jfS^D-rbqMInqQnElVcXJbJJ_Khyt`Yl480XM+l0uAGjz(grJ@ zP%wX>Q6xH-HrbOu#o-h{Z;%hpUADfq?WxDp^rgw|XPr$YA!2MluN=feKV3XNRunm!f-?&;O0Lm9_~Qd!afxdS3%R(yG4#-fu*YQ(jQVBK+&gid^kMdZI zr5$lg+=Lc;8>%7vsqmM9l*f4xSRqTm%fg+-axW}v?@}i~e-SKKPsI#=v4}|h?`Qtz z$k5s^aeHm{6`Z;A<`AY;>YoQ|Kajf**k!e4Ft;k?cXj$N`Sc>p;BZ(@uZ3DkEt0B4P#tML!qK|W zoE{$4zMHVVRO$`D7b!A*kb*JyzS9cHUy#A3FBpOX$U<@!3>#A3T+L)_k4U&d08=tD zBj)D}xRPWDCh5u)I4Bz!>dp*vY%^t@jrkW*|5wr0cx%X*8sH#OP?HkR%2o zP|~Y}bAw%JB46oM{IYmKrDd(XL8B?#-HI0sYMc))=0nbIW{K1m0psB(J>b+8EsAP= z&ea_%_)M$1jxsrb%AUWXH;lzra{mw!=+`GLmqlpDe*iU9+l85fQaV{Z%5euI?+HQX zmpt}@pK*SK3}EZ&!wRyQB)i6 z!=TgOH8+6i;ugklepyj7%dYbTjLLRn{9ehEi-Lpt61z%)$NZnwkeT5$YX_nl2Hp)TNy&-ks>QXE=Z_#tQ)7n&g3iRd4zNZt0auqY^DJ8(X%H0^IUfJpeFP++At5 zBK}IGN(X$fVPSzSZzu|5bky3VqIGR^_Q3IEpIhAhfoA?`fBg`$DWu#p8yGSXGjbzO z{Z2^i?Qc!qYQk54VZU(gjhy{~Iml*@@=x>U%2zY;B-Hti3`);V-fEpJ6}Zv2P;JZ^ zIG?}{N311VPX1|un!E*(9w-bjP~K3*zSz_J(^LT5a-J~Mv`5#C?*$j6 ziWH}PG>l2eQj-oFC!zMi2_qnfCJvR{#B-ujvA0O|ss=aF!l18NB(^R=d?b(UxX-vI z`$cprJc|Tu&ZWDe=dB!bDsqQVFG{>(NmbgH(&;1%RV@}C{-z$|D6P@l8l3uY2>P4| zjE;SSAbwTQ$wOgrO^0elsC;uAw9E$JC+JzBd12EmYWitn31ng2yE3ADmW=9BFM8Je zw)(Q6f~nP3)cjml71*ODSi;PrW}s`yA7&(s;PUnM)frswA72Nrx~^ZY@uf$6c0^=R zQzMqVMOO-V)pedl?SlHm{YTK9WzFY|;A;Lrr@H4TdPa3kbT~<${K7by`QdK#B3v1yRRa zhy~7ZJnK4TwtTb^+1%(e?nv{25k^X(7=+pz-2P&h&3u5_!f#g?)DiCYQ_eJ9`LXam?5?=YT1 zLOtcW=l|EUN)<*#nJ4)l=ZCQ5?P8Oux@Q*+9;?QOdBY>WnD}9=$k!qvanF*WxT zArDrx#9N6e$Kun1bVg9RoVY~0vFtJH8%Rfo#F`8welIdb^zsW#L0_%TC|}45I53aC_ojDn^-TuB>G5*jXlqUE1%|HHus*u9zXT)oJ`CGy%qMhg(s_x^+jt zM->YXCy-Q>>%8O^MrY$dWiY|PpJaFmvH#ot%acIe1ys<%k1m}9HFmai8=8b{yh~>Q z6P=(p$7HhWaw!epfJLJ#zZ*25z(Z8ciVX$5Sq4Fvfhw?oqS zz(SrRVK|$i{AfV~7+xjaP#Vfzcf}R2>14*|a$<7{Xt;AxLnJ@k^}>{aSeWybWxLz8 zF_IGwag34+HDgd*#2xQggg30VO>RH774ALl!dXf@P#!Z>0A&QFLz!l5C%LJ|8z-gi z3{w-U;4>nXBw!{MH4eI4*Gh^1(rd}0LR}DTQQVT`Q&a7dwfHN4<{zK#)^H}M8DaR~ z5bua!WTlg=2ZnmXtuTOl)75;+Ody8h39ObBakqSdLQmFA7d2z<-&m=gPFU0kLZwkd zGMIG`w;l8mJU8|(2!`Ids^j+11gLjJA!)iJWtzU3OHX_ahYF_II;SP0({^;{KkTNn zXhm?mhHzVEt@C6KZX51v0x>uvTu&$3s$@XoJm18ot~up7yZnl&B^&g-T_R|sAATBD zARPl3Jlh#@b+Y8n<{GAp$qi4 z%MwcLCi*47w_sAo?01EEl0#}niDYA63d2P_I9~wfFS{kP21omTw{zN?Ngw=_ z^s=0>aRi9){mxwWjYtl2N4_u%efMhKmKn)LF|!d1KTW$Ej}&tUM6ZTG~Jsi*`Ly6y7VB4TAkVRXpx zc9-%<@=M(LKLZBW3oR;{$RWfZ1MfD z`?T`>FhefLvK^6{bxXbJYY@TnGr6)(M$PNisa^R>Tuy#!7!pKgAFXmB7*UwUO;4)V z%v8&_NOe`Z;m99(jRXb?kefOc(&gKhH2Xs5eSPafx82VHKy{#C`!_Jdd<*9&t;1BG9t!eDiPqWF`+peCt zKhETR@Uba%+Qfg3gsBMb(@dZ~2P;kK+s2^uT7GHmB^)5U{Tch*8jB|aWh_hxEZnh; zJp~u%n&SzXofQLy1UXt<%J#4n9>7mPh_LA-mDq%UzBrtthlMd{&chynp}@1)RKl4T(})Sl!qBYFsXM;B%}NgMdI=C^h-0Ej+tH zz24LG+HPlKk(kdX5L&(z}Dn6jfJ`s2ne<+MJF$*CrsT5;3ZqvVgP}}AmMtT2+ehucnzq| zYJl0|BsiMbhIpgzu38X<%fjzyb|!agR=`lK)5qe7P91I<;YP$-(bky_11hZgE8Qu0 zXIE(!yYsF$+697yOHbaP4k`vx!bo#VYPp0ct!?GR&Ovm#HqFV6uKkEghCn1#8CiCR zuR5a#W$K885OuVN0U&>PvQd6lV}vmH@L}qy zGQxX`A9vbOv9(Exz>4*-L@ya4{jK`0s_`$%&dlFI(fNJKn;-2&qx3%m9|A!lTKLGi zo?MK+p(eC;< zh&`#9);88L7XdnIhjG$9U5-$p=pH*SC#AXUFq)J!3fmHZo2xclu*gw${)?^`Mt>~y&mXV=xF7G%n5h)d}J*dzN7%CMa zE36Eq%H=T-D4ETP+z1FS)YXp-m=CDZ4X+a@Y9OAtE-|!HAq-6^fDMEjF#o;_Dzpsb z@qfB46{1*W=4q$v6?%@1h&Ir9spwFOtPqZYs4)UmzdeHOlE$F;Ug**@)Riwc-E8_Y==%>)3q zdQ*o_F$!)^b+QWemjQ4d&!92qo8ZKt6%d(QL1^?B@=8`Oc;G2f6*^j_%mx&Z+?|H! zLsVZltmz5sG!!{7nr-qroQfd+=*1>9p?`$%aYvD@15BYu1lGL5?^#&_O8yo;fBfR( zAVF8hf9HpFSg|{rF7X>{v-{gT#qL`NN3@;CtfvdW?1};6xA4XOr4OkU#;FL*Dt?;f zP7c4ZAU#>d^%*G@efbE%>*nxFsH=9@n__bd|2ZP@SnMMNGWzecsBAY%6sLGH31Jyn zsBe3I^vzJH0zEkP(9_^=6q@;79F-&g?ziL-6|PuS@k|V}*B83@tBy+(%<3RLrUAA< z(^g7!YIbR}=FrB>AtxR7wTiBMA5TR%bJfy~y~I-_a`g85VZ?*~0k#afMSkL%6i)I>clg7!Tm=dAYQlK^f51u=guxdiL2$IPdr<5 zWsQG&`7O4!n!RqCoAWA`z=CBa;z?<>Kc7+gPzgVm%%LCd{E{wQY0Ietm{+qBs~yW$ zgG3UVSz>vEytcKMIVDyQN{zMcC|k95@?v&D=G@a9fa4Oxc&>+$DZ%L=6jmlWwf9Q< zM;~yZ`*8T@)z?6gsfQavR8}FI@(oW%fL~(|?Rqdy{s0GwpV`l9CZEFyzF#wP!QN9$m z!dj56o0IG~q=R`&iIbk7gFL(jeb$ecHEmp$DdbLNwdg;$KDwe?1PB6)+UHID_++<6DqSH@2TR{wu(~ z)T{tfNnCw3%Tth_AWk(yg8B6K6IlreUN0rZ*M5EO=hf8#kx#&e|bZppt-J?re)gyYsDPMp~DOPblg{E^6tWgnW zm;#8N6X{oRUjf#-#U+~Y65U4kM-U@_K>nIQv&E%u``b? zgj++E_G!f+zFW9tClivg@=&I2wxXPU`a$Y~0R;P9`L)o(|C}AaUlXPnp+hvU&nX4k zI5u!s`qJ7EkVDB-wupWqL0&I<#Yat{1gh7)p`vW2l^q?0lt zr~wN}Kz=}`=bTYjC3%+%)SJ-z%NRqBEV&~ir4niL3xhp{{5y(Ln>D5~a{>=U2;VCS zqop>zf#p`kV?u*r@*OT`<)b?|bZ`=M=k-&jY`k4y6(Il=&$P=N<+PU4Qt}A2U6~f3 zB@|3f5|=MfJGWKV@UjGMr(*bKU8W{VdvTF*m}(Z*(J3C4(;F=A@(I}%C8%N1nca*T zMkuv6N-oM3d32Os7va zvwKT~KNJ0=9;^(+pyQOFRW~r{^#mPMpw*kTkgIbrqM7S?M|mI{g&ji32(>W;-iIu@ zAM6;+CZ&zXpC^=rFv|b~U1x|<1TA@l7GBmEruS}$f z1Na*E+u`xwAekU@FSzqg!5PT5)6NQxiaH?lJRxHyoD2C4`9*Cg_5jl~8N+)BWk5sg z%}0O*UK8z9_gCD2BwDxr*{1yp8g@pkC#QA?_}S)k1f0U+pX1=pln0Rswx`_?a|spE zDM}X#rd>-0=+!~njIlHAijFmho#7iLXsPBrq-7r~o>*x;)C6G~ z?*AeS5cLHaU36|V!4GTy9;g>d`ldN36*dklAZfQ#t78=#Qs`U>ulj?`~_8W&9LMTDGYms3Db4-1sgeA;U+nj zrpA2YLKZAvkL!%C}ocsM#Rp}k|1L1pN-&9FMe1ra8S!}q%$67|1x{{M5mIAi<@C06oCfv!nIU zgXIO7J_JhyY2lFFNjdvC@TBOUamL(GWr~zP$?6qDTRYWZZx&I1{dED|c0#vl6l0e-R?L`Qs<1)g-7@WKbIqf#G|4?*$enBE`yi{Hyv@ zru!v{k*^rW538rQG8t(`Xer<}C;KoFphJs)cd)7QxVQ9ExpR4%SKQ_T##89c&aH^4 zW^$zdIcqEImD5WVl%9Vg&pn5VCW)7X)eNB;CJK4qrH0r*|w z`C1ThBc;@|gn*uDF}?MXhY;nnSeo>xa<!w zXkFijmonQ(xd$)&EFo8@l;9M8AZQ@uW^Ui(v|VCENeQo^dqmDi*`UI+3i02U)K_B( z5L6VNoW((HR@gn1g~FS62g+U_lm6erIKKp73JL3j(Gb{D~y0M~6l zC$dZbIlsW;V@4g+TMa}aBM$&}VgTnL`He6GPs~2L7Hr+_OIk+fh?>Us_I8Uqho4o( z9DN?@g}If>reqWEf;Tcl4Y?#0n_7frc`f>sz`KXmu)w`-HaVzXk3yng3ZeqUle~Pg z$dLgHIjMbH%x+IDHg*#P6eTdVqU5Pm7CHT~D56v9QVMFu^%~fDXEfW+3#D%7ZyqS1x|6?GAIN@XkR=@z=Hh2BLMyXRLBF@&7wv5jiuJULokWp9 zSnp`bRhu_Jj?k2oS0Y|110Ny~W+GCMa>ILxf*>APljfft*BEIVMmQq}Y(AHNw&S^U zi(_t~HlZ6B4;BTKBRpfwKtw0OBa{KhTJHGZySQ!PmgjL?U3#Qi4n*|y zr<6Hb)_OoP?o?AO!JrPcZ7}O${u8fq-0bA+vKIwb3@x@6eoQqJx-ZI^gtZ~zM*!O9 zPPh?~0;!men%2Eeld&_^YnAY=W%Z?U=*;j)VAL{Hr5L&kMBl~264?83j7ysKn48;Zyzfrob^bP zQ|11E9ZU||ZN)IW+uMKV9-F1ZHP26L1D0&3FKU zF7XIH03}pGVN6aOf6ud@denv$A~>TVd}It+^=;Qj?S%tl9ZSxzN%G~nQOFxvL+@6# zy`y%O%EJADMXCrMhL(R8TX{!<|MLCO5atf-qfCc;=^Yt_@ju!Vh2Y=}?Z-D5yMzb? z|CFaoPIdHwD4XE~x2A&17ANVfjr~Q+ZaL(Y29bMYce(}Op#(Wh`cRl|W$Qmhw<&{V z8OOSAL6{pcDp^bxg{{14fIKrzlG9L{@Ix-MKgxtQrhnEs6w1uWM)%Y`0x zaB1xC4raA3r)yG#PUUM7FG2^|bIbyE0c|;$7(IItX`Ykwyo0gk02FC}W>W&#TdcZT zG_s>|9Z!zs*+|3MSP$)T`=>$Fl{!R87o(9wl1Kw#LcShb~1177QfIw zQ3-+wNJ(W=Nf7MxBCXogN>}fN$ADOKJ4Ul!h}OuB8eu}TIw9S~eIEIX`dU&(lG_%r zmx4FIw-U`|UHmW*$`6>6&G9~Ba0Ey-O+FZjh7}^C@CN8Og10a?6nQmGoHm*|ev9FP zi6f{`Q}M;G&4)em=<`XPJ4VTC#VlmaD(*Yu^GQ3XigS&MwbiMeCTc4faf{49+5$?b z*8!BQ0(xzcOj22Jb?kApTPB_$fIHzXf!WL<8{ys|Qr6J3b3i3HFD zlmsV}fAN@4=RGd1v27`3mfaRxsj|eivorbpV(6{R*x1Y*bE?c9te;o-fi2h+eO{pl zdAKobIgrS*TO)?D07-^)NGg2f9`xN0A69Hx0Ucysu2+FH81 zuG^lY9oL`J?ssbYrKHjh8VHLmg$XUYH^`1f$%BcwszLow&4{1}SFv#XJl}tbv6ZK_ z)~=(r^M3U-=WXj%OSx}rkK5g8&f|KfUibLbEi0rPQqB&QAeL6Bg2QScjH1Y}`EY=y zT^r<?Z!KQn)BTK^|?Z~fNahq=rBuKs$Que|SGv9&&P z-tPYWG^aEFK5lw_m&$x$5t=O&mDTLU#-m8nZhleDKq4DJM!JyM0LqpBevW>!{IorD z)}A?jKc)r7OxI*_p#Q-s!t^a+YfH6+ceeTZ{QJ(_#`e?nbM(?qZ>^kup8sCk&U4HE z$JRDsGy@G=2t~^>@{^|WH_<_d8nzZmY)Bb3lwJaC4lrPBnUXBa$PS~#QAAI5Am$4y zCUt^&L8(wNG%_!lV`?JeiI z{dJqY=hc1x=Qc-h$eu6?62}}iY%5Al%g7?-5KCOhV(=Rzk8z{1h*7c-jpFUNoNveN ztt(23yEybMwe>)C)xq6gWe5t>_m2)EH9GNYUzhcw3`0FQp?H>fVVS_=rp%j~l zo`o+RPSH57;a0bj^a;YAn$* zE`;1LZZsA%N}r^29j4jxPk)qZ%~{)Vj&>d8pXNK)>N&eR$CUqzYu8`)uBDpux$Bm4 zS#uumGUvKiyX(2%d0V^ewT?~!W)YVnq#1@F&wqukpFfWO)xXqac}YzhoRb)1SqV~e zQkgZ=W_llIwn8KP=kyAv4bCZ#UNklu5708+Ng6cD`J|4XI5iZb4B)V(mS`H6L4-q@ zLkTRLu5hK2iHVsX>z4K{*=jMr^nYB|-o_m7^!L=_UG?Vdzxmr(Q=IqZtamx*yzjf* zw|8mx;qE)Td-s&{t?v_t-kd8KeW1p6V+l>;VvRC0$u%q%3`Qm<#;;c42*&E(4tybYkbM9!cpGl*PVEzb`QK@WhwwoxL7P~2FD1$H>vJEL?Xnmes z>Nd8w(txwU-~>*olh8JE*z3B-_m{Wd-|sm8d;j(0HrA*NvVho9G?8T~9OMcR<<0eS z8FN2(pL_XxyW9TrZt{$e}0b44ufK7&jUVof3ygXft6FmHFs^t`AW5}|NCz3)qcOLv||5#{M#1W`t6sq_kH!F znR@)Ot)lCDl{tlgLZDOrA%+m!OcPl)B$7LXC}$ErB8|*yqmd-bhIq;&3CQ_!mHM2U zjBs|gumM^&bb}}?Z9EwwwUGE$M1=|>Ie0k$@`kNG!e(B+w;IPEUAnl|jr&(U *S z{+z4*{Of3U|Mu#)AGTiQ-oDqSV5EQ%e8s#C1_o%_&F7|uc`I>=WX}pd`Ic`uFabMa8aE|Ib+BY zNjV;D6gyvC>x9t@MnW#}i?F@{X+qP6QizD?rD_Kwzhz&}Ai~jNF5Do7881TcqR5OM z;IM@oL{flInE5so&;7WXbL^L-ek)_iO3ykmR0=JbE}>N!_Em-LNgxzL}U<{^smvYtJz25(C<9c&Tj4G($P_irqgiHlJIW=)niF}Tq zrxm`sk#SDgt7okB`q*Iw75#`TOI@-)*&zw`pf`jEudStSm#*Da@0ly8@L_wOt?;#4`5^w&3WNARpz3BC8HHRzA4n^F_5ael?poTVTH3hU z>v+y8ROA3f(^4i9$CDIIUap|ug0-|Rbr_c} zw!7|H_ucgCoW1U<_I|h43x}Ak@YN?4%4jkbs|9FUN(b2?d18~Op@z|xk})cekl6}S zhFp=AEu#BVdqxpt`E=d`H9rrh;Xh;01XOl{{Zop z2gmv1-khi}!sbu-g5lvjCzelr5!PW`!q}ENj7xOqTQU7!^>n_x+D++KG4Hv499yhq zEws%=6fIjp3XxkYV<>H2VM>Y%8xS2*h6`m3E~h_&us?z@0Bl77P~Jc)k*p6?iVfvM zuB0e$=J-V&l1!@gUEvE`4ob3Y1=(LHb&OxMiK)5CGeP1g{u&rI9!aun1^HnpSrh?t zW4G)fXNgo=A%aSsR6$TFmlO;PYR)->g1!%X3!F3@1cI4HG5i_F)C^nG_5IGy3`ZUz z5u5><7BdkIl{-b)G23CANup`7N|cc$FM{L2LGr}7(O7WhCK}QqnONV0-1LD!kehP+ zggr-#*zyWLk+*p2^usv_lsZi@MJ6W3u5??CBeaZ%2-$UI>3@qWu6A3S`By7fX}w&= zwz|ggM+g0EnJqmcXMLv3VokG!8>8IMm(S>(xTwF0>di~lSbnLx+JmuMOf zOh-8YC0ggwwUql@zy5g~{x#dL`Q_Q_-TlY7^n1oHTyc3hsazKOGFhsP%ltq!_5~vz zHX5GLG@c_olvs%&9nb&Hipown&Yhk4X~#lY=xk~6-%l2n6;ckKG%l&roZ*O9dB=8I zl4U$Z>?ntl_;fzv{Fm#$%+`y?R@PQ+z07%s_y7B=p8lTi`+fGlzBqfGwbowjDXy5~ zw)@%Jm;Y*4KMe8F!)7YVG9DL*dMI0vFai&8lVXe;jRlV~MO3o&awZ?d8z0IYMNA#1 zO@@*z6K%+pQ>jA{b<;{|Y2#h8T&14tZ2rFeuf-czPUDOz=N?)$Om+tE*I>!4^E5*#vzRH>uIUAISPi@Pr0 zj=!(-viC4oug%!opY7mltCOPp$Wx)5`h1pXaQ;mBY5uE_1$i zvt{Uyp~{Xa`jlsZJwiSH4-Zlje_CWe7;Q64Xc`hu;s8<#VKQTeAaTr*1C^?o@qtpE zwqL>!{Xk~RGUW`>kPhhw))SXi6C+{)?qC^k6*Jg0b+lK(Qo(Q>C)uVHZc=5n2`-BCAdLlgP}dY;IiUN;T7C z6_Zj0xmlr9H!>=lniPynkQ*2qT*`ab-ZAxgWBl2!_ocXcyVHGR?tNePZ?D~cPX9}5 z%{$lX@p`MT^{e)Bmv?!8%scAg?z2|o-eJyG+*!_6`#bLMYJ0W=MhH-CN0!jE7!j)S zV^YP{YUM?672`E8ry;57U9M1EiQpW=9xaZu9oN0{zIrpIm}Y$|zuf z;(D*OxPHC&8;&fnC+7bM`|;oOavCy479o~zaZx6His2OcK2!XokOz97AA3vKQm=p2 ze_Ow*_s8+d`S)?|!`E{-Q{VNE`?mh;^!B&X@8hof`f}OtAoP89z^OVcvB6N3Wt|PO z(on=&V!sa$zt&IqniuElYsW3O`NvxGfA@F4uDh;#M(zS%vF`(Mk>$=7IPGMqG~b6` zT!p|)Y&uM2S+_}44MjdoRykT9<`vHI`_R?Jp=?Bs$g=JQnFEZX6mzA;Rb~r?L~Q-m z-(;59P?SnyU!(YA#?RybH(P%t`h+1XZ_oR-+soE}*K*k7deysrTK{Z6=Qyrur?-E4 zCr>MU#ikcu{JUJ>e1#&|GtzjhzsNlmR4n{B%l{VTAHZ$t>$?&?p*V zzyU{gkVlGCM6K`ze&U?d)g2|t<)N2w2iIM9BGUuzu1ZK!01Sn!5qA>&Ld!X+r@Pe#yqv%E2Ael@i)5`Y&c~s`C zfDQ@jP{hO5rXM4$PygaSEMOwLlp+>HEn&BgIri^4{~+>JrWWyOny` zcJ24(YW--JKipMb_r5yQ-+Q+ickg$t8F#tbop0{C+c;~M_p^k7zzDf$2R0;5vMdBb z+9;Y~$><4L3K%r7^{{|K1`z~61uZ}@LknEE@WKi&aL|CN!3Y>gY|z3D56FQEBrMp_ z0s*pcV+lIUU;*n351~bf4-r5?fpNkI41(~OVPZ%WIwisbwr>ip?p=j7<&7T1;>K zTYI|loUhNBqndWib92fK-*vz3<^0Ya=6vlv z<7-DS%0S&-IMK8ij#~1tOpOmBrx=~J8I&Ywe1%`e_?9|t z@9vee+_u`|H`}i*wV7g$zi(@2jWC+Q>TWQ25Vg3a&As=QHrAf?`1YRH&O6StovXHc z8vp*?dO5${=4e?GMj{L3AKqLjK+|GGh*oep1d}_3rsZ(|-R1Vf*=~A0-WPjZ?=-R$ zc{3GhTI>c9ESD}YVX?TBZ0ZdsNtX$%gcjDpCX*0N0RZ{{0}=orAQTdY!(!4{Bp#8B z2P+SR+;R7UxIcziIdMaRW7QvdWq^<*GpYR26&XyV7-n}6ib@5O&B`tc(*AH?Z(J3e zGN9@&4CTA~-dfdmd#UJ)o!g$Wq!g8YyW6=ak+I<{sEq9S!R&(JmL2$0?yoo|o0XpV zv-9rcwVXJ`WOLL)Z!E_S;Bj7z{bR$bmJ{dyvZXU@4} z!)rBG1ZbK|C$IcZ`rfvGinN3ppiHUzrN#*bL1uUdB?KA+EMQ;tBbky#D)lulFHWLo zBLJv0=x(HLd-9S)PMDnC!mp~S)UaF9_SxY|0I;JYNoE{G!%4mlP126^n^%d4iGSOUe%O51bipl*9||p?fnG1t zndIC+-=!ZbUxKg$dRIfkDH2l#p2NS&XI-+cwVN;Fh84WaxziS}b%aUd^^@4J{F(573y^4v z48U3zNqDYR%;|h2i*&zzac!RBF4H@T)W~%;Piu4)dliimRpT?2Dx~492lL^sB=IML z+<0jL;7D3&jy2!BToX00Ld;GyX&_j|SP3so@$C(Bz*xA-V@F85>>^32fuS}-K*k<& z5-IRwAZ)VU>qdg1wss^JsD%yaBG6~$hHl`9@$QSr)Swlq*agwIQMcf z6ZSg^BV9!pG*f%J=Qe(w83m2uJ6<}n({f3u8XiD(b9AovXf>GE)9ti-oGZ7Y1LPTs z=!m2!%l5C(dL@dB8zBm}7t|;iaHAVSLoFIyQc31DHJpoTZI_l11BpH~17n(_7OzQE zs%K20T>cU8g-F23Trnrx{?o*r9gt!fc0wM7W?>h?Vs{{J&xrH+(R;&A+w%1w=?Ws4 zesK2_c`W2CZ7C1W14c$V)B&F|UP)PXvDp9OsCc!gij_FAaF?MCl!8`H*EQTwWAZg|+hR+qsTkgdnmN^oADCmj%6b-_10W$_U@ z4ji;EExDMh4Y(86C!`jCnBr57KPInB1-FDOmxrjaFXP>@&N3qBjmLH~g|77{yWuFJ zKOkMT)WhTfb|lu)5@}I`7LWR-vV$oN-wvSsXW!|Ah6U?{b$qSx;9|abPxSXFNeU7I zs0WpPRkBSA>HO2qj$(?TjMA2y0dFB#c2P9EpMNcQL`@VDj9#nr&{{}DIWz(u_45Hb zT-=aygbY*vjz;dh9tH|bA)a7xTu6+p?UqhctB4P#!NdCs3Tz=>Q#TlA&*K2)`Wwi$ zBPH&E)%L{9!X2F%d-Wob=nj#5iJY)*xLOcDY|Y^Ig&NwhTtK0U-<287Q&;TZt(&^-yxW#__KvVWO}YRY#U?MP%s8o zmHomvNw6N362hB0D|W+VE1!7CSomcWkd3^kMrB2&jIki_TE8eE-Y~)kUYkOoe}ou8wxwkC#z&nH$cg@g}58 z&io0+dIs5;k92u-mCwT^3US*7GV;EK9?DGk@0DLrl`pc;oItQ>fzoy<^#zeHJKFot zL|98oyH(g9(|(y^FC{jpp`1r5HQ#rNsQHMHI>%N{N&^xf=m4=Fa$skNl%g3Nv1_9Z z0$DjWEQ9MKvqBPUnihK|2a=RR3mK`mL%z;_S*?qb-C7-lGrVFm6CUy8XU_Ey6<*a- zG(+KuhLE>5WnzNNdftG^dJ#0VtFF%qWl4$ZG7z=mm)@ooPv!_*ih1c_wCB$?AhCgo zvvHVigGnpX1yV7Sizsl^)y#X%e6dYK%ZY`pyw5AP=mP6`k`2Wf^Id-i8YZ9tyaJ$e z^@V~#xZp(hXe1rS@pRwtBth&iaK>{}a#r3Wa8}N(02MjN{08|PCXcbD7^~x}e0&un z;Gs3~41dYseB(DfB+uuhdVN)7A67ee|3Ie(SLF#d(10z?KoVrQTf>bh1BGgN%uk7+ z>2w8#Itzg%Y3rvRm2SXJ~jH1%b!EUjg#vafL_ zN7LD{=9FwngK~hz1>h23P_K&cVb`{G+?s5WfVUp>RTK_P@ zH8*3#{36W&@u@>BO=O3b&oM6*Sq=8eOEVab-YTJXVS`PC$hXOCpeqazw(DPo;uxEyaXsM1 zNBRmf>u((eW{3va9H5T%0G!xcpAWO)3?qDlScVK+(vA*O2w3G=?BzBTxf}&9qk4#p z+8Z}=9jpG>iLCc@R7prc%ywu0ff`1DS0C>Qm2MG^O=H1`{F=$~Nn#lg;XJ}-tHmUj zg3Ou&5xvYny&fG0Q1Y)fcY`0j1n&t{=+4UM|Cp3ft(CI4ZH`F6`f8ZB-2A-~rsl++lc1D3oJC*E+A{Msj^3enwI#UyfhFL$frnlh}PPFRxF@?I|` zxw9JLd@P+%6*7ecl)6-srCZsdC_6@bME$N+aqL95=ms&GlJ@1h%A9xUSCR^yrfv8! z6n%Og7B^!MlgDu%>IFKR#bI*#c4sPX<$}03*<@m}cM5o~)iLmdFBVSSk9DQR!VJI# z9QYUM#lk7Um{t}fUIUmYLz)q@iB2)Nf5A*hhk(S3-Da#W7XA?gcJ5w=LKv|5i`2!! z8StoBuQC)S$G{uwY!tnJ(RLT5I%r@_rwN)2E86IqL>D5&1f#D}NGTBjR)LhvmSJ-I z&Xa$2JpHr{!`lTUR=qB1GVOn{+ESValI_#`lB{J~y~yr=Al~An;P#SCM*Ie$<#5M! zqNUeG`_ofV&TwXyVyOaZ{o}b+-=~XPNFS44Ko!e%%E=M2l4Elm?Z(TgP+5V_2wv~U8O{m5;{ zFEdYRZ3Rd`{_ofEfXWL|?0FEi4Pzdvw{Hq;4<){G#o(zjazw$2WtCFPL)FmCk8n4~I7| zQg5T&43WK!A1-!Y^KKSkes}71UvtI9V!+MSy*Izwx1Qr+PaUTp#EsOySpCq5NLm%;W!)@bI{v^W?%Jsu*y%X;_Pg7ts{kGsdzaU~a$@fdaq9uII=+9;%`cY8Ai zCV>tXrmmrG22h0-w3@=2QaK?|j5Z{k$8egf5_7Ls!1)0LbfxfD zc;^^SQ{0BcsI|w}Pm{Fd*pMi^vDWd9j~9>u)}O+o1Fg3)SEtXrk5}knRr1dJlf@2y zVSV0V4`V}evZo zDG&T#SSCUOPWkmQJ9~smRE92ShuocTUdKpy!d^s+G^ez>GpfOnpqpc+cy;9bj7S`G zY{@y0jv(f1^hGH{+PRQLlFSj7cB!e2Bnq^TyHcNF&PiIogK(KKlLffzBZ0*r?k+qj zN&U^_xaANV-@=dI@T?5htJ-_6j7g4Kyl$L_S}F){91@wzY*z%0yY}$BJ#pOWP^XUO z;gc?cswM5w2&~YcmLD{z38D2F)ruO+6!^4`MTH!|i+<#aAl!RmM7j8r#!eCpK!V7i zFfKmB<>}p}OhyoF(~6)3q+Rt*r7iag_@`U8+~7Bre9yP)zo_tp(q&mIf*8E_SeK-6 zAZZlF6|;dFvd+14!S$2FPO{hoW~l65GJ_wOqjU*^DSgZ zg*h@2{YYlOp%~0W@PL&fXpeh8)U50fVks+(C`MNt55$%3Esf7|Z@W_Cb?6u9fwTMfnDDU=3oGhf|X$)sb4sqTPZ zBkBe#D<^4LH!{ppFn4H%hsWL0VKsGfn|OwE;Lhm~$QMI?)AC%Zz9TmHV-|IXr;ZhiuDR4U3zW(RYoeJCp*!R>_Yf#t>MOXC>QfLesxTOzNc8JJttTv5g!fi; z5}2Zzv4&45GX!DdGWGk(IVS^@VU%84#Bm5sr|IpY@Hfk9Ci*$T4 zIkd9Emo7H)FQL|aYtV2&Jd9_mG3)#z%5N5vqr~XBoQ<^lr)AE`r1i1^OB4c2ojhB( zyzR}ZLXs>e5DP0P(j}mOIlERBM|`;9y#+6ia6qh+%9AHlIouDvaLyJB%XyS(^p0~n zhz7Gq-fN&Q;KQ(aEk|3zs^Z{_-jOHj)|cbmR0A>W=r?g$Rict5nX;tKX%jDp6GtCD z??iRw#Js_vmpqV(9hwA`OEGV310#2~EhiHphAYs`ltmD<Iu)$9l@}qHFBy6HkxBL%Sgsgq+;(W+P%sJAK37)EW((2ZHZrx z;DRX+l5lsRosP1Xhl<+disKa;COhz@0xMvj`^~|jivZvSu}N#{Y&BSzR{O1+!2L+6 z=o8nc*E&MpjDZ;%`~+3BIaDZ}yANczL^30qB`J?#_}U!C{1E}juvvlQAFlkcl_@`| zEO-xB=F-ZP5;a&3BTZ9aTj&K^X2BfTf->isUH=+>J6#r{Bg-x{C`J%u&kR1>p)Th@ zae$d|V)WK_?D8+l6P$6Y|HQV>iXshSq@q26EovNi%(~ooDHRr7v@M!v2aZdJOT9F5 z>Y2-HbbDT;B;0%u+zBjahT*A@bjB^L9+PE?LF|=+t%o~}XsRCsCp|J~>$S4>KA<9j zOCUGvk$oNGk4$rF`2H}uMgokh-_yIO&Vl>8bnN~%5>jf<+iFjwEc$Nr9CYJ7{-61) zOG%Sn;~A@jL*o^z(!y7SRJ&b1$t)bcuQv-2HhK*evGo!AX!!pe{hX?qvEJ?J+~KP< zOmtTma*qCfG%dt*?pU$)nx=(+HAio!*N8(Mr6Nd3fo9G};R_}wj~_QHrNLSpvz5Vw z1RLuRF*GEjl+wAZ#Tm_vd8UPbvAjTQP<0G@BK98i;c@e-hO=@@!kKqW{hih->Ie+F z$w>tu68^E_EECcN%Y%&A&ABINXoKMOjLGuB<4}pp*}k|`Aq$(Kd0X}dvE7VU7x5EE zOYyEmXbv84qb~L*AiA_7O2v|24*_T+YNl)3f=eKH$k_if^B8CMR+x}x(&HWMV{CK> ztDa++bCo(aQxBHkI{G|I9$fSFwhVp&Y|+on>!u*l(W1P;iA|vmB4y$OM}k?eJ}pY? z+D$?JavLq%t-uXIjTxoH(dw-{n5doK_3VuD-N2D-$#Ym)J=jJaO<*xUjP2=ZAzfG2 zuk(2}0%Fo7nYJszHD_p9s~AHyd<`~MS(k{bHrHBA4WSvt;>UA2%5WC=Pu;2yau&Rl z27F1TU1%9_F!i+1eof$As@d9gf!gH`+X0BR!gx?yka>ZNT(Am9O1z^MABGj3xT;rx zq)Mx5(cX7OogMB>7p;fk|3KyFbD>I-mSVgkAViXfEQdOTvmIpZ*kahkyCS|ul00y} zbU3GdFCHuA7|OQAFwg7N*Adfryd%snVj?Ijnow!S4o8xQoa_prC9E1M?Gdk6O0ehs z|ADe{hu5oyl!FBtwS1Qa?+8RrYK(gyF%=J>IrSNsTWf{_tCR1l{)5Gw4!e-2<@0Au zRVZ~XLI9H5ehw30amAT3mjinw-QG0oqO?ylRP$F-hpY^EpLS)sq(?(-2fG8N4@>2{ z^cs;&Z9~ycliI`(=He{t)eV&x7uPMwZ{^dGXfY{5faQ6*1{GB#dKr9*d+37O*MPz4 z>ayo84L~5adEXH!8adNdhD`1TLmh~`5QT)|=0IG?SmFDs;Z@cTQF7YNB1s|zY3Y8S z1m5i*8SaNsFt}&6HPp{~_NEfY@uRU7iKJ13)9MY}i^WefmIBy{>L7hs6>AJ!4piA^ z^m3iOQy#zxji4VN2+F{LiWDx zqNNI{5x@orxvWoMs|yXgHoV$YeQ{QE0#h7SvvAK}s!cm=wNM(B?YJ|t<}o*7wQ^~p z6&`@m40a%DS`38`FZO2=YJx;1JVK}xw0W+L{-x!|xR{L}$L-H`w@T5SR0s#Ox%7Lg zd@mFjI;soMk5|( zY8whJ^6w9pmyd}?p@w-zI!z}wU*-?7L?AvU)m^ZAh7`DXhl=Rm4Usz$MG34b_fcTp z#ilWdC(@&E&^KG!f}71BQ3tve!9Uqre!v;^W2Nd>V8c;0r;jYU0H#f-77fzl`YpYKwCpr@XIL!0x8GabY zu%$7>_i0#v4&)#W|Rj>w7d70gLuPlXb4MX>SrXx6D9?zjp-miH>UJM zBuY`tNT^B-bzCAfvgwk91vCWDgZP#xsP_UU%cXF#Xcuv(Ez5faFroJYR{H+AkQ-AW zN(Y@N0D~v)u?!p3^GqdraDqRfh2QA-%elW-AxUy6#LJX7DL9ZTdXJCdvYjr2l-uZ! z2OErgB~M}>k@u)FlV^;yT>TaUW1NN)#Q$Vc0*oHYf0|r9{YVNr+nP4kH zYRMHXGl`k0j$^_bJ5vEXgI#^d7B8U$&WVP!5#(mSv>5XS8+uWEsb+2@W+oozUUL}( zV6g;7^wfEL1X<^4D`(Jw(r`*K06BioU6;>wyt1G)L<^P;9$#p0=kFJ?Mo?-lgmk0k z)+V~+L=65#VWr(a3!vjQXx6&zivrrL82lfYCcVB$F^W5|`kUD3*$*;B)8|qwu;tOo zxatKvw_g+yb>1i$sj&@;WRAO&;`#nwBtb6YcVsw@J$+G>B{T<;j&l|V7sb|4uBBHh z%zP)JajR&1{^{3TG}o=udrjp*`(HYUth?j~dNnciZZu7$TLRm82x3IHjSW;<1w>5h zi-6JW9c&kGLz@^r|FfXz?IWTwlAMGh_$s9iXPt6FPEdXH3Lz)Pw^Z?R5h-7B0!giy z!%Xo)y0;(m!wD9J$V2?#%wj7`&qi7kOZxnJew6$0Sakx z9bGKCH8h_c7oG?i@NI4;PzYY^VlC9b6Q=$h+`jk4Jz)-Cu@U$b7)N-p` znG-!}m0q3Zox=sFR-m`462It7%G&{E_n~wve^VB~L-wo>2fl4RT$w!fLj`y;S(RgB zOw=xdMFL?Ry$yf`^q|%9E0`y}i-GZFSIglyi7C0!ocoTJ!?R^kXigU`8ywYiDxHLS(?gxVwaHnsU zd(y23!zn05YndBe59cP56S7-vT^1@FH{ zsk^Gs@T$52q{u*47g_db;2j|QaxRd}SRkHG$e2m`HLu)j2RSIPa4%CEB)-X;cH(b>kkT;Yn!?sSS$UwBLs@Sdx0nl>1Awa>iF#RW*q%QlMixb6zN}vXz@TtYAT4xI{7QCR6Z8XbpMom1XuaV|M zf6T7+U(qC2Q!^=G6R%@h?8v&Dn;8&lIoqEebaXtmG8q75p4IS;QrjVdx>aS)ZOh`x z*p-}FP00Xt#A9UiAZ>#IlfO)_ofJf+QE!Q3>5}To4(osgloXN4#hgjz5F93_B<9p* z=%7Xibs^Z9%m;yH{{(Ja;QTRdW-`pN0&GHQ&msO`z}- zrr>d!2B=0B&i^Ulm67AbTbP>Logl8^o~q@0-7BXRRuc$)q+AqlyE8d^LvP^Jp1|2b zg6vgDe}`8yN{|rglqEYTpZs>sfo66kEsX#XhTSN-^OTKo5ng}s_nkwS1p_)A#tXIq zF3b{*uM9N`T!*f=J3=bg{VBoc7Rnd>vQZK)*M33qo>+78$dI7CJ&w!|hD{ellmuFH ztbL%ykfpfaDQJ1Y1Azg9tz=ryCiXP|UBfA%zJ1%Kd=1N|YydpGw_8L7+;Xh`fH9<_ zJhl-E2EH(~eK9t_PR{Eoi%C^lp!XKq8j`=tLg{f3`vR>Z8$iP_B2lZli9hFZ6&@sv zXiG84upmguzFq3G&d8|w8HX_z100r?nBY(Nyc9R4PQVnr2`{rs)R&Ax?&DID<;Q5!^~L-E6tW z@Jt1ga*be->_%6*wn*b|9_p;Entf)Rt^M5X4s$2O>Iz@p=~7SB2%^lKC~>M)*wM_W54r*<1YQr7$w zUhlPbOo;%DC9@$`fvo+^@>>BY>mtPJgG_7N?Sdcgw*IVHb3c|_dKFA%|H|HzF zf1&q-&o(7uI<>}mWmK+nIoF`6sE0YgJAWF_e&oY0!w9-OTY7^w+w|@w7~Nzkb(USTg_Qz29A#NNm-+N$AI9pAv>lbz;bE`I~xy z3CxXzX29zMPBX!eVxZV)44o5!O&F0){zhtXk0!v$m2W|NXuaTa!GN~h_vmvL6YRJp zuSL&rF{o==8uI$gT^#_(-S$#;@0UmJrIcL0Bo@WyR`G-#fk)X%*HHX#myOLY$-@ z!Gi`9uo~sHP=wi{UJ;?vmzx-eWt#ytgBLN-6h4mrV;7T2Cq5Q&^YfWClpaVZqrn{8 zl1vdD2&)VnXOrm_O`d)BQKfU4<$9l1w0Fe_W&YLsA!i9*$q1PI@5oYF!3-8|0>zq1 z4ghvK!Vr?)GkmUaq*QG5?<#=x05vQ12=zNXp`UtZE{rX7QQeHIxccD|MZRgeUgbF;xt zN=BvwIiN|}=1$V6NW_YJcKIowUcuM{?6GE2b}8SqXzUW}nKNL@7;9ptF_eIC!;%Lu zV*)KK!SMIDP{`^@5Cm%?d$xSggu>e-4p6L&xys%w^NU{`MzAkL7ij}#ciIf$w?8Y( z>7Sop7oN%yT_AoJS7~fhgds|--tQ>nq_B-zR+)C#p%5VN@D1JIUX2Y?7t^AKeC8+$ z&LM61U&t)ADCigMM3G6=qIhGpNr@~;!+Vu#@z9scc6{K1;$D~BE>h@7+ZgOXvr<&d zag~Hc|Cf=c({v7r9`jiO5QR z^tX`(FNY$wZARm~ub@Xsv|_}z=*mFILPRHIn{(6Q)P^MfN+tZK8gcmU2?O?zq+^KYSLg=~;{MrgI3Rss z(J*xlst?kF9hotP$=Ns^!RMyw-G-T|>rw`nv+<_h&iNP71Zv1{O%q2GJZBfFUG_Mbq=zK7d?a?s^IEV2_po1rGb0+( zzL`5AG-3Y0uXZE+p1Fm(G`2I*Yv86ajsk3!YL-B6pjcZ4G;RufOU@08W+Eu8^9EOpKYgj41FwCo;{d)eQ% zLuNaw07V86N*qwyiue-E1A>@wnbwb*r0orD2@I$5j?YWVH(HdIbjr844W1uEHn))>*qV&^Y&suns@a6{H3XW47}SYiv}9#P90vF; zp|1jR`$Y76$l!T^^E!K?aLJhGMbe_Qcw4>(PeOa(S6Io$R(7naYt)udmsE9i-9hj6 zM-f*`0NSjFsj;mr5y&!d9i@emb9!zB95AZS=$6!3qW#CaTxX7|%r6a%6x5iAU-pW^ zHo!gDtP+LOrt|KCdMnW&i)j1CXval?e0TicN+^mpAlORGbWl}ViTHXUthDl$tV9j$ z#-NB@cyZvbJevu@mYH=8)}cgvFwG5MGdqU{e()dmC&d?igGjE!qu(~uCKw+RydWiN zCIe=}3X4k@x9KB$Zu~ZoxHL;HC0O~5a%B%8dONI-Z@Kh@CUW@a2->dCdpYb&0&!Qh zlX&OQy3#nkJuJ4J1krL%KD61ufufJo_)S$iD-76L11;Z14LUfM12hBg~>%(Kp3NHm;ZQKJY_ ztrPSWqNe+~d8#i1#jI*XNM{tr9cP-#` z6C$kamJO|fygLN)M(6N7=<`+qHz6jn*4;TxktJA|Nk`a0XRb()`DzxObCz%~EnNDw zLw2k1y=!i~;Dnx^Vhf2}`ZX^ZuaZ_CnGo>A{6LgI60_1d8UABrB80PvBo}b_*9kJ) zd2EM$6#WTv*FEC8e)49U4nIs9ByrF+1d0aiy;InF#rObk3kp=0D5-Vk0_aH|C~*eS z@2MY|vHa%4YmXt5;;16%)ZmeU-C2OflH61=^JDF; zRwK#{%j8%VWuGTQ-I_KQny59Fc|)rf5FEllQPQ@XoNwrKC@0<0Ln`hMpbfZ=F|Wy0 z5yn+#M&J7)5G?1|O$!Fu)m1TJ;;MtqEsgYs5m#N&?!-|Nkz@dr#aJ=_DS_3QWiUOs zapJ0TSgmm=KE)U<--22dm*kM=>{HU-u@B5ofH7`rB!t%DrVGZnq5vsEM5fQ+HIEXX zO#y?b>+2(V#8ub!QYC1l2*t6~Dbq)*f>Va#4Y8(syLXz;R^zoa%0CTU{8x!$__+iV zY2Ig@WG3%dkUc*`mh35AJl$ZOE0gQAQ)I)+wLeaob`z9E3V=|Fj9LRlSZjfyZFUAN zMQf2NB^!Ml+#Ht`IMm#X-DSD`ShK-ZKhD3_E_1tqC*{c!+07%%oltshbEgk3d+cy? znvWgLiJtNWOE@+7) z6qCgLV6-m`y==qbJ%))W13&=#+fL_I2cqEmblw~H*Txhz?~jG$RVj>=D{U7GF^x|U zG}kpe!3GhLkD+Ab(_H^O=^>h&0|zjO1f;gA5sqP@$V-!i`?&l8OR14$Ka0zl|4p9! zaG)z`P_U4r2PGT$2Sdo|no@Y+UoYNqL>;Z@ROu3|$Uy%GDsE%z{GvWk=xkXp^m zyr#VXRLhi=AxRNF08%#8cCYbC_JC&c+`-*9Remg_Y|@Z__gQoTU@lD@13=7rOxc9< z2zd#Xyos?o34lW|yVn!CFvi6VJ~R=e_L#2_B(krcM4s`;h|&zCEGAfbF6m0%7BjCYKq3tFLu z?OV6%55@*(3mYNw6Y2FZF}LE!G!^xf|3&;^;lI$Wlfw)Bc4i<%Z`p+#pkc|x4e$7PL()Kdb z5l7SelH`Ri)Gz9Q)pZnNM*)#3p#uL)$g6u*evT)v5G42)HSm}b5L%=25lw)zK7^2w z*S4uc)6lW5Q2%;~nDb2`KD8Oa*e)ziUzHgFFmz1FOD($Y%&xp2w)!^T$KvM*tz4e<8syv9eyd@v4GGX z9&pFy;vLV<8CnNV!EDZr?VzEo4hn2IQ^}pP7^7iW_*a(<(V#NB@7&PdVW&3^dwpx4 z-z5-$fK=$CN#MoM5lNO?cqcmp5SX~JWqI>xM@F)dd1ts2Y&tH>`lRSe63P-2vY4Qy zb$9*99ccELeESyxnYZO}XtfrCG+19D3sHF9pwP!bQ^2KNjlDT-6k0oF-I%d~_IYWn zn{DS7^Vq|%xcJfc;lU`KEIcMoAA=hK>Vv)C7+A6n@ZSQEFrenDHZa*d#8DKohen6p z-3Ql201Q2y)XZ42nLUTR9LvnSHjU1vy~El*gx*b2bIdb$BRM;yNO5gcoK(9rBBAhpQ9_D*SN!BU(;WTlWplWiU((FQmdqRn=}`9gexsZc|Wk0m_5 zf60;GSF%}^sWj-VL@{mnMP5>SY@un0nYPM>eh?xn`IHT%nuiH+T5-)S<0*AeT*UgJa4mr;F65yHsQgl4g^rqHZGm#wV3F zFURuzJmYhY|D;4YUN}}C$8~_(d{{+ zHB7DU%gn5B@K$3e8%ruDz^z&;9yQKR7v0BUGKQeAN%-(=9*G0zhwOpWJ&~nN>VL|1 zO?47Bmm4PbdqBe#3A1eZ)KrN4RnFLhKhbv%;TDFI*ZofF!901mcGKf71W4tV%K5)c z&TFD@m|9ic27GNeR^Dqlj$cqDihks=1H>bx7tLkEZBE`WhF*O3LYY;NIJFv3`-Z0v z)V@O+*)3+&p+ImPo%oE+)z!Gwfsx78A5bQVqb+%xxn0Sp1TxDs)9-^V))kwq?_3KK zH0!yUbK+q2-wv^t*x^Q?w5?rc)AHEwr?Z6EW+x_M_8EqGb?9+4M)w3a3CkNuAup<9 zt*hP7>pZ9QJShQTVv-zsG2q0teVZ#f1LV)_i61B4zU>uMS&+SprU5rlC8D; zuvdACXk>v=-UvWwYN#y-YPD>I_tAV8sUzjkK&tA}9NL~2&7m1P!RBlSYb_swI58l5 zg~M9Q+G4+a8ZSJntMs>OK!wxtmtRsrjQ^4-ORTbF$Y62Q_3>jW#6U7G|+8I z{l4AIs#iGFcEe*(LTF)gR+}nUygBOxxK_p?5vu62NbDUm0#)mR5wr1CZnKNFqlOeYr6Ehd=@Z-X6gv5gu46iSU2%H6QJ;D`^x__tfE})#Ah{ew zwm0Oo2vjRkOZ)BfrHIKpfc|6Y^LOlM8!r`vH}2bwBo5k849TPz4M;|Q>i_FLviAV# z^NbV2zQ!?j5lhfczP7z$dookjMvY7oZ$ecjO<80dqpB=7*im@XgagRBp2fdGTD15U z+Y&K5OQ=Qb44W8fuM8pQ!_|S*muT&amt_V!S3|cxF84ICRR03(T>Ysp6`t~*ANL1< z2Wm-Gm9aO;9R|D2b5Czoyezc+fhMKL)QEZS3g!Gt?8d_06muACgyJ9|XOd9J(>Rg) z14*SUZ{ey|yV!oM3HJwcK>jQ$>d|mdi@8J-Dy@^MoITMV4uj?VN8h)~dn)ow*w@6K zeB^`#(NzOJefl0CNAsqf^vG@qRr$Etea4Rg5A6YSijF7O$I4c43s54W=vrj8Q&mNg zOjcWNtCo);=CX#d*p!!`MTt+x!p|^-;5t^8DEIcajF@0b1vTWAuEk^Z|GNAoe5|Ff z@wjqaut)8%ct6YDUR%SfmVgc=2}H1HQI<%Ww{nj(ZjdzU$>+WMJZh18ZK0(CEy$jd zZT8M#jtghslglY0>?C(6@!_;Y-UCm`RJ8t**I+c3`w1UIk?7Tt}ZZb~n_C9Sh{W!S=~zG>NMtJ`IXm0ttG zq##E6rgk{TP*QMzu6pu&)-QqGgRdS``mArR8Nix2=u!YHP3GOE7;=;@0?DgP5EDQe zxoo=@xEK5;9+p95|WcZl}&zTiQzeiF-l|LQ`K=Ve-nG67G@-?+0LJH?lzNhPM zMAdG{M9(Ie#}z=*T?USZ*+MRn958n&3$z!IyvYqAB;u9>0g?9@CZWQ>QV-jCr7xj@{cwZgTBA30dvRe(h62=} zB|&UaBMIm3!)mkuv$9W7A|3JyVk->*d5PtOJBW<{6qavsOArbVupqdw6Zto*QQd!M{QMscA~_bhXAzeFB?}0_SyC zKa?=U;IKwI_hoYY-R14k5VMKmUIak)9lCu-z~I$cuLwE)VvTZisOrqo$5`*ZHuz1* znx-94`nMXz!fka{=?l8EC%A~R{TSUW_2k2)l+}#o8{>j)q3wgPMF_IU#BXTsty^S= zZ0hrtcgS7Z>J>^4u>XybPv$^`*y@@is*Pzhde<*(VFH!d@AX&K&B6}CGu7;nHo{kj zzk#jLPI@o#zZ7Tim`O{RN$lP}VQ3i*s~+6^bB->5dwEvS_eey)B{t^_%7uO5Zr|f! zHh4W{!1RCe)q|*{>j1O3n3TZJ6nRbuXjUf$R|VDmI`SoQcYjM+Ns+;RD3rnZMPAL6 z>%{(tVO|+|k#{u=<2|^#=8aB_bGU$mf}Bg`)?<$tBB~W#lfQOM6B^)%u70USO(e;j zvx`e1biNb{xuZG%V|^3LU}iq)pj|vj$Q_bp#us@LuKNTF5#G#~qUl$K_L7ntOz{$C zys310Vw{xN6 z(PFI?l1fP4KObWH;Pbx}!LvevA_BOsy%nexkgXJ>2JVREh9a zM3Y-}V9-NWc^%auMDk}=(jI__3q|>B9LKqE3vfK<*Kn1qnsQJt4>Ge53it#l$x@LK z;_zrYv^oAZhDt^==$3#-N*=3^GH1*YKl#iW7!U68%o$dpH?$uKf;(eiKFCZ%-*?fx z;+ieq=hIi5?PT+lM3%rp^5Y(taA!2bw($EpPJL#dBT z3!(Ej0*TM+ufs`pC4=B~nCg8uQ2a_fsYW(isH)Cfra{oDsaZ03K%~T|v&eO9o?p)L zk+eOjeE_H?sbzMJXFXEz+V931di|iT!8Yvz_CD)cK9{{ElbfUSe*`t zry4ObNynJpTte8uar$$R+|Sg*a(yr^3Wc|yq~|B8*)ou%U?K_Uj-}bW^Az|1rTGX@ zm|sxh6Ixw@3-61MYw8SFk4whJ8u1-%Hw?xYs(9WUF^)ySZJ|Yi2c{qf;GGcd3<9t^ zu*rg{GSlXYWm5)k|A8GkRCKD$!^3qb$m+xmKin<1{bSms`hYl-+Z({WZ6Sh7sneBZ z7%97yLCr)gD2y?t8FAdI?!f(&Dgvtm^rQf)PK*#hupGAm{Gk7!-&662|K+d#jk~_j zf@?XlL^cb=rqa`r#U$)bN{MbZeHM(W0akUAl$4a%EdcV0S{`47woMN zoA=%gUk!sv+d3a*0?KZdT$kvUAAsm~HI4oD^ePFhiexZMuuDNn17svyQ08nmTb{43 zchlySP?HcCen+wxOE657SuiXm2L}#RVoPL!LAgUpx$$fz4h!{ZZ9VUkbf>%1+-A!+ z_TFj|_P(c8d)j86Z#Sl1Y{)LY%u_h_*;Qjr#Tg(tL~^K(XD1p^0bCV21ZAb@#VHjNaTEEB9dU&h$B#b7@`_UP{!+`>aQ>% zs)^xFDG>bT$%|Y_ays3V;ykQ7@|Rl7(5SdqXz`hr+xEkW0{!eND2 zwMfWFA>v|WTAWD1VnjFn(*%EMmb!@>$I#!r{k?VE^W3fH-0RKus{N&;?D6N6dpC32 zku>L><~8pM6ta;cBM^jLt5S|uwMaroc(NBhFl;sAY0212g5Sv#rwEGR_qxcDJ{!I{ z_B7vh+gERS+br*R_PEX8X8$wa{KeKB3eu7762n$qM=M4Msp>*<>Pmj`)KTOIb#l7S z1aYB+p6tj_KU{XSD*S||IfF%u8C4IvQa9c7FLJX_T}kkp_M$rYnWi{`;5U(Bhe~O^ zX`5;oTaNGcNZI4RdwthU@wMLdH{1>QnNpeR2ZoJAsn&p>D>*9>tW{2HbQ`efWH>CH zmQBx2)}iORR;Qho0G~gKAV~g_*Gr{Rw;nL4vaO|R@sJ}9>W^L*y@?9Pk3-X3vma~C zyY+hQ+Ow5bPSsrxZZNSkI5u`Nsx#NA@ zP5H-kN3K< z&7Iv{?cKIjT)iiGdOOB{>uATH>q>R)Y)8`mb4n>jop#2LN}*C<=`)qOX@;VU#vz5s zyu*97m8dpNn*pE;0b+d-F7L<+;vsL5B9ZC|K9C>~aCB*PSp#EzXSQO5biu}-VZeom zQLuO2gmEKtHbGdeWWk|6k2UTTN;40 z>0WR_hUdzB{Ke;I3)$j=k8f?_VikZvS^!gbz{Yr?=|Ui>Y=jTK)v2*fjo0WjMQhX$ zIp_Jw@d>>AM6tN}z=~NHr$zx5-tF7#2vD>d;|L?2SJ+JU2-66>;>eU$gYyaRuQYbh z=iFTpt4RVkQIgC{_dMYDq|T?)|*&6GJZFk(|$C%iHCP2tyEYQ{rF?3o6+FH6&D$~ zZ~qCKyBp6$E38Fx=x5i$@4+?kxFl&LaEw6GAo3ECdJi_bsHyEDk1GzGHFK&*Q3;+b zf-PT}NDL8^ud%;^)H88mf$1g|(wh}hdjG}mSus;uymZAe#L^C=G{+_XT;w0Ckgj(d zxlH2cRpzxjm0ym1_8z_)G``i5uRmE)?qSp4WQF-1s-%B;?^u_T#_H8eg0n=fBhbJi zprDHmYX>_WzBle$?^_x01If3-^Df;*W2}!cUIHL76H@!uPP<)8`~2IEZX06#ZHX#| zZUrzQv#o_SUajhvgsDDDh%naFY-g=lT9nHX%}C#l)5U^1Tv2B`<0sbtG0ZR~K2Zb> z05Dr-Jfd#G;gKO_j7!tBr`ctjYvB~dpH~E|^(`44YPJyrB(~d*X52#08FjL!xOBLu zQ@P-g&kQRA_TV{tAXti$fe&++V^16xl*n1}ngAx(WV8e1{PFJ{Fq@mvffXmvCtvm2 z>n*0RQmC=GUYWcaMYC>{m?3c8UHW$|u525k^&&gPh3+O;>UT*bf>KE>-{hDZ6nVSE z=~s`;E~#fjk5&xvmEj?%vt*lD25jFDDIH>JOg~k9z{rC!&cjY48{h zb5t`@!G_7$H>W;O&d7^HcpzB3A-V)oCprrK~dL06pb z$&wC(P06`?jOIO{D7=!`VjL=*&$OSmowPI;!^(nMCZE7{MyY2>L2G@B?oR3 zQP>2%jT{`XR;ya-l(f>*%$uh~`v@8J_ zC006ejQi$J7QGR25Lx_&;*#j0Lrqd-N;tro*!jAE0yBuwj$GiZ5GB3Z8s(v;#_qD9 z=5z&e6xOGH^WW3QgiOf1ZpYNiT2t7wk;X4jekmITmQRs>s4?C;LI+>NvW2HoX1jag zoKb-gvffe%SVf=?WGQ*7!$|706qBx4x8=pB72sm)_7 zGq~1(D>l)8chKlcD_g{@)B^dLP`b5i9+VcM_ooaaki-t+1-XXdl;Se0OmWM2@2}6` z%n3N;jeB8(Nj*`DsLp0#_4awod(2=G2nBw?q0d$~CMPd}P*eDHZRMw=OqFKg5zIG7BMK5pJIN zQCG8b?Qg_f1K}(!xE=%)3DQygH9FrB`htprI*Vg7!J>Ic%Jw34?)5n03%wLb?xQc+ zZQpb?q(PI2&?$CO%y|2)nZ!xOCV1X;3i6m2}~eDp}dVs+w^=^-(5I%}20OCuq+u+mp4`+l|O` zCQ`ip6x+sW&lyCnJU@)}w6yoM0lmd#_h*}P#{tQzWm6L#EbFVyKjY}j+JsgJ$pK5{ zjuok8jXaIay)xju!ADV!bdUORY=vXG%Q4H)#JOln?L+c9jY~hrgTwsr(MRJlz&jKM zDBXqkot)JN$y@{aV;(hp(hsQJMje||!#5S9Wj?j3xUdj|22Kq=?SsdrV+W7Nny0ZM z^(f>I9&>WgIRgW)L6C+TWSE&_3_m1_C9ZJt?vqiv>w#8(r zqSA?T5=Bdhcy~-`Jv>HtN7eLlDXQ47rk38-qZxUlGv|_b+c3mvqq|1{OM5d8IH^@#|AJCF zzV0D6naer8O5ia1dP0lZ)$$W~qxA7c>fIcw2=A3TqPJ&>W-qRb_-1di@_DEI?>2YC zxrL|?(P_+N`bF1po@#0@y76x52xf;fTi)iH;0k}yVqxavrQ1>$0d4wT3aVQ%?9s*Q z%kSVmjYgn~n{1~ldZ!!a7`r_cY)Q~nH<=5mx)EXew_$@L9Dm)9Br!3XU&9&z1r9FKAHjp^X8#G-}*_GO&unZ{kx%tC%Z9ifjk z@#|RsKUo=V4b3%|oKV0jy}v)epRQV5t4pQ#_C1Mwpn$#b3yJarI$UZz=UEg)T3f#- zVkBQ!nzGQq-Z|W*2i4}(0N{g^#2-tAqy8}lLOA!c4BWpOjZT3tyQ8sCnpi!B=W$8? z0WKY-|5!@$N$gs|7+oHwK1WU^+9R7qd!mphMhi3hY-8yVL~)G4CP>qyax1`0L>} zN-)Se!SGX<&Ua!tTa31%*uM3NNRnX^5oTN&ru$T#>Z5=YerU4kOwn{xhjJHTG{~P$ zdW_S8^w5G|J%+87?y3|Sl!78bsiL^om6lm;I#WnN{qylb7>{Uy3_UI;dw!voUvS{+ z!gOiFjKjMKEUIpHHr(*tT?D8yNu**}FpifTNdH!Pj#eOIEiSy{2pf`XLMwOnrIoU} zeU}{wM2hrmLdIt1y)q{HV^^Mjwc4?g0_5t7!B=lp-WqH5y!MWA8%#n5rGhmledyKp z=IT{5R4PIO8R)(R&(U^|B}xX>=q+Ahixn2~xHl_VBfX$x9vBAV{u9VN@ro)osY!02 z9cIWsrWd~d6Pp<|JTQmpRWj*jMuC8d7-GhwtN3S;bf`Jc3M}LZ9vw}IVbl`{fR@<4 zN+}#jHlw{xD>!&`-ycd~0ZM~cDMNqnv&et~d3bbPF$SL^xSrTD`_Z_WaX(){gt9$E zZ-p?0?l<2}3)zODBkq$|eUX zS|&-IL~O5Ps2OSvN_ITo;j)U9R0t7TH}mO*t;-VmbtOCdkp(%{&DM{_`Pk&-D^0hK zO&6ej7beg&8AjE-o=8)t*F*LP;Uh5TSH$;bW5%eMDczTZ_65&VId>b*f6P#w_j}0G zn)0RCC5NbdE;mudc6uOLA$CdZ^vVSLDm}y`mGk_3Gh&OyB;6ZsRjihe2pV;>cfk4Q zAbX~dd-ox@m8G4q{UByGrSz;#w2V20k)*JMQILmSfU9Bq%t?rT(2oZBmu(E5X!g2t zDKs`ygf<8i0;JN#j)tu)BKLy90vEfO^Lsc+n}?r^-HZ!~veWr57$)aaUe}cE$;5<- zvAn)=v3uAr7(IMp_q#Q^6Aa{akVFZ_D6*&b`sF4so#0Luv$nFAhY*h^ zoFXx)7n1vHZ?ypvwF8cxdoRRblhy%^t1C`$&!oMlf5w>*5}EQj<8IajDy~Sl`(2ma ze$|yLk;?qeyB2p-Li0`-8jtExYnd<3y6j8soxq!;w}IK!t>WFDBFL<9UCpmyHa3Vz zT4ssv>AgAgrBaW^s(FVW^(UDzcJQtX{Ia~R78*Vfba%~b&!zF{e9u(nNugi@LA=nE zE3K+SNLI)-PhwD9xs)=|Yz~-<$r*9QPjQ!SxDZY<5OiOzRzDaYf082beJc}I1a!$% z_nPBK5zsV<$VG!3p;H~?CpgyMADybhvO0pA+YZ7 z0m&z}t)P8l1IS%rIT$@4{E^>S#Sm?v_l`r?pe51&`0Q+ zDUJB_;t871)nB+&oi7i-IuIxzI@_rn@-`r3abN+Av{+ZO={as6yA$2%bdLUBA>06PoY;R z_tyzggYxB{@D6-3dV}Z7`2W*YfHYI)>j*XU&ZvLPC@&~(NIvfVkk!P)?@Uot-iqs! zsm=vhnEA!J8N3k5L~WHC>aA3eMaFuzFt;Z;0T(I7n&r4oC`E5&On`P(93EhoYOz7} z2%H#E)Tfiiy&^jp71vei4FZX{0eE#``|+XE#lI(+S{{o|o}YO(iQJ1!1HbO%TpJ4Y17U3L0cPRyDic__$+uw@1A zH%2z`TZ6}Lx5#;Q&uJBogKfD&N}~N*i@!aco<%_*F1t{orXPX=R028zQ3#L$M;AGw z7y$N&eZ)d!MQE6z0`drvl)cl0*AQq9k`@?<$${VjAUs@OkkF9RZ3y23H@`&8 zN>JHg$%YDgRB-`|4;VyIzjPrzpnWg}gcU;AB7zYmfGJ$CLXj4BFu-ECK?WQ%+~9yg z4kGCA!VW+1Kt&G-`0)B+2nb#f9|(cM5Hk=VMGB_;6(;t?8Z=$aia_*X3T&F&KjxS3zSqUbz?X0=65W2_>aZpck^!E z&AS<9BnbP>TK1+XVWChbaW~&e7H)=v60)xEZU%dsE@?CKV5wDy9hPty1*1Gm#8oV5q`Y(~s=9n&ByW z(_~*{xrV7~l07X;Q(a+UM@O=EF%(U4EWMbfz9h|54Pm5N_M*34z(!Q`wncCI#<&e^ zGoC!v{dPLon}$ONaiO4W`7PW2RyzOiDOG{)gSDtsv=}X;gEF0Lt6D9H=ejM@67Kr` z$n{_6VG?m zwkAPjd^b#cP2a3p$>h72Ypx;;x;_|&pzA*ejl0a zjo~Kdyz+K4ADJxnDH!sCy=_G_jt`nDCKWZo({00;^xjc9J#q|RFV>@L>br=-P}F4E z5>>^LB~Oofb%cp9R2)}QOi6MiUqd-|^KMqbEOFTz)}eo>qLMsGK=cB$Gvp--u5#lB z05B#EK-3X702q_r-;xf!6a}THWc%Gm=ZDK}+B!w<)0Q{`zi_LSJABmNE-UFiH?y$( zVU-3biIbTeuFVa>vzHeFGe7OUG}AayG1F~^=ZiwIJR$&$50<@`1n5N#HLaY-<0Sa= z+l|XJ%)z&PAvtGFhM3G+7?~@3FWrb5pefaRAwU2vU;=LEK|=HT9=G#Y@!`0#x6l)| z{$a9`y5ciw6|SDKtz!9uJ~Ek?TbUA-8YL>dNDnMNB`kZ}Ub*_FiLWg=o*gEdBRalR zm`I+gxT@l+n&>E|{zaBPg|X!6J&KBJh@RzGmLhqQu+lukZj$DDk|T=4%Ho?v?QNUU zIBl)Uk|c*G+Dj5$&7Q(a7)#n#D{y6_<2j)GTX$6}U1m$(;a0UMQF=xPwD?#ZY$-LA zEu;Kbe$(CfP3M}Tn3kny#}f7$LEDa#*w0Q6k74#jo~bB0cZ-im2dZt071d9+C|Qu= z;v?NUr-uYLSzBFyO>lgjEa^uQ_JVb@4Erdu<|?LTd8(nAmgA}hE>Lr$i*MFtZ&*lI z^JEcC&?HMX_2H?$P7>DrTBan)hO*L&&&URQ!#jGqAT(9XTeoBuMMH2T!Iuw7wM5kw z#-gy0gq<+*gq3BuvalB#mZ7NjzNVuON7#$)qhHTdeTl1@`trUw0{z=3YOpsf_CC7K zu|?!>lD~1zO>-nwRa`@nHT}7!<~n+jG*fY1O?C`Rud3uKlA?K*VMwMgkCrKWrY2d2 zuyX}hNN~LBrKIq7UwmEz)rMtyygGp`6oB!W^fs!`9@4+jgs#vP&d5n#j=QXDzbI3{ z8pA?s3=4(vv7GkCne>jhj=+k~1^M4|jua8&(^2KuODvFilH70qzb($EFjtH7Ib(d@ z%HFsg+K61+RO$2D9<}S+rfX5$b^N!l_bLB9m%IHoO(`QjTFvv{wk5mme1PBaf{yrl zZ*L=-lX?IPj>aDe_#H0@iSJ3%-bP-i-m${E=~$ezSR7=nF^t6GAdAI87G8VH!YzMe zJ058cN#Zkl+S|xys7}HZ%m4KNVL+b0N3qy-yT$Uq-IVx{uIz1Ods63UQnIada$+#c zbu_C_ckSDC9F}2uZB-A;v!I|q2c^&@zLzViZA1j6j=}`SZD%?B!=D}|J}O%FHuCfi zqI%|JYmt!PB)k>FERS0cgRo_#$S45`&RSHRzSPMtpo*B9R9WcBSx zI_{A^yc6OR#9t4Wy^UUaWG(dc(=K;VhUIjgxvoyglv;}Yp7X>9)1ul&I0@y}7m^&h$Ob@u{aCy2gCQas7?^rpzd#?VAl zlttH9KSfqW{Yr`@OQNVcs;3WOsQ6OFF%3)6e2FaowyvDymmK-{=Y^-Vvw3&+Nb%vi zsP+@F*I1poM(UR558iq^D$)!L$X-|FhiAv!deTVP$Wj7%i$bjuq%yKo;ZS ze|3}A-)eiy?d+=dW0xyIln{b=e`~TM3?h1;PtaKJ8vZPc@ zjR64Y1CtQ|02mk;2?S&lNhlbQEWGvxf+>c1Vv?BEED3!_PxCj?vi$uqfSz>tB)<~{ zorW`;Am{j$;N|#blE4rLx)==c&)sC>ckP;da_Exf8Ci0Jc$({VPhUTGf}%tH#|0X_ zJ|{|{035d|d|3VBMjjLg{QylOzl(=mm1Dk^gP4HAnG@7eg0${AP$l&5c}y)1jIlGp zB$xYJ2Z5xeonI2}2>;GeZ$_Kbae7N=J5Y_n7ulR}PN9)XHgjHFq9Hex@DgsOX^(FM#h^8V;TM7uc zmF1_FKsw3*Bqj-R_Mj~r#D*Z4uMyiPa;-z<8rcM^1V!vA)D*|@2-h6^@a(n_;vzyk3H=(aoMH_|!?!5tUdIwkYkZytsY-9dB*ry$OvPGe`Ip`5ZEs zH~)#rMUMAP^DbGe{?~VHe~ic2mX;eW1|Sd8tfZjzp$a}E<{Ll^fXk;dlz{K2%Mck8 z@m%%LHMdN91`&QL!2`!&nEv^(+8}`=K0w94N3#G7q-$k35_2D^+`r1ln^nF9jsl2) zWVk@6h`9P>Pz-CV^&aSVpdwQH{->ZQ(4qDIWFeDI#aLRY>m^0_CF9^Pi=|>iw^W4q z-=mQon?Eb_RhT|$kHo})A}fWBM@$!Ttl$CCpS&G)#mN5NH~~MVk{QX>@tmjO zPVhFg1z7xD`<{_^6%pwG%J5pny~I$bUKe0BpF#np%ZB8;$Sj&KUPGdUEiYV*Zv^K9 zCRU(IBC2ohDdGqSAW8$vEd!-9Y!=$>H|gZB%n=Q?P=lPqufdv$2*@H=Row8wo2`mI z{D~l|F8^>0YsDzig|ySq;AqxTldRfEk!@~n%B0?U#x-pNCs>SOWdOAJ3^_lgDG8hh z)>x{v5Fn-**L1`L6k2Q}bd~G^YgDl7Nou3Eo3iI6UpNNx?hY!TlaOj&aXoH87vJns zF(GPX@^(EuJ6i|RT=$B(V*zJ<$f86Lo%bREex+cTlCKu-9@GpvFwfl>$%2d)92m@M zk)J`pLsqqhK#U)k0>46(S$uFtF{xcJZaiVdV$_hbQ3J?4P{vWc5V+dH-;^oLzR-P^ zmRl3Fe9@3a;u3i0wS?bxrMS$*$&BjaMQ+gS*e6aYd)8)@wT7Am*~s$^3u!G$B-+ST zlFG7!#&^a}Y{vI!^ss+;yjT8cJmEpL0j|gG8Zhi1>h=l_(7Mtn&74`>nrU(9p(=%U z`8oVo%jKnfNc@SR%k~}IR-$n#NzsbDz$kzck6XhnYurukp|KzDSG~fV*#+;HA)+MYb=;?4>iXMM z2mOLQ$s`(Rhulg=dj!f6NM{!q=XfaDPks8u|Fpx0)Z|QE=6Cy54?wdF5w8k^_+WSi zq?(b32ZoNEt-oVA6)2VpiOuk|$h7{pivv3riOOFBZ=^3S#it1*Frcx_aeO4uV@%88Zf!LqxlQhdC4l&V@SMg!JY(&y1{rWAgm+dQaOgEGebTo0hV^{+=EWWk zaQeRo$_gN<&0N*8x5t0C8Wvy#&)if72>ACp`IwZJ{5yvbp#$%rNk!MN$}+5nvP6iq z)28JW4pzEhKupn0Wk|Kfnr2XYE^+>mr9o*NKPQ=N@e%l=YyjGtv}`@U+GZ)-_h7=_ z9`7!~R{8?NT|PpjsF=RxH)TmEJL)w}&L~bXdxivMRiI%j&zw)j|537^Wc{ckh4mn{ zD?I2WWKH{tmc|4qt%Oon?cfQw?&g3#ldlsVZR{*ygJ_YXoPeamE006A*Ydp@$4;o>P zpid~JlK`0)jmm{7`rdp;{D{|mOQ-2McP_V+Lcy4BjBvXbZM1#XKGcz!c~?<4M(TM{ z5Gnnn+t_nJ6eq$s53g1pVQEa$<-v3u)~%ZTS5%i0GYfrQ;g8-{!vexl2zT$ z=@r;q3>JX~adNH1D$uW?xoHF?B2Y%fVqs!W6K|vzxIzIsjI&Ir)}hz4Yu+6YX;8V9 z`6uymVX%2h5VxjFFQ%U~MRujVu?(*`Mfc2U#LJJ5uB@hcau$V&HgW=tR^;hZHb{3W1TceFb4kmD` z){|d;6mh6y-2*Nc0tHzsIXqy%Ksj3B5@);r2nLWWC>qFOZ2-7!e0OEy*b43yp;W(1 zP-?0^F~eUOYP6myf^$Bd7C2fSzOhn=0>HkL#v4-mA>aGZ>Cy982Sq;bP9~3t1lZ|D<{lf;RRJOmfI97g&Gg{4ijB(f!EaUat3W>( zZ_%h!GlO;blbL_4z5@tTu>zbIu|}=gza`TLQeVKzu&H^v-!tFAoybOfdck zyG;CjSja{nBw0~`eym+u0<8bKJ~XA>q=8R{<ZQpr1QVAX%|{;-Hn(9?({uikhr&|RIka<|gc zQVyYZw1>LM83?bvth=$1^+Cz*p2bBPi_5gCR>{O79L;G9`M|J*-r~BN+b1BGN z3fPWOdPM&22mRMD=@@M~zUM4uI8kYnYsDPs^~U^a1}d@|ZMuZNefU??KB` zt#qtXkjgzI=FeIa(13Z`a&GN1vqZJ2Ir7mQjI`jeCuzK=*Nl+|biPtSVQ}9p29AB6 zFczqq;GyiL#<>U}E8D54>G!4W&+^8P4Yc7Q_d?sQkKN}lTKps10)TJkp0M*}flMd3 zLy(Vb0mg$_3JY8*q;nZ)HzBv4ecMsfCQf zSKCBnQG}U$XJQ?e7-etp+hBb|oo6VO=$+{%7uT}8I4aHA@(r|zdRmk5*9=XaD0$kJ z1V!Ygrz8FTrLmnQ6Lzm2wzOl=*-s=u&*O* zVi2+JaL$+_nJ-FA=^rSyo^_7MB2p-P2Mrs@~B_xaI zTE+x_)SkXJgs1NLrs_G*xqo9#q%QE3{bPkugI6^P=rAAcO@m5zQP|xVHi=7getPPv zZs`;KK_n8FOm;FgLafoE6k-;6-KP|DFLY}^!ZRxusl75gBKfBun0K?!#cL-sC#c1! z)&|B%0z>zcsK6X;3+$fU2Rt$H-J?)=&P`oGly+fL9<7ighx(}9lc*@HlwhJ5=X%N4 zg;KFjHbkM<(h)$@a-y(2HQ?cbxh!?^NS8ual|#1l2sl=l0Ee+*fT|RJPvTk&6;M5; zy890aOvgn!`1rTu0&Zgyd^_AF#Fx-W6owwp5MfjwGh#<1PP=n(nSyC8H5klnwjuk< zLVy4md}XEPN3}>4B^^UL&Cg6oR7q!LoFIQ0L?g=YAIGbY6*=&p5#K}RYlb%|TOC`f z&}hr@5Sej1nswb5r~`#X-vUT6$gjD0^GRe)X+2b)#CPyE0_KxU>n`7BQJZm5f)*ep zDg-naa-Fg{YeU*m$|C{*Y;P8)q_DO5sC0{OZ@Y=Z>2qkKvjWm_${Mac*kJG4hoq@- z$c!fq^_{7y=(Vuio&;Gr{NSJ ziicS(gVx4&*J%3;n?|&<9h_-cxBs8+@?XmMGi;E6h;Z9G{LB1q%M^vAd(w*BB#yhL z>4%Ku!SYHoeLVEuxGRWRXSXXeb<^HX&~d5w_F9SPKP$9P(-)MG9J}Sv2yz52S*V1} zg&kig$zjs%J;zUCWVXh?m4mzVh!#;!?Mn~5(uRE2#Px!fz!l_$Snz^8EAhFLc|p?d z;!=r|-1o+Pk#2>l#4=vd$eoamvnHF}b=JNFxA0Bw?&E`JO#&tUUnT~h%!?dxiJ8=d zyYH3v6!iqf=&5g4IYjMVY;n%8I8t}9Q#>%X6tNp_;mh9H8FJy__7co+38r1?5|*aB zCpIi>T4oboT$g)B_EROd=VJFMrBYXmTzp25kG`-$-XX45D9c`Fxc1&f zll_OR$=MSc`UQDIuX$k=Zj1RnX>z9Jg$1A>X5P9+pazdnOj1rF-`_{EAX#TztuQdX z;rccqG&xfTIxk0j0eAC-RUN5$|Clhs5dvs_plgfcSg0E_aPopcA2ek?a95Zcl`kik zrvnfv6tTO22ctn=Wc<*BpLOq(*lO+lT!-d*iR&~nL?)=^T?0}GHIi`zqD#5;=`;p- zcL}Fu4H(%2;i65H0DZ-I$rF2v+sr7ktfujg?!pqGs3b_~n%%Nu?tkc(SgD;#s`rYP z)9p(F2Ks0NI%XHaQ_Y$dTlU0m^wDa5Tw^-H zG6rykK!g~7EilzVW7&wGM1kN=hJ*$CK%5GxX8%5kn)r5bHCuNJp8N|Q`#_9!67>rn zskC$6#-2o-uxv;zqSpJ(G58)DH=|&Y0TCjieW;xalvpd&orNRX94jW44J!(?VU{iQ znUCS!+3@iEY}h|gb_fY)Ds6S=;(nORB15tv1+|81_cETM2-4wfsMd*)ykXr=#g7_L ziv%TD8uGz9kO50mpeVTN?x)$C!N}+KODuI?zn`$QK8T4`C&Aqu2Un~V zcS~+i9#dn{v4n1P&mMrUI|!Arc%od8p}{;GuGvZr=L;KH@cwczciBZUEeG*Zaawvq z1%P0?%%&js3wxZC)y}A)5O6QM^R`{5QXz~hBR7zs-eQi?Qln!qMr)RpTyuRI>%N-U z!>c7=q9_Usl`XsZkt^p*v|XdZjQvKPi=$P6&j9iY9#JM+%d&(kY*J1ES0oW7>q|8 z#O+$~xIagwlFCLO5Jp(ss5i5F?`j(#Zx}Y*$MIaikY|E!q(|IkhUslswxYhz7&o+( zf|R2IdBXc;Gq}Jh8t_xGxtobI*rIZJ9dZx)9{^-`;VefcGi)Xv0>2L?U| z!pWqqU8IF=7q9Pa5$MovO8Tge0fE^5E(OJ{FVbc0o+5KM3MBJGKw(|`pd7_SmI|sz z*UvGbPef^D-iyZ6#w)DTCW>Q$RHjdz5OJ&pV8+Znpe$(GYI?K{Ry^Ge%S2EK!BE+R z4-6Ysm#O3AbK=^wG8ER(zV-xf)Dj<{u8EKo)luBA(mM67_2Gr4Jeiw5JUn=L1b4rG zd~O{Sk&@h07ZZN(#xwO2f%keb$4=GDWnBGPNW+Hz&@jBf*!$3{X8kM&G9#twVnkdL< z_)(uH)#}Jz6X!c8Z0r-3>)%f99yz_5w=&tJ4A;CZw}EcXQ>Sy?eE8=7^bLqkZJ33Bg)<%x|!rp9ArhCcFb4 zk2}2WNq&VlzUc}tpKi*oJI2mEor+QIaYiR|PVJ>t1N2Lja`JEQbBpVL1w`SJZTxmv z(zXjwhI_#PfmHkM_GLLT@vIWd{b8oUp$|F7W7>YyeGpbZQ`Rp8% ze2@iArG6gEs~Bne_qhvk1me-3N5@Jq93u+r7pk5oN3zSuP9YZxRMq-38mk+-!bji`w#O%3Q7PVo_QaAf{-5{W~ zgt=_K^P&xvznta6z4%LMIVP)FGFUJYl>OJ;I;}3@%s?lq*18Evf88jkqy@=82_lDf z>!oROZ$?SQ2gQfYExYm!dew{w++hPIe*0NDW!0l+;pf~92BOeKI?2E3Ci~MmG@lTc zQsd@=ep6d-c%wHf)H{}ZEz@z#{xXv9?+i`xM2FzG#|fu1oT99mLtcw(Xrx*-`mUG; z#m>X?;cuuifvVrqW-csOk}2lja(<&gFADX3y{mkXrh@TLfmri9g|6Ed5Dq{N{mhc~ zBi4lw2wJV-tU7aFA?{aw#irT11AB4Kg4xLh;|bS6^v{ia)>lljDaCuNW!ly;ck7~8 z3jJJ>dAv|q$X0smnF!55M8G{PZ$ApyOg`Dgcf7zVx8eOQK6dq^cmiYBmfW2+s< zElgl$^FH85>&aZyAXRH~X<&}H2CliSi82Y z8dzyHIsnvR(f(6Hz6e}05in&AFvvvgv<_Q-`|ra$7Xk88R;ID-$#5;1h&xTC_cU&VXZFoW@u*xtG33XGbaLijqa%4t84cs1 zbF3=jfsq;dAavJ*5_j_`eZ!^PydH}TgVXBuOnJ76sszN-L_?;Oz_qN|_j)VWrez5= zU+>5$Y>GoS6kN(|MJTXd@FjTMtcTo18obZ*4MShSkmz3)?>vBbXL4?)M>UY8QaX=3 zM>sQj8b1U^W`?A}KOn@j+0DP3)}q)UFeQL>K`7!zoJOq+wzG;*a}=U*oWM|t1Zo-U z6|?>qcfmIIK&T~u=wqnu{@!rF-H;VCZlHBfT@AIj&nJcl5+7EEM=o%1rKPW(q&S+Eq)xR|2_@~kP*WL)Mv!jqBnDX$>w-*jbF zvX&sc?*1Bdtjj-k_^fn#;3P2~YvZ9CTVZ2D8yj%B`n$K)b{> zRJkV0cjm|YF(|;FdO#2I3IK(ujn8+ZcNn^j1ob{LX+f2+$2x&8^fr%GwgESl+!xUF zX3%u^eLRSo;j@3fVz0D`qHaCJ^%27cY#3y=$W$4kZ%AF&Tz_fPutGO=Z9_ zwlRCaU%M>BvbyZXdBwvMaCOYEHa)N_rVLTSn$i&2Ht`0PA6<#!M6!;t5)y_gqdmLN zYY;468CI`wa3r(~b6?)rcroCO8GnY3RAu3eFfa5fEjOyujwy3-$viscpSW6Da_6-2 zljDWUBIXz8aK+kb*^3Du<1Mw5SO3S)X{Jq1)?A2o9BxJRItuDeYqFFU&R~2; zvFQas{#B&P*L_B$y**Ht0v__rRT;K3`!!ZA#Myz;f(D((leNnZ_EAa;^k?m8%&F}=ecC>!;=Wi@1qrPknDY_8aZaYqTTjON!obK4AD+gFC=h8Gw^B3?HOVF z`d`OPlXk2FmHWZo)O=~A&Z61iPoFI`34+O{dCg~QmCEWQb-VkE8am_yzqBYhW=f?b z!R0o-I%XbZ$CQ!<`qPsO#oCF0>wC_rJo1i56W8|8JOs+V3Y4i~ldddm&!OY-^O zt8b&?HjEMnW8nXBq(i>v0BFQ;?xKPeiQx~ zCeo`wa|yFG3XS2RtnMtdJYC5^>(C7r$C6@1YD(X6fj{a58053};AhyjnXH4{f1S%L zyiv*X*(+eS1%&ql!!?#Fgm-d!^-C_ymoR2j*z=Om!CDXU%iSWF)=pk2G|_$)TYY0! zr^_ND#pI-4UgCtj|DMBTY>4bGnyB=KEDWvCo7r2`N|UNqJ%3!r*2$m8wA;s~5m1TZ z=PZ%E$e^+AWGTCOi$!+DY+v@*rHplux&SadlFv4yJkvXjoX?tli2DF(+(uOT4fzu{ zdtM|P7Y4qx z&szeW;BR9u-SerPBoE7dV6RNTR(uoib_dVE0$LK;HqWlz{#ENC@wr2&In5$Cdf(M@vdDATQECG6IjnI=-X*-1gX1B{M1hy#e7OguwR1|sEy z?$rlrR{!Z8zzb*;X$*m8=g}ip#R?8pO7N4CW3pm5O|b0qhXKg~3WzoseO1Es_Cp1y z8chrI&8pXES~g?KcTzDVYy$dWJxp03TA6JFP+&j>zyJzh&{aeL@=3R)I7x10w)MR5R!F^+%;@Olgn%auE&loCX+&Frll0^`uO zZdm#1@vF5PRh9?1^s>~r29{u|{vRV%;OlrWeiYi&fszl2BLl z1IINrr9MJx%M%aD_mIS9AH_HoV*7dix!>Qu-d-QxKi-}#XnmN)JbqS>c_ee+IOhyv zFg3nD(<-y<38c$Wfv7M*5t01bZ*t*K^kb;Q;e_Lw%oJ!y^?@SlHj$dcf`t+bsHiJa z!xk-whFm~FNIFKHk$PiGVsq319jPf>tII8w$Ckb8?5^`~j6=)Eaew{VjQ#Cm1~=|l zykrJ#*IL9kUl$fgoec}yM^aN`>C!phl(ikS*^c43v-M#H^T+yYFLFKV%t@U&?tF)$ zAHgqb%fHS(gSLPAyFYi4#QEND`^KZSH-@ejh2xIMur;YkQQ8}SKEH?0dun_^9Nz8q zy`O{L=FeWs`yFlnw#Ywgz2BdU;MRKk(P`Gf_`q>TYS^5TR9e&zA>B?nAu7vun&v-e zB}$X=tb)wU8TsS+7fM~pL`e;LBF)d#>NAvPWHeEsuM96VbT)e6xFbECL4zuT z6LW@^G($R@CmeUgmC6{R>R>|S?Zk+54;?=bOr&!YNTI{hgcd@`Fam=HT2e7gZWuw61sygim}&ww zO0WU*5pRNzC@UL0B5MjjGDb0wgo5OdGy)+l8ju*Wq6n*oB`y^NAjN~?)3|{TGKs** z4loGv_^}9#M=;qgG>%Ql>7laNpDatP@eFh;sXm1LlDAc0W>gROCX6M6hH(J zia^4MBs}nu009YdnBWKnsuj3mzyl4SPy-@#kboW#!3XvSF(BL^2t$ytE`bS5=y8Gs zq_6}FC61^BDN&*XOD+O}0aGZ&VROsTT1iVrBVwgPah$nhZ?z@&9B&XMY(=byP$?<* zR^mxTlYtd{+fjrDd|XKL4TY`d2gy7Cf0;x!u!5t5zOC?N-KpbXp8Mp$icOP!3*r^= zTIXLcb1*b{McN?Exv$A6*E+bZ0P$?^;c0#iMw6zn5d5E%$nBj<3$)s6%bF3=!X7KS)mfG-XI=l59NB zCP)qst!1k{Qgwb5$*v_a`sh6sAxOuX>I5jZy! zGiHo{CZ+LlTWM)QBIm~0XwX11avcS&g0M&slg0qxrsUXsyhff4#SGC_^B zhR%(PiAe%Sieu+y!t-25Eu$eKmeC1#ZY(k~C}7=O*szhr`U#T6`U#Q(p7Nes%Z(c< z@VUW+GeJQsP^GFLHKTMUpNE2EYWmJ5K0h+YtMR0IQ@6i4U= z(`vDPVlb2r0lC=(2_l1`q$H!E)(;PB21GX(LWI!3CbcQLu`ov#;90RkGD5v5VY?7~{Wl9O^L+D{?Z?;mJ92Bg9&z4R z^R4~2++R|>UHd#UN=0 zW4Hd>ocH!ub1-{Q{Qp~#^x?-A?3?7`xPR!>Pp5w3fRUkd>Om!gTIA%r`+D)S*q>cTbp8V+9KiXU4PY>xibq^Iyo~BoK+FJk5hIxm$>(G+iXYR+n zNlW(Y#hiP#UK~SfL(Tbzy&wC?Z-{%xN<2C`*bf}n+4%yMM_`q>ZU@BvW+CtQc6Rn{kBiGT+5Kn5A#-vmk6ev(5FqB5W=D=#xs#D_m*a%vif%fCRTxl0K|w8{ zp4_>}>W`yV%d2@0)=DC3>5gTI+M`2~A~Gs-rW#``*T)}L_Lg^p!pzc}r_B2vMN6-w zp(d+=@$6296OJp&*{TYtuiGj2sX8qyzzzngLZ{4T{t|XaphA^<>cP9uRV7PqZqyw= zXkbcHXGno!)uSlI(L=r6ips-DN_XI>po|zQ6^|$TpQq)+m1ZAmjtdP{&XCk5N96&2 z=s(RJ034+=08ldzRW-Mi^_shCN^@2!P>v6kN>ziM(DT#Gl~;pxIvNU_j=E&y*-@!f zH8}t^Sxo?Pcr|&4c5Nn77o-%H9rc9{KRi5YkJ~~EtRj}GvVAAICM9(f&Hj>Xvvf0> z_^7cb6P^7d&Uq&97xFDyjQI{P_;a`YisKD^_>p&iqU}7qAu9zjq%;kPJaE_fhNIMgHacaS{CR4SBX> zks+}X$P2mm%(b;)9`pEj7=C1Uu@5~DU({qMYMx?u$xzo2ovR0qGhx`8))Y}mF3{yQ zhNx3_@09&GAHnba-eA8Vi1Gbm2;$D|b!nlK@q^7e6h%b0Q_ilfB1%Xu@$O(iI!f`T`1HoWkAjOJ1zW>`C1%{#NIIJ$cE|-m8Cj z_0{?AEsEl{d9p21S3hw;cRJ{9P@2kMI5;>8&L{6zFUNolg{wGoe?S zC=TpZ$M-*cENZe4bVHhOoNFkB;jyAmEJW~W(%)mvM|3#rw_*e?8>ws6fy5A)bjWIB z6)RfBibya7M+###p;CgC(NIro6-49`3JNmn#355;!G;GAf>3-S*`5^-A*~4 zb!97oggYX|2`bjacmjq(O<{r+EOc3g4i-hU0D_AmyikepGGhi9oM3kFq);Q>i71wGFj9lp z6kweR0@O$VLqIaSVI>C_CH`p!P%5At`G5c*Tm_(j5HT=-5)Pmu0W}H$vH%e@zyc9S zf&{QI1QCeX(E*te4fp^PLO?Wv7)mgN364ktr(lN_;D7}@cmc}{h-`2K2SCU{iybte z2lfXlgs=i4xG;i}BcwnHEntF@6QG`u6a^uqz^m|tE0jRO68c!_R!%rhiOROKq9TFv z$k+X{$i3Ixg8O2x#SrsLUd_XOMP206CHaBll-SrY7m?DoXnfz zI~CrGQ(#VoR}4RCt>!kwAL&7SdpYCbTL?z&oc+_aqWeK`+zR#MA&=h$f+_uxcX!Lj zyT1_J;U|J5$Dg=4xj_r&OMYU;T1N9;FY`n(QBCH0m7QeK{gvaAxL`w4Rcb9%QlC_% zh3Z{&J6%~}aiQMWQmDLGVtJwd*phTGVvtf;V5ln@Fk*B^no^q>su54NgO=Y7a|%p- zU;O!Ri^Y8R?cU$rwP-W<{N9lN&VS!oYtY{Df8=NNjLzu?j#EjUQXVRQ^1YyvDoJ-@ zkWyG)P`j)P{fs}j2WLiy!U@Nzq&>wkL)`&&S20@bN9O7H@8sp2LDGiUgSd}e48brQ zLEo?6WB7%i)#Lp(Z|q61izCK-Thi}Kiemq8&z8*M&;E{|2nR29nx~mHzT^)U44sn7 zaVqHzX$}l(4HOp4JjuU=8etlLYC0`l5fhG6NtasGAgCwKN*X$|vH43hI;-B>$3;kmpAb#5?yJ?h)+#f)*45--VVkDGgHt0;;Jq zJbk7ca|Scb)4MUxnj5uO*?DZ}c6hpNP>b|gA8VuOZ%}@NZ|Gcr;y9qy4eHO5A~N3j zzl_Mc@mM}qJ1ryslW^#So;VJOg@anNq$Wucb!DTOJ6|1I(T5ilN3!$F!>_Tn{$Cj? zKAA&zLlllPIh#@rDo@n+!N5S0rjpJD6D6wnly55QjCs$S{6enn7je(~g5G~wyrJIM zl6NFI{>a`hsPDMPuKoAj4f1fb*V}K4BxpO=129?GmP)KGqSA6lT5@andeK6SznnAJ zx9-{R?~51Qq9~F*+i|=eBtQE#cz-zNJD#Q9ovka1lCr*>yijk6_M*Me7NQw#=$An0 z4wco&k`pInvc9B+^#uhAR5ZsYoD{~y8bgW{rfD>znNU1ZmlNxYj10=8V?sNV4;_w+)y19xC&p= z3D@w{E(VI@gp{PU7;B8P!@QIXdjI)afu9(A@Xe~PZ+jgnk~gTA09YPNX3>!#+q`%^AofR0z|jd7giRwnjpHJ zax>N%wj@PPlSIoDI-_D*Dw$R*2+&<9j#Jt&tVBy=3Lz_Ie!##hNCnlHS=ow`^gz2%y3lHYvA z%f&5%`E!wDG0fR_kQ_4zf@1F*Bt?(OuJa5P)>;jLjz<)ZJ7U9DTYI9sA1xmste}<< zR?D^XW2A#*w8JzjlPryz2y~pu%ZJw?HTMz;Wgc=}~Gu z%aX}MQ()hp{CM2_>y!?xL=_k6Dx?t2002b`vl0LR7!Vc*g+p>U92gJ__gy>Q`~%-J zy-H)nQ$N@BV&+IzAe64Ciealk9B_-?yDAdNOE1{S>Bi2?%Xc;1{;o8S zvQ|VDh^G!wedmviL_y3XB%MjR3EJim0~x>Jv#nw>0tMmUI0PeKRZbtU4C4vU`{gY3#Q&^XL)NoEnLQbbpZSpCA& z=+V>FPjfiJ6*9P!#PPT@d~_^@2Oc$QWhp0`qL-*;u*rZBMOs1rg#M_?;y&~h@h5as zD%%hY)UHtEsf>U)3aP=91z?5xu_7UviTcCm<5eJrRyf85nW{-B0|1!^0@O_PzYcbz@d0vZOfzJgK>z#1^XeX_@quyKc2$dc4a+o1 zd_W%)?l)9mipB@FntgzScER=eGi9%Ond4``A86Or@pnMuEgJj^sX&iZD)s z?t=uiIIwY+Vbg*TbqSII7Fe3p9v3ycPA3mAK~Qd-=Vnn@LGKFjfy2_=N z-HIJqE>?}$V~kAqp3IX z^`DZPb>*`nEdzgclMbMeOsLXD9nwJnZ-VQ{cx0f|&t5cF?tnD(x<}}aj!B|8m+|p3~SKH_9&s- zrA6Wr00~mdIblSa%f|D!HA18i8Ot4T!9a#s6C`daP3MF|7F-C8^Pv-n7+Qr$m6sXi z1gkpXNvQ=2+jc}O^FrPucL&@Gdji^@EIhn%eD>JMF1krzoS*3}{4{1B#44RCu#PZ{ z{AdVpfBuE8*i!NVel=g^^&Hb!CBmMwjgYSc&v!xfRqde$!#iPfnK9dYH^2uYBjhOT zOHl3Bv@i981dxnl(>s`RoQHh3WaYScXcoqk`?@err(?vB8{I#iug>^i2S-3b+1zX4 z?I#P>Elz$Gnx3^QTuSvsc=dTz^lwh3((L&G>r$H z4A+)gh1CeHWC2HPtN;p`$6LmMpAJc1OWw65zd0ZF&G3MUEu>YkadQwE1`h*s{x()F zCtEyNi^X?wURH+_im=)BZM9*ici=(Y)|QaN>2+><(1pWU@HGoh4ln$n>3JS+>f>jh zpSFr7+)eDT3N2;EEaE~HH4bGFZVq`?;;h9a=9Y|Ik2{x8cfvXs;omqm#k}+N_hN|} z$x$0CVE!(_@`<1$Kj%rINL<7L+6R!Y?9oH722=7b-pquj=Ztw>!q)g?kKtm)oAMfk zy@;MRDNDldRcZ&RIRL@li>f&9*t>r7NKFw7nn98{GukE|>4YUY>&jcTKr&`ew8-^BI4KL-BwBAGl}ja5QH20Th{UY9b}$}7u~m${3@ ztw>f7khAQq0(4dA$Oxz-5=cLj9MS}G%3PB6)s|hi!VfO6DKgi=0B6=y{J;S)f=;J3SCbjxB zf-1V8Lp*^0cAVD^=&hSw@`>n^xE8a>KP7U#>7W{duEv1Jp{{>|#4Rmg7h@%!(<$Ho zxl1#LaJ#j%f-VFYRuOkdV#!6$ z19srW4hD`5_%M#l`Z9IV@lTAEhGw&x*qU3da|0CJn_9obik1aY%*~?pc9lPan4nil zCKg!u_At@aS^`fG&%`|X3gbEhStPJVZEXq0#KYTbiPfQ&>JH%>E=q(GPNE&mO(}l2 zF#>km_%!FWo$@yfp4M=2EV|aZk}arnm-wWjjc^&$BN|V58hbt}_K!%VvuAcr|$SLmFvX>+_`x zgA>MqedtnGVnjau)A^Z99n@rq?RAG{7wDOg#3_b+L;l^{`A)MkO#VsYaDmMD^C*it z8i_|8Ok?GLk@#vwj5Vzai63;v7xa9pb_i%#-q&LVG!+nol61ZzY{8J>SJ|i)mKc?= z3k!(E@C0)NI1;{PU{HlT0RbMKhCY3a5hZun^vpuaxlMHfJp}h8rKV~$74S`=igkKY zZ)&QHtP{{94VK|=!=Csrc{cPk^!06C^@h1y(U;;Zf=k!EdYO0P4 zbjV@|{X6>STQ$$+69_JJ9ZP{5%1-V(qqezXg%4BiI5ctKkx|8%#MA#|2`)6A# z;660lf^D)nFIq+J=^JQh9+V}F`n9Av#tFdA{@+nUl3K6-2t##JevV{Cn;%rXEn2jL z!T*Y-ElR$>;L!*`J0otHub)k6MQF>}|2Up7&nf_3VP0^7NPCAHF@7cZQY?@2)7c5U z7p0revXmw9y->ohd!?>z`-q;qbK1(sguaK{$emV%;_?k?`nvCys?E!}+lq?BY*44e zos0HiYYM`8608_=fkBKD86bz7Mkv38~yv77%cRQn`CbdX^ z%QF|^(gaC7;=g#H09|;1e(rRflA0jyIVf1Cls~L?rP#T|)HO~~5S5f9*+v1RzxB8+ zRN#=k+Q>i<5nXfVt@SCL=2C zqX};N03z9|u&&$4&}SZaz0rua(hfV+z{(TJT@TC<5CjHO=}SX$!C9$gcM`rhL@isT zD1IrUBX$u}NSK8xg)pu;m)6hOR4UiqoJzS*`@NGi(b|I{i@Th65fzq~$N~XURf*I0TTr`k$DkNE#n>-UW3VvYm-Ae4ALii`G5_aw42rt4xo+IX5a`mV#e6_# z;Hsgj_nb4DfjV)U9A15Kr&yClrM?g*@>%!$dd0%Zy(baQL-O~u)Y@`5DxxREnP~a( z4?zp>o|8s@Kf33{BX{zJdmMh4$!_Po2meFRqY;|r!Zv&nrMHF$@@UC-$Tc*Qck$2jt2vJ!_!nnW*d z#u4#v1zOV+w1iWqFw)+I&o7$)8eR_IYf&J$f24bZCy=e-Z3wCK-LmA813;q8Vwp~` zHM}tguSMa2PT>%B1}r+pgL*40HAKmy3i{v%<%O%0`jvdj3v?gLsbj-CD(mtU?V_hc zu0ovw?HXQsu+PEnUrA1&f@LS2b2L}pKt-09|P@tHjR&1KoK;91#mQEwHlfM z>q+;zDek^2M;L@_d;HYD)BZ1(n#1zv@c(xI?+V&gA{*Pff5B677$YFwazu*v^sxWF z#yO1?N&Oqip+iYu>spPWkTJyR^jDd*^()!ch{$-vwoWI zqLKt_fE8&LtNy0qxI`qJesF|KCHjs@&DaeJxm0%3r)l0X>4{S+!N`hX1PjY1A#>28 zxQ{sOVfn5WLbO0W*`y;7gmo<EpR&r@9}~*&yfI8%;`Dv z_B9XKYA#itOkcwKv;kW5p+u$gF#&QGk!*)>V2*(!XW)MfTEW2 z?#MV0MWiioGl!}_LBpNoL6d1!0=EzbHl36GW^6DhW;DzpqFqs)JMaL|O(i_OtQ-8) zpS}b2@HEm&UQ@jNg zqZ7TTPYL1?O*2G`RpQ4PAgnVb5GAql5!8mt>;7#u7~P--M!g;S=?~wad1JE=2nu8m zf|ew5-wF+vJQc^|%(xEOIC5h=7N3^gQn#rf+6{$EQc_o9@@4s*ky!_)(eun}U8lMO(2KWLK#%B>i1JwF1C|?Ev(LwFA0U+a zXsRFL{H70fh@}vX%ykr9L4r+i*2O>#`XV*ghb`kT6d!2$h)*pE8A6vmLHwf{0B!l- zygVL4Cu$3qzlZukpowaq9-FG5%6(%TYJ79K`V;8zTj(x#am3>XwDNCBJ-9o^H#txC zj%}D|E8%yyy3g%5lst3&_(N)#hH+tB!d-=GX|#!m%hMX{s=ZlMu{@+3%OI3%5uA_xZ@Kf|FB1diw~BPi|VxqBW0e4|v$(+jI7XSn#OEeOL^NT+&; z*Stb;86iUNwTN)QI#`B7OawM7Og(tUm-g~V`(MPeEl7oWa{w&EVeZm3(Oea%p?7I1 zk>uX@5PD%hkdrd71N465pCA}8W~YT(km(B=T~hA+Hsxe;u!~&F(*i^5nc-AJ!yk3*&V2(6}9s#gl@~*kW7* zKaPJ_Qu{&B&iz`M*AopSP2?xHl0DPMcCwk@K2anQ{`Tb_Tvr$R7cz@9ec^v@S!m&A z1&?noB$Aqq0ihT7I|u8T2P(&?7@*kDNV$;gj~ohAB-Ue_)_k7F(QVokn?#@l^%luA zl$Lww6R@F@D8k@XM9P>DTxjb25{_x-9^*+H zFw4d-U&NA)BnT09!q0ply@WWC#Yk#~%4>s8pNoRkFfh(Z>=| z9qX|xS5G3b+nTesW}EWb6GAKGrPX7?J0`i(KQvL_GhMgWti17aHgs0VRPNZMzDJ2w z@;6m$Q2GwtM!HywqrL}6I$5q>bSRce&-0!^SCOt)Ev?D|vrMv1omFlU- zM=77R$O*-<95G$#qNwfF4F?WK=CUEUo$e7#PRAlHxx6 zT^}Vw=A7~gIfI5Ton}I-SHAFTb#!xXz=9cxD0I1fKL{I_LkkMYMqLtl)sGuns!ek% zH2@ve)A>nf`??=SYQU9%`J0ADaJqi-N2;^pw$4Nm;=|hOaONmx-S*U z=GmaU2)A%JY2jvuxw|Y9FS`>*)xeJlZM$r2uzWw)Su4hc9(Q$5Tq$|Qc7?v_)EC07 z#V>6_HlPP>cgg)--2^YWo2_XrSh8Sqe$8f9$_;SR2nS>1QJ$L$)xh`!f1(DJqXXoj4M_w zyyMG?aMa_{AR%P)mfIhon$15GpJt zX~qT$5n3!>1#WpbZ4kBg6FuTiFQ9#Pa{S*zXS=~4Kf@e|4Vlx*uqZmB?XFD@e-h#I zte6?x2R5(+NrE;L;@mZ!IU4N%#Kop+d31h5WuBf^uN?^JU^5Aupr_+Dak-XV1uL%v^|G+bVm&BdfW==m3A9ymZ1_J=)AR z8zk8ywtrb=MTzcrG|0U!?PQy+bEsegf-jIvuC?1ihcch=XfjnwmuMoZF?+TJ^(3EF zymcst5ELS&dq_r?EOdXiePb#bX*V zdalr%!p;hejeuvr_+8}2#`l|Bsm8DoLRZkp0mFYPPQ0egQAthR7-ZT}p?Yc87_M@BVr1v`iLYU@ZFei({mfpz;96FfzE+!2-Rq^PG_IirqP_~MxVz6eba z7ik7ek4_LL3qx`%8N8>vZj**d9>bj9s-$p-HRqWZHsrXjEf|H#gOKD=BXvCWEYk9$ zUo!%#z5pWQwyYJ8sx{$wz(M?Pwnq^qHWlv_z+yUG~-%e5el}8Z(#tb@`$| z1Qd&JG)Jx$&$cWc2`{iosyGQsAP6Y-ueygP% zlj(fbkF3;&cN1o;5qf{xkNx#{lpKX3E)jH4IKsH6A{00dLjIPBR`Hpy!^9|ZP6Xgi zVAtuu#wS4%BT!H}pezS! zn006_qW+Gt156xgK2O|uin6(LRfSuoo#2yCqC`HMCNlgLXsT^_4Sm6%f*Icz<;2e{ zK0e`%D~=SO^`=n;D>oTHVEkb@tn#&rgWN%M$67BK)m%4(eYKm|?;XIdy5h}!*MlQG zv3_C6p_B+OT+rhb_`O58P)daJT-~0l0Y5y|M!+hL`cgFMndh*v9+-*q_YQtf9`4Kb zB0H`>a%UBXvL_FNlaFYtdz2FOIKmI~0&7w^Y!F1%M5<68u8&~X!?1v)TEGkk9!wuw z=7iyrZpe$kvZpaTYI4M}GnCpmtWFUzD!F@rO@v4YD=Fwutq=&R4K!+4{DoP7F>OwB zD`=(3?;vzFA|7X0P`vX}Nz)+Tx-v94i^j!%+0QPPUOGwISZ)cbD6L5(Fd#U993;`e z;mPGbk_@DxuqMWyv^lf!ip`5c*o3`KuoUbQzm9VHF9@KvY5@{do%VDs3r45>m0&G| zJIj|Rags2&5UdB3SavRh4w&wEsP-_Mg**|@bUlMt5aJw6PBG~7jP2B*)a+&5KY1m@ z$cu}@+>X2by4}BMY_uy`Dd7|#DSSYAiD7s}u2MAsJTb3fYb4Hw1INTg5$~EH6b-FE*S^Fn-s-~JhCdNpE~Aj;1R)+e`87q)k$7VWen6fUN<`2& zvuiFC7yHg0gTUSQdd-YC4ZFJb7LIQn1RGtmDnom0PfH`R+_x3jOx}5Fc(3$3C}V;M z1_?4PlW6r$k?IV+9b4=1%rrL+{}n`A`c*7Caqh>+p=I9grg|aDCio4ulaC8s`#Y2R$7%gI*;V zY}cL_i#p?kqf`1SJ zfV$hKgfqc@sBjVa|H0ac*$qrruV;4VF9dk+&cgyMkDH2XzY2raz!{pr8drf|2X_Al zy|q>(qxV>+Qrnm&LSDFZIM#xaBbWtVFeZIFuT7@KQu|Y>)N!vPv`^I+@zPk2PpqQi zZBF0}ZUWtP7_kwk*01VKX8z*bw0(HyFLoF@;_Fl@q*LDq>4ruOye%UO9Dv0@UAn68%))iE;+bJC9X2E>bFm_xLw~B2Md?Q)w z#fb#%h(t4D^47O?006HS=fRIDPq+gJfz#n*qN5vih#!_6wTc)>3c!(cS0ahhkj0K0 za7=jc#eWWZ%$2&K@S%K;t3xamf>i8!arkZUyTz4RsEo5F zx}Yt-u?i^5mPT+!2S;#GuF78wg$;`@Rf~8;eAMR+#)IHFE3fl0xvo>Q7~p1|m$NU+ z&$EG;YSC4VmDJl%scJaXS6@kJ_V#Zb%F%F)(q=*D40l>jX)}1+NzzY*fKgc^Y>C9! zO_8@o$j_)MhADSMB2``~ zu!cP{&ylT$0j!h(Sv^j9<0{S3M@B%9_WDF@XR?5zh63yM-xXxsu1{nD37;rd5-tzN zl9Slc5+QWMgSQevsHSGrCPx1cgIa7Sl4n`%miQ0~4<63WO0GPPh*<$)Qr-yn&KabI z;z#F8jt`-#n)cxcJvbt)yBuTo@0PB*YHApBtIii@wZqg5j<8Y2I?lRZcDl(xQOicpemy zt_y;@kruZ7V!vwDCO(8CIaezRPpxr7D>4)ho9L_)PlWhJyLaq&1Hf6_ov@D=xnl)z zXSO#Z&%@lfVZ;dS7_Hbn%Y0|B9H21eQ=%gv4?u_aQ2P_y3>>z)rTP6|Z#OVO++ALQ zTxnTje=Pb9d`#t+k=uJ?L9YR|O&bf_XlBAWG56& zmrTpZvz{rbW!1XD7Tp;fxjAjDCys}tS?$X5wbtlt2?$ydOJe##24?2Mnu3NL*jkYL zSr8mCw~g^1_kBizA~YOg)}TPqK{>*|&UqzY9mDq)#U-|xZ{!5wz!<+4{Iflj`yIx_ zV}NwAh?FC?5<3^11h$Ey&3wDdk(3}pZR}j|F}EguB#nf$PzC75Xw0pFi5*DCC`>E1 z<(EkjIKr<|E|Rf}Gg$zzKU(n~rH+m^^R11!HEqRlz%k9O%s|;lw_xf`G*c}4zmT`y zO%Iga6LO}I0fW;GnITNjO+(4utiSPON^}g_sGVN#nnX=qVXrNFD2NC$@brQTffE-( zP^rY&2x0}Dbi|`FIGtwt5Z6e+a*+8eiO|pv2OP|w+VIhec2L(3OErP1HMlR146Pd# z0Lu-&y8sjhg9x+dW zk)Rgx2xgXp@-J$ab^VfC1)_p7M-2Z61X0mr5)dFmG*P-JM|1i~!8EY!D=4`quKRE9 zi*4|zE3139>`?Ah5gx)8y3z2XU#|B@hN9MniliT}Xpmi2_x}CUO*W2#l(z04B)&3@ z$wA#Gprz%3)UQJhp#7$m}BsI1@a%R!Bw={$wGGD{zGn zMaZi1xHcWEW0KbdM4jAL|#4HRE5ju2YP=QC!5i8hY&c>YP=rv$O>N;BO=%mq^b{Z_XC||LBbacSu8o z6Okw)_Zp9NZlfnm(KHj$PtXDz&aUJ~lp>zFPD3=zol?jj+c4O=-D?^ou)r|*Ro*6- zQPaeHzxjh}Rj=>oiU&5^`uulk>`wT-N)s@5ifpo5m{MZs`@j9^uNR#vgg40a(L7LT zQagseYMtN=)aR~Bp;BG)-kWso<%`sif6v3N4x99mBC7vQ9+Max9XDm2JxwI9wPWnt z^#jptHeI%z5VrS2vxU@lFA}vO#L*6JYo_ZT%v%-9a6-_us(E30^obOCK5qD-Lbw*U z8n(j?0B_s@OmnhTG-Hyr)Z)ss{(zd9E zkslaMhffF-fMHhSR1>3!kut*M^GogzDNFZq?U^?%=AdA|Hi6!pu%G;bE0%iGC%wfU zE(9`{1o18cw+a!es6Lb9|)B5PB9gb^BOdtOW9JspZ&nd(=ND9JYr%9k&5NK#BY+0DCnl2?5}fy3YcUS{;{ zR2fsa*|?B7AyLZsB#}fLk_MTM4sH4kLp`yf&lIF!K#cxdf#b8^Aknmp520yncV`-9 zOia09MeZXc$#>O)$VUj8I#tI{-NPi*U4VrI7M=Ofz#Ye^&s@nZ{htxYz)l$>#22Zt zi^)I|$kyQ%SZZhXrDKqGicA<|Ak~HtvLCmJD#o<{?pA{-Rd{jD`7<506+4x|129y+ zmyzF2WV$ZDyr1xN%Fjno5}GK zawRGl57LgqO*#CIuT>753Y*iIZ@#dfLl3nkJ@+M6y?*L>DMbqE)CmgBHgny$A-HY{ zvg_A}-*%NqdaOLvZ`gb=5y)4AsmScI$ISfB!e^N0sL#SCDU9_yxoYc?p^1gzNLVA4 zHU{e&$YXTkp(P8Q%1O{RO$`|;ESkaHtHA{8bPHb46ub5L&Xks;zC6^Xc@Fs2IwX}n;t z`xzR?2AMfwdCxMhqG6ryZWayP&kDPd@&Z9YvP#*VF08{C$`BMETKIyl37WT-5)tm_ zC~{q20WkjTgXJ9h#TXfgEh^_$k%RKwk~V2#;)bzDw`h5by^$A@^#fNFfGLs7l3Wp2 zh!D?s`8_A)c)_Rd#GxP}X@06x@J&k@Y}0)=oJ~vX%deTjR70U5kw^DlFRp$oX=}c1 zH+@XFX%@Sq(VQ$ejt=Kc5s7X_o=QQN=!+P8dNSQw!iH9o!Of0KA<)!3IDLl2Q6u9$ z2{~moA23tX3VX|xG<~w@R?e&@%@Q+N*ApyTB!#7jSfC)gUStL(@{RT70NXOQ1JG|j zX@YWK94;>x&_09qXc0GUQdNPNNz>j5*vu2E4(tLcO(r0;^STS#` zxwcT!Xo1+uX-Ifsr!N@FRw`tMl||?ig!cY=zwNAs!dPhUKdW~Zl`^rj22pQu**Gc+ zOBh$W9#+bu7ZzdxaYR47H92*{7%*1ds48Jy&>xT?nZA3?my4k*37M$}s|=rpK&GE> zog?td)q%;7X|=>!R0(D&5eX9setW`ahB{AGS3DvRB=}5AdK{gdcSNqJg;Z*Bs>FHK z5fn3*p)M`9E?t_T|M7vTo(6)CKA@pjL?DB0HCp@HK$94mXZz?do%Y6vD9` zFV!H#P&OwzX=QMrx~X6c8runtA(-So*=Z(drZB^lPwISS1h`_oSkeGL)V($V>F~i4y#9fPib>7omvQ#P%19#CM zY%OTiv*-Nh(66rdkJ$0dt_}robPEuP0zcxVjx_ljBt-40E!`#E?mle!0LbC@$gh3c zEkh;q(DyQzi!^F)zcXnIN%^Y< z+j}gEQ?5xhKRwZpAkes#ey4P=6g19IB{#~WGC}Voh0i*0W|QyRsEvx-ri|hYuzi-& zk`q@!s(yX56*P+CxM8;DJ~hdU+Q*Y?W9&y>f%C7Yu^KxK*sy6f3JW!D(sC>ryNi zP5046jd^bl7V}(*6=lWB7)z{bD=;<(Dq@<yZ%S62DMHT@Rr7fH!3KcxVu2VVW`6TpVQ zy_j}r5EC#=su(oT+k2IkrJZl;kTUu#NCvoWst~f$ci25d?2&6S{xs%Kr(rdIFEdMv z5D+~Z7q595=j^!Gx5W!87&^0cIdP({TA)GgG>P!aBT~@%0A}CFgA0PhoutpHql?$E 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%0AAW#-#^KaP-1Z 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

J^I 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]NWZBtAS*s&oh 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 z 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%0Dvbp^M_-EmKk9xBn`$hrr&6Y!0;gLDzZ-Y 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∨lpc(d-Qr$g~;f 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�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

2_%kAJWQ5X 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 z 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

&oV+9D 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

hxo0W7&EhfBC!;0_lgm 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%0DmA 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

b->2)O}f&lhB6Ir61dZma}gF#GIGGz02{{_ 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
  • 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

    9c-zjmXKdu1hx%>-laM#>xVcVY+()B+ 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

  • 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%0Aow7$()4A3 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
    yxS+7ARhyLf=Qm#mDqr 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℞IJs~rrO9~?4V_{g01Wbf+T4Q zfY3^bjE6G-(i18fK5Y?<02ZFqB+H(pvU3m>m=dJ0Gw@}WUX9=alqCQI4J}DQXwwe_ zK}|Tt4Tv=-x&$$itHgESiT(s7EJ^}Zh{x{el*HbIQbV)gx)vlOK$ zdQ=#V_7o#|^?Yp3W=eAZbCot^|EM$86e&6?hDM_x7DDzw$hu-iL%-AQ+Vs?0%;Q+H zDa+Yx{etsF;sM(EH)Lyx(k#fNO}2$ zSqD}0sW1?9|62qqUh?wotk8i_{DHuCf5-i(G$QJ4pp&^QvY)^8QNbksdro^AUb5BF(`N0mu6LEi3U`eNV&en$h!&{3_Hs91sE0NsiXkhM4pnfilWr0rjDSbd1aG7O0@3nvps8BQcQx&a9q@chssHL;F6h&A@|ItSO-=1k zjXT{3prrdS=F;UlJ%l5rszJ7ap%3F^Y*vx#THZk`S=`gXM8Zsvuz3uF2V?CUat2;{tgkyc&}5w`XZmHT2pi6~qB^7%_BBcu|`?^uYDEk^n-tn zn7xVt`l|l7H1FNz8ObnN`5~7qp&+XpC>xAZU=ohG4;Du(VOuHEu*nbv?~iURi)Lf| z-ouc*C$Y@ydk-{m)s_bA;QhJFCLZ0B!(6%C!d(?sxA0lJKjs4q7{J%BSY5W2scA@@ z8k7V}2m8+wkU7>3=^yDOb>z4+Zd{e(cW-6?Q;I8124h3i;7etkV{cUkymvDDV6E2k zKDeB7_>3{s`vYFkmYGWw|Kk^}1Y;e5J;p=vcmi*@PecDT#|GjDLzp{UYxa&^u}^j; zj--}>b+)yP)$GM%(~a+#La8y7)~7T9$YZRAn8HUZE{<}nFli9p$BeNEQ#~(}Uaw$G zaJxLjLbT43zge!p1rHC9xpGUbbhOLaQFBLr;i8qOL}OP2h3ppN&#t>aJ$gn*1MDUM zCK_f$0F&JO|Nnn||NsC0|NsC0|Ns9V0000GqPFa8PgL_EYm}|NjaBQvg%|Q~)>94d#0;7Y=ZHN*9TV8S$BOtTqncytMM+ssT4*w*C3Sj>lmot970(41atYJRBpHNQP*0L0d<|FN+dMKMU; zpYxoXn^{VVNun%l=$VR2i`PDR&#M(SFE(9>&RJP`aol#NaJ47$LwzI8K6qb3mX81Dws9Bf}y9HjyDOa#pXj zPnHT@3To2!A`=S_8P9ajbP2j0{|IRmiXIwAj>y<^I{VU8s*CG#OdL2bEa%kF$K#_(H$@U-Zi3TZ!?GwbQJ21&!Mn!W-A5xEXKJ0gpcKW12b(!--4P;xdO!`a8$$@Fu zZ_-`JGm^>d?Qp#N%EHojt)ej`3a%!Yd3MCS#jFpqKy^c~n z-VjV2LNBTV{}8k!krMKbND!ITmF$52leY`xBN2g8WbTRsX>670-9-o^MW1hVZ^>X3 zAG=MRF`i>Kb$m=LG?%r(*ymKA!zquagrRaFQv14cXvWuVDCB#Sy}}WiTn%JU!6T&x zHy~xC*g#_-QPxVm8BqQ-rS3p6VPZ}_8Sv*d<*pq`CK{=j$-l71-bnVU*Vo*_^O|ft zmvUI@pLRocmZSpV+2ol^pMuWc`b<{rrZ>cJB?q8m(r@x0_^^voddpye;b^iPCC#Xx zv>#)^)E(&pEdz-Jh7(~wTdME-==a`7;E6GUipo;zgm3r0Orh|+;tA7*5VRtf+-#r# zrh45-fZWXky4NH=OW+uGX%tPidJMx;IcFP_@Jfb`mKoHjA9d`S*=t^~a>aC450K>H z)i~#v0{JCHGD%YXK*GH|wR^v4Gn$C#;sC$^!~nL^YCM`<+8txMubgGQ0T!Z|Q)0gp zSO?sOs1fRE3aX`gdYVd_QSRick324Hi@DsrC^%r=3r+0pKY$Z((C0zv zN1uBNUDrsSC&k`KUjYgn9sxmiI03H%RT8jK0K_1QX4PY+XT=YKni?WtkED`mo*Ir7D@!Ql}QHR*BW z*BO?uJi@F>nkQFd9(zvBKlYZ9Ct+svcWy$Pg-&btj_m`>G+oa#F42|8!aV-EXO#UflP4kK5Ka>(pC7U9xfKHIx}hwaym z32RktYkDd32N?2kJv$1kefM)=-;+-Z*KavLH_N*?r240gdHvihqnps{^itoz5cTNS z*@07*G7Z`E7qQVE9jZSlXZ7nBB5kva+W1JOFfES(O-XU^Ca#fPuX8qj1YT$2;ovXXSkZ-Cn^u5GzOhimpM1O#e zE7sDXCr7zS_P)JGZxsC?q0s9VbD5sHG7Jxx!-C|ZFt~&FdqC2Ex6oT3&A@<_$=72bF7>X0fvk43(US3 zgs$h-I@+yArlhK*Dh;?Xp5Zvw$}j_686I^(A5~}Bvs733?cFrfGJRi_6U9NJ-6+p| zeP`LnGq!}!AWT=gS?5}Emn+svPc%48eEiSZ-fff5{~0@JT?l%twREG5qANGm6+PqK zX`X5ig^#FHtTly_r2RYd5 z)!|Q1H=(Rp(k;1^N6$%U-426?WP}8O3W`7Re*-H(03!_pgnXl}WMEB6Z(mqy)i-SX z^i(%a{e(JblCn-ElkQW+Ha7LiJYaEca#*J(!!DZ-*_ z|DVxmWWl|`0%M=wILYG~{AUI$&*2GwynGsdV1zq?8nL1SsGZY+4upU<>4DocANM}Q zX37Bl#1ZhIK>@licamNh04YXb%0ienEhztiI3rLZ*;4Rqh`&$%^`VB-uP$?w*IWU~ z^-<~_OL&q34bBz<^-!BGi>G`9F3E5-m=kelQ9`(i7!RH~yIk|WJI}bqt#Hq5;1*%Y zx%q9M)O(sD7N(~{3&h+C4!jeO?UKN>zyHRj>l{2!0S87C5OWe+F)lsyLFj5A@+B3C z)b4Qjr8t2hRkmM_irB$JxMLxv9CiA+Oh*PD_j|YwB_j)k#1~Yd=`(?Uw_T{2DwSFZ zvlTw`B2WS^2AG2-(ea+tODsH{PsfgP#4)vl(#P3{@3g%xA-;OH;zfZc6$)JT=6n}K zT(a(I@}{egBK}K$%egyQ8~@$DQ*KD)X(t;&TxJJF!3F2Uizoqu6gurZ^2zM8c6W`{ zwy`a4rUj!@HV}pUoS^L9$tmnzaJ|uw!E8=pBc^fmSJXrBNZ#mDZb5C&J}mtd+BoUX zW<mv4;)EuBh6_WtF(mjBWb#v+(3MD(ml1CSb}qS2=Ey%r4Oa zR9!Euw}PfHs6s6OCx?@Ndr6ZQ&tpZ^?}mO%4(w3*z<3v?f^O_ap{n>-5q<6QJ#@d?Y$K!$eq31wU)Fv|b3upam$(N%GPNT8R(65C4B zdj<^8O#3M%)+f|ptgC?uOnR`yzmab`Q6Rl92;sz6CX7w685YT8ehDH>3kfpMoY}g= zH@FFO2|x5@69g=zUrk`*Zta!4^jks;58YY8JnNyx&KL#(S$@(PO&*$&xJk3>9CR;<0?cOlYzjAYysEHChb6ws1}eS?b0W?^xg?%g#h`3e3~q#W zrcG3RU=BIApc<h2!*3XPIa0>--Wy?nw+pYph+BJkKnER)_SRv$t`{vdWPsg2u{wC?XpJ1$o@Ni;ftJ1A6=TtO^KgJ)4fc<=728u*V>_J zbBfiln-Y56NF?`=ln&1yXvK-0h8QGJY3}tZ7=hVlzdY}(gU|H z2MP9ldff&A8D+CstS_xbz5{#4{0m<)uOt|sXt~CejYsvczXtTG4%H17HZ^<$#O$aC z_R9FU5t1k_uT}6PLH_{yJc@c`VI+Z$Fvx{Dc^Joq? z;|zPh@G&qR#b&cmC$~JoCRc$Mb8eelj=gZ(+@+%T;%uN9TjkP?HF4Y87$?p$hSHIW z;%J#YcS0yK!>sl}+DRXl&)g>^d6eOTtPJ};tZ=@Io8z; zG!!eN+(PG}f94)BfY$+Va<${HFIGnVc;vB6wyyd?1+_IV00Qs1i9OBYrKFrC*NnsR zUfOQU6E)YlrcFK`WG7Bjp?=Uo24fodn(zaFBdrT$n5yQNjW}u}voW+(zTvbH&Lr^qBpORi zg-$Jp*LXh%vEkZo1GC^~V(ATGw$Ynj)dJ3w-4_+|55lH6tO`f#{>TigoODp~x%`h( z_GSkz@OuliHGfOOb*2S`Gtm4c^VI&~MiL!xtm1>au+#Iaa`ZDWLhg~M$Jm^FUVR>M zIvFtWlH->$dUO@-l{2nTebM@&(grYdJh~!m$?4ve4_r^({z_{2;1(I#)OspXaS`P) zh<9oa82qLEsD1HU@)t-ME-YVzoa?%zY!VCDj?;abU`zGyMdP9v<&a-7JYD~aA-WU| z-Bx!N@ST{xnE}~Nnl1{t3u+;<_M_{fJ%C{WBNr*WVLt_r_f@9JMhuC7O6Kr`5WOYS z!j8Vz{{bvC#Oh0m><=^|S;MlL6W?isZHq`~c)LH<2Uo_R5r zG(RJ2yhf^C5M3Os{n`W}G{S+s#?hEBM~mpu&by{Jk5#h!>7#5>0L3YI81OwV`-X)Y zf}M-SvFcr(K!;CnIzho(W$gEx{|yK=E+-P#b|@xlxOp)m0ySUd;H6rd2v-vPDokJDwhscS=;BjXJ%FJ= zEK*9hkhPrRUFhld1{eIcOK*rfcsxta$yAGVM)P}x7OOKZgbF*iNls-EK#1M+m;bc> z(hP=kRXsNjLF-Q|V5m(v03;L#0K!lDgO?3J+aB+U48j+2+E>`%v64xMyf>>g>Puvm zla%;7nK8*jc-u84UV<|J&lqoO+Ob?mAX+{A=RPJ`3C5PT=~u(po#FEwNvSHaS|~9Y zeu-iNAM&ZdAWVYrF`<1;y1{U&7KRm>KpQY7SAi%dfQ!L&BFf}8U%uId$YPWGV8Ui< z7LX>ntgj}{J$u0fxa}pfRRhevt5sp2WYyk(%?TCl9_!6FQ0iI<@(aSQ-eM*Dr?CawXo=eUOyMURVB2{OA|sqKicTyccFwN0gYM%#kG(T0)U>hRqz43^Y6H7nL;v zoT#Z#lTd5A_D2y%I9E6X>TdRAHK8wvVr{dV{w{5~@0N!LG9+Nkjb5Z4P*Q~*)_{3* z-s~kOWN8UvFlBcvt7Mge@8(ucC~5190>cAR4Jy`$ju)FB$%;V$=>WtjVj^w(2Iwmj z1TZx#D>S<=Y;!2Zw26U7KAj?t&H{cT2Vf3t(-xsqvO34kk^ntfRVyI1Z44DgLYWj= z0Wr%WChk31NI!^0D4`{+N|)J1ZbKJ+=BS@H4#r{eKJ3SIFWd$B0G%wdc8F_WvItK2 zhW!?BVKpjfjF*~Aq=mYs?CX_xDKe|-XLQU36cFS9`d5(^m2e2W-TMa2jJ#U2a(AJU z3u*!Hx37f$bXE_A0J&lq3aGCi0$H9ZTzzskqynfM4Qph}pABy?7tC;-6eyBk+>?ew)pJYg?_IGO7&q&oOp6}WkApYt4JEY}Vs%ywel*-WD z5W9OqfY#Uoq|lk<*un-_#z1-tp)mz%!E{wn+~lPf9T|Cv1eb1N22N|LXOZqXZJvV- zq9#7^I{aHhY>-hg_B{ClwTiEt>#$;ChWW0IgM2zhhc71lO3ju z3y-OOiP9$o9Tysm;~3)SJI~y3RuEBxN-SAHfD}&(RrOGM~Jy(d;e|#pwc1sO&_dEwr{==0Lrxl0CM6R@=iQ`DvR(0CdmbI0o|CH=MeNe&TD-(5P%s{vHZKK0 zo`M|mD*5ZH2QGs8zN7cP2o0+|B8DObg-zL7Sx;zn1jzRv|)JU|%;`}Njk zGMTrsf-@*HP_~}4p`Cz@Gkw{cCQ){z}AzX1QFg~un1X)BdtF~3M>sX3guEWf|wORXXY$@WoFvA5LV03EeK?YHW zctN0s9v?=Iau=qTAu*UtK{DZq5+xj(aMaKf481}XVsE%ItvsP6n?MGx932%{4qJl8 z@RUJ811BJiB?NFn*a|=}=$PRE8o1c-aY(a+4r*>hM1Ta?5*(~);fXP%uweyK7A$N* z?re&P=Jp=V=hma{Bx#CRRmYxF+;uET>o9H-VI0vRv(<%;mMt2K!(n|kVjS7JU}%Ca zLKE+u?`Di6Nw)l;po`g89`VoT9kZMX}W5r+zoQ3I;!Jev}s5%(mKQw$-+N`MzwcZJoi}EIej>CoD@igD(sXzQuCw z(D+lVRuR4pKC4v#t|h=vGYa1bH8?GcD@cvsgm(*{3ZD%wLw9fMbs4eoaWQ-^+~7lB z_qH{<_KhEv!`}=z_>!-CTbwSAF^*H==>fQkUMC{j*mp-B@fQy z8)=~)oT$e~(p5wx)p502+oyJT4;Ig3dfNHCPNYo4+|$e0xwSfk1V2CV#;&Ex`hYUq%+zuvxHXzo?bC9c$|usbMT@mQ?M%R! zPZB62I`Z?Q<&RofRgWoMD4py4D5*M@Bu+*2OuHENtmUhw+994OA+Xi*M?y-V5O~A~ zoDG@S3mXkio`vRKD5molwV?-w4a!L!`XvA$sXr>0 z^O&mViDfp{Ne6+%m=BN!%1TR1ED-+r@lpEuIkJ>-4$XK6aH2BzLMiS8`YD{v%W&lma;5NC+sbQY?uGig zj@FL=-sYLn9EM)hBdJR@qnHd!AEs*7SyV@H+@aniiT1upSygAPSlpCP2l2i_t^=G0 z1`hm~7Pf7Bv}|ij+d$Z7N@`^S0?{%U12GUD(*(`Zsw+0jwo-8%-Mv%+X#P^cn$V|P zsq7BbcrMg3;7P&&*(m^qt@tXen%b^-G(}yW9jhH@GuwhvFD2Ehl;|8c2u}m91Kff6 zs^e~+s@ATfCbu?oP3x>3(E{XG*1qXD;r6KdGcEP@UF;c$u0<`D6wKb$Ns7y9$W94Zksd}( zPCl4%nM8I!T}HHSvmq!oqoWy8t;R_%&qH80q-#qng{J7`Xx)M|H@mJEr3e?0-K@*z zj5o!_0U(KtuNocTfZ1UJT7f_)^B^QL{tz-l6u?Z(lO;_WKxr{W2~UHnycLi|LxLb& zGjds8WxjOK{b&j5B7_(i(hT`viG&bR*Z~6zGi2E?1_vs7z)~Yah@l}~Fd~W)dyN!P z+^zs33pD=*hRvBW^F)nUCWf*-mZs-~4R13*@{PI-d0dEZp+A9n7BBDU_Y*ds6&^R2h7Q$#-P zn(+gk6_o=@xD(jDv{lK!@e0F}G|v%lpUIxjOpZCH>gY=^(r8*$G8=0H(VnJGRA@b2 z6`ZgE30z%G^nkIRFI-LZKv4nxY2{mQ>$lznw5>pG8f%Z55P?^$5}iE=mT+F?UYd?B zrzKy=7E#B-+(~WXY?5U-;xlip{aD6$qCyc36CM;MNMCqNVBs{xkT*LOOvKr|)ROJQ zy47r~i8IbIQir6tGl^!soo*PO7uPv6V|Q-xRQKtN-+ZC53zFql(QzWLduvhx?7Uot z;9_0gnmd|I2F7jdjNCAOV+W+`3fGRNsD(4-aO|iqz-7+k#$DVbCNWV0Teysi=jGU; zUo8l>>_u;us!8q<)!igD>dkZX z_H^uM7CRo%jMK4?znJ?Vbu`^<&4Yxj1G!?}VtG(l0o*8GnGwSo1c2VzA_wGwf zV!Sarg5kIe0lOX@&gxfd70$P|wKj?|ZRRml$24c0bLi1fOtp%NW(ZQD!uG7>Xl5{M zV6X|pcgX1M)-X)U@W!wa!*A$z?2>X?B(e5pjNw(f-0Q@|WyE90q|<3wim57=UJbPm zGJKEev6F>mb&N|L8&y-(+WXSy6NkC|&X{H_MDukHZ|Jl^6qlGiGOJBMjpw z?@1ZHQ@~D2XIohZu&6a2H&(qUGu+9?4g#H6TgO;w`p~PRNaKOBb3Ws(XSfjqcDOB8 z=t-(3Sz@xAs>k98UIu9Rlb0X6EnUbHqwz#RA^mL61a?isedrF@-Ev@?(NxT;iK^i) zH3W+mm*$1PTS1j!yG=jxZ<9Yg?R038lvlwKyXu`5trUh4bU zrP0m|;VBeL`!M%0iQ|tMj69MJqeh73H${kz8%0P->7E6P@E?N3sPd_N zp1JY8`>_ZXlrW)y5b)$hldx&LmKlz)Qt1_sOj~3PhJPyDcJsUK@yN~lGkA&<4=@C0 zfsa*>LKZY>3M13uO$K^)Ku|(o_}Ee+hnD~NDJaQt;RRO7^q)jwySq?8_@=?(i1P;H zU@1ZB9tmLX|d*JXPi}9cD3fh{nh@kNerTwthrC` zr1(8;+{>aL&iJANjUd52x}N6XhC+-I&wepiInd{n-13(^P`TJ_<7zPFU(!67Xfa#? z^oYR}Lgxz>RWN-Q5y>%-yt^Z~_xyaY^THoTt>W*}+=G4hb@%DblD|D0gbKEESk;M@ zFJ?%o(QKOzQNmL%?$VrdFYh&2#68{(iQtD*z0|p!w+CN`x^Qmw;+%@ekM^lxXz}HH z8FE;mGxC(K8yw?pY>r&~1X7Y$c6jn|VT@1NBn*jqeadz%kU_x@P;E74Qsu*s+}Wf zhyo8$sS6lMuR>MWN4yB!YXe^`y4|J{p4xWg#Sal1o6e>t{wq(u5J2ASm?f6802R}=}b<# zR83ihK1iu#NCVT+M8;hYo7|88o`)o1iBnvD?<@^FSnS2K_YPtRC*%6GUeCr@tZ5E3SFDsc}I8wm9H!%Q;eR}Tt zFP%i?4&emwvXb@K`9Mx0c?>4@gV}BdAEF(V?kZnF?K4H^9e#5lV|Fp9!Q*+xj}Va2 zQ?s&DdLC)5sgAuUc7xc&T*+1CynyVEDzV!mtD&{sov#F3DDvti$tO>XT^&;tSJJuNPdK);;6%wm$N@xpB4qxI&wKWw zc!FQ*+Vs&fexW-BU0y`J+puNef&b9jTGJ>9vgxrB%cH0Am>kn`Ck&xP0OAE6dPE=~ zDZ2``RoRO+J+T4RdRV5!1};&V;cA~>xR4D$tKe#jHrPQtGV4IvKb*&Pz)N6_0V2@d z-#)!rSl!n^#OB51#7M}aMh^S@S47ic!)80iMQ?RDTCGV;n9ufTc}vLkd!qIWE0d|D zq$dSSYSJYqbL}i-=8!Ioml2~BIyGVoC9GlkISklBrPnw{Od(X>lv*n?neQ%+1DXm)i2{wn9Qto)AAHcAS=a$ z((ow0>J1w%a4$H>sv7jGL(?e<)?f-FAKFM5(R4oe$>sxbTrvR2%izft72?vE5sz+9&a(v^#U$!rM7WF9-M@#4Q6~dyX^p+3?!ahEZ%SGztNWEfrDAvG zNjS-#jHqPdw9HeIjA|~N;5`B2EQlfc`Ze5ZYk+d;bFJz``6ac_owK$>qH|mt=z?Wu z(}4r=>w^cu?B&JDPHSD4#82GW5qtWQHegJl4eG#**#(_ixA0RV5zShevW`(WjO5+lV+fu**5HD3B3RIqB0Er z?f&E@`{~Q~eyc-5T=*%%vY5@0q}b(U(me5b$*CIv1iC{)Vi=S%^R<&BJ3zuB(Oao9 zhN2QN-ungR+5wU`i41Kp!+hEUQ!ZW;aV*z3_Jn`wzKVG6!LUPC&L zZoz6=@$@_2V^|{Li9DWmD&Yl)aPlInopC3f@Ud?S8RVb<&+-N!;SLHg@TCi?bqh8= zRDuVd6Bu?hV01w2ii9@=O+uxn&X?s`n7o}VlK>` zU(M$!nWLI?YG5_&uq;qd=zUZx-G}JB;AnEW^H4<`=v{wQfG2&QQRqOL4PraHc}mxc z%b3P_ajiI!8W2a8gH74B7n+UV^pT-$Ao#u*wB1~&g>{l&IYcxiXVI-!crmIf4oae; zLGpXoha<#1IT!pbol^Q#0U(QGUW|0APX~ew1Yo#h6m~MyOOP;DBLH00+`BTq;sxD@ zuX&0g7BssgET9HICA7I1d`r+%qap&b()n)+JJLPvsGxyw2|6A`C{KcaLh2l}PGwkt z!%#HrcYhHoV0Z8!0>n~*IF)9V$0R~+yluep3*Qxgk}VuJ4hX}S2?zXBSc2%+FH%&z zmLMmLWd{B_7qoRb0sFe)vw4M;A`&_y68kzw4%2M;A^JzyK}C(Wv8iX?^K|mT@w+hN z(TCb$4O#hBf9>LsHQRF$1$fZL5C0-RnZE^=kRG~4DIx&7j{~q2$yxs zN51kZr%MnGf>?0-jwQc6d@hhB^HAwEh80uxUK2kenJwF<*)w?-uoX-Uk@7cA0hGCNPOq`V)e)9d`M`5a!JBtFALfis4OQ_6Ux4^ zy8UC|$@4LiM25^LE&i|Tq0X>Mjga`%^N4+GQkv(ZVL!IOL)v~5fFv*jw9QtQ;ayc5 zF5Ou~$S;P>B8=RcD+cSXSC$N(?-8L@5I0`tPB6r+9xOAF%_{1pbYQB$Xo?!MxHEz0 zM>ZvN9IFfV;JJqGGVaWH3SV_L{yxV>PDxrJLRQZvXZCmgT^!ShB^Kipg|C}!-TYbm z21dijpsl2?GM5zMMt8{7HEH)jB!N{`*eZ{LVAeKl6%RYAYFLcb z05a?(e`qWqc#Z~R_5Ory3naR6F;>o!)jHFIuu-o(e!`)R_C!DYG0%{Y9l&(fZ{BMe z;{j7TJ`YYzIpk)0tPq6PHC<%-BX zAvIbk&Hz~-z-KK4cc>LyA5Dp~Th8@?|KD|Y)NeqE^N!5=aHZ(Ab;)3lwCm$rvOb{A zX%ZR}zU$ZGd~EtoT@vc*)8E+zo#)A6=!l2{2;u%mPpyw^7qAtCejTQPiHS0y9SI?_ z9TD{<^GVj!g}J-pk3dR|tgv-k-uHj_CJa}qiC1@Rosz4sRs^gt zHx`95c<#ZohxM{Aj#MuEMVzw)%5u5e8SyfVrc&CGDr#}?Mu_B<83RoSpj6(|!BA-b z?^64UHPL9z%-qQ|E*{zyIz%hPzNM)-O z!UfZd_SUSwbe)pRl`L`Pf5KBui6>-hC;xXrRRd6f9^1Ag$PzmhxJtO1tyzEq2mf+@ zlG~#8otFh*INhENf-6iv*-dEMnLr8V&|wd~QZA&IksNq}BQda@Wk6SJBl_Hf9YDOS zz-(b3yAq&dW>u|y`&UV!p?K!i93{&C>kMXGJ=Lxd*BddQJLxb1NpgeZ=X>jqwzWVe zX&V(5EcaQz+X_Z{* zSL3Xu4dzn$jQ(*96Ms1v%V zg1cpWOJu;cDhvY`l&fsnMwz|nQI(Wnxu9nGeemA_rb6|213=6) z!$EDU?XfQ*_9-Lc#boJMzOSAh84OF3bw-v<(PSFCb%5t>JKk|ML7HiM&5aX&zk46k zyTgt8afJ_TUbTa)O657Z%K${22ZomoFe7M8tQwTRxe3)H{Kmva<4QqU7GEqk8*tz3 z%@WM&@T@^LDu}K$=ViHoFYeNcH)X<4Na#%jbgmxx$+&Jlk)pnC)=%E(&XAh!LG=oE zFv~3AMtNX(QgZi}#uS3>#|}$#o=T4qknP9LB)K`Onh@B(wqVtmSec{_Snlat^#i`w z$&~`tYze4F1XHp*mR^^%OTl&EJWbdYE9OJlcQ4Rrk-;mRRwY9S6>(74cFY6^XWc;Q zX%&k`%RwiY(v$OJ#%NWEds*Y;8C@g{-TsCG4E%;i<-Nb=XLPl*$S&5@+WbE82- zNFIS%iZr%c?W)DqLEdpOV#bdi7o&BN06c#ZVFbCDw$D4$*R6`4fz7k6W=^L|;@&W= zZ*UdUB0*V#Nf_vspRcV5ZyfU?Wv$kkY=%=Qks18ZfB!w*tR14P3Gw&0`8wmnuxg%` ze0VKp&}ED+a2%an`67FXI9_%r<->Qz0nae* zus%5gv>%R74bmTOSVWchCvXH(O=azUqDJ4IN~XP#b#@lq-&80fmGdE5BI!Z$yly8+ znj9D18V@F|js&+SUi8!ByjL2SmCxyy$F=G-u4k$kK~K@`eH?(VWTlBFn{j|UNFE*L zQ#SV^k1>IFh8TJ{thwdu5dL&)D5Iw>taPc4Pm8;C=^i!LvDN_y_#L^rvR!at5Xs>I zx@|*hVvRbYw8go2M2PT0>1)l{^Hl;tS>kB{4zek2X@WO%N~4ICyg1J~H5pe|cLetX z2)H*Od7v?vtRi0!iF@|a<49GE`Wu-ld1sp{24G~?LAldtiKny4NQXOMV1aX3nHJ8z zfPk_?ZI%xb@mO1?*PLvzccz*u1-4^97(*@H8PcOsWs^1&u|rTC^j9#6Fi5lRRT`-ZSE z-UI>r#3{O2c*rNPY$Cod6pacUAn*6q29Yh&9j45}!?BnsGX{_;HP{vh2Nnd)n!{oM!_^Qe z`nEqb0wP@leme|}8(g5MmX5hPkf&VD?r2~~|1QyS+sEuDEK=xi#gu?np35I z;SWlfS-an^qC!AjQK+{BUgRX?4yAL1{1I0^%U6Stx?YR?kt4+}oY}vzDnIENjY7{` zL0DHTBH3|=I8R3HDiNS#XC$y;L^*uUrpZ*XYioJlYw;$G>mJ2hwmOcBPL+XS(lb@k zkjX|63+4tOk2%^z9bM^jih_@XALh`yW-9|56RGGwt^^((UcC++KVH)t^_ebvox*c| zf1uI8=?T^E?aNm=^6~A!c!NXA7(%4h21sgtb6VHSzI#(f(BMKHcnyf?5+R{<06YQr zIP?MZt)aP-w_Ukwx9JqnBMuufYr=o;CJtte+FSedZ22fedMawLqB_{75Yi&mqQP4( zd#Tx&)=G6m$)q~W2(Wk%F{>G#kBgj2)B6hdKQ*ZbRyo)2vuH)%lHsRU`LwMZZfXnd zUGEr_lt|+@ICKWw4=-^D`B{ztlsWYElzMEt($665yp^!Edfy=%x*PK&lELQbZ95s8(7VgpgD>)#cg(^{Y#DXn>xLx!qV4=YUAz5;(`k z^g6yqf$qM|`sTYBF=z30T_RR@M0-b+!Yhb_gbh31@S(Q1({ZAA6j-mjqX0obzQ5O1 zssVt7>|e*q5U^KA5N`S(Qlmub(m}5lL8$8hUF{a1uaQ=;*1JQw!H&=&64b)-gD|MB zF2tkyx7qkjL38R)G;YzWr);KWoc~*(6CDqQ@7h~)LBU$5c|}@vpzeP6S~LWMZ}#sh zl!DOKt>Z_)wjGH@bQ-dwNf;jHSw0u0cdFgxmA#2k_pXPeF5(m;{%>O=dI4ik@nT>S zU~>&gH<{@T`#r_)p3(Ib{{$bm*1D>S$l{omY(80MZxb2%%s=MsFbkg}yKnboT58$d z4x%C>zV20;YD=Z!F)&hyf;SCJOtxMuSHFXza$Vk2V7ky)m^Aq*U74F=UeJiB<7}#N z%OHHk((aNBC4JygR%pc{E;T@9;~2>)0pR<{U{;uaJSEV@(dF3*uF+4@u?ObPycw61 zYzPd43Kr90=1bgmx)S#WT2hzF>hX&ve^k3^{HbUaGP;*z1qVi5)3Zvw*n#CaB(&so zEKWRgT7V52vFIpuzF3@Z~EI<42cfs15xHC4Er|sb>SzI85O|Gu9 z#$W$U{P?^80LJRY5BC5m{>P`t919JYzNQgq(?Womc}3R-<9BoaxoMTdSo#(9VVeBp zASdskStYDUf;NBiK}`fzhKL=dCtk0KY*(|X01x)&z$B7 zgS=BSYtg}lV<||YblGQ^g$B$P}eZlWYuOL3urlU(poSR(w$(Tv=`muP%=#bUZY=w4@66Ir-Dh$HnHP%;ggFk4J? z;X{xX>LTSf34av7(f&uTJWde2VUwr-90_ZDzOgQ@Wfi7mFi80tKb-9QhH zFjQ3JfQEGXvV{?@GEk6X&fF?)*MPayG!73Ef6Ty!H{3I) zi|_KakZfne3~BY9@kWPNaMnpt73P81hA^d?ArW-(+nq(!vH{^;-WbD$Nx%Ed2Xd9Y zEf$`GQdTnqKzUK3KQKGcvH82UJ;gC0I0VA_MK>yIO%O{D5BAAqYe=wrZUTCnBlDBIzz7_%7TyNZHF!p?%p z!>W=Npo9WW?&v^OMLrq83i~Ip=yNQh4Z%Teh^ZF4zmJ4NodRy4wh(WVM0QGfyg7UgpXB7Lq5>I-9f_wfFh_=2 zvfU;;7T3D~2S2bgS5o-2q|6juWY2uw?P1Tp~XCglesVB;@UN=Ql-t^l`AN#v&X9GboEVogb8I7nJu);|da&*}3psP; zK`(D1by3^6)$JW7F(>}DLCR6>{+jZF)?*_~=5FJ?jG>6GRnWKr3w*Bq%5s1ts3ZJ} z85CSs4XsdEP5FCQa|-pY2@P2xtdX6eL@AL9knq+j0dWWc1pe@6L6ffR`W5F4+xvqf zS(@rlT0lJXO+w13FI+SS!AzI#XtPlJpjs*RyiEi< zKMw@^?*Dk^+v-oGt7@rPstY}xr_fLFm1oJV;>185k+}+!+-aaZlpi?%y<4L+DbFVt zJA-Bxlbkv14CTum;w zS|=d=gXS;a3OX5;Mwn&wd&aDerI1TIteH}gZo;`5a%m>dyc)BQ>KWN(@B zr=TV9dB{2|BpA|5py4w8ZEL1`^7uD8$qRpn*a>)7Kr@n}0BUinPid>*cW6HaP43%j zWYpPFdBH?WL{Ka%BoSbk`Qg&q z38ThkLS7Fkfs4M0ZOR)T#hdAKQ6`Zx5KZUcH@v?ozDLN99no$|9Uq6x@sTWP-RTVa zF-arlB?khaSPs{k{jqhbU(Fy`el+q_6?iq|XPWb)8r4gPm+(_YBr6{HGa&9yL2r_K z$*DP4Ib0o5ov=f-#8yK>s#DNMF-hw*u7Ta~jXfPvg3yC}VEoIC6W!#E?O}$`zg470 z@Ra0*c+_Ia?;*h&-Sg;&)36gh)X6f4x+uN#FhZx=493{ebd~@U_^m5gF~`LqMp-S> z21UL#IoN3Ipg0^But!zTo6w3ue6!pVa!w%M(8j4|BCYbYaIBcxC@Emc#;Ha$u8nC7 z2g<}1(0+ib;Dn?dc<=sh7ih&GSn?dL$;{{}Q3>#H1=T{R5AduQ#C*A>njQ-YvyDv6 z-I>D@9wq~oX0?|SB4OW18bJ6!8)dc6ryy}ag@%hvN|BK^f&j+iu4!qN{`}rk!IJr< zDpup_Y&3lb=7KkBAgrp6;0DsEIO_a}Jh#h$PH7z_yD#izRFoFjN|;`t08Q?%3DaxM zwtEHHR;{iH)2W(zpyuMWslE4tL8t%sEh7)gMkrGFl! z)uM5!KJkPVtndN!GD;(?7Q+iV!)i%24R}0m{E!rz#mZc1wT0)v5}B?QPLO_4kE4dx zK12P0wb4~)zjy1&^ifL-i_m0d4AQRdx4@&K|Ecxl9mv4>YUR8CfJmp zSRyQvq!W@)VKs*Ilcobhn}WVL&PcpB<){~cwVw2-8jVD7>CLIv)c*>xhD~a52d={NUVrq!)o{Xl0)ga>^+N-AK~EgwIjiqz4Lf6R45sb_uy`$OP=?@M z_+iP;#CBZTm_-Y`bu^CH;3q!an=$Jhk#s}jLbL)@7zg7P)QFkVxPfMdQi}KXM3{wEx$QGFk zYdXk}h|%3^O^jq+^VL2mVBMZlg%WbRr_nEL-IJ$|&X1%0EKin^c)})TvcdMs{y^bCXU6TZ zZj&fq=8BTtrQ_sm@k}3}$biL=yMhm_nD}g!GJ=CYgs5papaq-a%wXA)nXGKcO!Lad zbW;C6qg!=K*j0WA*GTD!!cpIe26Y;t%&jt+l=}SWD zm$SPm|4ayJs!|;Kc8Ft1(F|=E8sV+mwV-*{n_aaKNb?;Z!Ol8<173m_^-I{VSW>3rm|&n?%6*Q#;Bx7xb{LrtMk2*J0H`d&CZ zs*6l+<@yOCAvsZB`eH<&U}tGVDQ`PdxIhQYB0N{wGR~u`b z9D=;owOL4{O<}Rx*S6wyh8-N)8~v?CAO#2$Hw_)uf|J)Z z0K4Yn=tQ9Z4PfZ@w_04{>b>k07mKN47a>4vR-4z7i=mbj#3mw5Pp%%qfJk_^M<< z$EIS5Go(5mbN~Db`sbnAL+5Lzwa8+ucHG&*idTNU}6Jb)UF+! z$evT|0(J1`q`fhGQ8R(Lf1A2%MC^5HaK-2^6iE`I!r3)Jfl+IqjrVMfp=L#lhB0qz z>Bwyy_69eormmZAk$)~%oa?bVoko071RQWL=5uCJMt?Of!8-Hc)dv*TR1RTwOBvW@o0DuOZ5ja^qBSi}+wA+LN5pH`280P~D zIFg__u?n#T^`8yD=k&R&^VxK8dHSdKWIE=tqG3&@*NGyxb#wu-Z2>`Oj9xnRH1)Ou zS~nQjQ}~`rSfvKQybR%lR>c>=m@E#;+WlY(3Nx#P^&yY4?>^6}&ELU=wlN#=EiR?n zyPA+()@kt|hrPnlNVjn#Y!c@baMRrm6@%jVNz#HarD&HE}L{ej#V$ zbn=UYYRQMZ?$n?9ON|HLkbFGmNLMARJd4P-F5GBTDxz>q%p7OV;-!3vit7D(N=yZ&ROTvY;l#&t)aGHlbR-F^^xM#~P(V!iw9}Ijs+J zB1#>y{lAAFiAmU;6#8x_3gww}1QBLBf;-1z-pZsPYuxv{L=@neSVQ60o$Hf9;SlNF zvJrC*QK&x~ZT@nuVL^tTc`Kpq^=G5m@wh@}`2$J)*&G>i`O?qABTq^gHc11Ifp<}M z;jryCRqUGt2&0X{r`_NTt3TWFF_Jk&T*_+LRCy##FnUTrA9@|4Q~S679gKa^R{o1? zFt5-VzfpmVr4ODdRMR8CP88+@bWrWDf%X*-)JLn+1>L3^VdmWSTJ(^1LWG}B2VRX^Pux1+1%41N%qJn5*p8?6Jbww+s&XG1X$1v$jl*# zW{Pn#Q>VKi<;A7`D39kXP9BGbfSCgoF@y@GeDR1zw^}605@}%^riDJ?9zpLu(i3L_ zf-Ou=>4U`qym~;aV9|+H0|iQ>B*ra)tUk-4OXV@E5hFRww1UGiP>|=i37j?Wu^76r zcfadn>)8PlBp*!ShI}P7O-;t&tHKi{5zgrN8;9F4L_sSp0~RA_0-c0#C~edudNg}2 zjA1>qp<$RvKlXyY*NYNn`b+ zvzaUc5+AnoYLJ9{prG6&cQ`stL{S38Iw62tyM09Hx=3f5UL=o7eO%?_HlDdwc_Ge) zQS+P6lDgJlH*|<<5o-=7kcbwRY$URR)=DM5l&Egfe^#jq_SuWBV$kkil^(X6NU-CN z`G~e6!Afw5=8LR=-Grx=*`ZyB@hW1ij(2wsV}U_(fuv`ac_6-F=k!5|%U3y+ERq^;Vm$R#q6}Q?cdB7yK^gC7C#t>p;i-L|!fXGr za)#YY3>#ZNLJx2;7iD36E2n6tWRaBP!{W$1hH(3?H_4AEyvA3dUedYHdKnBvJ#J5a z zJVlvPr>mqw(SCpv5Pnv651W|I@TSTtZ|td_E-S_z7h3MX72W-rV_gDzX{RI=8&d9$ z!yo+vs|R&lVCQ=Y9&a&1kBKtL?D&2thZ}|6p}0FdcB`H@$?`~)VRI!oWC1KzgH}9w zO)1U+Ln^XAHlJD5%rAB<6pqci7lod3C@TBQNy{k;A{LZWF+9c@B*LX9HK6&CxlSBE zHQICpDWC_8Qg;%$0N63$ermCFaaJW2i3p-`<+!Cco4&*iqmKi7)sWt_cwLsyYgxI{ zvtso1mJA=BFVSx1xqMwtU;I|BSGT!*KLIW)aNgejuExY^#XIxx4wDHQEV30jOeRV zda!&~Oy3h0aBCQO@419Q=j~w=owzwFs)@-Ky<*vg!~Iyh`H|CO1}>3H8xcf1u*<%h6Z@zoE~Ujc9Y~8l7D~r z1o(kp zIOXb`x{BY`-8dT@S&>4$#|n=R8oe!g9_VJ1vKMWs?5^CcQCVr#v|*@1_m8m%j|2Q4 zX2h?bqMYR@EohL4bGchXT#(uX?Je?YSv-u#{vtf2H>vRj?3$?&CNkKwPu@zpn_VC1 z27dY)0U3BULK*CB(g~0Lqqsag4rClr&Sj+)78!aHuFW$yB;j1u=D6u*_Mo1=dC|ZS z`bcqNhpl(Y)Jg>phP|E-UDyj0ut|J6pZR+`uBL~s?RCi%0DNUtA$h$jW8yml|3F-c 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

  • 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