import { createClient } from "@supabase/supabase-js"; import type { Database } from "@/integrations/supabase/types"; import { ECHO_ENDPOINT, ECHO_INSTRUCTIONS, geminiFailure } from "./gemini-policy"; type Part = { text?: string; thought?: boolean; thoughtSignature?: string; functionCall?: { name: string; args?: unknown }; functionResponse?: { name: string; response: unknown } }; type Content = { role: string; parts: Part[] }; export type EchoMessage = { role: "user" | "assistant"; text: string }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function historyFor(client: any) { const { data, error } = await client.from("voice_conversations") .select("started_at, ended_at, turn_count, voice_messages(position, role, content)") .order("started_at", { ascending: false }).limit(50); if (error) throw new Error("History lookup failed."); return { total_conversations: (data ?? []).length, conversations: data }; } export async function history() { const url = process.env["SUPABASE_URL"]; const key = process.env["SUPABASE_PUBLISHABLE_KEY"]; if (!url || !key) throw new Error("History is unavailable."); const client = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false }, global: { fetch: (input, init) => { const headers = new Headers(init?.headers); if (key.startsWith("sb_") && headers.get("Authorization") === `Bearer ${key}`) headers.delete("Authorization"); headers.set("apikey", key); return fetch(input, { ...init, headers }); } }, }); const { data, error } = await client.from("voice_conversations") .select("started_at, ended_at, turn_count, voice_messages(position, role, content)") .order("started_at", { ascending: false }).limit(50); if (error) throw new Error("History lookup failed."); const { count } = await client.from("voice_conversations").select("id", { count: "exact", head: true }); return { total_conversations: count, conversations: data }; } export type AgentMode = "chat" | "plan" | "research" | "code"; const MODE_NOTES: Record = { chat: "", plan: "\nMode: PLANNING. Answer with a clear, numbered plan: goal, phases, concrete tasks, tools/stack, risks, and the first next step.", research: "\nMode: RESEARCH. Give a structured, well-reasoned briefing with short headings, key facts, trade-offs and a recommendation. Say clearly when something may be out of date or uncertain. Never invent sources or links.", code: "\nMode: CODING. Give complete, working code with brief explanations. Prefer modern, secure practices.", }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function answerWithGemini(messages: EchoMessage[], client?: any, mode: AgentMode = "chat") { const key = process.env["GEMINI_API_KEY"]; if (!key) return { answer: null, error: "Save your GEMINI_API_KEY before starting Echo." }; const contents: Content[] = messages.map(m => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.text }] })); try { // No automatic retries or paid fallback. Preserve complete model parts, including thought signatures, for tool continuations. for (let step = 0; step < 4; step++) { const response = await fetch(ECHO_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", "x-goog-api-key": key }, body: JSON.stringify({ systemInstruction: { parts: [{ text: ECHO_INSTRUCTIONS + MODE_NOTES[mode] }] }, contents, tools: [{ functionDeclarations: [{ name: "get_conversation_history", description: "Read the public saved conversation dashboard and total conversation count.", parameters: { type: "OBJECT", properties: {} } }] }], }), }); if (!response.ok) return { answer: null, error: geminiFailure(response.status) }; const result = await response.json() as { promptFeedback?: { blockReason?: string }; candidates?: { content?: Content; finishReason?: string }[] }; const candidate = result.candidates?.[0]; if (result.promptFeedback?.blockReason || !candidate?.content || candidate.finishReason && !["STOP", "MAX_TOKENS"].includes(candidate.finishReason)) { return { answer: null, error: "Google could not provide an answer to this request." }; } const calls = candidate.content.parts.filter(p => p.functionCall); if (!calls.length) { const answer = candidate.content.parts.filter(p => !p.thought).map(p => p.text ?? "").join("").trim(); return answer ? { answer, error: null } : { answer: null, error: "Gemini returned no answer." }; } contents.push(candidate.content); const parts: Part[] = []; for (const part of calls) { const call = part.functionCall; if (!call || call.name !== "get_conversation_history") return { answer: null, error: "Echo requested an unsupported action." }; parts.push({ functionResponse: { name: call.name, response: client ? await historyFor(client) : await history() } }); } contents.push({ role: "user", parts }); } return { answer: null, error: "Echo reached the task limit. Start a new call." }; } catch { return { answer: null, error: "Echo could not complete the request. Start a new call later." }; } }