feat(agent): LLM proxy integration shape (Pi-AI compatible)
src/lib/llmClient.ts: browser POSTs to /internal/canvas-llm/chat with
{messages, max_tokens}. Mirrors the normalized request shape used by
earendil-works/pi's @earendil-works/pi-ai. Returns:
- ok:true with content+provider when configured and successful
- ok:false, configured:false when proxy 404/503/unreachable
- ok:false, configured:true when provider returned an error
routeAgentInput now falls back to the LLM with a system prompt that
includes the user identity, tool catalogue, and the top-3 vault recall
hits when no deterministic tool matches. When the proxy is not
configured the deterministic vault-hint reply is used (no regression
in the demo tier).
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Browser-side LLM client. Speaks to a backend proxy that brokers to the
|
||||
* configured provider (Anthropic, OpenAI, etc.) using the same request
|
||||
* shape as pi-ai's normalized completions. When the proxy is unreachable
|
||||
* or unconfigured the client returns { configured: false } and the caller
|
||||
* is expected to fall back to a deterministic path.
|
||||
*/
|
||||
|
||||
import { api } from "./api";
|
||||
|
||||
export interface LlmMessage {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface LlmRequest {
|
||||
messages: LlmMessage[];
|
||||
max_tokens?: number;
|
||||
}
|
||||
|
||||
export interface LlmReply {
|
||||
ok: true;
|
||||
content: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export interface LlmUnconfigured {
|
||||
ok: false;
|
||||
configured: false;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface LlmError {
|
||||
ok: false;
|
||||
configured: true;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type LlmResponse = LlmReply | LlmUnconfigured | LlmError;
|
||||
|
||||
export const llmClient = {
|
||||
async chat(req: LlmRequest, signal?: AbortSignal): Promise<LlmResponse> {
|
||||
try {
|
||||
const res = await fetch(`${api.config.baseUrl}/internal/canvas-llm/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}`,
|
||||
},
|
||||
body: JSON.stringify({ messages: req.messages, max_tokens: req.max_tokens ?? 512 }),
|
||||
signal,
|
||||
});
|
||||
if (res.status === 404) return { ok: false, configured: false, reason: "LLM proxy not wired" };
|
||||
if (res.status === 503) return { ok: false, configured: false, reason: "LLM provider not configured" };
|
||||
if (!res.ok) return { ok: false, configured: true, error: `Provider error ${res.status}` };
|
||||
const body = await res.json();
|
||||
if (typeof body?.content !== "string") return { ok: false, configured: true, error: "Provider returned unparseable body" };
|
||||
return { ok: true, content: body.content, provider: body.provider };
|
||||
} catch (e) {
|
||||
return { ok: false, configured: false, reason: `LLM proxy unreachable: ${(e as Error).message}` };
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user