Skip to content

Commit 96f8370

Browse files
itsmvdjkppr
andauthored
Enhance LLM configuration handling and settings UI (#3366)
* Improve LLM settings & vertexai bugfix * Update test_system_settings_resource test * Update ollama.py to check for required settings, update ollama default config * Simplify SettingsDialog.vue --------- Co-authored-by: Janosch <[email protected]>
1 parent 12da8bd commit 96f8370

File tree

8 files changed

+291
-85
lines changed

8 files changed

+291
-85
lines changed

data/timesketch.conf

+2-2
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,8 @@ LLM_PROVIDER_CONFIGS = {
389389
},
390390
'default': {
391391
'ollama': {
392-
'server_url': 'http://ollama:11434',
393-
'model': 'gemma:7b',
392+
'server_url': '',
393+
'model': '',
394394
},
395395
}
396396
}

timesketch/api/v1/resources/settings.py

+85-36
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""System settings."""
15-
1615
import logging
16+
from typing import Any
17+
1718
from flask import current_app, jsonify
1819
from flask_restful import Resource
1920
from flask_login import login_required
21+
from timesketch.lib.llms.providers import manager as llm_manager
2022

2123
logger = logging.getLogger("timesketch.system_settings")
2224

@@ -31,42 +33,89 @@ def get(self):
3133
Returns:
3234
JSON object with system settings.
3335
"""
34-
# Settings from timesketch.conf to expose to the frontend clients.
35-
settings_to_return = ["DFIQ_ENABLED"]
36-
result = {}
37-
38-
for setting in settings_to_return:
39-
result[setting] = current_app.config.get(setting)
40-
41-
# Derive the default LLM provider from the new configuration.
42-
# Expecting the "default" config to be a dict with exactly one key:
43-
# the provider name.
44-
llm_configs = current_app.config.get("LLM_PROVIDER_CONFIGS", {})
45-
default_provider = None
46-
default_conf = llm_configs.get("default")
47-
if default_conf and isinstance(default_conf, dict) and len(default_conf) == 1:
48-
default_provider = next(iter(default_conf))
49-
result["LLM_PROVIDER"] = default_provider
50-
51-
# TODO(mvd): Remove by 2025/06/01 once all users have updated their config.
52-
old_llm_provider = current_app.config.get("LLM_PROVIDER")
36+
result = {
37+
"DFIQ_ENABLED": current_app.config.get("DFIQ_ENABLED", False),
38+
"SEARCH_PROCESSING_TIMELINES": current_app.config.get(
39+
"SEARCH_PROCESSING_TIMELINES", False
40+
),
41+
"LLM_FEATURES_AVAILABLE": self._get_llm_features_availability(
42+
current_app.config.get("LLM_PROVIDER_CONFIGS", {})
43+
),
44+
}
45+
46+
return jsonify(result)
47+
48+
def _get_llm_features_availability(
49+
self, llm_configs: dict[str, Any]
50+
) -> dict[str, bool]:
51+
"""Get availability status for all LLM features.
52+
53+
Args:
54+
llm_configs: LLM provider configuration dictionary
55+
56+
Returns:
57+
dict mapping feature names to availability status (bool)
58+
"""
59+
default_provider_working = False
60+
61+
if not isinstance(llm_configs, dict):
62+
logger.debug(
63+
"LLM_PROVIDER_CONFIGS is not a dictionary: %s",
64+
type(llm_configs).__name__,
65+
)
66+
return {"default": default_provider_working}
67+
5368
if (
54-
old_llm_provider and "default" not in llm_configs
55-
): # Basic check for old config
56-
warning_message = (
57-
"Your LLM configuration in timesketch.conf is outdated and may cause "
58-
"issues with LLM features. "
59-
"Please update your LLM_PROVIDER_CONFIGS section to the new format. "
60-
"Refer to the documentation for the updated configuration structure."
69+
"default" in llm_configs
70+
and isinstance(llm_configs["default"], dict)
71+
and len(llm_configs["default"]) == 1
72+
):
73+
default_provider = next(iter(llm_configs["default"]))
74+
default_provider_config = llm_configs["default"][default_provider]
75+
default_provider_working = self._check_provider_working(
76+
default_provider, default_provider_config
6177
)
62-
result["llm_config_warning"] = warning_message
63-
logger.warning(warning_message)
6478

