Die Modellwahl läuft über eine Umgebungsvariable im Hosting-Dashboard. Damit sah man nirgends in der Anwendung, welches Modell gerade urteilt — und eine Einstellung, die man nur im Dashboard prüfen kann, prüft niemand. Der Refresh gibt das Modell jetzt zurück, die Kopfzeile zeigt es neben dem Zeitstempel. Wer wissen will, ob die Umstellung angekommen ist, sieht es dort, wo er ohnehin hinschaut. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
/**
|
|
* Livia Research-PoC — der eine serverseitige Endpoint (§20).
|
|
*
|
|
* POST /api/livia/research/refresh
|
|
*
|
|
* Ruft die drei Quellen ab, normalisiert, beurteilt einmal per LLM und gibt
|
|
* die relevanten Leads zurück. Kein Scheduler, keine Queue, keine Datenbank —
|
|
* der Lauf beginnt und endet mit dieser Anfrage.
|
|
*
|
|
* Läuft als serverlose Funktion auf derselben Vercel-Instanz, die schon die
|
|
* Anwendung ausliefert. Das ist der Grund, warum für den PoC gar keine neue
|
|
* Infrastruktur nötig war: der Hoster kann bereits Serverfunktionen, es hat
|
|
* bisher nur niemand eine gebraucht.
|
|
*/
|
|
|
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
import {
|
|
SOURCES,
|
|
fetchZhkResearch,
|
|
fetchGreaterZurichResearch,
|
|
fetchZefixResearch,
|
|
} from '../../_lib/researchSources.js'
|
|
import type { ResearchItem } from '../../_lib/researchSources.js'
|
|
import { MODEL, analyzeResearchItems } from '../../_lib/researchAnalysis.js'
|
|
import type { ResearchRefreshResult, ResearchSourceResult } from '../../../src/domain/researchLead.js'
|
|
|
|
interface Quelle {
|
|
id: string
|
|
name: string
|
|
url: string
|
|
laden: () => Promise<ResearchItem[]>
|
|
}
|
|
|
|
const QUELLEN: Quelle[] = [
|
|
{ ...SOURCES.ZHK, laden: fetchZhkResearch },
|
|
{ ...SOURCES.GZA, laden: fetchGreaterZurichResearch },
|
|
{ ...SOURCES.ZEFIX, laden: fetchZefixResearch },
|
|
]
|
|
|
|
export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
// Ein Live-Lauf soll nie aus einem Zwischenspeicher beantwortet werden.
|
|
res.setHeader('Cache-Control', 'no-store')
|
|
|
|
if (req.method !== 'POST') {
|
|
res.statusCode = 405
|
|
res.end(JSON.stringify({ error: 'Nur POST' }))
|
|
return
|
|
}
|
|
|
|
// Alle drei parallel: eine langsame Quelle soll die beiden anderen nicht aufhalten.
|
|
const ergebnisse = await Promise.all(
|
|
QUELLEN.map(async (q): Promise<{ meta: ResearchSourceResult; items: ResearchItem[] }> => {
|
|
try {
|
|
const items = await q.laden()
|
|
return {
|
|
meta: { id: q.id, name: q.name, url: q.url, ok: true, itemCount: items.length },
|
|
items,
|
|
}
|
|
} catch (err) {
|
|
// Eine kaputte Quelle darf die Demo nicht zerstören (§21).
|
|
return {
|
|
meta: {
|
|
id: q.id, name: q.name, url: q.url, ok: false, itemCount: 0,
|
|
error: err instanceof Error ? err.message : 'Unbekannter Fehler',
|
|
},
|
|
items: [],
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
|
|
const sources = ergebnisse.map(e => e.meta)
|
|
const items = ergebnisse.flatMap(e => e.items)
|
|
|
|
const basis: ResearchRefreshResult = {
|
|
sources,
|
|
analyzed: items.length,
|
|
discarded: 0,
|
|
watchlist: 0,
|
|
leads: [],
|
|
refreshedAt: new Date().toISOString(),
|
|
model: MODEL,
|
|
}
|
|
|
|
if (items.length === 0) {
|
|
res.statusCode = 200
|
|
res.end(JSON.stringify({
|
|
...basis,
|
|
analysisError: 'Keine der drei Quellen lieferte Einträge.',
|
|
}))
|
|
return
|
|
}
|
|
|
|
try {
|
|
const { leads, discarded, watchlist } = await analyzeResearchItems(items)
|
|
res.statusCode = 200
|
|
res.end(JSON.stringify({ ...basis, leads, discarded, watchlist }))
|
|
} catch (err) {
|
|
// Gelesen wurde trotzdem — das sagen wir offen, statt eine leere Liste als
|
|
// «nichts gefunden» auszugeben.
|
|
res.statusCode = 200
|
|
res.end(JSON.stringify({
|
|
...basis,
|
|
analysisError: err instanceof Error ? err.message : 'Die Beurteilung schlug fehl.',
|
|
}))
|
|
}
|
|
}
|