Compare commits

..

1 Commits

Author SHA1 Message Date
dgtlmoon
e6572efecc Re #3337 - Various fixes for 'Extract Data' 2025-07-28 17:38:42 +02:00
24 changed files with 57 additions and 308 deletions

View File

@@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_simple_field, render_button, render_time_schedule_form %} {% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form %}
{% from '_common_fields.html' import render_common_settings_form %} {% from '_common_fields.html' import render_common_settings_form %}
<script> <script>
const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', mode="global-settings")}}"; const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', mode="global-settings")}}";
@@ -23,7 +23,6 @@
<li class="tab"><a href="#fetching">Fetching</a></li> <li class="tab"><a href="#fetching">Fetching</a></li>
<li class="tab"><a href="#filters">Global Filters</a></li> <li class="tab"><a href="#filters">Global Filters</a></li>
<li class="tab"><a href="#ui-options">UI Options</a></li> <li class="tab"><a href="#ui-options">UI Options</a></li>
<li class="tab"><a href="#ai-options"><i data-feather="aperture" style="width: 14px; height: 14px; margin-right: 4px;"></i> AI</a></li>
<li class="tab"><a href="#api">API</a></li> <li class="tab"><a href="#api">API</a></li>
<li class="tab"><a href="#timedate">Time &amp Date</a></li> <li class="tab"><a href="#timedate">Time &amp Date</a></li>
<li class="tab"><a href="#proxies">CAPTCHA &amp; Proxies</a></li> <li class="tab"><a href="#proxies">CAPTCHA &amp; Proxies</a></li>
@@ -263,24 +262,6 @@ nav
</div> </div>
</div> </div>
<div class="tab-pane-inner" id="ai-options">
<p><strong>New:</strong> click here (link to changedetection.io tutorial page) find out how to setup and example</p>
<br>
key fields should be some password type field so you can see its set but doesnt contain the key on view and doesnt lose it on save<br>
<div class="pure-control-group inline-radio">
{{ render_simple_field(form.application.form.ai.form.LLM_backend) }}
<span class="pure-form-message-inline">Preferred LLM connection</span>
</div>
<div class="pure-control-group">
{{ render_checkbox_field(form.application.form.ai.form.API_keys.form.openai) }}
<span class="pure-form-message-inline">Go here to read more about OpenAI integration</span>
</div>
<div class="pure-control-group">
{{ render_checkbox_field(form.application.form.ai.form.API_keys.form.gemini) }}
<span class="pure-form-message-inline">Go here to read more about Google Gemini integration</span>
</div>
</div>
<div class="tab-pane-inner" id="proxies"> <div class="tab-pane-inner" id="proxies">
<div id="recommended-proxy"> <div id="recommended-proxy">
<div> <div>

View File

@@ -25,7 +25,7 @@
<div class="tabs collapsable"> <div class="tabs collapsable">
<ul> <ul>
<li class="tab" id=""><a href="#general">General</a></li> <li class="tab" id=""><a href="#general">General</a></li>
<li class="tab"><a href="#filters-and-triggers">AI, Filters &amp; Triggers</a></li> <li class="tab"><a href="#filters-and-triggers">Filters &amp; Triggers</a></li>
{% if extra_tab_content %} {% if extra_tab_content %}
<li class="tab"><a href="#extras_tab">{{ extra_tab_content }}</a></li> <li class="tab"><a href="#extras_tab">{{ extra_tab_content }}</a></li>
{% endif %} {% endif %}

View File