65-
# Get the search processing timelines setting, default is False if not set.
66-
# if set to True, the search processing timelines will be displayed in the UI.
67-
search_processing_timelines = current_app.config.get(
68-
"SEARCH_PROCESSING_TIMELINES", False
69-
)
70-
result["SEARCH_PROCESSING_TIMELINES"] = search_processing_timelines
79+
llm_feature_availability = {"default": default_provider_working}
7180

72-
return jsonify(result)
81+
for feature_name, feature_conf in llm_configs.items():
82+
if feature_name == "default":
83+
continue
84+
85+
feature_provider_working = False
86+
if isinstance(feature_conf, dict) and len(feature_conf) == 1:
87+
feature_provider = next(iter(feature_conf))
88+
feature_provider_config = feature_conf[feature_provider]
89+
feature_provider_working = self._check_provider_working(
90+
feature_provider, feature_provider_config
91+
)
92+
93+
# Feature is available if either specific provider works
94+
# or default provider works
95+
llm_feature_availability[feature_name] = (
96+
feature_provider_working or default_provider_working
97+
)
98+
99+
return llm_feature_availability
100+
101+
def _check_provider_working(self, provider_name: str, config: dict) -> bool:
102+
"""Check if a specific LLM provider works with given configuration.
103+
104+
Args:
105+
provider_name: Name of the provider to check
106+
config: Configuration dict for the provider
107+
108+
Returns:
109+
bool: Whether the provider is working
110+
"""
111+
try:
112+
provider_class = llm_manager.LLMManager.get_provider(provider_name)
113+
provider_class(config=config)
114+
return True
115+
except Exception as e: # pylint: disable=broad-except
116+
logger.debug(
117+
"LLM provider '%s' failed to initialize: %s",
118+
provider_name,
119+
str(e),
120+
)
121+
return False

timesketch/api/v1/resources_test.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -1368,15 +1368,28 @@ def test_system_settings_resource(self):
13681368
"""Authenticated request to get system settings."""
13691369
self.app.config["LLM_PROVIDER_CONFIGS"] = {"default": {"test": {}}}
13701370
self.app.config["DFIQ_ENABLED"] = False
1371+
self.login()
1372+
response = self.client.get(self.resource_url)
1373+
1374+
self.assertEqual(response.json["DFIQ_ENABLED"], False)
1375+
self.assertEqual(response.json["SEARCH_PROCESSING_TIMELINES"], False)
13711376

1377+
self.assertIn("LLM_FEATURES_AVAILABLE", response.json)
1378+
self.assertIn("default", response.json["LLM_FEATURES_AVAILABLE"])
1379+
1380+
def test_system_settings_invalid_llm_config(self):
1381+
"""Test with invalid LLM configuration."""
1382+
self.app.config["LLM_PROVIDER_CONFIGS"] = "invalid_config"
13721383
self.login()
13731384
response = self.client.get(self.resource_url)
1385+
13741386
expected_response = {
13751387
"DFIQ_ENABLED": False,
1376-
"LLM_PROVIDER": "test",
13771388
"SEARCH_PROCESSING_TIMELINES": False,
1389+
"LLM_FEATURES_AVAILABLE": {"default": False},
13781390
}
1379-
self.assertEqual(response.json, expected_response)
1391+
1392+
self.assertDictEqual(response.json, expected_response)
13801393

13811394

13821395
class ScenariosResourceTest(BaseTest):
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
<!--
22
Copyright 2025 Google Inc. All rights reserved.
3-
43
Licensed under the Apache License, Version 2.0 (the "License");
54
you may not use this file except in compliance with the License.
65
You may obtain a copy of the License at
7-
86
http://www.apache.org/licenses/LICENSE-2.0
9-
107
Unless required by applicable law or agreed to in writing, software
118
distributed under the License is distributed on an "AS IS" BASIS,
129
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,7 +17,11 @@ limitations under the License.
2017
<v-subheader>Layout</v-subheader>
2118
<v-list-item>
2219
<v-list-item-action>
23-
<v-switch v-model="settings.showLeftPanel" color="primary" @change="saveSettings()"></v-switch>
20+
<v-switch
21+
v-model="settings.showLeftPanel"
22+
color="primary"
23+
@change="saveSettings()"
24+
></v-switch>
2425
</v-list-item-action>
2526
<v-list-item-content>
2627
<v-list-item-title>Show side panel</v-list-item-title>
@@ -31,9 +32,21 @@ limitations under the License.
3132
</v-list-item>
3233

