mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-05-29 21:11:50 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c28d639019 |
@@ -198,10 +198,12 @@ 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])
|
||||
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'
|
||||
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
|
||||
|
||||
def _prep(text):
|
||||
"""Optionally normalise whitespace on each line before diffing."""
|
||||
@@ -263,21 +265,17 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
return jsonify({'summary': None, 'error': 'No differences found'})
|
||||
|
||||
from changedetectionio.llm.evaluator import (
|
||||
summarise_change, get_effective_summary_prompt,
|
||||
summarise_change, get_effective_summary_prompt, build_summary_cache_prompt,
|
||||
is_global_token_budget_exceeded, get_global_token_budget_month,
|
||||
LLMInputTooLargeError,
|
||||
)
|
||||
|
||||
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
|
||||
# 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 = (
|
||||
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}'
|
||||
cache_prompt = build_summary_cache_prompt(
|
||||
effective_prompt=get_effective_summary_prompt(watch, datastore),
|
||||
max_summary_tokens=_max_summary_tokens,
|
||||
prefs=prefs,
|
||||
)
|
||||
|
||||
# Check cache — keyed by version pair + prompt hash (invalidates if prompt changes)
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
|
||||
@@ -416,6 +417,58 @@ 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
|
||||
|
||||
@@ -1024,8 +1024,10 @@ 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,10 +210,19 @@ 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
|
||||
from changedetectionio.llm.evaluator import (
|
||||
get_effective_summary_prompt, build_summary_cache_prompt,
|
||||
)
|
||||
_prompt = get_effective_summary_prompt(watch, datastore)
|
||||
llm_summary_prompt = _prompt
|
||||
llm_diff_summary = watch.get_llm_diff_summary(from_version, to_version, 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)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load llm-diff-summary for {uuid}: {e}")
|
||||
|
||||
|
||||
+25
-16
@@ -478,22 +478,6 @@ 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)
|
||||
@@ -519,6 +503,31 @@ 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))
|
||||
|
||||
Reference in New Issue
Block a user