[update] added gemma2:9b model to example.env, refactored prompt generation with stricter JSON rules, adjusted context size, integrated system prompt for better summaries, and improved error handling in backend services

This commit is contained in:
2025-08-07 15:05:20 +02:00
parent 0a97a57c76
commit 011b256662
3 changed files with 68 additions and 32 deletions

View File

@@ -8,7 +8,7 @@ MIN_CRON_HOURS = float(os.getenv("MIN_CRON_HOURS", 0.5))
DEFAULT_CRON_HOURS = float(os.getenv("CRON_HOURS", MIN_CRON_HOURS)) DEFAULT_CRON_HOURS = float(os.getenv("CRON_HOURS", MIN_CRON_HOURS))
CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS) CRON_HOURS = max(MIN_CRON_HOURS, DEFAULT_CRON_HOURS)
SYNC_COOLDOWN_MINUTES = int(os.getenv("SYNC_COOLDOWN_MINUTES", 30)) SYNC_COOLDOWN_MINUTES = int(os.getenv("SYNC_COOLDOWN_MINUTES", 30))
LLM_MODEL = os.getenv("LLM_MODEL", "phi3:3.8b-mini-128k-instruct-q4_0") LLM_MODEL = os.getenv("LLM_MODEL", "gemma2:9b")
LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", 180)) LLM_TIMEOUT_SECONDS = int(os.getenv("LLM_TIMEOUT_SECONDS", 180))
OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10)) OLLAMA_API_TIMEOUT_SECONDS = int(os.getenv("OLLAMA_API_TIMEOUT_SECONDS", 10))
ARTICLE_FETCH_TIMEOUT = int(os.getenv("ARTICLE_FETCH_TIMEOUT", 30)) ARTICLE_FETCH_TIMEOUT = int(os.getenv("ARTICLE_FETCH_TIMEOUT", 30))

View File

