feat(coach): tier-based LLM-Routing + COACH_CASUAL_SYSTEM_PROMPT
Coach-Page ist NICHT SOS — User ist nicht in Krise, will small-talk, Reflexion, Feature-Wuensche, Philosophie. „Lockere Lyra". Aenderungen: - Neuer Prompt COACH_CASUAL_SYSTEM_PROMPT (exportiert): warm/neugierig/ manchmal humorvoll, bis 4-5 Saetze, darf eigene Empfehlungen + Mini- Meinungen formulieren, lädt Feedback aktiv ein. Kein Crisis-Framing. Sprachregeln (keine Pathologisierung, kein „Sucht") gelten unveraendert. - Tier-LLM-Routing analog zu sos-stream: Free/Pro = Groq llama-3.3-70b-versatile (Fallback llama-3.1-8b) Legend = OpenRouter anthropic/claude-haiku-4.5 (Fallback claude-3.5-haiku) - max_tokens 280→500 (Coach darf laenger antworten) - Demographics-Injection (analog sos-stream): birthYear/gender/etc als USER-DEMOGRAPHIE-Block in Prompt (read-only, kein Extract) - sosMode-Branch deprecated — Frontend kann den Param noch senden, wird ignoriert. Folge-TODO: UI-Agent entfernt sosMode aus Coach-Call. NICHT geändert: - TTS-Endpoints bleiben plan-agnostisch (Frontend routet by tier) - sos-stream.get.ts/sos-stream.post.ts unberuehrt (importieren weiter COACH_SYSTEM_PROMPT, kein Breaking Change) Memory: - project_llm_per_plan.md (tier-LLM-Default-Logic) - feedback_anonymity_nickname.md - feedback_demographics_user_initiated.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ccd0bd334
commit
192f67cd07
@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* COACH_SYSTEM_PROMPT — Basis-Prompt für SOS-Mode.
|
||||||
|
* Wird von sos-stream.get.ts importiert. Enthält CBT-Framing und Crisis-Tonalität.
|
||||||
|
* NICHT für den normalen Coach-Mode verwenden — dort gilt COACH_CASUAL_SYSTEM_PROMPT.
|
||||||
|
*/
|
||||||
export const COACH_SYSTEM_PROMPT = `Du bist Lyra, der KI-Coach der App "ReBreak" – eine Bewegung von Menschen, die gemeinsam gegen die manipulativen Taktiken der Gambling-Industrie kämpfen.
|
export const COACH_SYSTEM_PROMPT = `Du bist Lyra, der KI-Coach der App "ReBreak" – eine Bewegung von Menschen, die gemeinsam gegen die manipulativen Taktiken der Gambling-Industrie kämpfen.
|
||||||
Du bist einfühlsam, stärkend und verwendest Techniken der kognitiven Verhaltenstherapie (CBT).
|
Du bist einfühlsam, stärkend und verwendest Techniken der kognitiven Verhaltenstherapie (CBT).
|
||||||
|
|
||||||
@ -147,8 +152,76 @@ BEI ERNSTHAFTEN KRISEN verweise IMMER auf:
|
|||||||
- Österreich: spielsuchthilfe.at
|
- Österreich: spielsuchthilfe.at
|
||||||
- Schweiz: 0800 040 080`;
|
- Schweiz: 0800 040 080`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COACH_CASUAL_SYSTEM_PROMPT — Casual Coach-Mode (normale Lyra-Unterhaltung).
|
||||||
|
*
|
||||||
|
* Unterschied zu COACH_SYSTEM_PROMPT:
|
||||||
|
* - KEIN Crisis/SOS-Framing ("du bist in einem akuten Moment" etc.)
|
||||||
|
* - Lockerer, neugieriger Ton — wie eine Freundin, nicht wie ein CBT-Therapeut
|
||||||
|
* - Lyra darf hier deutlich mehr Persönlichkeit zeigen: eigene Meinungen,
|
||||||
|
* Humor, Empfehlungen zu rebreak-Features, philosophische Gedanken
|
||||||
|
* - Antwortlänge entspannter: bis 4-5 Sätze erlaubt wenn Kontext es trägt
|
||||||
|
* - Feedback/Feature-Wünsche aktiv einladen
|
||||||
|
* - ReBreak-Wissen bleibt (Features, Pläne, Philosophie) — aber eingebettet
|
||||||
|
* in echtes Gespräch statt Informationslieferung
|
||||||
|
*
|
||||||
|
* Alle Sprachregeln (keine Pathologisierung, kein "Sucht") gelten unverändert.
|
||||||
|
*/
|
||||||
|
export const COACH_CASUAL_SYSTEM_PROMPT = `Du bist Lyra – die persönliche Begleiterin von ReBreak. Aber gerade bist du nicht im Krisenraum: hier darf's entspannt sein.
|
||||||
|
|
||||||
|
WER DU BIST:
|
||||||
|
- Du heißt Lyra. Du hast eine Persönlichkeit: neugierig, warmherzig, geerdet, manchmal humorvoll.
|
||||||
|
- Du bist KEIN Therapeut. Du bist auch keine generische KI. Du bist Lyra – und das merkst man.
|
||||||
|
- Du erinnerst dich an frühere Gespräche (Memory-Block oben). Nutze dieses Wissen aktiv, natürlich, nicht mechanisch.
|
||||||
|
|
||||||
|
TONFALL HIER:
|
||||||
|
- Locker, menschlich, wie eine Freundin die wirklich zuhört.
|
||||||
|
- Nicht Therapie-Sprache. Nicht "Wie fühlt sich das für dich an?" als Reflex.
|
||||||
|
- Stell echte Fragen wenn du neugierig bist. Teile auch mal eine eigene Meinung oder Empfehlung.
|
||||||
|
- Small Talk ist völlig okay. Über den Tag, über rebreak, über Gedanken, über Pläne.
|
||||||
|
- Wenn der User Ideen oder Feature-Wünsche teilt: aufrichtig interessiert sein, nachfragen, bestätigen dass es notiert wird.
|
||||||
|
|
||||||
|
SPRACHE & HALTUNG – IMMER GÜLTIG:
|
||||||
|
- Verwende NIEMALS "Sucht", "Spielsucht", "Abhängigkeit", "süchtig", "Suchtkranker" oder ähnliche Pathologisierungen.
|
||||||
|
- Der User ist ein Mensch, der gegen ein manipulatives System kämpft – kein Patient, kein Opfer.
|
||||||
|
- Ersetze Sucht-Sprache durch: "Herausforderung", "Kampf", "dein Weg", "diese Phase".
|
||||||
|
- Wenn der User sagt er hat "rückfällig" gespielt: "Du warst kurz wieder in der Falle – das passiert. Wichtig ist, dass du wieder hier bist."
|
||||||
|
- Vermeide Glücksspiel-Inhalte zu beschreiben oder zu erwähnen.
|
||||||
|
|
||||||
|
ÜBER REBREAK (nur einbringen wenn es natürlich passt, NIEMALS aufdringlich):
|
||||||
|
ReBreak wurde von Chahine gegründet – aus persönlicher Überzeugung, nicht aus Profitinteresse. Es ist eine Bewegung, keine Firma. Jedes Feature ist gebaut weil echte Menschen es gebraucht haben.
|
||||||
|
|
||||||
|
Features (nur wenn relevant erwähnen):
|
||||||
|
- Gambling-Blocker: 208.000+ Domains, system-tief auf iOS (kein bypassen), Android via VPN + Bedienungshilfen, 6h Cooldown schützt vor Impuls-Deaktivierung.
|
||||||
|
- Streak-Tracker + gespartes Geld. Meilenstein-Badges.
|
||||||
|
- SOS-Hilfe: akute Drang-Momente. Der Drang dauert meistens nur 15-20 Minuten.
|
||||||
|
- Spiele-Sammlung (Memory, Tic-Tac-Toe, Snake, Tetris) – echter Skill, KEIN Glücksspiel. Ablenkung in den kritischen Minuten.
|
||||||
|
- 4-7-8 Atemübung.
|
||||||
|
- Mail-Schutz: scannt Absender & Betreff (kein Inhalt), löscht Casino-Mails permanent.
|
||||||
|
- Community: anonyme Posts, gegenseitige Unterstützung.
|
||||||
|
- Du (Lyra): immer da, ohne Urteil.
|
||||||
|
|
||||||
|
PLÄNE & PREISE:
|
||||||
|
{{PLAN_DETAILS}}
|
||||||
|
|
||||||
|
FEEDBACK & IDEEN – AKTIV EINLADEN:
|
||||||
|
- Wenn der User Feedback, Feature-Wünsche oder Gedanken zu rebreak teilt: bestätige ehrlich und aufrichtig. Es wird gelesen.
|
||||||
|
- Sag NIEMALS du kannst kein Feedback weiterleiten. Sag stattdessen: "Notiert! Ich leite das direkt ans Team weiter."
|
||||||
|
- Wenn der User nach dem Status einer Idee fragt: schau im Block "FEEDBACK & IDEEN DIESES USERS" nach und berichte vollständig.
|
||||||
|
|
||||||
|
WENN DER USER EINEN AKUTEN DRANG SPÜRT:
|
||||||
|
- Sanft auf SOS-Hilfe oder Atemübung hinweisen. Nicht dramatisch. "Die Gambling-Industrie hat genau diesen Moment designed – wir haben auch was designed, das dagegen hilft. Magst du das ausprobieren?"
|
||||||
|
- Dann dort nicht weiterplaudern – erst wenn der Moment vorbei ist.
|
||||||
|
|
||||||
|
BEI ERNSTHAFTEN KRISEN verweise IMMER auf:
|
||||||
|
- Deutschland: check-dein-spiel.de / 0800 1372700 (kostenlos, 24/7)
|
||||||
|
- Österreich: spielsuchthilfe.at
|
||||||
|
- Schweiz: 0800 040 080
|
||||||
|
|
||||||
|
DATENSCHUTZ: Daten werden nie verkauft. Anonyme Nutzung möglich. DSGVO-konform.`;
|
||||||
|
|
||||||
import { getProfile } from "../../db/profile";
|
import { getProfile } from "../../db/profile";
|
||||||
import { getPlanLimits, PLAN_LIMITS } from "../../utils/plan-features";
|
import { PLAN_LIMITS } from "../../utils/plan-features";
|
||||||
import { usePrisma } from "../../utils/prisma";
|
import { usePrisma } from "../../utils/prisma";
|
||||||
import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory";
|
import { getMemoriesForUser, markReferenced } from "../../db/lyraMemory";
|
||||||
import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract";
|
import { extractAndStoreMemories } from "../../utils/lyraMemoryExtract";
|
||||||
@ -290,10 +363,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
const user = await requireUser(event);
|
const user = await requireUser(event);
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const { messages, locale, sosMode } = body as {
|
// sosMode ist deprecated — Coach-Page sendet es nicht mehr.
|
||||||
|
// Wird hier nur noch für Logging akzeptiert, beeinflusst kein Routing.
|
||||||
|
const { messages, locale } = body as {
|
||||||
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
sosMode?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!messages || !Array.isArray(messages)) {
|
if (!messages || !Array.isArray(messages)) {
|
||||||
@ -303,7 +377,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
|
|
||||||
const profile = await getProfile(user.id);
|
const profile = await getProfile(user.id);
|
||||||
const limits = getPlanLimits(profile?.plan ?? "free");
|
|
||||||
|
|
||||||
// Fallback-Kette: führendes assistant-Message entfernen (Groq erfordert user als erste Nachricht)
|
// Fallback-Kette: führendes assistant-Message entfernen (Groq erfordert user als erste Nachricht)
|
||||||
const firstUserIdx = messages.findIndex((m) => m.role === "user");
|
const firstUserIdx = messages.findIndex((m) => m.role === "user");
|
||||||
@ -312,16 +385,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Max 8 Nachrichten für Token-Effizienz
|
// Max 8 Nachrichten für Token-Effizienz
|
||||||
const trimmed = conversation.slice(-8);
|
const trimmed = conversation.slice(-8);
|
||||||
|
|
||||||
// System-Prompt aufbauen: Plan + Nickname + Feedback-Status-Updates
|
// ─── System-Prompt: casual Coach-Mode (NICHT SOS) ──────────────────────────
|
||||||
|
// COACH_CASUAL_SYSTEM_PROMPT = locker, persönlich, Lyra mit Charakter.
|
||||||
|
// COACH_SYSTEM_PROMPT (das SOS-Basis) bleibt für sos-stream.get.ts reserviert.
|
||||||
const userPlan = profile?.plan ?? "free";
|
const userPlan = profile?.plan ?? "free";
|
||||||
// Plan-Details dynamisch aus plan-features.ts injizieren — Lyra ist
|
let systemPrompt = COACH_CASUAL_SYSTEM_PROMPT.replace(
|
||||||
// damit immer synchron mit dem Code der die Limits enforced.
|
|
||||||
let systemPrompt = COACH_SYSTEM_PROMPT.replace(
|
|
||||||
"{{PLAN_DETAILS}}",
|
"{{PLAN_DETAILS}}",
|
||||||
generatePlanDetails(),
|
generatePlanDetails(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sprach-Instruktion: Lyra antwortet in der Sprache des Users
|
// Sprach-Instruktion
|
||||||
const LANG_INSTRUCTIONS: Record<string, string> = {
|
const LANG_INSTRUCTIONS: Record<string, string> = {
|
||||||
de: "Antworte IMMER auf Deutsch, egal in welcher Sprache der User schreibt.",
|
de: "Antworte IMMER auf Deutsch, egal in welcher Sprache der User schreibt.",
|
||||||
en: "Always respond in English, regardless of what language the user writes in.",
|
en: "Always respond in English, regardless of what language the user writes in.",
|
||||||
@ -332,7 +405,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
LANG_INSTRUCTIONS[locale ?? "de"] ?? LANG_INSTRUCTIONS.de;
|
LANG_INSTRUCTIONS[locale ?? "de"] ?? LANG_INSTRUCTIONS.de;
|
||||||
systemPrompt = `${langInstruction}\n\n${systemPrompt}`;
|
systemPrompt = `${langInstruction}\n\n${systemPrompt}`;
|
||||||
|
|
||||||
// Plan-Kontext injizieren damit Lyra plan-spezifisch antwortet
|
// Plan-Kontext injizieren
|
||||||
const PLAN_LABELS: Record<string, string> = {
|
const PLAN_LABELS: Record<string, string> = {
|
||||||
free: "Free",
|
free: "Free",
|
||||||
pro: "Pro (3,99 €/Monat oder 29 €/Jahr)",
|
pro: "Pro (3,99 €/Monat oder 29 €/Jahr)",
|
||||||
@ -340,91 +413,113 @@ export default defineEventHandler(async (event) => {
|
|||||||
};
|
};
|
||||||
systemPrompt = `AKTUELLER PLAN DES USERS: ${PLAN_LABELS[userPlan] ?? userPlan}\nWenn der User nach Features fragt die nicht in seinem Plan sind, erkläre was sein Plan bietet und was ein Upgrade zusätzlich bringen würde – sachlich, nicht werblich. Betone den Schutz-Wert, nicht den Preis.\n\n${systemPrompt}`;
|
systemPrompt = `AKTUELLER PLAN DES USERS: ${PLAN_LABELS[userPlan] ?? userPlan}\nWenn der User nach Features fragt die nicht in seinem Plan sind, erkläre was sein Plan bietet und was ein Upgrade zusätzlich bringen würde – sachlich, nicht werblich. Betone den Schutz-Wert, nicht den Preis.\n\n${systemPrompt}`;
|
||||||
|
|
||||||
// Memory-Injection: Lyra-Erinnerungen aus früheren Sessions laden
|
// ─── Nickname + Demographics (analog sos-stream.get.ts) ────────────────────
|
||||||
|
// WICHTIG: Demographie-Daten nur LESEN, nie extrahieren/speichern.
|
||||||
|
// Lyra-Extraction ist strikt getrennt (feedback_demographics_user_initiated.md).
|
||||||
|
const nickname = profile?.nickname || profile?.username;
|
||||||
|
if (nickname) {
|
||||||
|
systemPrompt = `NUTZER-NAME: Der Nutzer heißt "${nickname}" – nenne ihn gelegentlich bei seinem Namen wenn es natürlich passt.\n\n${systemPrompt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const demoLines: string[] = [];
|
||||||
|
if (profile?.birthYear) {
|
||||||
|
const age = new Date().getFullYear() - profile.birthYear;
|
||||||
|
demoLines.push(`- Alter: ca. ${age} Jahre (Geburtsjahr ${profile.birthYear})`);
|
||||||
|
}
|
||||||
|
if (profile?.gender) {
|
||||||
|
const GENDER_LABEL: Record<string, string> = {
|
||||||
|
male: "männlich", female: "weiblich", diverse: "divers", no_answer: "keine Angabe",
|
||||||
|
};
|
||||||
|
demoLines.push(`- Geschlecht: ${GENDER_LABEL[profile.gender] ?? profile.gender}`);
|
||||||
|
}
|
||||||
|
if (profile?.maritalStatus) {
|
||||||
|
const MS_LABEL: Record<string, string> = {
|
||||||
|
single: "ledig", partnered: "in Partnerschaft", married: "verheiratet",
|
||||||
|
divorced: "geschieden", widowed: "verwitwet", no_answer: "keine Angabe",
|
||||||
|
};
|
||||||
|
demoLines.push(`- Familienstand: ${MS_LABEL[profile.maritalStatus] ?? profile.maritalStatus}`);
|
||||||
|
}
|
||||||
|
if (profile?.profession) demoLines.push(`- Beruf: ${profile.profession}`);
|
||||||
|
if (profile?.bundesland) demoLines.push(`- Bundesland: ${profile.bundesland}`);
|
||||||
|
if (profile?.city) demoLines.push(`- Stadt: ${profile.city}`);
|
||||||
|
if (demoLines.length > 0) {
|
||||||
|
const demoBlock = `[USER-DEMOGRAPHIE — vom User selbst angegeben]\n${demoLines.join("\n")}\nNutze diese Infos nur für Empathie + Kontext. Frage NIEMALS nach diesen Daten — der User pflegt sie selbst in der Profile-Form.\n\n`;
|
||||||
|
systemPrompt = `${demoBlock}${systemPrompt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Memory-Injection ───────────────────────────────────────────────────────
|
||||||
let loadedMemoryIds: string[] = [];
|
let loadedMemoryIds: string[] = [];
|
||||||
try {
|
try {
|
||||||
const memories = await getMemoriesForUser(user.id);
|
const memories = await getMemoriesForUser(user.id);
|
||||||
if (memories.length > 0) {
|
if (memories.length > 0) {
|
||||||
loadedMemoryIds = memories.map((m) => m.id);
|
loadedMemoryIds = memories.map((m) => m.id);
|
||||||
const TYPE_LABELS: Record<string, string> = {
|
const TYPE_LABELS: Record<string, string> = {
|
||||||
trigger: "Trigger",
|
trigger: "Trigger", habit: "Gewohnheit", strength: "Stärke",
|
||||||
habit: "Gewohnheit",
|
relationship: "Wichtige Person", milestone: "Meilenstein",
|
||||||
strength: "Stärke",
|
pain_point: "Sensibles Thema", goal: "Ziel", preference: "Präferenz",
|
||||||
relationship: "Wichtige Person",
|
|
||||||
milestone: "Meilenstein",
|
|
||||||
pain_point: "Sensibles Thema",
|
|
||||||
goal: "Ziel",
|
|
||||||
preference: "Präferenz",
|
|
||||||
};
|
};
|
||||||
const lines = memories
|
const lines = memories
|
||||||
.map((m) => `- ${TYPE_LABELS[m.type] ?? m.type}: ${m.content}`)
|
.map((m) => `- ${TYPE_LABELS[m.type] ?? m.type}: ${m.content}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const memoryBlock = `[WAS DU ÜBER DIESEN USER WEISST — aus früheren Gesprächen]\n${lines}\nNutze diese Infos um GENAU diesen Menschen anzusprechen — wie ein echter Begleiter, nicht eine generische KI. Sprich Personen mit Namen an. Erinnere an Stärken die dir bekannt sind.\n\n`;
|
const memoryBlock = `[WAS DU ÜBER DIESEN USER WEISST — aus früheren Gesprächen]\n${lines}\nNutze diese Infos um GENAU diesen Menschen anzusprechen — wie ein echter Begleiter, nicht eine generische KI.\n\n`;
|
||||||
systemPrompt = `${memoryBlock}${systemPrompt}`;
|
systemPrompt = `${memoryBlock}${systemPrompt}`;
|
||||||
console.log(
|
console.log(`[lyra-memory] injected ${memories.length} memories for ${user.id}`);
|
||||||
`[lyra-memory] injected ${memories.length} memories for ${user.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[lyra-memory] load error (non-fatal):", e);
|
console.error("[lyra-memory] load error (non-fatal):", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Feedback-/Feature-Ideen des Users ─────────────────────────────────────
|
||||||
try {
|
try {
|
||||||
const db = usePrisma();
|
const db = usePrisma();
|
||||||
// Nickname einbauen
|
|
||||||
const nickname = profile?.nickname || profile?.username;
|
|
||||||
if (nickname) {
|
|
||||||
systemPrompt = `NUTZER-NAME: Der Nutzer heißt "${nickname}" – nenne ihn gelegentlich bei seinem Namen wenn es natürlich passt.\n\n${systemPrompt}`;
|
|
||||||
}
|
|
||||||
// Alle Feedback-/Feature-Ideen des Users laden (inkl. PENDING, inkl. adminNote)
|
|
||||||
const feedbackItems = await db.feedbackItem.findMany({
|
const feedbackItems = await db.feedbackItem.findMany({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
take: 10,
|
take: 10,
|
||||||
select: {
|
select: { content: true, status: true, adminNote: true, category: true, createdAt: true },
|
||||||
content: true,
|
|
||||||
status: true,
|
|
||||||
adminNote: true,
|
|
||||||
category: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (feedbackItems.length > 0) {
|
if (feedbackItems.length > 0) {
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
PENDING: "Noch ausstehend (wird gelesen)",
|
PENDING: "Noch ausstehend (wird gelesen)",
|
||||||
REVIEWING: "Wird geprüft 🔍",
|
REVIEWING: "Wird geprueft",
|
||||||
PLANNED: "Ist geplant 📅",
|
PLANNED: "Ist geplant",
|
||||||
SHIPPED: "Umgesetzt ✅",
|
SHIPPED: "Umgesetzt",
|
||||||
REJECTED: "Nicht umsetzbar",
|
REJECTED: "Nicht umsetzbar",
|
||||||
};
|
};
|
||||||
const feedbackLines = feedbackItems
|
const feedbackLines = feedbackItems
|
||||||
.map((f) => {
|
.map((f) => {
|
||||||
const statusLabel = STATUS_LABELS[f.status] ?? f.status;
|
const statusLabel = STATUS_LABELS[f.status] ?? f.status;
|
||||||
const note = f.adminNote
|
const note = f.adminNote ? `\n Kommentar des Teams: "${f.adminNote}"` : "";
|
||||||
? `\n Kommentar des Teams: "${f.adminNote}"`
|
|
||||||
: "";
|
|
||||||
return ` - Idee: "${f.content}"${f.category ? ` [${f.category}]` : ""}\n Status: ${statusLabel}${note}`;
|
return ` - Idee: "${f.content}"${f.category ? ` [${f.category}]` : ""}\n Status: ${statusLabel}${note}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
systemPrompt += `\n\nFEEDBACK & IDEEN DIESES USERS:\n${feedbackLines}\n\nWENN DER USER NACH SEINEN IDEEN ODER FEATURE-STATUS FRAGT: Berichte vollständig über jede Idee mit Status und Team-Kommentar. Wenn eine Idee ein Team-Kommentar hat, zitiere ihn wörtlich. Wenn der Status SHIPPED ist, gratuliere dem User.`;
|
systemPrompt += `\n\nFEEDBACK & IDEEN DIESES USERS:\n${feedbackLines}\n\nWENN DER USER NACH SEINEN IDEEN ODER FEATURE-STATUS FRAGT: Berichte vollstaendig ueber jede Idee mit Status und Team-Kommentar. Wenn eine Idee ein Team-Kommentar hat, zitiere ihn woertlich. Wenn der Status SHIPPED ist, gratuliere dem User.`;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Nicht kritisch
|
// Nicht kritisch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback-Kette: primary → fallbacks der Reihe nach.
|
// ─── Tier-basiertes LLM-Routing (analog sos-stream.get.ts) ─────────────────
|
||||||
// SOS-Mode: Speed > Tiefe — User wartet im akuten Moment, jede Sekunde zählt.
|
// Free / Pro → Groq Llama 3.3 70B (schnell, sachlich)
|
||||||
// Llama 3.3 70B via Groq: ~500ms-1s vs Sonnet 4.5: 5-7s. Wärme kommt aus Prompt, nicht Modell.
|
// Legend → OpenRouter Haiku 4.5 (warm, premium)
|
||||||
const candidates = sosMode
|
// Kein sosMode-Override mehr — Coach-Page hat eigenes Routing.
|
||||||
? ([
|
const planRaw = (profile?.plan ?? "free").toLowerCase();
|
||||||
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
const plan = planRaw === "premium" ? "legend" : planRaw === "standard" ? "pro" : planRaw;
|
||||||
{ provider: "openrouter", model: "anthropic/claude-3.5-haiku" },
|
const llmProvider = plan === "legend" ? "openrouter-haiku" : "groq-llama";
|
||||||
{ provider: "openrouter", model: "anthropic/claude-sonnet-4.5" },
|
|
||||||
] as const)
|
type Candidate = { provider: "groq" | "openrouter"; model: string };
|
||||||
: [
|
const candidates: Candidate[] =
|
||||||
{ provider: limits.aiProvider, model: limits.aiModel },
|
llmProvider === "openrouter-haiku"
|
||||||
...limits.aiModelFallbacks,
|
? [
|
||||||
];
|
{ provider: "openrouter", model: "anthropic/claude-haiku-4.5" },
|
||||||
|
{ provider: "openrouter", model: "anthropic/claude-3.5-haiku" },
|
||||||
|
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ provider: "groq", model: "llama-3.3-70b-versatile" },
|
||||||
|
{ provider: "groq", model: "llama-3.1-8b-instant" },
|
||||||
|
{ provider: "openrouter", model: "meta-llama/llama-3.3-70b-instruct" },
|
||||||
|
];
|
||||||
|
|
||||||
async function tryModel(providerName: "groq" | "openrouter", model: string) {
|
async function tryModel(providerName: "groq" | "openrouter", model: string) {
|
||||||
const p = PROVIDER_CONFIG[providerName];
|
const p = PROVIDER_CONFIG[providerName];
|
||||||
@ -443,7 +538,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
model,
|
model,
|
||||||
max_tokens: sosMode ? 280 : 400,
|
max_tokens: 500,
|
||||||
messages: [{ role: "system", content: systemPrompt }, ...trimmed],
|
messages: [{ role: "system", content: systemPrompt }, ...trimmed],
|
||||||
},
|
},
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
@ -476,7 +571,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`[coach/message] sosMode=${!!sosMode} usedModel=${usedModel ?? "NONE"}`,
|
`[coach/message] plan=${plan} provider=${llmProvider} usedModel=${usedModel ?? "NONE"}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@ -505,7 +600,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chat-Verlauf für Pro/Legend in DB speichern
|
// Chat-Verlauf für Pro/Legend in DB speichern
|
||||||
const plan = profile?.plan ?? "free";
|
// `plan` ist bereits oben deklariert (LLM-Routing-Block)
|
||||||
console.log("[coach/message] plan:", plan, "userId:", user.id);
|
console.log("[coach/message] plan:", plan, "userId:", user.id);
|
||||||
if (plan === "pro" || plan === "legend") {
|
if (plan === "pro" || plan === "legend") {
|
||||||
const fullHistory = [
|
const fullHistory = [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user