/** * Gemini Integration for Product OS * * Supports: * - Chat completions with streaming * - Tool/function calling * - Context-aware responses * * Set GOOGLE_CLOUD_PROJECT and optionally GEMINI_MODEL env vars. * For local dev without Vertex AI, set GEMINI_API_KEY for AI Studio. */ import { config } from "./config.js"; export interface ChatMessage { role: "user" | "assistant" | "system"; content: string; } export interface ToolCall { name: string; arguments: Record; } export interface ChatResponse { message: string; toolCalls?: ToolCall[]; finishReason: "stop" | "tool_calls" | "error"; } // Tool definitions that Gemini can call export const PRODUCT_OS_TOOLS = [ { name: "deploy_service", description: "Deploy a Cloud Run service. Use when user wants to deploy, ship, or launch code.", parameters: { type: "object", properties: { service_name: { type: "string", description: "Name of the service to deploy" }, repo: { type: "string", description: "Git repository URL" }, ref: { type: "string", description: "Git branch, tag, or commit" }, env: { type: "string", enum: ["dev", "staging", "prod"], description: "Target environment" } }, required: ["service_name"] } }, { name: "get_funnel_analytics", description: "Get funnel conversion metrics. Use when user asks about funnels, conversions, or drop-offs.", parameters: { type: "object", properties: { range_days: { type: "integer", description: "Number of days to analyze", default: 30 } } } }, { name: "get_top_drivers", description: "Identify top factors driving a metric. Use when user asks why something changed or what drives conversions.", parameters: { type: "object", properties: { metric: { type: "string", description: "The metric to analyze (e.g., 'conversion', 'retention')" }, range_days: { type: "integer", description: "Number of days to analyze", default: 30 } }, required: ["metric"] } }, { name: "generate_marketing_posts", description: "Generate social media posts for a campaign. Use when user wants to create marketing content.", parameters: { type: "object", properties: { goal: { type: "string", description: "Campaign goal (e.g., 'launch announcement')" }, product: { type: "string", description: "Product or feature name" }, channels: { type: "array", items: { type: "string" }, description: "Social channels (e.g., ['x', 'linkedin'])" } }, required: ["goal"] } }, { name: "get_service_status", description: "Check the status of a deployed service. Use when user asks about service health or deployment status.", parameters: { type: "object", properties: { service_name: { type: "string", description: "Name of the service" }, region: { type: "string", description: "GCP region", default: "us-central1" } }, required: ["service_name"] } }, { name: "generate_code", description: "Generate or modify code. Use when user asks to write, fix, refactor, or change code.", parameters: { type: "object", properties: { task: { type: "string", description: "What code change to make" }, file_path: { type: "string", description: "Target file path (if known)" }, language: { type: "string", description: "Programming language" }, context: { type: "string", description: "Additional context about the codebase" } }, required: ["task"] } }, { name: "deploy_app", description: "Deploy a specific app from the project monorepo. Use when user wants to deploy or ship one of their apps (product, website, admin, storybook).", parameters: { type: "object", properties: { project_id: { type: "string", description: "The project ID" }, app_name: { type: "string", enum: ["product", "website", "admin", "storybook"], description: "Which app to deploy" }, env: { type: "string", enum: ["dev", "staging", "prod"], description: "Target environment" } }, required: ["project_id", "app_name"] } }, { name: "scaffold_app", description: "Add a new app to the project monorepo. Use when user wants to add a new application beyond the defaults.", parameters: { type: "object", properties: { project_id: { type: "string", description: "The project ID" }, app_name: { type: "string", description: "Name for the new app (e.g. 'mobile', 'api', 'dashboard')" }, framework: { type: "string", enum: ["nextjs", "astro", "express", "fastify"], description: "Framework to scaffold" } }, required: ["project_id", "app_name"] } } ]; // System prompt for Product OS assistant const SYSTEM_PROMPT = `You are the AI for a software platform where every project is a Turborepo monorepo containing multiple apps: product, website, admin, and storybook. You have full visibility and control over the entire project. Each project has: - apps/product — the core user-facing application - apps/website — the marketing and landing site - apps/admin — internal admin tooling - apps/storybook — component browser and design system - packages/ui — shared React component library - packages/tokens — shared design tokens (colors, spacing, typography) - packages/types — shared TypeScript types - packages/config — shared eslint and tsconfig You can help with: - Deploying any app using turbo run build --filter= - Writing and modifying code across any app or package in the monorepo - Adding new apps or packages to the project - Analyzing product metrics and funnels - Generating marketing content - Understanding what drives user behavior When a user says "deploy" without specifying an app, ask which one or default to "product". When a user asks to change something visual, consider whether it belongs in packages/ui or packages/tokens. When users ask you to do something, use the available tools to take action. Be concise and specific about which app or package you are working in.`; /** * Chat with Gemini * Uses Vertex AI in production, or AI Studio API key for local dev */ export async function chat( messages: ChatMessage[], options: { stream?: boolean } = {} ): Promise { const apiKey = process.env.GEMINI_API_KEY; const projectId = config.projectId; const model = process.env.GEMINI_MODEL ?? "gemini-1.5-flash"; // Build the request const contents = [ { role: "user", parts: [{ text: SYSTEM_PROMPT }] }, { role: "model", parts: [{ text: "Understood. I'm Product OS, ready to help you build and operate your SaaS product. How can I help?" }] }, ...messages.map(m => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] })) ]; const tools = [{ functionDeclarations: PRODUCT_OS_TOOLS }]; // Use AI Studio API if API key is set (local dev) if (apiKey) { return chatWithAIStudio(apiKey, model, contents, tools); } // Use Vertex AI if project is set (production) if (projectId && projectId !== "productos-local") { return chatWithVertexAI(projectId, model, contents, tools); } // Mock response for local dev without credentials return mockChat(messages); } async function chatWithAIStudio( apiKey: string, model: string, contents: any[], tools: any[] ): Promise { const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents, tools, generationConfig: { temperature: 0.7, maxOutputTokens: 2048 } }) }); if (!response.ok) { const error = await response.text(); console.error("Gemini API error:", error); throw new Error(`Gemini API error: ${response.status}`); } const data = await response.json(); return parseGeminiResponse(data); } async function chatWithVertexAI( projectId: string, model: string, contents: any[], tools: any[] ): Promise { // Vertex AI endpoint const location = process.env.VERTEX_LOCATION ?? "us-central1"; const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:generateContent`; // Get access token (requires gcloud auth) const { GoogleAuth } = await import("google-auth-library"); const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }); const client = await auth.getClient(); const token = await client.getAccessToken(); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token.token}` }, body: JSON.stringify({ contents, tools, generationConfig: { temperature: 0.7, maxOutputTokens: 2048 } }) }); if (!response.ok) { const error = await response.text(); console.error("Vertex AI error:", error); throw new Error(`Vertex AI error: ${response.status}`); } const data = await response.json(); return parseGeminiResponse(data); } function parseGeminiResponse(data: any): ChatResponse { const candidate = data.candidates?.[0]; if (!candidate) { return { message: "No response from Gemini", finishReason: "error" }; } const content = candidate.content; const parts = content?.parts ?? []; // Check for function calls const functionCalls = parts.filter((p: any) => p.functionCall); if (functionCalls.length > 0) { const toolCalls = functionCalls.map((p: any) => ({ name: p.functionCall.name, arguments: p.functionCall.args ?? {} })); return { message: "", toolCalls, finishReason: "tool_calls" }; } // Regular text response const text = parts.map((p: any) => p.text ?? "").join(""); return { message: text, finishReason: "stop" }; } /** * Mock chat for local development without Gemini credentials */ function mockChat(messages: ChatMessage[]): ChatResponse { const lastMessage = messages[messages.length - 1]?.content.toLowerCase() ?? ""; // Check marketing/campaign FIRST (before deploy) since "launch" can be ambiguous if (lastMessage.includes("marketing") || lastMessage.includes("campaign") || lastMessage.includes("post") || (lastMessage.includes("launch") && !lastMessage.includes("deploy"))) { return { message: "", toolCalls: [{ name: "generate_marketing_posts", arguments: { goal: "product launch", channels: ["x", "linkedin"] } }], finishReason: "tool_calls" }; } // Simple keyword matching to simulate tool calls if (lastMessage.includes("deploy") || lastMessage.includes("ship") || lastMessage.includes("staging") || lastMessage.includes("production")) { return { message: "", toolCalls: [{ name: "deploy_service", arguments: { service_name: "my-service", env: lastMessage.includes("staging") ? "staging" : lastMessage.includes("prod") ? "prod" : "dev" } }], finishReason: "tool_calls" }; } if (lastMessage.includes("funnel") || lastMessage.includes("conversion") || lastMessage.includes("analytics")) { return { message: "", toolCalls: [{ name: "get_funnel_analytics", arguments: { range_days: 30 } }], finishReason: "tool_calls" }; } if (lastMessage.includes("why") || lastMessage.includes("driver") || lastMessage.includes("cause")) { return { message: "", toolCalls: [{ name: "get_top_drivers", arguments: { metric: "conversion", range_days: 30 } }], finishReason: "tool_calls" }; } if (lastMessage.includes("status") || lastMessage.includes("health")) { return { message: "", toolCalls: [{ name: "get_service_status", arguments: { service_name: "my-service" } }], finishReason: "tool_calls" }; } if (lastMessage.includes("code") || lastMessage.includes("function") || lastMessage.includes("write") || lastMessage.includes("create")) { return { message: "", toolCalls: [{ name: "generate_code", arguments: { task: lastMessage, language: "typescript" } }], finishReason: "tool_calls" }; } // Default response return { message: `I'm Product OS, your AI assistant for building and operating SaaS products. I can help you: • **Deploy** services to Cloud Run • **Analyze** funnel metrics and conversions • **Generate** marketing content • **Understand** what drives user behavior What would you like to do? _(Note: Running in mock mode - set GEMINI_API_KEY for real AI responses)_`, finishReason: "stop" }; }