[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:
@@ -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))
|
||||||
|
@@ -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: 100–160 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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user