mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-06-06 17:01:33 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9c9ecc329 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user