@@ -119,12 +119,11 @@ class NewsFetcher:
except Exception as e: except Exception as e:
logger.warning( logger.warning(
f"❌ Error fetching article content from {url}: { f"❌ Error fetching article content from {url}: {
type(e).__name__}: {e}") type(e).__name__}: {e}")
return "" return ""
@staticmethod @staticmethod
def build_prompt( def build_prompt(
url: str,
title: str = "", title: str = "",
summary: str = "", summary: str = "",
content: str = "") -> str: content: str = "") -> str:
@@ -132,14 +131,13 @@ class NewsFetcher:
Generate a prompt for the LLM to summarize an article. Generate a prompt for the LLM to summarize an article.
Args: Args:
url: Public URL of the article to summarize
title: Article title from RSS feed (optional) title: Article title from RSS feed (optional)
summary: Article summary from RSS feed (optional) summary: Article summary from RSS feed (optional)
content: Extracted article content (optional) content: Extracted article content (optional)
Returns: Returns:
A formatted prompt string that instructs the LLM to generate A formatted prompt string that instructs the LLM to generate
a JSON response with title and summaries in German and English a JSON response with title, summary and tags in German
""" """
context_info = [] context_info = []
if title: if title:
@@ -148,28 +146,42 @@ class NewsFetcher:
context_info.append(f"RSS-Beschreibung: {summary}") context_info.append(f"RSS-Beschreibung: {summary}")
if content: if content:
content_preview = content[:500] + \ content_preview = content[:500] + \
"..." if len(content) > 500 else content "..." if len(content) > 500 else content
context_info.append(f"Artikel-Inhalt: {content_preview}") context_info.append(f"Artikel-Inhalt: {content_preview}")
context = "\n".join( context = "\n".join(
context_info) if context_info else "Keine zusätzlichen Informationen verfügbar." context_info) if context_info else "Keine zusätzlichen Informationen verfügbar."
return ( return (
"### Aufgabe\n" "### Vorliegende Informationen\n"
f"Du sollst eine Nachricht basierend auf der URL und den verfügbaren Informationen zusammenfassen.\n" f"{context}\n\n"
f"URL: {url}\n" "### Längenbegrenzungen\n"
f"Verfügbare Informationen:\n{context}\n\n" "title: max 100 Zeichen\n"
"summary: 100160 Wörter\n"
"tags: bis zu 6 Schlüsselwörter, durch Komma getrennt, alles Kleinbuchstaben.\n\n"
"### Regeln\n" "### Regeln\n"
"1. Nutze VORRANGIG den Artikel-Inhalt falls verfügbar, ergänze mit RSS-Informationen\n" "1. Nutze ausschließlich Informationen, die im bereitgestellten Material eindeutig vorkommen. Externes Wissen ist untersagt.\n"
"2. Falls kein Artikel-Inhalt verfügbar ist, nutze RSS-Titel und -Beschreibung\n" "2. Liegt sowohl Artikel-Text als auch RSS-Metadaten vor, hat der Artikel-Text Vorrang; verwende RSS nur ergänzend.\n"
"3. Falls keine ausreichenden Informationen vorliegen, erstelle eine plausible Zusammenfassung basierend auf der URL\n" "3. Liegt nur RSS-Titel und/oder -Beschreibung vor, stütze dich ausschließlich darauf.\n"
"4. Gib ausschließlich **gültiges minifiziertes JSON** zurück kein Markdown, keine Kommentare\n" "4. Sind die Informationen unzureichend, gib exakt {\"title\":\"\",\"summary\":\"\",\"tags\":\"\"} zurück.\n"
"5. Struktur: {\"title\":\"\",\"summary\":\"\"}\n" "5. Gib nur gültiges, minifiziertes JSON zurück keine Zeilenumbrüche, kein Markdown, keine Kommentare.\n"
"6. title: Aussagekräftiger deutscher Titel (max 100 Zeichen)\n" "6. Verwende keine hypothetischen Formulierungen (\"könnte\", \"möglicherweise\" etc.).\n"
"7. summary: Deutsche Zusammenfassung (zwischen 100 und 160 Wörter)\n" "7. Wörtliche Zitate dürfen höchstens 15 % des Summary-Texts ausmachen.\n"
"8. Kein Text vor oder nach dem JSON\n\n" "8. Kein Text vor oder nach dem JSON.\n\n"
"### Ausgabe\n" "### Ausgabe\n"
"Jetzt antworte mit dem JSON:" "Antworte jetzt ausschließlich mit dem JSON:\n"
)
@staticmethod
def build_system_prompt():
return (
"Du bist ein hochpräziser JSON-Summarizer und Experte für die Zusammenfassung von Artikeln.\n\n"
"### Vorgehen\n"
"Schritt 1: Identifiziere Hauptthema und Zweck.\n"
"Schritt 2: Extrahiere die wichtigsten Fakten und Ergebnisse.\n"
"Schritt 3: Erkenne die zentralen Argumente und Standpunkte.\n"
"Schritt 4: Ordne die Informationen nach Wichtigkeit.\n"
"Schritt 5: Erstelle eine prägnante, klare und sachliche Zusammenfassung.\n\n"
) )
@staticmethod @staticmethod
@@ -199,17 +211,39 @@ class NewsFetcher:
logger.warning( logger.warning(
f"⚠️ Could not fetch article content, using RSS data only") f"⚠️ Could not fetch article content, using RSS data only")
prompt = NewsFetcher.build_prompt( prompt = NewsFetcher.build_prompt(title, summary, article_content)
url, title, summary, article_content) system_prompt = NewsFetcher.build_system_prompt()
payload = { payload = {
"model": LLM_MODEL, "model": LLM_MODEL,
"prompt": prompt, "prompt": prompt,
"system": system_prompt,
"stream": False, "stream": False,
"temperature": 0.1, "temperature": 0.1,
"format": "json", "format": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"summary": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"title",
"summary",
"tags"
]
},
"options": { "options": {
"num_gpu": 1, # Force GPU usage "num_gpu": 1, # Force GPU usage
"num_ctx": 64_000, # Context size "num_ctx": 8192, # Context size
} }
} }
@@ -263,7 +297,7 @@ class NewsFetcher:
except Exception as e: except Exception as e:
logger.error( logger.error(
f"❌ Unexpected error summarizing {url}: { f"❌ Unexpected error summarizing {url}: {
type(e).__name__}: {e}") type(e).__name__}: {e}")
return None return None
@staticmethod @staticmethod
@@ -300,7 +334,7 @@ class NewsFetcher:
except Exception as e: except Exception as e:
logger.error( logger.error(
f"❌ Critical error during harvest: { f"❌ Critical error during harvest: {
type(e).__name__}: {e}") type(e).__name__}: {e}")
raise raise
@staticmethod @staticmethod
@@ -327,18 +361,18 @@ class NewsFetcher:
if hasattr(feed_data, 'bozo') and feed_data.bozo: if hasattr(feed_data, 'bozo') and feed_data.bozo:
logger.warning( logger.warning(
f"⚠️ Feed has parsing issues: { f"⚠️ Feed has parsing issues: {
feed_row['url']}") feed_row['url']}")
if hasattr(feed_data, 'bozo_exception'): if hasattr(feed_data, 'bozo_exception'):
logger.warning( logger.warning(
f"⚠️ Feed exception: { f"⚠️ Feed exception: {
feed_data.bozo_exception}") feed_data.bozo_exception}")
total_entries = len(feed_data.entries) total_entries = len(feed_data.entries)
if total_entries == 0: if total_entries == 0:
logger.warning( logger.warning(
f"⚠️ No entries found in feed: { f"⚠️ No entries found in feed: {
feed_row['url']}") feed_row['url']}")
return stats return stats
for i, entry in enumerate(feed_data.entries, 1): for i, entry in enumerate(feed_data.entries, 1):
@@ -403,8 +437,9 @@ class NewsFetcher:
cursor.execute( cursor.execute(
""" """
INSERT INSERT
OR IGNORE INTO news OR IGNORE
(title, summary, url, published, country) INTO news
(title, summary, url, published, country)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
( (
@@ -429,7 +464,7 @@ class NewsFetcher:
except Exception as e: except Exception as e:
logger.error( logger.error(
f"❌ Error processing feed { f"❌ Error processing feed {
feed_row['url']}: { feed_row['url']}: {
type(e).__name__}: {e}") type(e).__name__}: {e}")
return stats return stats

View File

@@ -16,6 +16,7 @@ LLM_MODEL=phi3:3.8b-mini-128k-instruct-q4_0 # ca 6-8GB (langer kontext)
LLM_MODEL=mistral-nemo:12b # ca 16-24+GB LLM_MODEL=mistral-nemo:12b # ca 16-24+GB
LLM_MODEL=cnjack/mistral-samll-3.1:24b-it-q4_K_S # ca 22GB LLM_MODEL=cnjack/mistral-samll-3.1:24b-it-q4_K_S # ca 22GB
LLM_MODEL=yarn-mistral:7b-64k-q4_K_M # ca 11GB LLM_MODEL=yarn-mistral:7b-64k-q4_K_M # ca 11GB
LLM_MODEL=gemma2:9b # ca 8GB
# Timeout in seconds for LLM requests # Timeout in seconds for LLM requests
LLM_TIMEOUT_SECONDS=180 LLM_TIMEOUT_SECONDS=180