@@ -312,27 +312,8 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
'''For when viewing the "preview" of the rendered text from inside of Edit''' '''For when viewing the "preview" of the rendered text from inside of Edit'''
from flask import jsonify from flask import jsonify
from changedetectionio.processors.text_json_diff import prepare_filter_prevew from changedetectionio.processors.text_json_diff import prepare_filter_prevew
result = prepare_filter_prevew(watch_uuid=uuid, form_data=request.form, datastore=datastore)
watch = datastore.data["watching"].get(uuid) return jsonify(result)
if not watch:
return jsonify({
"error": "Watch not found",
"code": 400
}), 400
if not watch.history_n:
return jsonify({
"error": "Watch has empty history, at least one fetch of the page is required.",
"code": 400
}), 400
#
try:
result = prepare_filter_prevew(watch_uuid=uuid, form_data=request.form, datastore=datastore)
return jsonify(result)
except Exception as e:
return abort(500, str(e))
@edit_blueprint.route("/highlight_submit_ignore_url", methods=['POST']) @edit_blueprint.route("/highlight_submit_ignore_url", methods=['POST'])
@login_optionally_required @login_optionally_required

View File

@@ -93,12 +93,15 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
return redirect(url_for('watchlist.index')) return redirect(url_for('watchlist.index'))
# For submission of requesting an extract # For submission of requesting an extract
extract_form = forms.extractDataForm(request.form) extract_form = forms.extractDataForm(formdata=request.form,
data={'extract_regex': request.form.get('extract_regex', '')}
)
if not extract_form.validate(): if not extract_form.validate():
flash("An error occurred, please see below.", "error") flash("An error occurred, please see below.", "error")
return _render_diff_template(uuid, extract_form)
else: else:
extract_regex = request.form.get('extract_regex').strip() extract_regex = request.form.get('extract_regex', '').strip()
output = watch.extract_regex_from_all_history(extract_regex) output = watch.extract_regex_from_all_history(extract_regex)
if output: if output:
watch_dir = os.path.join(datastore.datastore_path, uuid) watch_dir = os.path.join(datastore.datastore_path, uuid)
@@ -109,12 +112,11 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
response.headers['Expires'] = "0" response.headers['Expires'] = "0"
return response return response
flash('Nothing matches that RegEx', 'error') flash('No matches found while scanning all of the watch history for that RegEx.', 'error')
redirect(url_for('ui_views.diff_history_page', uuid=uuid) + '#extract') return redirect(url_for('ui.ui_views.diff_history_page', uuid=uuid) + '#extract')
@views_blueprint.route("/diff/<string:uuid>", methods=['GET']) def _render_diff_template(uuid, extract_form=None):
@login_optionally_required """Helper function to render the diff template with all required data"""
def diff_history_page(uuid):
from changedetectionio import forms from changedetectionio import forms
# More for testing, possible to return the first/only # More for testing, possible to return the first/only
@@ -128,8 +130,11 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
flash("No history found for the specified link, bad link?", "error") flash("No history found for the specified link, bad link?", "error")
return redirect(url_for('watchlist.index')) return redirect(url_for('watchlist.index'))
# For submission of requesting an extract # Use provided form or create a new one
extract_form = forms.extractDataForm(request.form) if extract_form is None:
extract_form = forms.extractDataForm(formdata=request.form,
data={'extract_regex': request.form.get('extract_regex', '')}
)
history = watch.history history = watch.history
dates = list(history.keys()) dates = list(history.keys())
@@ -170,7 +175,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
datastore.set_last_viewed(uuid, time.time()) datastore.set_last_viewed(uuid, time.time())
output = render_template("diff.html", return render_template("diff.html",
current_diff_url=watch['url'], current_diff_url=watch['url'],
from_version=str(from_version), from_version=str(from_version),
to_version=str(to_version), to_version=str(to_version),
@@ -193,7 +198,10 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
watch_a=watch watch_a=watch
) )
return output @views_blueprint.route("/diff/<string:uuid>", methods=['GET'])
@login_optionally_required
def diff_history_page(uuid):
return _render_diff_template(uuid)
@views_blueprint.route("/form/add/quickwatch", methods=['POST']) @views_blueprint.route("/form/add/quickwatch", methods=['POST'])
@login_optionally_required @login_optionally_required
@@ -212,14 +220,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
add_paused = request.form.get('edit_and_watch_submit_button') != None add_paused = request.form.get('edit_and_watch_submit_button') != None
processor = request.form.get('processor', 'text_json_diff') processor = request.form.get('processor', 'text_json_diff')
extras = {'paused': add_paused, 'processor': processor} new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras={'paused': add_paused, 'processor': processor})
LLM_prompt = request.form.get('LLM_prompt', '').strip()
if LLM_prompt:
extras['LLM_prompt'] = LLM_prompt
extras['LLM_send_type'] = request.form.get('LLM_send_type', 'text')
new_uuid = datastore.add_watch(url=url, tag=request.form.get('tags').strip(), extras=extras)
if new_uuid: if new_uuid:
if add_paused: if add_paused:

