Spaces:
Configuration error
Configuration error
| import { Agent } from "@earendil-works/pi-agent-core"; | |
| import { createAssistantMessageEventStream, Type } from "@earendil-works/pi-ai"; | |
| import { env, pipeline } from "@huggingface/transformers"; | |
| const MODEL_ID = "Mike0021/MiniCPM5-1B-ONNX-Web"; | |
| const LOCAL_MODEL = { | |
| id: MODEL_ID, | |
| name: "MiniCPM5-1B ONNX Web", | |
| api: "transformers-js", | |
| provider: "huggingface-transformers-js", | |
| baseUrl: "https://huggingface.co", | |
| reasoning: false, | |
| input: ["text"], | |
| cost: { | |
| input: 0, | |
| output: 0, | |
| cacheRead: 0, | |
| cacheWrite: 0, | |
| }, | |
| contextWindow: 4096, | |
| maxTokens: 512, | |
| }; | |
| const EMPTY_USAGE = { | |
| input: 0, | |
| output: 0, | |
| cacheRead: 0, | |
| cacheWrite: 0, | |
| totalTokens: 0, | |
| cost: { | |
| input: 0, | |
| output: 0, | |
| cacheRead: 0, | |
| cacheWrite: 0, | |
| total: 0, | |
| }, | |
| }; | |
| function textFromContent(content) { | |
| if (typeof content === "string") return content; | |
| if (!Array.isArray(content)) return ""; | |
| return content | |
| .filter((part) => part.type === "text") | |
| .map((part) => part.text) | |
| .join("\n"); | |
| } | |
| function now() { | |
| return Date.now(); | |
| } | |
| function createMessage(content, stopReason = "stop") { | |
| return { | |
| role: "assistant", | |
| content, | |
| api: LOCAL_MODEL.api, | |
| provider: LOCAL_MODEL.provider, | |
| model: LOCAL_MODEL.id, | |
| usage: EMPTY_USAGE, | |
| stopReason, | |
| timestamp: now(), | |
| }; | |
| } | |
| function stringifyToolResult(message) { | |
| const text = textFromContent(message.content); | |
| return `${message.toolName}(${message.toolCallId}) ${message.isError ? "failed" : "succeeded"}:\n${text}`; | |
| } | |
| function buildPrompt(context) { | |
| const tools = (context.tools || []).map((tool) => ({ | |
| name: tool.name, | |
| description: tool.description, | |
| parameters: tool.parameters, | |
| })); | |
| const transcript = context.messages | |
| .slice(-8) | |
| .map((message) => { | |
| if (message.role === "toolResult") return `TOOL_RESULT:\n${stringifyToolResult(message)}`; | |
| return `${message.role.toUpperCase()}:\n${textFromContent(message.content)}`; | |
| }) | |
| .join("\n\n"); | |
| return `${context.systemPrompt || ""} | |
| You are running as a pi Agent inside a browser-only app. The sandbox is a WebContainer: it has a virtual filesystem and can spawn browser-contained Node.js processes. | |
| Use tools by returning strict JSON only. Do not use markdown. | |
| To call tools: | |
| {"toolCalls":[{"tool":"write_file","args":{"path":"hello.js","content":"console.log(2 + 2)\\n"}},{"tool":"run_command","args":{"command":"node","args":["hello.js"]}}]} | |
| To answer the user after tool results: | |
| {"final":"Short answer that explains what happened."} | |
| Available tools: | |
| ${JSON.stringify(tools, null, 2)} | |
| Conversation: | |
| ${transcript} | |
| Return JSON now.`; | |
| } | |
| function extractJsonPayload(text) { | |
| const trimmed = String(text || "").trim(); | |
| const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i); | |
| const candidate = fence ? fence[1].trim() : trimmed; | |
| const firstBrace = candidate.indexOf("{"); | |
| const firstBracket = candidate.indexOf("["); | |
| const starts = [firstBrace, firstBracket].filter((index) => index >= 0); | |
| if (starts.length === 0) return null; | |
| const start = Math.min(...starts); | |
| const lastBrace = candidate.lastIndexOf("}"); | |
| const lastBracket = candidate.lastIndexOf("]"); | |
| const end = Math.max(lastBrace, lastBracket); | |
| if (end <= start) return null; | |
| try { | |
| return JSON.parse(candidate.slice(start, end + 1)); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function normalizeToolCalls(payload) { | |
| if (!payload) return []; | |
| const rawCalls = Array.isArray(payload) ? payload : payload.toolCalls || payload.tools || payload.actions || []; | |
| if (!Array.isArray(rawCalls)) return []; | |
| return rawCalls | |
| .map((call, index) => ({ | |
| type: "toolCall", | |
| id: `tool-${now()}-${index}`, | |
| name: String(call.tool || call.name || ""), | |
| arguments: call.args || call.arguments || {}, | |
| })) | |
| .filter((call) => call.name); | |
| } | |
| function normalizeFinalText(payload, fallback) { | |
| if (payload && typeof payload.final === "string") return payload.final; | |
| if (payload && typeof payload.message === "string") return payload.message; | |
| if (payload && typeof payload.answer === "string") return payload.answer; | |
| return String(fallback || "").trim() || "Done."; | |
| } | |
| function mockPlan(context) { | |
| const last = context.messages[context.messages.length - 1]; | |
| if (last?.role === "toolResult") { | |
| const results = context.messages.filter((message) => message.role === "toolResult").slice(-4).map(stringifyToolResult); | |
| return { | |
| final: `The sandbox work completed.\n\n${results.join("\n\n")}`, | |
| }; | |
| } | |
| const userText = textFromContent(last?.content || "").toLowerCase(); | |
| if (userText.includes("read")) { | |
| return { | |
| toolCalls: [{ tool: "read_file", args: { path: "hello.js" } }], | |
| }; | |
| } | |
| if (userText.includes("list")) { | |
| return { | |
| toolCalls: [{ tool: "list_files", args: { path: "." } }], | |
| }; | |
| } | |
| return { | |
| toolCalls: [ | |
| { | |
| tool: "write_file", | |
| args: { | |
| path: "hello.js", | |
| content: 'const value = 21 * 2;\nconsole.log(`pi sandbox result: ${value}`);\n', | |
| }, | |
| }, | |
| { | |
| tool: "run_command", | |
| args: { | |
| command: "node", | |
| args: ["hello.js"], | |
| }, | |
| }, | |
| ], | |
| }; | |
| } | |
| function emitFinal(stream, message, text = "") { | |
| stream.push({ type: "start", partial: { ...message, content: [{ type: "text", text: "" }] } }); | |
| if (text) { | |
| const partial = { ...message, content: [{ type: "text", text }] }; | |
| stream.push({ type: "text_start", contentIndex: 0, partial: { ...message, content: [{ type: "text", text: "" }] } }); | |
| stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial }); | |
| stream.push({ type: "text_end", contentIndex: 0, content: text, partial }); | |
| } | |
| if (message.stopReason === "error" || message.stopReason === "aborted") { | |
| stream.push({ type: "error", reason: message.stopReason, error: message }); | |
| } else { | |
| stream.push({ type: "done", reason: message.stopReason, message }); | |
| } | |
| } | |
| export function createPiAgent({ sandbox, modelMode, device, maxTokens, temperature, onModelStatus = () => {} }) { | |
| env.allowLocalModels = false; | |
| env.allowRemoteModels = true; | |
| env.backends.onnx.wasm.numThreads = Math.min(4, navigator.hardwareConcurrency || 4); | |
| let generatorPromise = null; | |
| let generatorKey = ""; | |
| async function getGenerator() { | |
| const key = `${MODEL_ID}:${device()}`; | |
| if (!generatorPromise || generatorKey !== key) { | |
| generatorKey = key; | |
| onModelStatus(`Loading ${device()}`); | |
| generatorPromise = pipeline("text-generation", MODEL_ID, { | |
| dtype: "q4", | |
| device: device(), | |
| progress_callback: (event) => { | |
| if (event.status === "progress") { | |
| onModelStatus(`${event.file} ${Math.round(event.progress)}%`); | |
| } else if (event.status) { | |
| onModelStatus(event.status); | |
| } | |
| }, | |
| }); | |
| } | |
| return generatorPromise; | |
| } | |
| async function producePlan(context, signal) { | |
| if (modelMode() === "mock") { | |
| return JSON.stringify(mockPlan(context)); | |
| } | |
| const generator = await getGenerator(); | |
| if (signal?.aborted) throw new Error("Aborted"); | |
| const result = await generator(buildPrompt(context), { | |
| max_new_tokens: Number(maxTokens()) || 128, | |
| temperature: Number(temperature()) || 0, | |
| do_sample: Number(temperature()) > 0, | |
| return_full_text: false, | |
| }); | |
| onModelStatus("Model ready"); | |
| return result?.[0]?.generated_text ?? ""; | |
| } | |
| function streamFn(_model, context, options = {}) { | |
| const stream = createAssistantMessageEventStream(); | |
| queueMicrotask(async () => { | |
| try { | |
| const generated = await producePlan(context, options.signal); | |
| const payload = extractJsonPayload(generated); | |
| const lastMessage = context.messages[context.messages.length - 1]; | |
| const forceFinal = lastMessage?.role === "toolResult"; | |
| const fallbackPayload = payload ? null : mockPlan(context); | |
| const toolCalls = forceFinal ? [] : normalizeToolCalls(payload || fallbackPayload); | |
| if (toolCalls.length > 0) { | |
| const message = createMessage([{ type: "text", text: "Using sandbox tools." }, ...toolCalls], "toolUse"); | |
| emitFinal(stream, message, "Using sandbox tools."); | |
| return; | |
| } | |
| const text = normalizeFinalText(payload || fallbackPayload, generated); | |
| const message = createMessage([{ type: "text", text }], "stop"); | |
| emitFinal(stream, message, text); | |
| } catch (error) { | |
| const text = error instanceof Error ? error.message : String(error); | |
| const message = { | |
| ...createMessage([{ type: "text", text }], options.signal?.aborted ? "aborted" : "error"), | |
| errorMessage: text, | |
| }; | |
| emitFinal(stream, message, text); | |
| } | |
| }); | |
| return stream; | |
| } | |
| const tools = [ | |
| { | |
| name: "list_files", | |
| label: "List files", | |
| description: "List files in the sandbox workspace.", | |
| parameters: Type.Object({ | |
| path: Type.Optional(Type.String()), | |
| }), | |
| execute: async (_id, args) => { | |
| const output = await sandbox.listFiles(args.path || "."); | |
| return { content: [{ type: "text", text: output }], details: { output } }; | |
| }, | |
| }, | |
| { | |
| name: "read_file", | |
| label: "Read file", | |
| description: "Read a UTF-8 file from the sandbox workspace.", | |
| parameters: Type.Object({ | |
| path: Type.String(), | |
| }), | |
| execute: async (_id, args) => { | |
| const output = await sandbox.readFile(args.path); | |
| return { content: [{ type: "text", text: output }], details: { path: args.path, output } }; | |
| }, | |
| }, | |
| { | |
| name: "write_file", | |
| label: "Write file", | |
| description: "Create or replace a UTF-8 file inside the sandbox workspace.", | |
| parameters: Type.Object({ | |
| path: Type.String(), | |
| content: Type.String(), | |
| }), | |
| execute: async (_id, args) => { | |
| const output = await sandbox.writeFile(args.path, args.content); | |
| return { content: [{ type: "text", text: output }], details: { path: args.path } }; | |
| }, | |
| }, | |
| { | |
| name: "run_command", | |
| label: "Run command", | |
| description: "Spawn a process inside the browser-only WebContainer sandbox.", | |
| parameters: Type.Object({ | |
| command: Type.String(), | |
| args: Type.Optional(Type.Array(Type.String())), | |
| timeoutMs: Type.Optional(Type.Number()), | |
| }), | |
| execute: async (_id, args) => { | |
| const result = await sandbox.runCommand(args.command, args.args || [], args.timeoutMs || 10000); | |
| const text = `$ ${result.command}\nexit ${result.exitCode}\n${result.output}`; | |
| return { | |
| content: [{ type: "text", text }], | |
| details: result, | |
| }; | |
| }, | |
| executionMode: "sequential", | |
| }, | |
| ]; | |
| return new Agent({ | |
| initialState: { | |
| model: LOCAL_MODEL, | |
| systemPrompt: | |
| "You are Pi Web Agent. Use the sandbox tools for filesystem or command tasks, then give concise results.", | |
| tools, | |
| }, | |
| streamFn, | |
| toolExecution: "sequential", | |
| }); | |
| } | |
| export { MODEL_ID }; | |