Compare commits

..

1 Commits

Author SHA1 Message Date
dgtlmoon c9c9ecc329 LLM integration - LiteLLM config - UI tweaks 2026-05-12 11:09:30 +02:00
5 changed files with 33 additions and 104 deletions
+14 -12
View File
@@ -198,12 +198,10 @@ def construct_blueprint(datastore: ChangeDetectionStore):
best_from = watch.get_from_version_based_on_last_viewed
from_version = request.args.get('from_version', best_from if best_from else dates[-2])
to_version = request.args.get('to_version', dates[-1])
from changedetectionio.llm.evaluator import DiffPrefs
prefs = DiffPrefs.from_request_args(request.args)
all_changes = prefs.all_changes
ignore_whitespace = prefs.ignore_whitespace
show_removed = prefs.show_removed
show_added = prefs.show_added
all_changes = request.args.get('all_changes', '0') == '1'
ignore_whitespace = request.args.get('ignore_whitespace', '0') == '1'
show_removed = request.args.get('removed', '1') == '1'
show_added = request.args.get('added', '1') == '1'
def _prep(text):
"""Optionally normalise whitespace on each line before diffing."""
@@ -265,17 +263,21 @@ def construct_blueprint(datastore: ChangeDetectionStore):
return jsonify({'summary': None, 'error': 'No differences found'})
from changedetectionio.llm.evaluator import (
summarise_change, get_effective_summary_prompt, build_summary_cache_prompt,
summarise_change, get_effective_summary_prompt,
is_global_token_budget_exceeded, get_global_token_budget_month,
LLMInputTooLargeError,
)
# Diff-pref flags + system prompt are part of the cache key so prompt changes bust the cache.
effective_prompt = get_effective_summary_prompt(watch, datastore)
from changedetectionio.llm.prompt_builder import build_change_summary_system_prompt
# Diff-pref flags + system prompt are part of the cache key so prompt changes bust the cache
_max_summary_tokens = datastore.data['settings']['application'].get('llm_max_summary_tokens', 3000)
cache_prompt = build_summary_cache_prompt(
effective_prompt=get_effective_summary_prompt(watch, datastore),
max_summary_tokens=_max_summary_tokens,
prefs=prefs,
cache_prompt = (
effective_prompt
+ f'\x00prefs:all={int(all_changes)},ws={int(ignore_whitespace)}'
f',rm={int(show_removed)},add={int(show_added)}'
+ f'\x00sys:{build_change_summary_system_prompt()}'
+ f'\x00max_tokens:{_max_summary_tokens}'
)
# Check cache — keyed by version pair + prompt hash (invalidates if prompt changes)
+1 -54
View File
@@ -16,7 +16,6 @@ Environment variable overrides (take priority over datastore settings):
import hashlib
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from loguru import logger
@@ -88,7 +87,7 @@ LLM_DEFAULT_MAX_SUMMARY_TOKENS = 3000
JSON_RESPONSE_MAX_TOKENS = 400
# Default prompt used when the user hasn't configured llm_change_summary
DEFAULT_CHANGE_SUMMARY_PROMPT = "You are given a standard unix patch/diff document (lines starting with + are added, lines starting with - are removed, and lines starting with ~ are content that was merely moved/reordered and exists on both sides — do NOT report ~ lines as added or removed). Describe in plain English what changed — first you will scan for items that were simply moved around in the order and just mention that they were changed. list what was added or removed as bullet points, including key details for each item. Be careful of content that merely just moved around, you should mention that it moved but dont report that it was added/removed etc. Be considerate of the style content you are summarising the change of, adjust your report accordingly. Do not quote non-English text verbatim; translate and summarise all content into English. Your entire response must be in English."
DEFAULT_CHANGE_SUMMARY_PROMPT = "Describe in plain English what changed — list what was added or removed as bullet points, including key details for each item. Be careful of content that merely just moved around, you should mention that it moved but dont report that it was added/removed etc. Be considerate of the style content you are summarising the change of, adjust your report accordingly. Do not quote non-English text verbatim; translate and summarise all content into English. Your entire response must be in English."
def _summary_max_tokens(diff: str, max_cap: int = LLM_DEFAULT_MAX_SUMMARY_TOKENS) -> int:
@@ -417,58 +416,6 @@ def compute_summary_cache_key(diff_text: str, prompt: str) -> str:
return h.hexdigest()[:16]
@dataclass(frozen=True)
class DiffPrefs:
"""
User-facing diff display preferences. Part of the LLM summary cache key so
that toggling a preference produces a fresh summary.
Field defaults are the single source of truth — the UI query-arg defaults in
diff.py's from_request_args() and the worker pre-cache's bare DiffPrefs()
both rely on these.
"""
all_changes: bool = False
ignore_whitespace: bool = False
show_removed: bool = True
show_added: bool = True
@classmethod
def from_request_args(cls, args) -> 'DiffPrefs':
"""Parse from a Flask request.args (or any .get(key, default)-shaped mapping)."""
return cls(
all_changes = args.get('all_changes', '0') == '1',
ignore_whitespace = args.get('ignore_whitespace', '0') == '1',
show_removed = args.get('removed', '1') == '1',
show_added = args.get('added', '1') == '1',
)
def cache_key_suffix(self) -> str:
return (
f'\x00prefs:all={int(self.all_changes)},ws={int(self.ignore_whitespace)}'
f',rm={int(self.show_removed)},add={int(self.show_added)}'
)
def build_summary_cache_prompt(effective_prompt: str, max_summary_tokens: int,
prefs: DiffPrefs = None) -> str:
"""
Compose the full cache-key string passed to save/get_llm_diff_summary.
Default prefs are DiffPrefs() — must match the UI's query-arg defaults so a
worker-side pre-cache is hit by an unmodified UI request. Same helper must
be used by both the worker pre-cache write and the UI diff route read,
otherwise the prompt hashes diverge and the cache file isn't found.
"""
if prefs is None:
prefs = DiffPrefs()
return (
effective_prompt
+ prefs.cache_key_suffix()
+ f'\x00sys:{build_change_summary_system_prompt()}'
+ f'\x00max_tokens:{max_summary_tokens}'
)
def summarise_change(watch, datastore, diff: str, current_snapshot: str = '') -> str:
"""
Generate a plain-language summary of the change using the watch's
-2
View File
@@ -1024,10 +1024,8 @@ class model(EntityPersistenceMixin, watch_base):
prompt_hash = self._llm_summary_prompt_hash(prompt)
fname = os.path.join(self.data_dir, f'change-summary-{from_version}-to-{to_version}-{prompt_hash}.txt')
if not os.path.isfile(fname):
logger.debug(f"LLM cached diff summary '{fname}' NOT found")
return ''
with open(fname, 'r', encoding='utf-8') as f:
logger.debug(f"LLM cached diff summary '{fname}' FOUND")
return f.read().strip()
def save_llm_diff_summary(self, summary: str, from_version, to_version, prompt: str = ''):
@@ -210,19 +210,10 @@ def render(watch, datastore, request, url_for, render_template, flash, redirect,
llm_summary_prompt = ''
if llm_configured:
try:
from changedetectionio.llm.evaluator import (
get_effective_summary_prompt, build_summary_cache_prompt,
)
from changedetectionio.llm.evaluator import get_effective_summary_prompt
_prompt = get_effective_summary_prompt(watch, datastore)
llm_summary_prompt = _prompt
# Must match the cache_prompt the worker writes and the UI ajax route reads —
# using UI default diff prefs so the initial render finds the worker's pre-cache.
_max_summary_tokens = datastore.data['settings']['application'].get('llm_max_summary_tokens', 3000)
_cache_prompt = build_summary_cache_prompt(
effective_prompt=_prompt,
max_summary_tokens=_max_summary_tokens,
)
llm_diff_summary = watch.get_llm_diff_summary(from_version, to_version, prompt=_cache_prompt)
llm_diff_summary = watch.get_llm_diff_summary(from_version, to_version, prompt=_prompt)
except Exception as e:
logger.warning(f"Could not load llm-diff-summary for {uuid}: {e}")
+16 -25
View File
@@ -478,6 +478,22 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
datastore.update_watch(uuid=uuid, update_obj=update_obj)
# Save AI summary file now that the new snapshot has been committed
# and its version timestamp is the last key in history
if update_obj.get('_llm_change_summary') and _llm_from_version:
try:
from changedetectionio.llm.evaluator import get_effective_summary_prompt
_llm_to_version = list(watch.history.keys())[-1]
_llm_prompt = get_effective_summary_prompt(watch, datastore)
watch.save_llm_diff_summary(
update_obj['_llm_change_summary'],
_llm_from_version,
_llm_to_version,
prompt=_llm_prompt,
)
except Exception as _fe:
logger.warning(f"Could not write change-summary file for {uuid}: {_fe}")
if changed_detected or not watch.history_n:
if update_handler.screenshot:
watch.save_screenshot(screenshot=update_handler.screenshot)
@@ -503,31 +519,6 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
timestamp=int(fetch_start_time),
snapshot_id=update_obj.get('previous_md5', 'none'))
# Save AI summary file now that the new snapshot is committed —
# watch.history.keys()[-1] now reflects the just-saved version,
# so the cache filename matches what the UI will later look up.
# Cache key must use build_summary_cache_prompt() with UI defaults so
# the worker write and the UI read hash to the same prompt_hash.
if update_obj.get('_llm_change_summary') and _llm_from_version:
try:
from changedetectionio.llm.evaluator import (
get_effective_summary_prompt, build_summary_cache_prompt,
)
_llm_to_version = list(watch.history.keys())[-1]
_llm_max_summary_tokens = datastore.data['settings']['application'].get('llm_max_summary_tokens', 3000)
_llm_cache_prompt = build_summary_cache_prompt(
effective_prompt=get_effective_summary_prompt(watch, datastore),
max_summary_tokens=_llm_max_summary_tokens,
)
watch.save_llm_diff_summary(
update_obj['_llm_change_summary'],
_llm_from_version,
_llm_to_version,
prompt=_llm_cache_prompt,
)
except Exception as _fe:
logger.warning(f"Could not write change-summary file for {uuid}: {_fe}")
empty_pages_are_a_change = datastore.data['settings']['application'].get('empty_pages_are_a_change', False)
if update_handler.fetcher.content or (not update_handler.fetcher.content and empty_pages_are_a_change):
watch.save_last_fetched_html(contents=update_handler.fetcher.content, timestamp=int(fetch_start_time))