feat: Gemini image generation in Workshop chat (#19)
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 1s
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user