push
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
type LlmProvider = 'openai' | 'openrouter'
|
||||
|
||||
function getProvider(): LlmProvider {
|
||||
const configured = (process.env.LLM_PROVIDER ?? '').toLowerCase()
|
||||
if (configured === 'openrouter') return 'openrouter'
|
||||
if (configured === 'openai') return 'openai'
|
||||
return process.env.OPENROUTER_API_KEY ? 'openrouter' : 'openai'
|
||||
}
|
||||
|
||||
function createClient(provider: LlmProvider) {
|
||||
if (provider === 'openrouter') {
|
||||
const apiKey = process.env.OPENROUTER_API_KEY || ''
|
||||
return new OpenAI({
|
||||
apiKey,
|
||||
baseURL: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
|
||||
defaultHeaders: {
|
||||
...(process.env.OPENROUTER_SITE_URL
|
||||
? { 'HTTP-Referer': process.env.OPENROUTER_SITE_URL }
|
||||
: {}),
|
||||
...(process.env.OPENROUTER_APP_NAME
|
||||
? { 'X-Title': process.env.OPENROUTER_APP_NAME }
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || '',
|
||||
})
|
||||
}
|
||||
|
||||
function getModel(provider: LlmProvider): string {
|
||||
if (provider === 'openrouter') {
|
||||
return process.env.OPENROUTER_MODEL || 'minimax/minimax-m2.5'
|
||||
}
|
||||
return process.env.OPENAI_MODEL || 'gpt-4o-mini'
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { orgName, context } = body
|
||||
|
||||
if (!orgName || !context) {
|
||||
return NextResponse.json({ error: 'orgName and context are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const provider = getProvider()
|
||||
const client = createClient(provider)
|
||||
const model = getModel(provider)
|
||||
|
||||
const systemMessage = `Sie sind ein professioneller Copywriter für eine moderne deutsche Innung oder Kreishandwerkerschaft.
|
||||
Erstellen Sie eine moderne, ansprechende Überschrift (Heading) und einen Einleitungstext für eine Landingpage.
|
||||
|
||||
WICHTIG: Geben Sie AUSSCHLIESSLICH ein valides JSON-Objekt zurück, komplett ohne Markdown-Formatierung (kein \`\`\`json ... \`\`\`), in dieser Struktur:
|
||||
{
|
||||
"title": "Eine moderne, ansprechende Überschrift (max. 6-8 Wörter)",
|
||||
"text": "Ein überzeugender Einleitungstext, der erklärt, wofür die Organisation steht, fokussiert auf die Region und den Kontext (max. 3-4 Sätze)."
|
||||
}`
|
||||
|
||||
const userMessage = `Name der Organisation: ${orgName}\nZusätzliche Stichpunkte vom Benutzer:\n${context}`
|
||||
|
||||
const completion = await client.chat.completions.create({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemMessage },
|
||||
{ role: 'user', content: userMessage },
|
||||
],
|
||||
// some openrouter models ignore response_format, so doing it purely by prompt
|
||||
temperature: 0.7
|
||||
})
|
||||
|
||||
let textResponse = completion.choices[0]?.message?.content || ''
|
||||
|
||||
// safely remove potential markdown blocks just in case
|
||||
textResponse = textResponse.trim()
|
||||
if (textResponse.startsWith('```json')) {
|
||||
textResponse = textResponse.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim()
|
||||
} else if (textResponse.startsWith('```')) {
|
||||
textResponse = textResponse.replace(/^```\n?/, '').replace(/\n?```$/, '').trim()
|
||||
}
|
||||
|
||||
const result = JSON.parse(textResponse)
|
||||
|
||||
return NextResponse.json(result)
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error generating AI landing page content:', error)
|
||||
return NextResponse.json({ error: error?.message || 'Failed to generate content' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
160
innungsapp/apps/admin/app/api/ai/generate/route.ts
Normal file
160
innungsapp/apps/admin/app/api/ai/generate/route.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
type LlmProvider = 'openai' | 'openrouter'
|
||||
|
||||
function getProvider(): LlmProvider {
|
||||
const configured = (process.env.LLM_PROVIDER ?? '').toLowerCase()
|
||||
if (configured === 'openrouter') return 'openrouter'
|
||||
if (configured === 'openai') return 'openai'
|
||||
return process.env.OPENROUTER_API_KEY ? 'openrouter' : 'openai'
|
||||
}
|
||||
|
||||
function createClient(provider: LlmProvider) {
|
||||
if (provider === 'openrouter') {
|
||||
const apiKey = process.env.OPENROUTER_API_KEY || ''
|
||||
return new OpenAI({
|
||||
apiKey,
|
||||
baseURL: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
|
||||
defaultHeaders: {
|
||||
...(process.env.OPENROUTER_SITE_URL
|
||||
? { 'HTTP-Referer': process.env.OPENROUTER_SITE_URL }
|
||||
: {}),
|
||||
...(process.env.OPENROUTER_APP_NAME
|
||||
? { 'X-Title': process.env.OPENROUTER_APP_NAME }
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || '',
|
||||
})
|
||||
}
|
||||
|
||||
function getModel(provider: LlmProvider): string {
|
||||
if (provider === 'openrouter') {
|
||||
return process.env.OPENROUTER_MODEL || 'minimax/minimax-m2.5'
|
||||
}
|
||||
return process.env.OPENAI_MODEL || 'gpt-5-mini'
|
||||
}
|
||||
|
||||
function hasApiKey(provider: LlmProvider): boolean {
|
||||
if (provider === 'openrouter') return !!process.env.OPENROUTER_API_KEY
|
||||
return !!process.env.OPENAI_API_KEY
|
||||
}
|
||||
|
||||
async function generateText({
|
||||
provider,
|
||||
model,
|
||||
systemMessage,
|
||||
prompt,
|
||||
}: {
|
||||
provider: LlmProvider
|
||||
model: string
|
||||
systemMessage: string
|
||||
prompt: string
|
||||
}) {
|
||||
const client = createClient(provider)
|
||||
const completion = await client.chat.completions.create({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: systemMessage },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
})
|
||||
|
||||
return completion.choices[0]?.message?.content || ''
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { prompt, type, format } = await req.json()
|
||||
const primaryProvider = getProvider()
|
||||
const primaryModel = getModel(primaryProvider)
|
||||
|
||||
if (!prompt) {
|
||||
return NextResponse.json({ error: 'Prompt is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
let systemMessage = ''
|
||||
|
||||
if (type === 'news') {
|
||||
systemMessage = `Du bist ein erfahrener Newsletter- und PR-Experte für eine Innung (Handwerksverband).
|
||||
Deine Aufgabe ist es, professionelle, ansprechende und informative News-Beiträge zu schreiben.
|
||||
Achte auf eine klare Struktur, eine einladende Tonalität und hohe inhaltliche Qualität.
|
||||
Das gewünschte Ausgabeformat ist: ${format === 'markdown' ? 'Markdown' : 'Einfacher unformatierter Text'}.`
|
||||
} else if (type === 'stelle') {
|
||||
systemMessage = `Du bist ein erfahrener HR- und Recruiting-Experte für das Handwerk.
|
||||
Deine Aufgabe ist es, attraktive und präzise Stellenanzeigen (Lehrlingsbörse / Jobbörse) zu verfassen.
|
||||
Die Stellenanzeige soll Begeisterung wecken und klar die Aufgaben sowie Anforderungen kommunizieren.
|
||||
Das gewünschte Ausgabeformat ist: ${format === 'markdown' ? 'Markdown' : 'Einfacher unformatierter Text'}.`
|
||||
} else {
|
||||
systemMessage = `Du bist ein hilfreicher KI-Assistent. Antworte immer auf Deutsch.`
|
||||
}
|
||||
|
||||
const attempts: Array<{ provider: LlmProvider; model: string; reason: string }> = []
|
||||
|
||||
if (hasApiKey(primaryProvider)) {
|
||||
attempts.push({
|
||||
provider: primaryProvider,
|
||||
model: primaryModel,
|
||||
reason: 'primary',
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback requested: if primary fails, try OpenAI GPT-5 mini when OPENAI_API_KEY is present.
|
||||
if (primaryProvider !== 'openai' && hasApiKey('openai')) {
|
||||
attempts.push({
|
||||
provider: 'openai',
|
||||
model: 'gpt-5-mini',
|
||||
reason: 'fallback_openai',
|
||||
})
|
||||
}
|
||||
|
||||
if (attempts.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No AI provider key configured (OPENROUTER_API_KEY or OPENAI_API_KEY).' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
let lastError: any = null
|
||||
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
const text = await generateText({
|
||||
provider: attempt.provider,
|
||||
model: attempt.model,
|
||||
systemMessage,
|
||||
prompt,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
text,
|
||||
provider: attempt.provider,
|
||||
model: attempt.model,
|
||||
fallbackUsed: attempt.reason !== 'primary',
|
||||
})
|
||||
} catch (error: any) {
|
||||
lastError = error
|
||||
console.error('AI attempt failed:', {
|
||||
provider: attempt.provider,
|
||||
model: attempt.model,
|
||||
message: error?.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: lastError?.message || 'All AI providers failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
} catch (error: any) {
|
||||
console.error('AI Generate Error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error?.message || 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user