diff --git a/backend/internal/domain/market/research.go b/backend/internal/domain/market/research.go index 0ab0d47..dae98f7 100644 --- a/backend/internal/domain/market/research.go +++ b/backend/internal/domain/market/research.go @@ -1,11 +1,13 @@ package market import ( + "context" "encoding/json" "errors" "fmt" "log/slog" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -89,6 +91,13 @@ func (h *ResearchHandler) Research(c *gin.Context) { return } + // Nil out image URLs that don't return a successful HTTP response. + if raw.Felder.BildURL.Wert != nil && *raw.Felder.BildURL.Wert != "" { + if !imageURLReachable(ctx, *raw.Felder.BildURL.Wert) { + raw.Felder.BildURL.Wert = nil + } + } + result := toLLMResearchResult(raw, m) c.JSON(http.StatusOK, gin.H{"data": result}) } @@ -228,3 +237,20 @@ func toLLMResearchResult(raw llmOutput, m Market) ResearchResult { Sources: sources, } } + +func imageURLReachable(ctx context.Context, rawURL string) bool { + reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(reqCtx, http.MethodHead, rawURL, nil) + if err != nil { + return false + } + req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Marktvogt/1.0)") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + _ = resp.Body.Close() + // 405 means HEAD not allowed but the resource exists. + return resp.StatusCode < 400 || resp.StatusCode == http.StatusMethodNotAllowed +} diff --git a/backend/internal/domain/market/research/assets/researcher_prompt.de.md b/backend/internal/domain/market/research/assets/researcher_prompt.de.md index d4de310..fc65cbf 100644 --- a/backend/internal/domain/market/research/assets/researcher_prompt.de.md +++ b/backend/internal/domain/market/research/assets/researcher_prompt.de.md @@ -13,10 +13,15 @@ Extraktion aus Quellen. suchen und Veranstalter-Website oeffnen. Fallback: Facebook-Event oder Kalender (mittelalterkalender.info, marktkalendarium.de, mittelalterfeste.com, mittelalter-termine.de). -2. **Zweitquelle pflicht**: verifiziere Datum + Ort gegen mindestens eine +2. **Jahr zuerst verifizieren**: Pruefe SOFORT ob die Seite Daten fuer das + Recherchejahr zeigt (Jahr aus recherche_datum). Viele Veranstalter-Websites + zeigen erst nach Ankuendigung aktuelle Daten - bis dahin ist die Vorjahresseite + noch sichtbar. Steht kein Jahr auf der Seite oder stimmt das Jahr nicht: + Zweitquelle oeffnen bevor du weiter extrahierst. +3. **Zweitquelle pflicht**: verifiziere Datum + Ort gegen mindestens eine weitere Quelle. Schuetzt vor veralteten Daten auf schlecht gepflegten Seiten. -3. **Felder extrahieren** (siehe unten). -4. **status** auf Top-Level setzen: +4. **Felder extrahieren** (siehe unten). +5. **status** auf Top-Level setzen: - "bestaetigt": ALLE Felder fuer Recherchejahr bestaetigt - "unklar": Quellen widerspruechlich ODER einzelne Felder aus Vorjahr - "vorjahr_unbestaetigt": ueberwiegend Vorjahresdaten @@ -45,14 +50,21 @@ Extraktion aus Quellen. - **land**: "Deutschland" | "Oesterreich" | "Schweiz". - **veranstalter**: Verein, Firma oder Person. Impressum ist gute Quelle. - **start_datum** / **end_datum**: YYYY-MM-DD, im Recherchejahr. Eintages- - Markt: beide gleich. + Markt: beide gleich. Pruefe das Jahr explizit - nicht nur Monat und Tag + uebernehmen. Wenn unklar ob Datum fuer Recherchejahr gilt: `wert: null` + + hinweis mit dem Jahr der Quelle. - **oeffnungszeiten**: Array von Zeitfenstern {datum_von, datum_bis, von, bis}. - Nimm NUR explizit genannte Zeiten. Keine Zeiten fuer Tage ohne Angabe - erfinden. Bei Muster ueber mehrere Wochenenden (z.B. "Fr 17-02, Sa 16-00:30 - an allen Wochenenden"): Muster anwenden, keine widersprechenden Eintraege - erzeugen. Vor Abgabe: KEINE Duplikate (gleiches Datum mehrfach). Format 24h - "HH:MM", nach Mitternacht "00:30"/"02:00". - Kompakt: identische Zeiten ueber mehrere Tage -> ein Eintrag mit Datumsbereich. + Nimm NUR explizit genannte Zeiten aus der aktuellen Veranstaltungsseite. + Wichtig: Zeiten sind jahresabhaengig und aendern sich haeufig. Nur dann + eintragen wenn die Quelle eindeutig das Recherchejahr adressiert (z.B. auf + der aktuellen Veranstaltungsseite oder im aktuellen FB-Event). Vorjahresdaten + oder allgemeine "typische Zeiten" -> `wert: null` + hinweis mit Quell-Jahr. + Keine Zeiten fuer Tage ohne Angabe erfinden. Bei Muster ueber mehrere + Wochenenden (z.B. "Fr 17-02, Sa 16-00:30 an allen Wochenenden"): Muster + anwenden, keine widersprechenden Eintraege erzeugen. Vor Abgabe: KEINE + Duplikate (gleiches Datum mehrfach). Format 24h "HH:MM", nach Mitternacht + "00:30"/"02:00". Kompakt: identische Zeiten ueber mehrere Tage -> ein + Eintrag mit Datumsbereich. - **eintrittspreise**: Array {name, betrag, waehrung}. ALLE Kategorien extrahieren wenn mehrere gelistet (Erwachsene, Kinder, Ermaessigt, Familie, Gewandete, Abendkasse etc.), nicht nur eine. @@ -60,6 +72,8 @@ Extraktion aus Quellen. Gebuehren und sind NICHT der Eintrittspreis. Veranstalter-Website bevorzugen. Nur Portal verfuegbar: extrahieren + hinweis "inkl. Servicegebuehr". Eintritt frei: ein Eintrag name="Eintritt frei", betrag=0. + Preise aendern sich jaehrlich - nur Preise extrahieren die nachweislich fuer + das Recherchejahr gelten. Kein Nachweis -> `wert: null` + hinweis. - **bild_url**: Offizielles Plakat/Banner/Header, kein Stockfoto, kein Sponsor-Logo. Social-Media-Vorschaubilder ok. Nur URLs die du tatsaechlich als src/og:image gesehen hast. Nichts findbar -> `null`. @@ -83,6 +97,10 @@ Extraktion aus Quellen. - Feld nicht findbar: `wert: null`, `quellen: []`, `extraktion: "direkt"`, `hinweis` mit knapper Begruendung. - NICHTS erfinden. Halluzinationen sind der teuerste Fehler. +- Jahreszugehoerigkeit: Jedes Datum, jede Zeit, jeden Preis vor der Ausgabe + auf das Recherchejahr pruefen. Steht "2025" auf der Quelle und das + Recherchejahr ist 2026: Wert auf null setzen, hinweis mit "Quelle zeigt + 2025-Daten" eintragen. - Widerspruch zwischen Quellen: Veranstalter-Website > Kalender > Social Media > Presse. Widerspruch IMMER im hinweis dokumentieren, auch wenn die offiziellste Quelle klar gewinnt. Format: