Files
alva/felsokning/supabase/functions/generate-leads/index.ts
T
Claude 301c477e25 Merge main: Guidad Felsökning flyttar ur roten till felsokning/
Main är sedan den här grenen skapades en helt annan produkt — Semantika,
en mobilapp med egen CDK-infrastruktur. Den äger nu repots rot: en
npm-workspaces-monorepo med apps/mobile, services/api och infra.

För att båda ska rymmas i samma repo flyttar Guidad Felsökning in i en
egen katalog i stället för att göra anspråk på roten:

  felsokning/app        webbklienten (Vite, egen package.json och
                        eslint-/vitest-konfiguration)
  felsokning/services   plattformstjänsten och AI-orkestern
  felsokning/infra      Terraform och databasschemat
  felsokning/docs       vision, moduler, drift
  felsokning/supabase   edge-funktion och migrationer

Merge:n hade tagit bort 128 filer som Guidad Felsökning bygger på —
värdapplikationens komponenter, Supabase-klienten, tillgångar — eftersom
main raderat dem och den här grenen inte råkat ändra just dem. De är
återställda på sin nya plats. Utan dem gick varken bygget eller
testerna: ai.ts och synk.ts importerar Supabase-klienten.

Semantikas rotfiler är orörda: package.json, eslint.config.js och
.github/workflows/ är deras. Guidad Felsökning har egna motsvarigheter i
sin katalog.

CI flyttar samtidigt från GitHub Actions till .gitea/workflows — samma
syntax, egna runners. .github/workflows/ tillhör Semantika härefter.

Verifierat på den nya platsen: 96 vitest-tester, typkontroll, eslint på
både klient och tjänster, bygge, och integrationstest mot riktig Postgres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-04 12:22:49 +00:00

134 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { requireAdmin } from "../_shared/admin-auth.ts";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform, x-supabase-client-platform-version, x-supabase-client-runtime, x-supabase-client-runtime-version",
};
const STORSTOCKHOLM = [
"Stockholm", "Solna", "Sundbyberg", "Täby", "Danderyd", "Lidingö",
"Nacka", "Sollentuna", "Järfälla", "Upplands Väsby", "Huddinge",
"Botkyrka", "Haninge", "Tyresö", "Nynäshamn", "Vallentuna", "Vaxholm",
"Österåker", "Ekerö", "Salem", "Nykvarn", "Södertälje",
];
serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
const auth = await requireAdmin(req, corsHeaders);
if (!auth.ok) return auth.response;
try {
const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
if (!LOVABLE_API_KEY) throw new Error("LOVABLE_API_KEY is not configured");
const response = 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: "google/gemini-3-flash-preview",
messages: [
{
role: "system",
content: `Du är B2B-säljare för Lennart Svenssons Konditorivaror, ett bageri i Stockholm som levererar färska handgjorda kakor på abonnemang till företag.
Din uppgift: identifiera potentiella B2B-kunder i STORSTOCKHOLM.
ABSOLUTA REGLER:
- Endast företag inom följande kommuner räknas som Storstockholm:
${STORSTOCKHOLM.map((k) => `${k}`).join("\n")}
- Företag utanför Storstockholm får ALDRIG genereras.
- Använd endast verifierbar, realistisk information. Hitta inte på fakta.
- Fokusera på kontor 10+ anställda, hotell, restauranger, coworking, eventlokaler, större företag med personalrum/fika.
- Variera bransch och kommun inte bara innerstaden.`,
},
{
role: "user",
content: "Generera 15 potentiella B2B-kunder för kakabonnemang i Storstockholm.",
},
],
tools: [
{
type: "function",
function: {
name: "generate_leads",
description: "Returnerar en lista med potentiella B2B-kunder i Storstockholm",
parameters: {
type: "object",
properties: {
leads: {
type: "array",
items: {
type: "object",
properties: {
company_name: { type: "string" },
industry: { type: "string" },
employee_count: { type: "number" },
district: {
type: "string",
description: "Kommun i Storstockholm (t.ex. Solna, Nacka, Södertälje)",
},
reason: { type: "string" },
},
required: ["company_name", "industry", "employee_count", "district", "reason"],
additionalProperties: false,
},
},
},
required: ["leads"],
additionalProperties: false,
},
},
},
],
tool_choice: { type: "function", function: { name: "generate_leads" } },
}),
});
if (!response.ok) {
if (response.status === 429) {
return new Response(JSON.stringify({ error: "Rate limit överskriden, försök igen senare." }), {
status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
if (response.status === 402) {
return new Response(JSON.stringify({ error: "Krediter slut, vänligen fyll på." }), {
status: 402, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
const errorText = await response.text();
console.error("AI gateway error:", response.status, errorText);
throw new Error(`AI gateway error: ${response.status}`);
}
const data = await response.json();
const toolCall = data.choices?.[0]?.message?.tool_calls?.[0];
if (!toolCall || toolCall.function.name !== "generate_leads") {
throw new Error("Unexpected AI response format");
}
const parsed = JSON.parse(toolCall.function.arguments);
// Extra säkerhet: filtrera bort allt utanför Storstockholm
const allowed = new Set(STORSTOCKHOLM.map((s) => s.toLowerCase()));
const leads = (parsed.leads || []).filter((l: any) =>
allowed.has(String(l.district || "").toLowerCase().trim()),
);
return new Response(JSON.stringify({ leads }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error generating leads:", error);
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return new Response(JSON.stringify({ error: errorMessage }), {
status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
});