Compare commits

..

5 Commits

Author SHA1 Message Date
dgtlmoon cba745e205 UI - Preview problem fix for extract_text/ignore_text #4138 2026-05-20 13:34:29 +02:00
dgtlmoon d04862d2fa 0.55.5
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / lint-translations (push) Has been cancelled
ChangeDetection.io App Test / lint-template-i18n (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-14 (push) Has been cancelled
2026-05-19 19:05:53 +02:00
dgtlmoon 9d9a58e763 LLM - Master on/off switch (enable/disable) (#4162) 2026-05-19 19:05:13 +02:00
dgtlmoon 649c153bf4 Notifications - Fix 'str' object is not callable when {{ diff(...) }} callable tokens are used with HTML/htmlcolor output (#4161)
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / lint-translations (push) Has been cancelled
ChangeDetection.io App Test / lint-template-i18n (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-14 (push) Has been cancelled
2026-05-19 18:09:28 +02:00
Manuel Pérez be3ba3bca3 Fix Spanish translations for 'Changed' and 'Last Changed' (#4160) 2026-05-19 17:23:15 +02:00
27 changed files with 335 additions and 22 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
# Semver means never use .01, or 00. Should be .1.
__version__ = '0.55.4'
__version__ = '0.55.5'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError
@@ -39,6 +39,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
'llm_provider_kind': _stored_llm.get('provider_kind', ''),
'llm_local_token_multiplier': _stored_llm.get('local_token_multiplier', 5),
'llm_change_summary_default': datastore.data['settings']['application'].get('llm_change_summary_default', ''),
'llm_enabled': datastore.data['settings']['application'].get('llm_enabled', True),
'llm_override_diff_with_summary': datastore.data['settings']['application'].get('llm_override_diff_with_summary', True),
'llm_restock_use_fallback_extract': datastore.data['settings']['application'].get('llm_restock_use_fallback_extract', True),
'llm_debug': datastore.data['settings']['application'].get('llm_debug', False),
@@ -120,6 +121,9 @@ def construct_blueprint(datastore: ChangeDetectionStore):
datastore.data['settings']['application']['llm_change_summary_default'] = (
llm_data.get('llm_change_summary_default') or ''
).strip()
datastore.data['settings']['application']['llm_enabled'] = (
bool(llm_data.get('llm_enabled', True))
)
datastore.data['settings']['application']['llm_override_diff_with_summary'] = (
bool(llm_data.get('llm_override_diff_with_summary', True))
)
@@ -69,6 +69,17 @@
{% call stab_pane('provider') %}
<p class="stab-section-title">{{ _('AI Provider') }}</p>
<div class="pure-control-group">
<label></label>
{{ form.llm.form.llm_enabled() }}
<label for="{{ form.llm.form.llm_enabled.id }}" style="display:inline; font-weight:normal;">
{{ form.llm.form.llm_enabled.label.text }}
</label>
<span class="pure-form-message-inline">
{{ _('Master switch — when off, all AI lookups are skipped even if a provider is configured below.') }}
</span>
</div>
{% if not llm_env_configured and not (llm_config and llm_config.get('model')) %}
<div class="stab-overview-disclaimer">
<div class="stab-disclaimer-icon"></div>
+8
View File
@@ -1193,6 +1193,14 @@ class globalSettingsLLMForm(Form):
"style": "width: 10em;",
},
)
# Master on/off switch for ALL LLM lookups at runtime. When False, every entry point
# in evaluator.py (and the restock fallback) short-circuits with a logger.debug
# message — even if a provider+model is still configured. Saved config and the
# "configured" badge remain visible so the user can toggle back on without re-entering.
llm_enabled = BooleanField(
_l('Enable AI / LLM features'),
default=True,
)
llm_override_diff_with_summary = BooleanField(
_l('Replace {{diff}} notification token with AI summary'),
default=True,
+26 -4
View File
@@ -228,6 +228,28 @@ def llm_configured_via_env() -> bool:
return bool(os.getenv('LLM_MODEL', '').strip())
def _runtime_llm_config(datastore) -> dict | None:
"""
Runtime gate used by every LLM entry point in this module (and the restock
fallback). Returns the resolved config dict only when both:
- the master 'llm_enabled' toggle is on (default True)
- a provider+model is actually configured
When the toggle is off but a config exists, logs a debug message and returns
None so callers fall through their existing "not configured" early-return path.
The settings UI deliberately still calls get_llm_config() directly so the
"AI / LLM configured: ..." badge keeps showing the saved provider even while
the toggle is off.
"""
cfg = get_llm_config(datastore)
if not bool(datastore.data['settings']['application'].get('llm_enabled', True)):
if cfg:
logger.debug("LLM features disabled via settings (llm_enabled=False) — skipping LLM lookup")
return None
return cfg
# ---------------------------------------------------------------------------
# Global monthly token budget
# ---------------------------------------------------------------------------
@@ -379,7 +401,7 @@ def run_setup(watch, datastore, snapshot_text: str) -> None:
Stores result in watch['llm_prefilter'] (str selector or None).
Called once when intent is first set, and again if pre-filter returns zero matches.
"""
cfg = get_llm_config(datastore)
cfg = _runtime_llm_config(datastore)
if not cfg:
return
@@ -509,7 +531,7 @@ def summarise_change(watch, datastore, diff: str, current_snapshot: str = '') ->
The result replaces {{ diff }} in notifications so the user gets a
readable description instead of raw +/- diff lines.
"""
cfg = get_llm_config(datastore)
cfg = _runtime_llm_config(datastore)
if not cfg:
return ''
@@ -597,7 +619,7 @@ def preview_extract(watch, datastore, content: str) -> dict | None:
Returns {'found': bool, 'answer': str} or None if LLM not configured / no intent.
"""
cfg = get_llm_config(datastore)
cfg = _runtime_llm_config(datastore)
if not cfg:
return None
@@ -648,7 +670,7 @@ def evaluate_change(watch, datastore, diff: str, current_snapshot: str = '') ->
Results are cached by (intent, diff) hash each unique diff is evaluated exactly once.
"""
cfg = get_llm_config(datastore)
cfg = _runtime_llm_config(datastore)
if not cfg:
return None
+1
View File
@@ -71,6 +71,7 @@ class model(dict):
'shared_diff_access': False,
'strip_ignored_lines': False,
'tags': None, # Initialized in __init__ with real datastore_path
'llm_enabled': True,
'llm_thinking_budget': LLM_DEFAULT_THINKING_BUDGET,
'llm_max_summary_tokens': LLM_DEFAULT_MAX_SUMMARY_TOKENS,
'webdriver_delay': None , # Extra delay in seconds before extracting text
@@ -203,15 +203,17 @@ def get_itemprop_availability_override(content, fetcher_name, fetcher_instance,
return None
try:
from changedetectionio.llm.evaluator import get_llm_config, accumulate_global_tokens
from changedetectionio.llm.evaluator import _runtime_llm_config, accumulate_global_tokens
from changedetectionio.llm import client as llm_client
except ImportError as e:
logger.debug(f"LLM restock fallback: LLM libraries not available ({e})")
return None
llm_cfg = get_llm_config(datastore)
# _runtime_llm_config returns None (with a debug log) when the master 'llm_enabled'
# toggle is off, so this path is gated for free.
llm_cfg = _runtime_llm_config(datastore)
if not llm_cfg or not llm_cfg.get('model'):
logger.debug("LLM restock fallback: no LLM model configured, skipping")
logger.debug("LLM restock fallback: no LLM model configured or LLM disabled, skipping")
return None
text_content = _strip_html(content) if content else ''
@@ -35,6 +35,50 @@ def _task(watch, update_handler):
return text_after_filter
def _compute_ignore_line_numbers_for_preview(text_pre_extract, ignore_patterns, extract_patterns):
"""1-indexed output line numbers in the post-extract display that correspond
to input lines matching ignore_text patterns.
Needed because extract_text (#4138) transforms line content — e.g. "0.54.10"
becomes ".54.10" so a substring match for "0.54.10" against the post-extract
text fails and the preview UI can no longer mark the line as ignored. We find
the ignored line numbers in the pre-extract text and replay extract_by_regex
line-by-line to map them forward.
"""
from changedetectionio import html_tools
from changedetectionio.processors.text_json_diff.processor import ContentTransformer
if not text_pre_extract or not ignore_patterns:
return []
ignored_input_lines = set(
html_tools.strip_ignore_text(
content=text_pre_extract,
wordlist=ignore_patterns,
mode='line numbers'
)
)
if not ignored_input_lines:
return []
if not extract_patterns:
return sorted(ignored_input_lines)
# Replay extract_by_regex per-line. Each emitted match ends with exactly one
# '\n', so counting newlines tells us how many output lines this input produced.
output_line_counter = 0
result = []
for input_idx, line in enumerate(text_pre_extract.splitlines()):
is_ignored = (input_idx + 1) in ignored_input_lines
matches_in_line = ContentTransformer.extract_by_regex(line, extract_patterns).count('\n')
for _ in range(matches_in_line):
output_line_counter += 1
if is_ignored:
result.append(output_line_counter)
return result
def prepare_filter_prevew(datastore, watch_uuid, form_data):
'''Used by @app.route("/edit/<uuid_str:uuid>/preview-rendered", methods=['POST'])'''
from changedetectionio import forms, html_tools
@@ -50,6 +94,7 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data):
text_after_filter = ''
text_before_filter = ''
text_pre_extract = ''
trigger_line_numbers = []
ignore_line_numbers = []
blocked_line_numbers = []
@@ -89,15 +134,22 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data):
update_handler.fetcher.content = str(decompressed_data) # str() because playwright/puppeteer/requests return string
update_handler.fetcher.headers['content-type'] = tmp_watch.get('content-type')
# Process our watch with filters and the HTML from disk, and also a blank watch with no filters but also with the same HTML from disk
# Process our watch with filters and the HTML from disk, and also a blank watch with no filters but also with the same HTML from disk.
# The third task runs with extract_text cleared so we can compute ignore_line_numbers
# against the pre-extract text (extract_text transforms lines so post-extract substring
# matching for ignore patterns would otherwise fail — see #4138 follow-up).
# Do this as parallel threads (not processes) to avoid pickle issues with Lock objects
tmp_watch_no_extract = deepcopy(tmp_watch)
tmp_watch_no_extract['extract_text'] = []
try:
with ThreadPoolExecutor(max_workers=2) as executor:
with ThreadPoolExecutor(max_workers=3) as executor:
future1 = executor.submit(_task, tmp_watch, update_handler)
future2 = executor.submit(_task, blank_watch_no_filters, update_handler)
future3 = executor.submit(_task, tmp_watch_no_extract, update_handler)
text_after_filter = future1.result()
text_before_filter = future2.result()
text_pre_extract = future3.result()
except Exception as e:
x=1
@@ -111,10 +163,11 @@ def prepare_filter_prevew(datastore, watch_uuid, form_data):
try:
text_to_ignore = tmp_watch.get('ignore_text', []) + datastore.data['settings']['application'].get('global_ignore_text', [])
ignore_line_numbers = html_tools.strip_ignore_text(content=text_after_filter,
wordlist=text_to_ignore,
mode='line numbers'
)
ignore_line_numbers = _compute_ignore_line_numbers_for_preview(
text_pre_extract=text_pre_extract,
ignore_patterns=text_to_ignore,
extract_patterns=tmp_watch.get('extract_text', [])
)
except Exception as e:
text_before_filter = f"Error: {str(e)}"
@@ -9,6 +9,10 @@ function request_textpreview_update() {
$('textarea:visible, input:visible').each(function () {
const $element = $(this); // Cache the jQuery object for the current element
const name = $element.attr('name'); // Get the name attribute of the element
// Radios share a name across multiple inputs; .val() returns the value
// attribute regardless of checked state, so iterating would let the last
// unchecked radio overwrite the user's actual selection. Skip unchecked.
if ($element.is(':radio') && !$element.is(':checked')) return;
data[name] = $element.is(':checkbox') ? ($element.is(':checked') ? $element.val() : false) : $element.val();
});
@@ -77,3 +77,82 @@ def test_content_filter_live_preview(client, live_server, measure_memory_usage,
assert reply.get('trigger_line_numbers') == [1] # Triggers "Awesome" in line 1
delete_all_watches(client)
def _setup_version_list_preview(datastore_path, client):
"""Shared HTML fixture for #4138 preview regressions (version tag list)."""
import time
data = """<html><body>
0.55.5<br>
0.55.4<br>
0.55.3<br>
0.54.10<br>
0.54.9<br>
</body></html>"""
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
f.write(data)
test_url = url_for('test_endpoint', _external=True)
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url)
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
time.sleep(0.5)
wait_for_all_checks(client)
return test_url, uuid
def test_preview_ignore_highlight_with_extract_text(client, live_server, measure_memory_usage, datastore_path):
"""Regression for #4138 follow-up: when extract_text rewrites a line (e.g. "0.54.10"".54.10"),
the preview must still highlight that row as 'ignored' even though substring matching against the
post-extract text fails."""
import json
test_url, uuid = _setup_version_list_preview(datastore_path, client)
res = client.post(
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
data={
"include_filters": "",
"fetch_backend": 'html_requests',
"ignore_text": "0.54.10",
"extract_text": r"/(.\d+\.\d+)/",
"url": test_url,
},
)
reply = json.loads(res.data.decode('utf-8'))
# The regex strips the leading "0", so the post-extract line for the ignored input is ".54.10".
# The preview should still mark its position (line 4) as ignored.
assert reply.get('ignore_line_numbers') == [4], \
f"Expected line 4 to be highlighted as ignored, got {reply.get('ignore_line_numbers')!r}"
delete_all_watches(client)
def test_preview_strip_ignored_lines_with_extract_text(client, live_server, measure_memory_usage, datastore_path):
"""Regression for #4138 follow-up: with strip_ignored_lines enabled, an ignored line must be
removed from the preview output even when extract_text would otherwise rewrite it (0.54.10 .54.10)."""
import json
test_url, uuid = _setup_version_list_preview(datastore_path, client)
res = client.post(
url_for("ui.ui_edit.watch_get_preview_rendered", uuid=uuid),
data={
"include_filters": "",
"fetch_backend": 'html_requests',
"ignore_text": "0.54.10",
"extract_text": r"/(.\d+\.\d+)/",
"strip_ignored_lines": "true",
"url": test_url,
},
)
reply = json.loads(res.data.decode('utf-8'))
after_filter = reply.get('after_filter', '')
assert '.54.10' not in after_filter, \
f"Stripped ignored line should not appear in preview output, got:\n{after_filter!r}"
assert '0.54.10' not in after_filter
assert reply.get('ignore_line_numbers') == [], \
f"Stripped lines need no highlight, got {reply.get('ignore_line_numbers')!r}"
delete_all_watches(client)
@@ -842,6 +842,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3178,6 +3182,10 @@ msgstr "Měsíční rozpočet tokenů"
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -858,6 +858,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3230,6 +3234,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -840,6 +840,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3172,6 +3176,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -840,6 +840,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3172,6 +3176,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -878,6 +878,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -2314,11 +2318,11 @@ msgstr "Último Comprobado"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Changed"
msgstr "Cambiadp"
msgstr "Cambiado"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Last Changed"
msgstr "Último Cambiadp"
msgstr "Último Cambiado"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "No web page change detection watches configured, please add a URL in the box above, or"
@@ -3245,6 +3249,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -846,6 +846,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3185,6 +3189,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -842,6 +842,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3174,6 +3178,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -847,6 +847,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3191,6 +3195,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -844,6 +844,10 @@ msgstr "AI 프로바이더 설정"
msgid "AI Provider"
msgstr "AI 프로바이더"
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr "제3자 데이터 전송 - 읽어 주세요"
@@ -3182,6 +3186,10 @@ msgstr "월간 토큰 예산"
msgid "Max input characters"
msgstr "최대 입력 문자 수"
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr "{{diff}} 알림 토큰을 AI 요약으로 대체"
+10 -2
View File
@@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.55.4\n"
"Project-Id-Version: changedetection.io 0.55.5\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-05-19 11:38+0200\n"
"POT-Creation-Date: 2026-05-19 19:05+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -839,6 +839,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3171,6 +3175,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -865,6 +865,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3222,6 +3226,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -875,6 +875,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3225,6 +3229,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -855,6 +855,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3204,6 +3208,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -844,6 +844,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3177,6 +3181,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
@@ -843,6 +843,10 @@ msgstr ""
msgid "AI Provider"
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
msgstr ""
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
msgid "Third-party data transfer — please read"
msgstr ""
@@ -3176,6 +3180,10 @@ msgstr ""
msgid "Max input characters"
msgstr ""
#: changedetectionio/forms.py
msgid "Enable AI / LLM features"
msgstr ""
#: changedetectionio/forms.py
msgid "Replace {{diff}} notification token with AI summary"
msgstr ""
+13 -4
View File
@@ -432,9 +432,13 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
update_obj['_llm_result'] = None
update_obj['_llm_intent'] = ''
update_obj['_llm_change_summary'] = ''
# skip_check: when budget exceeded, don't run LLM or the check
# skip_check: when budget exceeded, don't run LLM or the check.
# Also gated on llm_enabled — a disabled LLM can't be spending tokens,
# so the budget enforcement shouldn't suppress changes when the user
# has explicitly switched LLM off.
_llm_master_enabled = bool(datastore.data['settings']['application'].get('llm_enabled', True))
_llm_budget_action = datastore.data['settings']['application'].get('llm_budget_action', 'skip_llm')
if _llm_budget_action == 'skip_check':
if _llm_master_enabled and _llm_budget_action == 'skip_check':
from changedetectionio.llm.evaluator import is_global_token_budget_exceeded
if is_global_token_budget_exceeded(datastore):
logger.info(f"LLM monthly budget exceeded — skipping check for {uuid} (budget_action=skip_check)")
@@ -444,9 +448,14 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
try:
from changedetectionio.llm.evaluator import (
evaluate_change, resolve_intent, resolve_llm_field,
summarise_change, get_llm_config,
summarise_change, _runtime_llm_config,
)
_llm_cfg = get_llm_config(datastore)
# _runtime_llm_config returns None (and logs a debug skip
# message) when the master 'llm_enabled' toggle is off, so
# the whole block — diff computation, status minitext, and
# the two executor dispatches — is skipped, not just the
# inner LLM lookups.
_llm_cfg = _runtime_llm_config(datastore)
if _llm_cfg:
# Compute unified diff once — used by both intent and summary
_watch_dates = list(watch.history.keys())