View File

@@ -5,7 +5,12 @@
<script src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
<script>let nowtimeserver={{ now_time_server }};</script> <script>let nowtimeserver={{ now_time_server }};</script>
<script>let favicon_baseURL="{{ url_for('static_content', group='favicon', filename="PLACEHOLDER")}}";</script> <script>let favicon_baseURL="{{ url_for('static_content', group='favicon', filename="PLACEHOLDER")}}";</script>
<script>
// Initialize Feather icons after the page loads
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
});
</script>
<style> <style>
.checking-now .last-checked { .checking-now .last-checked {
background-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.1) 100%); background-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.1) 100%);
@@ -26,12 +31,8 @@
{{ render_nolabel_field(form.edit_and_watch_submit_button, title="Edit first then Watch") }} {{ render_nolabel_field(form.edit_and_watch_submit_button, title="Edit first then Watch") }}
</div> </div>
<div id="watch-group-tag"> <div id="watch-group-tag">
<i data-feather="tag" style="width: 14px; height: 14px; stroke: white; margin-right: 4px;"></i>
{{ render_field(form.tags, value=active_tag.title if active_tag_uuid else '', placeholder="Watch group / tag", class="transparent-field") }} {{ render_field(form.tags, value=active_tag.title if active_tag_uuid else '', placeholder="Watch group / tag", class="transparent-field") }}
</div> </div>
{%- include 'edit/llm_prompt.html' -%}
<div id="quick-watch-processor-type"> <div id="quick-watch-processor-type">
{{ render_simple_field(form.processor) }} {{ render_simple_field(form.processor) }}
</div> </div>

View File

