feat: add call history API endpoints and TTS service client

Adds read-only access to persisted call records for the dashboard
and implements a client for the Rhema text-to-speech service.

- api/call_history.py: New router providing paged call lists
  and detailed call records with transcript metadata.
- services/tts.py: Async client for OpenAI-compatible TTS
  endpoints (Rhema/Kokoro) used for call-flow steps.
This commit is contained in:
2026-05-22 06:28:33 -04:00
parent dbdb03beb9
commit 63f1a270bb
28 changed files with 2275 additions and 11 deletions

View File

@@ -1,4 +1,13 @@
import type { CallSummary, DeviceStatus, GatewayEvent, GatewayStatus, HealthStatus } from './types';
import type {
CallHistoryRow,
CallSummary,
DeviceStatus,
GatewayEvent,
GatewayStatus,
HealthStatus,
RoutingRule,
TranscriptRow,
} from './types';
async function get<T>(path: string): Promise<T> {
const res = await fetch(path);
@@ -27,6 +36,67 @@ export async function hangupCall(callId: string): Promise<void> {
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
}
export async function fetchCallHistory(
limit = 50,
offset = 0,
): Promise<CallHistoryRow[]> {
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
return get<CallHistoryRow[]>(`/api/calls/history?${params}`);
}
export async function fetchCallRecord(callId: string): Promise<CallHistoryRow> {
return get<CallHistoryRow>(`/api/calls/${callId}/record`);
}
export async function fetchTranscript(callId: string): Promise<TranscriptRow[]> {
return get<TranscriptRow[]>(`/api/calls/${callId}/transcript`);
}
export function recordingUrl(callId: string): string {
return `/api/calls/${callId}/recording`;
}
export async function fetchRoutingRules(): Promise<RoutingRule[]> {
return get<RoutingRule[]>('/api/routing/rules');
}
export async function createRoutingRule(rule: Partial<RoutingRule>): Promise<RoutingRule> {
const res = await fetch('/api/routing/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rule),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<RoutingRule>;
}
export async function updateRoutingRule(
ruleId: string,
patch: Partial<RoutingRule>,
): Promise<RoutingRule> {
const res = await fetch(`/api/routing/rules/${ruleId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<RoutingRule>;
}
export async function deleteRoutingRule(ruleId: string): Promise<void> {
const res = await fetch(`/api/routing/rules/${ruleId}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
}
export async function setDeviceDnd(deviceId: string, enabled: boolean): Promise<void> {
const res = await fetch(`/api/routing/devices/${deviceId}/dnd`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
}
export function connectEventStream(
onEvent: (e: GatewayEvent) => void,
onClose: () => void,

View File

@@ -72,3 +72,65 @@ export interface GatewayEvent {
data: Record<string, unknown>;
message: string;
}
export interface CallHistoryRow {
id: string;
direction: string;
remote_number: string;
status: string;
mode: string;
intent?: string;
started_at?: string;
ended_at?: string;
duration: number;
hold_time: number;
device_used?: string;
summary?: string;
}
export interface TranscriptRow {
seq: number;
t_offset_ms: number;
speaker: string;
text: string;
confidence?: number;
}
export type RoutingActionType =
| 'ring_device'
| 'ring_chain'
| 'take_message'
| 'reject'
| 'dnd';
export interface TimeRange {
start: string;
end: string;
tz: string;
days: number[];
}
export interface RoutingMatch {
caller_pattern?: string | null;
dnis?: string | null;
time_range?: TimeRange | null;
}
export interface RoutingAction {
type: RoutingActionType;
device_id?: string | null;
device_ids: string[];
ring_timeout: number;
message?: string | null;
}
export interface RoutingRule {
id: string;
name: string;
priority: number;
enabled: boolean;
match: RoutingMatch;
action: RoutingAction;
created_at?: string;
updated_at?: string;
}