3334
<!-- AI Powered Features Main Setting -->
34-
<v-list-item v-if="systemSettings.LLM_PROVIDER">
35+
<v-list-item>
3536
<v-list-item-action>
36-
<v-switch v-model="settings.aiPoweredFeaturesMain" color="primary" @change="updateAiFeatures" ></v-switch>
37+
<v-tooltip bottom>
38+
<template v-slot:activator="{ on, attrs }">
39+
<div v-on="isAnyFeatureAvailable ? {} : on" v-bind="attrs">
40+
<v-switch
41+
v-model="settings.aiPoweredFeaturesMain"
42+
color="primary"
43+
@change="updateAiFeatures"
44+
:disabled="!isAnyFeatureAvailable"
45+
></v-switch>
46+
</div>
47+
</template>
48+
<span>This feature requires an LLM provider to be configured. Please contact your administrator.</span>
49+
</v-tooltip>
3750
</v-list-item-action>
3851
<v-list-item-content>
3952
<v-list-item-title>AI powered features (experimental)</v-list-item-title>
@@ -42,14 +55,21 @@ limitations under the License.
4255
</v-list-item>
4356

4457
<!-- Child Setting: Event Summarization -->
45-
<v-list-item v-if="systemSettings.LLM_PROVIDER">
58+
<v-list-item>
4659
<v-list-item-action class="ml-8">
47-
<v-switch
48-
v-model="settings.eventSummarization"
49-
color="primary"
50-
@change="saveSettings()"
51-
:disabled="!settings.aiPoweredFeaturesMain"
52-
></v-switch>
60+
<v-tooltip bottom>
61+
<template v-slot:activator="{ on, attrs }">
62+
<div v-on="isFeatureAvailable('llm_summarize') ? {} : on" v-bind="attrs">
63+
<v-switch
64+
v-model="settings.eventSummarization"
65+
color="primary"
66+
@change="saveSettings()"
67+
:disabled="!settings.aiPoweredFeaturesMain || !isFeatureAvailable('llm_summarize')"
68+
></v-switch>
69+
</div>
70+
</template>
71+
<span>Event summarization requires an LLM provider to be configured. Please contact your administrator.</span>
72+
</v-tooltip>
5373
</v-list-item-action>
5474
<v-list-item-content class="ml-8">
5575
<v-list-item-title>Event summarization</v-list-item-title>
@@ -60,14 +80,21 @@ limitations under the License.
6080
</v-list-item>
6181