@@ -55,18 +55,6 @@ valid_method = {
default_method = 'GET' default_method = 'GET'
allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False')) allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False'))
LLM_example_texts = ['Tell me simply "Price, In stock"',
'Give me a list of all products for sale in this text',
'Tell me simply "Yes" "No" or "Maybe" if you think the weather outlook is good for a 4-day small camping trip',
'Look at this restaurant menu and only give me list of meals you think are good for type 2 diabetics, if nothing is found just say "nothing"',
]
LLM_send_type_choices = [('text', 'Plain text after filters'),
('above_fold_text', 'Text above the fold'),
('Screenshot', 'Screenshot / Selection'),
('HTML', 'HTML Source')
]
class StringListField(StringField): class StringListField(StringField):
widget = widgets.TextArea() widget = widgets.TextArea()
@@ -408,6 +396,19 @@ def validate_url(test_url):
# This should be wtforms.validators. # This should be wtforms.validators.
raise ValidationError('Watch protocol is not permitted by SAFE_PROTOCOL_REGEX or incorrect URL format') raise ValidationError('Watch protocol is not permitted by SAFE_PROTOCOL_REGEX or incorrect URL format')
class ValidateSinglePythonRegexString(object):
def __init__(self, message=None):
self.message = message
def __call__(self, form, field):
try:
re.compile(field.data)
except re.error:
message = field.gettext('RegEx \'%s\' is not a valid regular expression.')
raise ValidationError(message % (field.data))
class ValidateListRegex(object): class ValidateListRegex(object):
""" """
Validates that anything that looks like a regex passes as a regex Validates that anything that looks like a regex passes as a regex
@@ -426,6 +427,7 @@ class ValidateListRegex(object):
message = field.gettext('RegEx \'%s\' is not a valid regular expression.') message = field.gettext('RegEx \'%s\' is not a valid regular expression.')
raise ValidationError(message % (line)) raise ValidationError(message % (line))
class ValidateCSSJSONXPATHInput(object): class ValidateCSSJSONXPATHInput(object):
""" """
Filter validation Filter validation
@@ -527,15 +529,11 @@ class ValidateCSSJSONXPATHInput(object):
class quickWatchForm(Form): class quickWatchForm(Form):
from . import processors from . import processors
import random
url = fields.URLField('URL', validators=[validateURL()]) url = fields.URLField('URL', validators=[validateURL()])
tags = StringTagUUID('Group tag', [validators.Optional()]) tags = StringTagUUID('Group tag', [validators.Optional()])
watch_submit_button = SubmitField('Watch', render_kw={"class": "pure-button pure-button-primary"}) watch_submit_button = SubmitField('Watch', render_kw={"class": "pure-button pure-button-primary"})
processor = RadioField(u'Processor', choices=processors.available_processors(), default="text_json_diff") processor = RadioField(u'Processor', choices=processors.available_processors(), default="text_json_diff")
LLM_prompt = TextAreaField(u'AI Prompt', [validators.Optional()], render_kw={"placeholder": f'Example, "{random.choice(LLM_example_texts)}"'})
LLM_send_type = RadioField(u'LLM Send', choices=LLM_send_type_choices, default="text")
edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"}) edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"})
@@ -543,7 +541,6 @@ class quickWatchForm(Form):
# Common to a single watch and the global settings # Common to a single watch and the global settings
class commonSettingsForm(Form): class commonSettingsForm(Form):
from . import processors from . import processors
import random
def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs): def __init__(self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs):
super().__init__(formdata, obj, prefix, data, meta, **kwargs) super().__init__(formdata, obj, prefix, data, meta, **kwargs)
@@ -561,8 +558,6 @@ class commonSettingsForm(Form):
timezone = StringField("Timezone for watch schedule", render_kw={"list": "timezones"}, validators=[validateTimeZoneName()]) timezone = StringField("Timezone for watch schedule", render_kw={"list": "timezones"}, validators=[validateTimeZoneName()])
webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1, message="Should contain one or more seconds")]) webdriver_delay = IntegerField('Wait seconds before extracting text', validators=[validators.Optional(), validators.NumberRange(min=1, message="Should contain one or more seconds")])
LLM_prompt = TextAreaField(u'AI Prompt', [validators.Optional()], render_kw={"placeholder": f'Example, "{random.choice(LLM_example_texts)}"'})
LLM_send_type = RadioField(u'LLM Send', choices=LLM_send_type_choices, default="text")
class importForm(Form): class importForm(Form):
from . import processors from . import processors
@@ -761,29 +756,6 @@ class globalSettingsApplicationUIForm(Form):
socket_io_enabled = BooleanField('Realtime UI Updates Enabled', default=True, validators=[validators.Optional()]) socket_io_enabled = BooleanField('Realtime UI Updates Enabled', default=True, validators=[validators.Optional()])
favicons_enabled = BooleanField('Favicons Enabled', default=True, validators=[validators.Optional()]) favicons_enabled = BooleanField('Favicons Enabled', default=True, validators=[validators.Optional()])
class globalSettingsApplicationAIKeysForm(Form):
openai = StringField('OpenAI Key',
validators=[validators.Optional()],
render_kw={"placeholder": 'xxxxxxxxx'}
)
gemini = StringField('Google Gemini Key',
validators=[validators.Optional()],
render_kw={"placeholder": 'ooooooooo'}
)
class globalSettingsApplicationAIForm(Form):
#@todo use only configured types?
LLM_backend = RadioField(u'LLM Backend',
choices=[('openai', 'Open AI'), ('gemini', 'Gemini')],
default="text")
# So that we can pass this to our LLM/__init__.py as a keys dict
API_keys = FormField(globalSettingsApplicationAIKeysForm)
# datastore.data['settings']['application'].. # datastore.data['settings']['application']..
class globalSettingsApplicationForm(commonSettingsForm): class globalSettingsApplicationForm(commonSettingsForm):
@@ -816,8 +788,6 @@ class globalSettingsApplicationForm(commonSettingsForm):
message="Should contain zero or more attempts")]) message="Should contain zero or more attempts")])
ui = FormField(globalSettingsApplicationUIForm) ui = FormField(globalSettingsApplicationUIForm)
ai = FormField(globalSettingsApplicationAIForm)
class globalSettingsForm(Form): class globalSettingsForm(Form):
# Define these as FormFields/"sub forms", this way it matches the JSON storage # Define these as FormFields/"sub forms", this way it matches the JSON storage
@@ -835,5 +805,5 @@ class globalSettingsForm(Form):
class extractDataForm(Form): class extractDataForm(Form):
extract_regex = StringField('RegEx to extract', validators=[validators.Length(min=1, message="Needs a RegEx")]) extract_regex = StringField('RegEx to extract', validators=[validators.DataRequired(), ValidateSinglePythonRegexString()])
extract_submit_button = SubmitField('Extract as CSV', render_kw={"class": "pure-button pure-button-primary"}) extract_submit_button = SubmitField('Extract as CSV', render_kw={"class": "pure-button pure-button-primary"})

