This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { ApiError, getSupabase } from './api';
|
||||
|
||||
export interface PiggyChatContext {
|
||||
type: 'account' | 'contact' | 'demand_deal' | 'supply_deal' | 'contract' | 'commitment';
|
||||
id: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface PiggyChatTurn {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type PiggyChatEvent =
|
||||
| { type: 'meta'; model: string }
|
||||
| { type: 'reasoning_delta'; delta: string }
|
||||
| { type: 'content_delta'; delta: string }
|
||||
| { type: 'tool_call'; id: string; name: string; arguments: unknown }
|
||||
| { type: 'tool_result'; id: string; name: string; ok: boolean; result?: unknown; error?: string }
|
||||
| { type: 'done'; inputTokens: number | null; outputTokens: number | null }
|
||||
| { type: 'error'; message: string };
|
||||
|
||||
export interface PiggyStatus {
|
||||
enabled: boolean;
|
||||
canUse: boolean;
|
||||
}
|
||||
|
||||
export async function* streamPiggyChat(
|
||||
request: {
|
||||
message: string;
|
||||
history?: PiggyChatTurn[];
|
||||
context?: PiggyChatContext;
|
||||
},
|
||||
signal?: AbortSignal,
|
||||
): AsyncGenerator<PiggyChatEvent> {
|
||||
const supabase = getSupabase();
|
||||
const token = supabase ? (await supabase.auth.getSession()).data.session?.access_token : null;
|
||||
const response = await fetch('/api/piggy/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = response.statusText;
|
||||
let code: string | undefined;
|
||||
try {
|
||||
const body = (await response.json()) as { error?: string; code?: string };
|
||||
message = body.error ?? message;
|
||||
code = body.code;
|
||||
} catch {
|
||||
// The authenticated proxy normally returns JSON, but an upstream proxy may not.
|
||||
}
|
||||
throw new ApiError(message, response.status, code);
|
||||
}
|
||||
if (!response.body) throw new Error('Piggy returned no response stream.');
|
||||
|
||||
yield* readNdjson<PiggyChatEvent>(response.body, signal);
|
||||
}
|
||||
|
||||
export async function* readNdjson<Value>(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
signal?: AbortSignal,
|
||||
): AsyncGenerator<Value> {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
try {
|
||||
while (true) {
|
||||
if (signal?.aborted) throw signal.reason;
|
||||
const { done, value } = await reader.read();
|
||||
buffer += decoder.decode(value, { stream: !done });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() ?? '';
|
||||
for (const line of lines) {
|
||||
if (line.trim()) yield JSON.parse(line) as Value;
|
||||
}
|
||||
if (done) {
|
||||
if (buffer.trim()) yield JSON.parse(buffer) as Value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user