Spaces:
Running
Running
"use server"; | |
import { openai } from "@ai-sdk/openai"; | |
import { generateObject } from "ai"; | |
import { z } from "zod"; | |
import { startMcpSandbox } from '@/lib/mcp-sandbox'; | |
// Use a global map to store active sandbox instances across requests | |
const activeSandboxes = (global as any).activeSandboxes || new Map(); | |
(global as any).activeSandboxes = activeSandboxes; | |
// Helper to extract text content from a message regardless of format | |
function getMessageText(message: any): string { | |
// Check if the message has parts (new format) | |
if (message.parts && Array.isArray(message.parts)) { | |
const textParts = message.parts.filter((p: any) => p.type === 'text' && p.text); | |
if (textParts.length > 0) { | |
return textParts.map((p: any) => p.text).join('\n'); | |
} | |
} | |
// Fallback to content (old format) | |
if (typeof message.content === 'string') { | |
return message.content; | |
} | |
// If content is an array (potentially of parts), try to extract text | |
if (Array.isArray(message.content)) { | |
const textItems = message.content.filter((item: any) => | |
typeof item === 'string' || (item.type === 'text' && item.text) | |
); | |
if (textItems.length > 0) { | |
return textItems.map((item: any) => | |
typeof item === 'string' ? item : item.text | |
).join('\n'); | |
} | |
} | |
return ''; | |
} | |
export async function generateTitle(messages: any[]) { | |
// Convert messages to a format that OpenAI can understand | |
const normalizedMessages = messages.map(msg => ({ | |
role: msg.role, | |
content: getMessageText(msg) | |
})); | |
const { object } = await generateObject({ | |
model: openai("gpt-4.1"), | |
schema: z.object({ | |
title: z.string().min(1).max(100), | |
}), | |
system: ` | |
You are a helpful assistant that generates titles for chat conversations. | |
The title should be a short description of the conversation. | |
The title should be no more than 30 characters. | |
The title should be unique and not generic. | |
`, | |
messages: [ | |
...normalizedMessages, | |
{ | |
role: "user", | |
content: "Generate a title for the conversation.", | |
}, | |
], | |
}); | |
return object.title; | |
} | |
export interface KeyValuePair { | |
key: string; | |
value: string; | |
} | |
/** | |
* Server action to start a sandbox | |
*/ | |
export async function startSandbox(params: { | |
id: string; | |
command: string; | |
args: string[]; | |
env?: KeyValuePair[]; | |
}): Promise<{ url: string }> { | |
const { id, command, args, env } = params; | |
console.log(`[startSandbox] Starting sandbox for ID: ${id}`); | |
// Validate required fields | |
if (!id || !command || !args) { | |
throw new Error('Missing required fields'); | |
} | |
// Check if we already have a sandbox for this ID | |
if (activeSandboxes.has(id)) { | |
// If we do, get the URL and return it without creating a new sandbox | |
const existingSandbox = activeSandboxes.get(id); | |
console.log(`[startSandbox] Reusing existing sandbox for ${id}, URL: ${existingSandbox.url}`); | |
// Re-fetch the URL to make sure it's current | |
try { | |
const freshUrl = await existingSandbox.sandbox.getUrl(); | |
console.log(`[startSandbox] Updated sandbox URL for ${id}: ${freshUrl}`); | |
// Update the URL in the map | |
activeSandboxes.set(id, { | |
sandbox: existingSandbox.sandbox, | |
url: freshUrl | |
}); | |
return { url: freshUrl }; | |
} catch (error) { | |
console.error(`[startSandbox] Error refreshing sandbox URL for ${id}:`, error); | |
// Fall through to create a new sandbox if we couldn't refresh the URL | |
activeSandboxes.delete(id); | |
console.log(`[startSandbox] Removed stale sandbox for ${id}, will create a new one`); | |
} | |
} | |
// Build the command string | |
let cmd: string; | |
// Prepare the command based on the type of executable | |
if (command === 'uvx') { | |
// For uvx, use the direct format | |
const toolName = args[0]; | |
cmd = `uvx ${toolName} ${args.slice(1).join(' ')}`; | |
} else if (command.includes('python')) { | |
// For python commands | |
cmd = `${command} ${args.join(' ')}`; | |
} else { | |
// For node or other commands | |
cmd = `${command} ${args.join(' ')}`; | |
} | |
// Convert env array to object if needed | |
const envs: Record<string, string> = {}; | |
if (env && env.length > 0) { | |
env.forEach((envVar) => { | |
if (envVar.key) envs[envVar.key] = envVar.value || ''; | |
}); | |
} | |
// Start the sandbox | |
console.log(`[startSandbox] Creating new sandbox for ${id} with command: ${cmd}`); | |
const sandbox = await startMcpSandbox({ cmd, envs }); | |
const url = await sandbox.getUrl(); | |
console.log(`[startSandbox] Sandbox created for ${id}, URL: ${url}`); | |
// Store the sandbox in our map | |
activeSandboxes.set(id, { sandbox, url }); | |
return { url }; | |
} | |
/** | |
* Server action to stop a sandbox | |
*/ | |
export async function stopSandbox(id: string): Promise<{ success: boolean }> { | |
if (!id) { | |
throw new Error('Missing sandbox ID'); | |
} | |
// Check if we have a sandbox with this ID | |
if (!activeSandboxes.has(id)) { | |
throw new Error(`No active sandbox found with ID: ${id}`); | |
} | |
// Stop the sandbox | |
const { sandbox } = activeSandboxes.get(id); | |
try { | |
await sandbox.stop(); | |
console.log(`Stopped sandbox with ID: ${id}`); | |
} catch (stopError) { | |
console.error(`Error stopping sandbox ${id}:`, stopError); | |
// Continue to remove from the map even if stop fails | |
} | |
// Remove from our map | |
activeSandboxes.delete(id); | |
return { success: true }; | |
} | |