View File

@@ -65,10 +65,6 @@ class model(dict):
'socket_io_enabled': True, 'socket_io_enabled': True,
'favicons_enabled': True 'favicons_enabled': True
}, },
'ai': {
'openai_key': None,
'gemini_key': None
}
} }
} }
} }

View File

@@ -639,7 +639,7 @@ class model(watch_base):
if res: if res:
if not csv_writer: if not csv_writer:
# A file on the disk can be transferred much faster via flask than a string reply # A file on the disk can be transferred much faster via flask than a string reply
csv_output_filename = 'report.csv' csv_output_filename = f"report-{self.get('uuid')}.csv"
f = open(os.path.join(self.watch_data_dir, csv_output_filename), 'w') f = open(os.path.join(self.watch_data_dir, csv_output_filename), 'w')
# @todo some headers in the future # @todo some headers in the future
#fieldnames = ['Epoch seconds', 'Date'] #fieldnames = ['Epoch seconds', 'Date']

View File

@@ -38,9 +38,6 @@ class watch_base(dict):
'last_error': False, 'last_error': False,
'last_notification_error': None, 'last_notification_error': None,
'last_viewed': 0, # history key value of the last viewed via the [diff] link 'last_viewed': 0, # history key value of the last viewed via the [diff] link
'LLM_prompt': None,
'LLM_send_type': None,
'LLM_backend': None,
'method': 'GET', 'method': 'GET',
'notification_alert_count': 0, 'notification_alert_count': 0,
'notification_body': None, 'notification_body': None,

View File

