Build the agent-native compute CRM platform
CI / verify (push) Successful in 3m6s

This commit is contained in:
2026-08-13 01:39:01 -07:00
parent bfd2f8d95a
commit 853bde2265
160 changed files with 61812 additions and 483 deletions
+90
View File
@@ -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();
}
}