aut4rk's picture
Deploy static voice agent
009fd18 verified
Raw
History Blame
4.54 kB
import type {
ConversationTurn,
LLMAdapter,
LLMGenerateOptions,
RuntimeWarmupOptions,
} from "../app/types";
import { SYSTEM_PROMPT } from "../prompts/system";
type ChatMessage = {
role: "system" | "user" | "assistant";
content: string;
};
type WebLLMModule = typeof import("@mlc-ai/web-llm");
type EngineType = Awaited<ReturnType<WebLLMModule["CreateMLCEngine"]>>;
type InitProgress = {
progress?: number;
text?: string;
timeElapsed?: number;
};
const MODEL_PREFERENCES = [
"Qwen2.5-0.5B-Instruct-q4f16_1-MLC",
"SmolLM2-360M-Instruct-q4f16_1-MLC",
"Llama-3.2-1B-Instruct-q4f16_1-MLC",
"Qwen2.5-1.5B-Instruct-q4f16_1-MLC",
];
const formatProgress = (progress: InitProgress): string => {
const percentage =
typeof progress.progress === "number"
? `${Math.round(progress.progress * 100)}%`
: null;
if (progress.text && percentage) {
return `${progress.text} (${percentage})`;
}
if (progress.text) {
return progress.text;
}
if (percentage) {
return `Loading language model (${percentage})`;
}
return "Loading language model...";
};
export class WebLLMChatAdapter implements LLMAdapter {
#engine: EngineType | null = null;
#modelId: string | null = null;
constructor(private readonly preferredModelId?: string) {}
async initialize(options: RuntimeWarmupOptions = {}): Promise<void> {
if (this.#engine) {
options.onProgress?.(
`Language ready (${this.#modelId ?? this.preferredModelId ?? "model loaded"}).`,
);
return;
}
const webllm = await import("@mlc-ai/web-llm");
const modelId =
this.preferredModelId ?? WebLLMChatAdapter.#resolveModelId(webllm);
options.onProgress?.(`Preparing ${modelId}...`);
this.#engine = await webllm.CreateMLCEngine(modelId, {
initProgressCallback: (progress: InitProgress) => {
options.onProgress?.(formatProgress(progress));
},
});
this.#modelId = modelId;
options.onProgress?.(`Language ready (${modelId}).`);
}
async generate(options: LLMGenerateOptions): Promise<string> {
await this.initialize();
if (!this.#engine) {
throw new Error("LLM engine failed to initialize.");
}
const messages = WebLLMChatAdapter.#toMessages(options.turns);
const stream = await this.#engine.chat.completions.create({
messages,
temperature: 0.7,
stream: true,
});
let finalText = "";
for await (const chunk of stream) {
if (options.signal?.aborted) {
break;
}
const delta = chunk.choices[0]?.delta?.content ?? "";
if (!delta) {
continue;
}
finalText += delta;
options.onChunk(delta);
}
return finalText.trim();
}
async warmup(options: RuntimeWarmupOptions = {}): Promise<void> {
await this.initialize(options);
if (!this.#engine) {
throw new Error("LLM engine failed to initialize.");
}
options.onProgress?.("Compiling first language response...");
await this.#engine.chat.completions.create({
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
{
role: "user",
content: "Reply with one short word.",
},
],
temperature: 0,
max_tokens: 1,
});
options.onProgress?.("Language runtime warmed.");
}
static #toMessages(turns: ConversationTurn[]): ChatMessage[] {
const messages: ChatMessage[] = [
{
role: "system",
content: SYSTEM_PROMPT,
},
];
for (const turn of turns) {
if (turn.role === "system" || !turn.transcript.trim()) {
continue;
}
messages.push({
role: turn.role,
content: turn.transcript,
});
}
return messages;
}
static #resolveModelId(webllm: WebLLMModule): string {
const candidates = webllm.prebuiltAppConfig?.model_list ?? [];
const normalized = candidates
.map((candidate) => {
const modelId = "model_id" in candidate ? candidate.model_id : "";
return { candidate, modelId };
})
.filter((entry) => Boolean(entry.modelId));
for (const preference of MODEL_PREFERENCES) {
const match = normalized.find((entry) =>
entry.modelId.includes(preference),
);
if (match) {
return match.modelId;
}
}
const fallback = normalized.at(0)?.modelId;
if (!fallback) {
throw new Error("WebLLM did not expose any prebuilt models.");
}
return fallback;
}
}