@@ -1,64 +0,0 @@
import importlib
from langchain_core.messages import SystemMessage, HumanMessage
SYSTEM_MESSAGE = (
"You are a text analyser who will attempt to give the most concise information "
"to the request, the information should be returned in a way that if I ask you again "
"I should get the same answer if the outcome is the same. The goal is to cut down "
"or reduce the text changes from you when i ask the same question about similar content "
"Always list items in exactly the same order and wording as found in the source text. "
)
class LLM_integrate:
PROVIDER_MAP = {
"openai": ("langchain_openai", "ChatOpenAI"),
"azure": ("langchain_community.chat_models", "AzureChatOpenAI"),
"gemini": ("langchain_google_genai", "ChatGoogleGenerativeAI")
}
def __init__(self, api_keys: dict):
"""
api_keys = {
"openai": "sk-xxx",
"azure": "AZURE_KEY",
"gemini": "GEMINI_KEY"
}
"""
self.api_keys = api_keys
def run(self, provider: str, model: str, message: str):
module_name, class_name = self.PROVIDER_MAP[provider]
# Import the class dynamically
module = importlib.import_module(module_name)
LLMClass = getattr(module, class_name)
# Create the LLM object
llm_kwargs = {}
if provider == "openai":
llm_kwargs = dict(api_key=self.api_keys.get("openai", ''),
model=model,
# https://api.python.langchain.com/en/latest/chat_models/langchain_openai.chat_models.base.ChatOpenAI.html#langchain_openai.chat_models.base.ChatOpenAI.temperature
temperature=0 # most deterministic,
)
elif provider == "azure":
llm_kwargs = dict(
api_key=self.api_keys["azure"],
azure_endpoint="https://<your-endpoint>.openai.azure.com",
deployment_name=model
)
elif provider == "gemini":
llm_kwargs = dict(api_key=self.api_keys.get("gemini"), model=model)
llm = LLMClass(**llm_kwargs)
# Build your messages
messages = [
SystemMessage(content=SYSTEM_MESSAGE),
HumanMessage(content=message)
]
# Run the model asynchronously
result = llm.invoke(messages)
return result.content

View File

@@ -1,6 +1,5 @@
from abc import abstractmethod from abc import abstractmethod
from changedetectionio.content_fetchers.base import Fetcher from changedetectionio.content_fetchers.base import Fetcher
from changedetectionio.processors.LLM import LLM_integrate
from changedetectionio.strtobool import strtobool from changedetectionio.strtobool import strtobool
from copy import deepcopy from copy import deepcopy
from loguru import logger from loguru import logger

View File

@@ -7,7 +7,7 @@ import re
import urllib3 import urllib3
from changedetectionio.conditions import execute_ruleset_against_all_plugins from changedetectionio.conditions import execute_ruleset_against_all_plugins
from changedetectionio.processors import difference_detection_processor, LLM_integrate from changedetectionio.processors import difference_detection_processor
from changedetectionio.html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text, TRANSLATE_WHITESPACE_TABLE from changedetectionio.html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text, TRANSLATE_WHITESPACE_TABLE
from changedetectionio import html_tools, content_fetchers from changedetectionio import html_tools, content_fetchers
from changedetectionio.blueprint.price_data_follower import PRICE_DATA_TRACK_ACCEPT, PRICE_DATA_TRACK_REJECT from changedetectionio.blueprint.price_data_follower import PRICE_DATA_TRACK_ACCEPT, PRICE_DATA_TRACK_REJECT
@@ -293,30 +293,6 @@ class perform_site_check(difference_detection_processor):
# we end up with 'Some text\n\n', sorting will add all those extra \n at the start, so we remove them here. # we end up with 'Some text\n\n', sorting will add all those extra \n at the start, so we remove them here.
stripped_text_from_html = stripped_text_from_html.replace("\n\n", "\n") stripped_text_from_html = stripped_text_from_html.replace("\n\n", "\n")
stripped_text_from_html = '\n'.join(sorted(stripped_text_from_html.splitlines(), key=lambda x: x.lower())) stripped_text_from_html = '\n'.join(sorted(stripped_text_from_html.splitlines(), key=lambda x: x.lower()))
### OPENAI?
# And here we run LLM integration based on the content we received
LLM_keys = self.datastore.data['settings']['application']['ai'].get('API_keys', {})
if watch.get('LLM_prompt') and stripped_text_from_html and LLM_keys:
response = ""
try:
integrator = LLM_integrate(api_keys=LLM_keys)
response = integrator.run(
provider="openai",
model="gpt-4.1", #gpt-4-turbo
message=f"{watch.get('LLM_prompt')}\n----------- Content follows-----------\n\n{stripped_text_from_html}"
)
except Exception as e:
logger.critical(f"Error running LLM integration {str(e)} (type etc)")
raise(e)
x = 1
# todo is there something special when tokens are used up etc?
else:
stripped_text_from_html = response
# logger.trace("LLM done")
finally:
logger.debug("LLM request done (type etc)")
### CALCULATE MD5 ### CALCULATE MD5
# If there's text to ignore # If there's text to ignore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -21,7 +21,6 @@ function request_textpreview_update() {
namespace: 'watchEdit' namespace: 'watchEdit'
}).done(function (data) { }).done(function (data) {
console.debug(data['duration']) console.debug(data['duration'])
$('#error-text').text(data['duration']);
$('#filters-and-triggers #text-preview-before-inner').text(data['before_filter']); $('#filters-and-triggers #text-preview-before-inner').text(data['before_filter']);
$('#filters-and-triggers #text-preview-inner') $('#filters-and-triggers #text-preview-inner')
.text(data['after_filter']) .text(data['after_filter'])
@@ -38,8 +37,9 @@ function request_textpreview_update() {
}).fail(function (error) { }).fail(function (error) {
if (error.statusText === 'abort') { if (error.statusText === 'abort') {
console.log('Request was aborted due to a new request being fired.'); console.log('Request was aborted due to a new request being fired.');
} else {
$('#filters-and-triggers #text-preview-inner').text('There was an error communicating with the server.');
} }
$('#error-text').text(error.responseJSON['error']);
}) })
} }

