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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user