feat: Gemini image generation in Workshop chat (#19)
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 1s

- Add image intent detection (draw/illustrate/visualize/create an image)
  via `detectImageRequest()` in agent.ts; exports used by jobs and sessions
- Add `executeImageWork()` to AgentService: calls Gemini generateImage with
  graceful fallback stub PNG when Gemini credentials are absent
- Add `job_media` table (migration 0010) for base64 image storage with 7-day TTL;
  entity_id is polymorphic for both jobs and session requests
- Add `media_type TEXT` column to jobs table (flagged during eval phase)
- Add `calculateImageFeeSats()` / `calculateImageFeeUsd()` to PricingService;
  uses IMAGE_GENERATION_FLAT_RATE_USD env var (default $0.04)
- Jobs route: detect image jobs in eval phase, route to Gemini in execution,
  store image in job_media; expose GET /api/jobs/:id/media endpoint
- Sessions route: detect image requests, call executeImageWork, store in
  job_media, return mediaUrl and mediaType in response
- Estimate route: return image pricing and mediaType:'image' for image requests
- Event bus: add optional mediaUrl/mediaType to job:completed event
- Frontend session.js: render generated images inline with download button

Fixes #19

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-23 22:36:10 -04:00
parent 1e2edeee77
commit f13a1d0235
11 changed files with 413 additions and 55 deletions

View File

@@ -2,6 +2,23 @@ import { makeLogger } from "./logger.js";
const logger = makeLogger("agent");
// ── Image request detection ───────────────────────────────────────────────────
const IMAGE_INTENT_RE =
/\b(draw|illustrate|create\s+an?\s+image\s+of|generate\s+an?\s+image\s+of|visualize|visualise|make\s+an?\s+image\s+of|paint\s+me|sketch|render\s+an?\s+image\s+of|picture\s+of)\b/i;
/**
* Returns true if the request text signals an image-generation intent.
*/
export function detectImageRequest(text: string): boolean {
return IMAGE_INTENT_RE.test(text);
}
export interface ImageWorkResult {
b64_json: string;
mimeType: string;
}
export interface EvalResult {
accepted: boolean;
reason: string;
@@ -442,6 +459,36 @@ Respond ONLY with valid JSON: {"accepted": true/false, "reason": "..."}`,
return "";
}
}
/**
* Generate an image via Gemini for the given prompt.
* Falls back to a stub 1×1 transparent PNG when Gemini credentials are absent.
*/
async executeImageWork(prompt: string): Promise<ImageWorkResult> {
const geminiAvailable =
!!process.env["AI_INTEGRATIONS_GEMINI_API_KEY"] &&
!!process.env["AI_INTEGRATIONS_GEMINI_BASE_URL"];
if (!geminiAvailable) {
logger.warn("Gemini credentials absent — returning stub image", { component: "agent" });
// 1×1 transparent PNG (base64)
return {
b64_json:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
mimeType: "image/png",
};
}
try {
const mod = (await import("@workspace/integrations-gemini-ai")) as {
generateImage: (prompt: string) => Promise<{ b64_json: string; mimeType: string }>;
};
return await mod.generateImage(prompt);
} catch (err) {
logger.error("Gemini image generation failed", { error: String(err) });
throw err;
}
}
}
export const agentService = new AgentService();