View File

@@ -1,5 +1,3 @@
@use "_llm-prompt";
ul#conditions_match_logic { ul#conditions_match_logic {
list-style: none; list-style: none;
input, label, li { input, label, li {

View File

@@ -1,59 +0,0 @@
#form-quick-watch-add #openai-prompt {
color: var(--color-white);
}
#llm-prompt-all {
.label {
display: block !important;
}
textarea {
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word; /* legacy support */
font-size: 13px;
}
ul {
list-style: none;
padding-left: 0px;
li {
display: flex;
align-items: center;
gap: 0.5em;
padding-bottom: 0.3em;
> * {
margin: 0px;
padding: 0px;
}
label {
font-weight: normal;
}
}
}
}
@media (min-width: 768px) {
#llm-prompt-all {
display: grid;
grid-template-columns: 1fr auto auto;
column-gap: 1.5rem;
align-items: start;
font-size: 0.9rem;
padding: 0.3rem;
#llm-prompt {
/* ensure the textarea stretches horizontally */
width: 100%;
textarea {
width: 100%;
box-sizing: border-box;
}
}
}
}

View File

@@ -792,9 +792,7 @@ textarea::placeholder {
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
background-color: var(--color-background-tab); background-color: var(--color-background-tab);
svg {
stroke: var(--color-text-tab);
}
&:not(.active) { &:not(.active) {
&:hover { &:hover {
background-color: var(--color-background-tab-hover); background-color: var(--color-background-tab-hover);
@@ -804,13 +802,11 @@ textarea::placeholder {
&.active, &.active,
:target { :target {
background-color: var(--color-background); background-color: var(--color-background);
a { a {
color: var(--color-text-tab-active); color: var(--color-text-tab-active);
font-weight: bold; font-weight: bold;
} }
svg {
stroke: var(--color-text-tab-active);
}
} }
a { a {

File diff suppressed because one or more lines are too long

View File

@@ -38,12 +38,6 @@
<script src="{{url_for('static_content', group='js', filename='socket.io.min.js')}}"></script> <script src="{{url_for('static_content', group='js', filename='socket.io.min.js')}}"></script>
<script src="{{url_for('static_content', group='js', filename='realtime.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='realtime.js')}}" defer></script>
{% endif %} {% endif %}
<script>
// Initialize Feather icons after the page loads
document.addEventListener('DOMContentLoaded', function() {
feather.replace();
});
</script>
</head> </head>
<body class=""> <body class="">

View File

@@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% from '_helpers.html' import render_field, render_simple_field, render_checkbox_field, render_button, render_time_schedule_form, playwright_warning, only_playwright_type_watches_warning, render_conditions_fieldlist_of_formfields_as_table %} {% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form, playwright_warning, only_playwright_type_watches_warning, render_conditions_fieldlist_of_formfields_as_table %}
{% from '_common_fields.html' import render_common_settings_form %} {% from '_common_fields.html' import render_common_settings_form %}
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script>
@@ -52,7 +52,7 @@
<!-- should goto extra forms? --> <!-- should goto extra forms? -->
{% if watch['processor'] == 'text_json_diff' %} {% if watch['processor'] == 'text_json_diff' %}
<li class="tab"><a id="visualselector-tab" href="#visualselector">Visual Filter Selector</a></li> <li class="tab"><a id="visualselector-tab" href="#visualselector">Visual Filter Selector</a></li>
<li class="tab" id="filters-and-triggers-tab"><a href="#filters-and-triggers">AI, Filters &amp; Triggers</a></li> <li class="tab" id="filters-and-triggers-tab"><a href="#filters-and-triggers">Filters &amp; Triggers</a></li>
<li class="tab" id="conditions-tab"><a href="#conditions">Conditions</a></li> <li class="tab" id="conditions-tab"><a href="#conditions">Conditions</a></li>
{% endif %} {% endif %}
<li class="tab"><a href="#notifications">Notifications</a></li> <li class="tab"><a href="#notifications">Notifications</a></li>
@@ -316,6 +316,7 @@ Math: {{ 1 + 1 }}") }}
</li> </li>
</ul> </ul>
</div> </div>
{% include "edit/include_subtract.html" %} {% include "edit/include_subtract.html" %}
<div class="text-filtering border-fieldset"> <div class="text-filtering border-fieldset">
<fieldset class="pure-group" id="text-filtering-type-options"> <fieldset class="pure-group" id="text-filtering-type-options">
@@ -363,7 +364,6 @@ Math: {{ 1 + 1 }}") }}
</div> </div>
</div> </div>
</div> </div>
<p id="error-text"></p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,4 @@
<div class="pure-control-group"> <div class="pure-control-group">
{%- include 'edit/llm_prompt.html' -%}
</div>
<div class="pure-control-group">
{% set field = render_field(form.include_filters, {% set field = render_field(form.include_filters,
rows=5, rows=5,
placeholder=has_tag_filters_extra+"#example placeholder=has_tag_filters_extra+"#example

View File

@@ -1,12 +0,0 @@
<div class="pure-control-group" id="ai-filter-options">
<div id="openai-prompt">
<div id="llm-prompt-all">
<div id="llm-prompt">
{{ render_simple_field(form.LLM_prompt, rows=5) }}
</div>
<div id="llm-send-type">
{{ render_simple_field(form.LLM_send_type) }}
</div>
</div>
</div>
</div>

View File

@@ -46,7 +46,7 @@ def test_check_extract_text_from_diff(client, live_server, measure_memory_usage)
follow_redirects=False follow_redirects=False
) )
assert b'Nothing matches that RegEx' not in res.data assert b'No matches found while scanning all of the watch history for that RegEx.' not in res.data
assert res.content_type == 'text/csv' assert res.content_type == 'text/csv'
# Read the csv reply as stringio # Read the csv reply as stringio

View File

@@ -69,9 +69,6 @@ werkzeug==3.0.6
# Templating, so far just in the URLs but in the future can be for the notifications also # Templating, so far just in the URLs but in the future can be for the notifications also
jinja2~=3.1 jinja2~=3.1
jinja2-time jinja2-time
langchain~=0.3
langchain-openai~=0.3
openpyxl openpyxl
# https://peps.python.org/pep-0508/#environment-markers # https://peps.python.org/pep-0508/#environment-markers
# https://github.com/dgtlmoon/changedetection.io/pull/1009 # https://github.com/dgtlmoon/changedetection.io/pull/1009