6282
<!-- Child Setting: AI Generated Queries -->
63-
<v-list-item v-if="systemSettings.LLM_PROVIDER">
83+
<v-list-item>
6484
<v-list-item-action class="ml-8">
65-
<v-switch
66-
v-model="settings.generateQuery"
67-
color="primary"
68-
@change="saveSettings()"
69-
:disabled="!settings.aiPoweredFeaturesMain"
70-
></v-switch>
85+
<v-tooltip bottom>
86+
<template v-slot:activator="{ on, attrs }">
87+
<div v-on="isFeatureAvailable('nl2q') ? {} : on" v-bind="attrs">
88+
<v-switch
89+
v-model="settings.generateQuery"
90+
color="primary"
91+
@change="saveSettings()"
92+
:disabled="!settings.aiPoweredFeaturesMain || !isFeatureAvailable('nl2q')"
93+
></v-switch>
94+
</div>
95+
</template>
96+
<span>AI query generation requires an LLM provider to be configured. Please contact your administrator.</span>
97+
</v-tooltip>
7198
</v-list-item-action>
7299
<v-list-item-content class="ml-8">
73100
<v-list-item-title>AI generated queries</v-list-item-title>
@@ -76,9 +103,15 @@ limitations under the License.
76103
>
77104
</v-list-item-content>
78105
</v-list-item>
106+
107+
<!-- Setting: Searching processing timelines -->
79108
<v-list-item v-if="systemSettings.SEARCH_PROCESSING_TIMELINES">
80109
<v-list-item-action>
81-
<v-switch v-model="settings.showProcessingTimelineEvents" color="primary" @change="saveSettings()"></v-switch>
110+
<v-switch
111+
v-model="settings.showProcessingTimelineEvents"
112+
color="primary"
113+
@change="saveSettings()"
114+
></v-switch>
82115
</v-list-item-action>
83116
<v-list-item-content>
84117
<v-list-item-title>Include Processing Events</v-list-item-title>
@@ -90,18 +123,15 @@ limitations under the License.
90123
</v-list>
91124
</v-card>
92125
</template>
93-
94126
<script>
95127
import ApiClient from '../utils/RestApiClient'
96-
97128
const DEFAULT_SETTINGS = {
98129
showLeftPanel: true,
99130
aiPoweredFeaturesMain: false,
100131
eventSummarization: false,
101132
generateQuery: false,
102133
showProcessingTimelineEvents: false,
103134
}
104-
105135
export default {
106136
data() {
107137
return {
@@ -124,37 +154,41 @@ export default {
124154
userSettings() {
125155
return this.$store.state.settings
126156
},
157+
llmFeatures() {
158+
return this.systemSettings.LLM_FEATURES_AVAILABLE || {}
159+
},
160+
isAnyFeatureAvailable() {
161+
return Object.values(this.llmFeatures).some(available => available === true)
162+
},
127163
},
128164
methods: {
129165
saveSettings() {
130166
ApiClient.saveUserSettings(this.settings)
131-
.then(() => this.$store.dispatch('updateUserSettings'))
167+
.then(() => {
168+
return this.$store.dispatch('updateUserSettings')
169+
})
170+
.then(() => {
171+
this.settings = { ...this.userSettings }
172+
})
132173
.catch((error) => {
133174
console.log(error)
134175
})
135176
},
136177
updateAiFeatures() {
137178
if (!this.settings.aiPoweredFeaturesMain) {
138-
this.settings.eventSummarization = false;
139-
this.settings.generateQuery = false;
179+
this.settings.eventSummarization = false
180+
this.settings.generateQuery = false
140181
}
141-
this.saveSettings();
182+
this.saveSettings()
183+
},
184+
isFeatureAvailable(featureName) {
185+
return this.llmFeatures[featureName] === true
142186
},
143187
},
144188
mounted() {
145-
this.settings = { ...this.userSettings }
146-
147-
// Set default values when a user don't have any settings saved.
148-
if (!this.settings || !Object.keys(this.settings).length) {
149-
this.settings = { ...DEFAULT_SETTINGS }
150-
this.saveSettings()
151-
} else {
152-
// Ensure default values for new settings are applied if user settings are older
153-
this.settings.aiPoweredFeaturesMain = this.settings.aiPoweredFeaturesMain !== undefined ? this.settings.aiPoweredFeaturesMain : DEFAULT_SETTINGS.aiPoweredFeaturesMain;
154-
this.settings.eventSummarization = this.settings.eventSummarization !== undefined ? this.settings.eventSummarization : DEFAULT_SETTINGS.eventSummarization;
155-
this.settings.generateQuery = this.settings.generateQuery !== undefined ? this.settings.generateQuery : DEFAULT_SETTINGS.generateQuery;
156-
this.settings.showProcessingTimelineEvents = this.settings.showProcessingTimelineEvents !== undefined ? this.settings.showProcessingTimelineEvents : DEFAULT_SETTINGS.showProcessingTimelineEvents;
157-
}
189+
// Set default settings if no user settings are defined.
190+
this.settings = { ...DEFAULT_SETTINGS, ...this.userSettings };
191+
this.saveSettings();
158192
},
159193
}
160194
</script>

0 commit comments

Comments
 (0)