This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
master-ai/platform/backend/control-plane/src/gemini.ts
mawkone 2c3e7f9dfb feat: add Turborepo per-project monorepo scaffold and project API
- Add Turborepo scaffold templates (apps: product, website, admin, storybook; packages: ui, tokens, types, config)
- Add ProjectRecord and AppRecord types to control plane
- Add Gitea integration service (repo creation, scaffold push, webhooks)
- Add Coolify integration service (project + per-app service provisioning with turbo --filter)
- Add project routes: GET/POST /projects, GET /projects/:id/apps, POST /projects/:id/deploy
- Update chat route to inject project/monorepo context into AI requests
- Add deploy_app and scaffold_app tools to Gemini tool set
- Update deploy executor with monorepo-aware /execute/deploy endpoint
- Add TURBOREPO_MIGRATION_PLAN.md documenting rationale and scope

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 15:07:35 -08:00

411 lines
13 KiB
TypeScript

/**
* 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<string, any>;
}
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=<app>
- 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<ChatResponse> {
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<ChatResponse> {
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<ChatResponse> {
// 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"
};
}