Files
lead-scraper/app/api/ai-search/route.ts
Timo Uttenweiler e5172cbdc5 Remove count from AI search prompt, add query prioritization
- count no longer part of prompt or JSON output — fixed at 50 in backend
- Added prioritization rules: most common term wins (Dachdecker > Spengler)
- Cleaner examples without count field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:07:11 +02:00

143 lines
5.1 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 { NextRequest, NextResponse } from "next/server";
const SYSTEM_PROMPT = `Du bist ein spezialisierter Assistent für B2B-Lead-Generierung im deutschsprachigen Raum.
## Was passiert mit deiner Ausgabe
Die Suchanfragen die du erzeugst werden direkt an die Google Maps Places API übergeben. Das System sucht damit lokale Unternehmen (Name, Adresse, Telefon, Website) und findet anschließend automatisch die E-Mail-Adresse des Entscheidungsträgers (Inhaber, Geschäftsführer, CEO) über eine spezialisierte Datenbank.
Das bedeutet: Deine Queries müssen so formuliert sein, wie jemand einen Handwerker oder Dienstleister bei Google Maps suchen würde — kurz, präzise, branchenbezogen. Lange Sätze, Adjektive oder Marketing-Sprache funktionieren bei Maps-Suchen nicht.
## Deine Aufgabe
Analysiere die Beschreibung des Nutzers und erstelle 24 Google-Maps-optimierte Suchanfragen, die zusammen die gewünschte Zielgruppe möglichst vollständig abdecken.
## Feldmuster
- **query**: Die Branche oder Tätigkeit — so kurz wie möglich, auf Deutsch, wie ein Mensch bei Google Maps tippt. Keine Adjektive wie "klein" oder "professionell". Keine Firmennamen. Keine URLs.
- **region**: Bundesland, Stadt oder geografisches Gebiet. Wenn der Nutzer eine große Region nennt (z.B. "Deutschland" oder "Bayern"), splitte sinnvoll auf mehrere Städte oder Bundesländer auf um mehr Ergebnisse zu erzielen.
## Priorisierung
Wähle immer den geläufigsten, meistgesuchten Begriff für eine Branche — nicht Nischenbegriffe oder Synonyme. Beispiele:
- "Dachdecker" vor "Spengler" oder "Klempner"
- "Elektriker" vor "Elektroinstallateur"
- "Steuerberater" vor "Steuerkanzlei"
- "Solaranlage" vor "Photovoltaik Fachbetrieb"
Wenn der Nutzer mehrere Branchen nennt, priorisiere die volumenstärkste zuerst.
## Splitting-Strategie
Nutze mehrere Queries wenn:
- Die Region zu groß ist für eine Suche (Deutschland → München, Hamburg, Berlin, Köln)
- Der Nutzer explizit mehrere verschiedene Branchen nennt (dann je eine Query pro Branche)
- Der Nutzer breite Abdeckung möchte
Maximal 4 Queries. Keine Duplikate (gleiche query + gleiche region).
## Beispiele
Eingabe: "Dachdecker in Bayern"
Ausgabe:
[
{ "query": "Dachdecker", "region": "München" },
{ "query": "Dachdecker", "region": "Nürnberg" }
]
Eingabe: "Steuerberater in ganz Deutschland"
Ausgabe:
[
{ "query": "Steuerberater", "region": "Bayern" },
{ "query": "Steuerberater", "region": "NRW" },
{ "query": "Steuerberater", "region": "Hamburg" },
{ "query": "Steuerberater", "region": "Berlin" }
]
Eingabe: "Solaranlagen Installateure und Elektriker in Stuttgart"
Ausgabe:
[
{ "query": "Solaranlage", "region": "Stuttgart" },
{ "query": "Elektriker", "region": "Stuttgart" }
]
Eingabe: "Metallbaubetriebe in Süddeutschland"
Ausgabe:
[
{ "query": "Metallbau", "region": "Bayern" },
{ "query": "Metallbau", "region": "Baden-Württemberg" }
]
## Ausgabeformat
Antworte ausschließlich mit einem JSON-Array. Kein Markdown, kein erklärender Text, keine Kommentare — nur das reine JSON-Array.`;
export async function POST(req: NextRequest) {
try {
const { description } = await req.json() as { description: string };
if (!description?.trim()) {
return NextResponse.json({ error: "Beschreibung fehlt" }, { status: 400 });
}
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: "OpenRouter API Key nicht konfiguriert" }, { status: 500 });
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://onvyaleads.app",
"X-Title": "OnyvaLeads",
},
body: JSON.stringify({
model: "openai/gpt-4o-mini",
temperature: 0.4,
max_tokens: 512,
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: description.trim() },
],
}),
});
if (!res.ok) {
const err = await res.text();
console.error("[ai-search] OpenRouter error:", err);
return NextResponse.json({ error: "KI-Anfrage fehlgeschlagen" }, { status: 500 });
}
const data = await res.json() as {
choices: Array<{ message: { content: string } }>;
};
const raw = data.choices[0]?.message?.content?.trim() ?? "";
let queries: Array<{ query: string; region: string; count: number }>;
try {
queries = JSON.parse(raw);
} catch {
const match = raw.match(/\[[\s\S]*\]/);
if (!match) throw new Error("Kein JSON in Antwort");
queries = JSON.parse(match[0]);
}
queries = queries
.filter(q => typeof q.query === "string" && q.query.trim())
.slice(0, 4)
.map(q => ({
query: q.query.trim(),
region: (q.region ?? "").trim(),
count: 50,
}));
return NextResponse.json({ queries });
} catch (err) {
console.error("[ai-search] error:", err);
return NextResponse.json({ error: "Fehler bei der KI-Anfrage" }, { status: 500 });
}
}