// Parser de descrições de cronjobs em linguagem natural. // Usa Lovable AI Gateway (gemini-2.5-flash) para extrair cron + ação + MCPs, // valida com cron-parser, gera human_readable em pt-BR via cronstrue. import { corsHeaders } from "../_shared/cors.ts"; import { createClient } from "npm:@supabase/supabase-js@2"; // @ts-expect-error remote esm import cronParser from "https://esm.sh/cron-parser@4.9.0"; // @ts-expect-error remote esm import cronstrue from "https://esm.sh/cronstrue@2.50.0/i18n"; const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY")!; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const MODEL = "google/gemini-2.5-flash"; const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); } const VALID_MCP_SLUGS = new Set([ "google_workspace", "notion", "todoist", "calcom", "microsoft_365", ]); function buildSystemPrompt(tz: string): string { return `Você é um parser de descrições de cronjobs em português para o assistente Mika. Receba uma descrição em linguagem natural e retorne APENAS JSON válido, sem markdown, sem explicações. Regras: - Timezone: ${tz} - 'dia útil' = segunda a sexta (1-5) - 'manhã' = 09:00, 'tarde' = 14:00, 'noite' = 19:00 (só se não especificado horário) - 'primeiro dia do mês' = dia 1 - 'último dia do mês' = dia 28 (com warning sobre aproximação) - Detecte MCPs mencionados (Gmail, Calendar, Drive, Notion, Todoist, Cal.com, Outlook, OneDrive) e mapeie para slugs: gmail→google_workspace, calendar→google_workspace, drive→google_workspace, notion→notion, todoist→todoist, cal.com→calcom, outlook→microsoft_365, onedrive→microsoft_365 - Confidence: 'high' se claro, 'medium' se fez suposições razoáveis, 'low' se vago Formato (APENAS este JSON): {"cron_expression": "0 9 * * 1", "human_readable": "(gerado pelo sistema, não preencha)", "action_description": "enviar resumo da semana", "required_mcp_slugs": ["google_workspace"], "warnings": ["Interpretei 'manhã' como 09:00"], "confidence": "high"}`; } interface ParsedJob { cron_expression: string; action_description: string; required_mcp_slugs: string[]; warnings: string[]; confidence: "high" | "medium" | "low"; } function tryParseJson(text: string): ParsedJob | null { // 1) direto try { return JSON.parse(text) as ParsedJob; } catch (_) { /* segue */ } // 2) extrai bloco { ... } const match = text.match(/\{[\s\S]*\}/); if (match) { try { return JSON.parse(match[0]) as ParsedJob; } catch (_) { /* falhou */ } } return null; } Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); // Auth const authHeader = req.headers.get("Authorization"); if (!authHeader) return json({ error: "Não autenticado" }, 401); const token = authHeader.replace("Bearer ", ""); const { data: userData, error: userErr } = await admin.auth.getUser(token); if (userErr || !userData?.user) return json({ error: "Não autenticado" }, 401); let body: { natural_language_input?: string; user_timezone?: string }; try { body = await req.json(); } catch { return json({ error: "JSON inválido" }, 400); } const input = (body.natural_language_input ?? "").trim(); const tz = (body.user_timezone ?? "America/Sao_Paulo").trim() || "America/Sao_Paulo"; if (input.length < 5) { return json({ error: "Descrição muito curta. Tente algo mais detalhado." }, 400); } if (input.length > 1000) { return json({ error: "Descrição muito longa (máx 1000 caracteres)." }, 400); } // Chama Lovable AI Gateway let aiRes: Response; try { aiRes = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${LOVABLE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: MODEL, max_tokens: 500, messages: [ { role: "system", content: buildSystemPrompt(tz) }, { role: "user", content: input }, ], }), }); } catch (e) { console.error("AI fetch error", e instanceof Error ? e.message : "unknown"); return json({ error: "Falha ao contatar IA. Tente novamente." }, 502); } if (aiRes.status === 429) { return json({ error: "Muitas requisições. Aguarde 1 minuto." }, 429); } if (aiRes.status === 402) { return json({ error: "Créditos de IA esgotados. Adicione créditos no workspace." }, 402); } if (!aiRes.ok) { console.error("AI gateway error status", aiRes.status); return json({ error: "Falha ao processar com IA." }, 502); } const aiData = await aiRes.json(); const content: string = aiData?.choices?.[0]?.message?.content ?? ""; const parsed = tryParseJson(content); if (!parsed) { return json( { error: "Não conseguimos entender a descrição. Tente reescrever de forma mais clara." }, 422, ); } // Sanitiza required_mcp_slugs const reqSlugs = Array.isArray(parsed.required_mcp_slugs) ? parsed.required_mcp_slugs.filter((s): s is string => typeof s === "string" && VALID_MCP_SLUGS.has(s)) : []; const warnings = Array.isArray(parsed.warnings) ? parsed.warnings.filter((w): w is string => typeof w === "string") : []; const confidence: "high" | "medium" | "low" = parsed.confidence === "high" || parsed.confidence === "medium" || parsed.confidence === "low" ? parsed.confidence : "medium"; const cron = (parsed.cron_expression ?? "").trim(); if (!cron) { return json({ error: "IA não retornou expressão cron. Tente reescrever." }, 422); } // Valida cron e calcula next_run_at let nextRunAt: string | null = null; try { const interval = cronParser.parseExpression(cron, { tz }); nextRunAt = interval.next().toDate().toISOString(); } catch (e) { return json( { error: `Expressão cron inválida: ${e instanceof Error ? e.message : "erro"}` }, 422, ); } // Gera human_readable em pt-BR let humanReadable = ""; try { humanReadable = cronstrue.toString(cron, { locale: "pt_BR" }); } catch (_) { humanReadable = cron; // fallback } const action = (parsed.action_description ?? "").trim(); return json({ cron_expression: cron, human_readable: humanReadable, action_description: action, required_mcp_slugs: reqSlugs, warnings, confidence, next_run_at: nextRunAt, }); });