summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2025-11-10 00:27:11 +0100
committerMichael Peter Christen <mc@yacy.net>2025-11-10 00:27:11 +0100
commit37c26bddf399fcf94e34cec110af023c8e35abbd (patch)
treecf253b341ffa3b6e535e60322e01b12cd43d6941
parent4fcf4d5eaa7544294cde0e9145e33a7e010fcb05 (diff)
added persistence to production models table
-rw-r--r--defaults/yacy.init2
-rw-r--r--htroot/LLMSelection_p.html137
-rw-r--r--source/net/yacy/htroot/LLMSelection_p.java169
3 files changed, 133 insertions, 175 deletions
diff --git a/defaults/yacy.init b/defaults/yacy.init
index 63db597c0..d61ddafe8 100644
--- a/defaults/yacy.init
+++ b/defaults/yacy.init
@@ -1422,3 +1422,5 @@ decoration.grafics.linkstructure = true
decoration.hostanalysis = false
decoration.simpleheadernavbar = navbar-default
+# ai settings
+ai.production_models = {} \ No newline at end of file
diff --git a/htroot/LLMSelection_p.html b/htroot/LLMSelection_p.html
index ff95e98d3..4d3362a44 100644
--- a/htroot/LLMSelection_p.html
+++ b/htroot/LLMSelection_p.html
@@ -23,18 +23,18 @@
const PRODUCTION_MODEL_FEATURE_COLUMN_END = 11;
const PRODUCTION_MODEL_ACTION_COLUMN_INDEX = PRODUCTION_MODEL_TOTAL_COLUMNS - 1;
const PRODUCTION_MODEL_COLUMN_NAMES = [
- "Provider",
- "Model",
+ "service",
+ "model",
"hoststub",
- "API Key",
- "Max Token",
- "Answers",
- "Chat",
- "Translation",
- "Q/A-Generation",
- "Classification",
- "Shortener",
- "Vision"
+ "api_key",
+ "max_tokens",
+ "answers",
+ "chat",
+ "translation",
+ "qa-generation",
+ "classification",
+ "tldr-shortener",
+ "vision"
];
const PRODUCTION_MODEL_SUBMIT_URL = "LLMSelection_p.html";
@@ -121,20 +121,20 @@
return fetchJsonOrThrow(`${hoststub}/v1/models`);
}
- async function requestModelsForProvider(provider, hoststub) {
- return provider === "OLLAMA"
+ async function requestModelsForService(service, hoststub) {
+ return service === "OLLAMA"
? fetchOllamaModels(hoststub)
: fetchOpenAICompatibleModels(hoststub);
}
- function handleModelLoadError(provider, error) {
+ function handleModelLoadError(service, error) {
console.error("Error fetching models:", error);
const status = error && typeof error.status === "number" ? error.status : null;
if (status) {
const apikeyEl = document.getElementById("apikey");
apiKeyValue = apikeyEl ? apikeyEl.value.trim() : "";
- if (provider !== "OLLAMA" && provider !== "LMSTUDIO" && !apiKeyValue) {
- alert("an api key is required for this provider");
+ if (service !== "OLLAMA" && service !== "LMSTUDIO" && !apiKeyValue) {
+ alert("an api_key is required for this service");
} else {
alert(`Failed to load models. HTTP status: ${status}`);
}
@@ -145,13 +145,13 @@
async function loadModelList() {
availableModels = [];
- const provider = document.getElementById("provider").value;
+ const service = document.getElementById("service").value;
const hoststub = document.getElementById("hoststub").value;
try {
- const responsej = await requestModelsForProvider(provider, hoststub);
- renderAvailableModels(provider, responsej);
- if (provider === "OLLAMA") {
+ const responsej = await requestModelsForService(service, hoststub);
+ renderAvailableModels(service, responsej);
+ if (service === "OLLAMA") {
renderRecommendedModels(hoststub);
} else {
const loadModelContainer = document.getElementById("loadModelContainer");
@@ -160,7 +160,7 @@
loadModelContainer.style.display = "none";
}
} catch (error) {
- handleModelLoadError(provider, error);
+ handleModelLoadError(service, error);
}
}
@@ -249,18 +249,18 @@
***/
function setHoststub() {
- // this is called when the user changes the provider
- const provider = document.getElementById("provider").value;
+ // this is called when the user changes the service
+ const service = document.getElementById("service").value;
const hoststubInput = document.getElementById("hoststub");
const apikeyInput = document.getElementById("apikey");
- if (provider === "OLLAMA") {
+ if (service === "OLLAMA") {
hoststubInput.value = "http://localhost:11434";
- } else if (provider === "LMSTUDIO") {
+ } else if (service === "LMSTUDIO") {
hoststubInput.value = "http://localhost:1234";
- } else if (provider === "OPENAI") {
+ } else if (service === "OPENAI") {
hoststubInput.value = "https://api.openai.com";
- } else if (provider === "OPENROUTER") {
+ } else if (service === "OPENROUTER") {
hoststubInput.value = "https://openrouter.ai/api";
} else {
hoststubInput.value = "";
@@ -268,7 +268,7 @@
if (!apikeyInput) return;
- if (provider === "OLLAMA" || provider === "LMSTUDIO") {
+ if (service === "OLLAMA" || service === "LMSTUDIO") {
apikeyInput.disabled = true;
apikeyInput.value = "";
} else {
@@ -312,12 +312,12 @@
}
}
- function renderAvailableModels(provider, payload) {
+ function renderAvailableModels(service, payload) {
const container = document.getElementById("availableModelsContainer");
if (!container) return;
- const models = provider === "OLLAMA" ? (payload.models || []) : (payload.data || []);
- const getId = provider === "OLLAMA" ? m => m.model : m => m.id;
+ const models = service === "OLLAMA" ? (payload.models || []) : (payload.data || []);
+ const getId = service === "OLLAMA" ? m => m.model : m => m.id;
const rows = [];
models.forEach(m => {
@@ -330,9 +330,9 @@
ranking: info.ranking || "",
size: info.size || "",
description: info.description || "",
- provider: info.provider || "",
+ service: info.service || "",
license: info.license || "",
- renderActions: () => createAvailableModelActionButtons(provider, id)
+ renderActions: () => createAvailableModelActionButtons(service, id)
});
});
@@ -351,7 +351,7 @@
ranking: info.ranking || "",
size: info.size || "",
description: info.description || "",
- provider: info.provider || "",
+ service: info.service || "",
license: info.license || "",
renderActions: () => createDownloadButton(hoststub, m[0])
};
@@ -424,8 +424,8 @@
container.style.display = "block";
}
- function createAvailableModelActionButtons(provider, modelId) {
- return [createSelectButton(modelId), createDeleteButton(provider, modelId)];
+ function createAvailableModelActionButtons(service, modelId) {
+ return [createSelectButton(modelId), createDeleteButton(service, modelId)];
}
function createSelectButton(modelId) {
@@ -438,13 +438,13 @@
return selectBtn;
}
- function createDeleteButton(provider, modelId) {
+ function createDeleteButton(service, modelId) {
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "btn btn-danger btn-sm";
deleteBtn.textContent = "Delete";
styleActionButton(deleteBtn);
- if (provider === "OLLAMA") {
+ if (service === "OLLAMA") {
deleteBtn.addEventListener("click", () => {
const confirmed = window.confirm(`Do you really want to delete model "${modelId}"?`);
if (!confirmed) {
@@ -478,12 +478,12 @@
if (!tbody) return;
const hadRowsBeforeInsert = tbody.querySelectorAll("tr").length > 0;
- const providerField = document.getElementById("provider");
+ const serviceField = document.getElementById("service");
const hoststubField = document.getElementById("hoststub");
const apikeyField = document.getElementById("apikey");
const maxTokenField = document.getElementById("maxtoken");
- const provider = providerField ? providerField.value : "";
+ const service = serviceField ? serviceField.value : "";
const hoststub = hoststubField ? hoststubField.value.trim() : "";
const apikey = apikeyField ? apikeyField.value.trim() : "";
const maxToken = maxTokenField ? maxTokenField.value : "";
@@ -508,7 +508,7 @@
}
const cells = targetRow.cells;
- const values = [provider, modelName, hoststub, apikey, maxToken];
+ const values = [service, modelName, hoststub, apikey, maxToken];
values.forEach((value, index) => {
if (cells[index]) {
cells[index].textContent = value || "";
@@ -643,10 +643,11 @@
return false;
}
- function collectProductionTablePayload() {
+ function persistProductionModels() {
+ // read out table
const tbody = getProductionTableBody();
if (!tbody) return [];
- const rows = [];
+ const production_models_table = [];
Array.from(tbody.querySelectorAll("tr")).forEach(row => {
if (!row.cells || row.cells.length < PRODUCTION_MODEL_TOTAL_COLUMNS) {
return;
@@ -662,21 +663,16 @@
rowData[columnName] = row.cells[index] ? row.cells[index].textContent.trim() : "";
}
});
- const hasContent = rowData.Provider || rowData.Model || rowData["hoststub"];
+ const hasContent = rowData.service || rowData.model || rowData["hoststub"];
if (hasContent) {
- rows.push(rowData);
+ production_models_table.push(rowData);
}
});
- return rows;
- }
-
- function persistProductionModels() {
- const table = collectProductionTablePayload();
- const payload = { table };
+
+ // push to server
fetch(PRODUCTION_MODEL_SUBMIT_URL, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload)
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ production_models: production_models_table })
}).catch(err => {
console.error("Failed to persist production models", err);
});
@@ -724,18 +720,18 @@
<h2>LLM Selection</h2>
<p>
- Here you can pick models from a LLM model provider to select them as production model.
+ Here you can pick models from a LLM model service to select them as production model.
In the "Production Models" - Matrix you can then assign each selected model a function inside YaCy
</p>
<p>
- <b>Install your local LLM provider!</b> You need either a local <a href="https://ollama.com/">ollama</a> or <a href="https://lmstudio.ai/">LM Studio</a> instance running on your local host or inside the intranet.
+ <b>Install your local LLM service!</b> You need either a local <a href="https://ollama.com/">ollama</a> or <a href="https://lmstudio.ai/">LM Studio</a> instance running on your local host or inside the intranet.
</p>
<form id="llmForm">
- <fieldset><legend>Provider</legend>
+ <fieldset><legend>Service Selection</legend>
<dl>
- <dt class="TableCellDark">Select a LLM Provider</dt>
+ <dt class="TableCellDark">service</dt>
<dd>
- <select name="provider" id="provider" class="form-control" onchange="setHoststub()">
+ <select name="service" id="service" class="form-control" onchange="setHoststub()">
<option value="OLLAMA" selected="selected">Ollama</option>
<option value="LMSTUDIO">LMStudio</option>
<option value="OPENAI">OpenAI</option>
@@ -743,15 +739,15 @@
</select>&nbsp; This makes a preset to the Hoststub value
</dd>
- <dt class="TableCellDark">Hoststub</dt>
+ <dt class="TableCellDark">hoststub</dt>
<dd><input type="text" name="hoststub" id="hoststub" value="http://localhost:11434" size="30" maxlength="60" class="form-control"/>&nbsp; you can probably leave this to the default value
</dd>
- <dt class="TableCellDark">API Key</dt>
+ <dt class="TableCellDark">api_key</dt>
<dd><input type="text" name="apikey" id="apikey" value="" disabled=true size="30" maxlength="60" class="form-control"/>&nbsp; (not required for Ollama or LMStudio)
</dd>
- <dt class="TableCellDark">Max Token</dt>
+ <dt class="TableCellDark">max_tokens</dt>
<dd>
<select id="maxtoken" name="maxtoken" class="form-control">
<option selected="selected">4096</option>
@@ -761,7 +757,7 @@
<option>65536</option>
<option>131072</option>
<option>262440</option>
- </select>&nbsp; You must set the Context Length in the LLM provider to fit to your selected max token; in Ollama you find a Context Length slider in the settings
+ </select>&nbsp; You must set the Context Length in the LLM service to fit to your selected max_tokens; in Ollama you find a Context Length slider in the settings
</dd>
<dt>&nbsp;</dt>
@@ -781,11 +777,22 @@
<fieldset id="productionModelsContainer" style="display: block;"><legend>Production Models</legend>
<table class="table table-striped" id="productionModelsTable">
<thead class="thead-dark">
- <tr><td>Provider</td><td>Model</td><td>hoststub</td><td>API Key</td><td>Max Token</td><td>Answers</td><td>Chat</td><td>Translation</td><td>Q/A-Generation</td><td>Classification</td><td>Shortener</td><td>Vision</td><td>Actions</td></tr>
+ <tr><td>service</td><td>model</td><td>hoststub</td><td>api_key</td><td>max_tokens</td><td>answers</td><td>chat</td><td>translation</td><td>qa-generation</td><td>classification</td><td>shortener</td><td>vision</td><td>Actions</td></tr>
</thead>
<tbody>
#{productionmodels}#
- <tr class="TableCell#(dark)#Light::Dark#(/dark)#"><td>#[model]#</td><td>#[type]#</td><td>#[size]#</td><td>#[apikey]#</td><td>#[max_token]#</td><td>#[answers]#</td><td>#[chat]#</td><td>#[translation]#</td><td>#[qageneration]#</td><td>#[classification]#</td><td>#[shortener]#</td><td>#[vision]#</td><td></td></tr>
+ <tr class="TableCell#(dark)#Light::Dark#(/dark)#">
+ <td>#[service]#</td><td>#[model]#</td><td>#[hoststub]#</td><td>#[api_key]#</td><td>#[max_tokens]#</td>
+ <td><input type="checkbox" #(answers)#::checked=true#(/answers)#></td>
+ <td><input type="checkbox" #(chat)#::checked=true#(/chat)#></td>
+ <td><input type="checkbox" #(translation)#::checked=true#(/translation)#></td>
+ <td><input type="checkbox" #(qa-generation)#::checked=true#(/qa-generation)#></td>
+ <td><input type="checkbox" #(classification)#::checked=true#(/classification)#></td>
+ <td><input type="checkbox" #(shortener)#::checked=true#(/shortener)#></td>
+ <td><input type="checkbox" #(vision)#::checked=true#(/vision)#></td>
+ <td><input type="checkbox" #(chat)#::checked=true#(/chat)#></td>
+ <td></td>
+ </tr>
#{/productionmodels}#
</tbody>
</table>
diff --git a/source/net/yacy/htroot/LLMSelection_p.java b/source/net/yacy/htroot/LLMSelection_p.java
index 2e8a556e9..0a0b43d0a 100644
--- a/source/net/yacy/htroot/LLMSelection_p.java
+++ b/source/net/yacy/htroot/LLMSelection_p.java
@@ -23,18 +23,13 @@
package net.yacy.htroot;
-import java.io.File;
-import java.io.IOException;
-import java.util.Date;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
-import net.yacy.cora.date.GenericFormatter;
import net.yacy.cora.protocol.RequestHeader;
-import net.yacy.data.WorkTables;
import net.yacy.search.Switchboard;
-import net.yacy.search.SwitchboardConstants;
-import net.yacy.search.index.Fulltext;
-import net.yacy.search.index.Segment;
-import net.yacy.search.schema.CollectionSchema;
import net.yacy.server.serverObjects;
import net.yacy.server.serverSwitch;
@@ -45,115 +40,69 @@ public class LLMSelection_p {
final Switchboard sb = (Switchboard) env;
final serverObjects prop = new serverObjects();
-
- final Segment segment = sb.index;
- // we have two counts of document: total number and such that are exportable with status code 200
- final long ucount = segment.fulltext().collectionSize();
- long ucount200 = ucount;
- try {
- ucount200 = segment.fulltext().getDefaultConnector().getCountByQuery(CollectionSchema.httpstatus_i.getSolrFieldName() + ":200");
- } catch (final IOException e1) {}
-
- // set default values
- prop.put("reload", 0);
- prop.put("lurlexport", 0);
- prop.putNum("ucount", ucount);
- prop.putNum("ucount200", ucount200);
-
- // show Pack folder contents
- int i = 0;
- boolean dark = true;
- for (final String file: sb.packsInHold()) {
- prop.put("packs_" + i + "_file", file);
- prop.put("packs_" + i + "_type", "hold");
- prop.put("packs_" + i + "_size", new File(sb.packsHoldPath, file).length() / 1024);
- prop.put("packs_" + i + "_dark", dark ? "1" : "0");
- i++;
- dark = !dark;
+ String body = post == null ? "" : post.get("BODY", "");
+ JSONObject bodyj = new JSONObject();
+ if (body.length() > 0) {
+ try {
+ bodyj = new JSONObject(new JSONTokener(body));
+ } catch (JSONException e) {
+ // silently catch this
+ }
}
- for (final String file: sb.packsInLoaded()) {
- prop.put("packs_" + i + "_file", file);
- prop.put("packs_" + i + "_type", "loaded");
- prop.put("packs_" + i + "_size", new File(sb.packsLoadedPath, file).length() / 1024);
- prop.put("packs_" + i + "_dark", dark ? "1" : "0");
- i++;
- dark = !dark;
+ JSONArray production_models = bodyj.optJSONArray("production_models");
+ if (production_models != null) {
+ // simply store the model array
+ try {
+ sb.setConfig("ai.production_models", production_models.toString(0));
+ } catch (JSONException e) {
+ //e.printStackTrace();
+ }
}
- for (final String file: sb.packsInLive()) {
- prop.put("packs_" + i + "_file", file);
- prop.put("packs_" + i + "_type", "live");
- prop.put("packs_" + i + "_size", new File(sb.packsLivePath, file).length() / 1024);
- prop.put("packs_" + i + "_dark", dark ? "1" : "0");
- i++;
- dark = !dark;
+ /*
+ {"production_models":[{
+ "service":"OLLAMA",
+ "model":"hf.co\/janhq\/Jan-v1-edge-gguf:Q4_K_M",
+ "hoststub":"http:\/\/localhost:11434",
+ "api_key":"",
+ "max_tokens":"4096",
+ "answers":true,
+ "chat":true,
+ "translation":true,
+ "qa-generation":true,
+ "classification":true,
+ "tldr-shortener":true,
+ "vision":true
+ }]}
+ */
+
+ // generate table for production_models
+ String pms = sb.getConfig("ai.production_models", "[]");
+ try {
+ production_models = new JSONArray(new JSONTokener(pms));
+ for (int i = 0; i < production_models.length(); i++) {
+ JSONObject row = production_models.getJSONObject(i);
+ prop.put("productionmodels_" + i + "_service", row.optString("service", "OLLAMA"));
+ prop.put("productionmodels_" + i + "_model", row.optString("model", ""));
+ prop.put("productionmodels_" + i + "_hoststub", row.optString("hoststub", ""));
+ prop.put("productionmodels_" + i + "_api_key", row.optString("api_key", ""));
+ prop.put("productionmodels_" + i + "_max_tokens", row.optString("max_tokens", "4096"));
+ prop.put("productionmodels_" + i + "_answers", row.optBoolean("answers", false));
+ prop.put("productionmodels_" + i + "_chat", row.optBoolean("chat", false));
+ prop.put("productionmodels_" + i + "_translation", row.optBoolean("translation", false));
+ prop.put("productionmodels_" + i + "_qa-generation", row.optBoolean("qa-generation", false));
+ prop.put("productionmodels_" + i + "_classification", row.optBoolean("classification", false));
+ prop.put("productionmodels_" + i + "_tldr-shortener", row.optBoolean("tldr-shortener", false));
+ prop.put("productionmodels_" + i + "_vision", row.optBoolean("vision", false));
+ }
+ prop.put("productionmodels", production_models.length());
+ } catch (JSONException e) {
+ e.printStackTrace();
}
- prop.put("packs", i);
if (post == null || env == null) {
return prop; // nothing to do
}
- if (post.containsKey("lurlexport")) {
- try {
- // parse format
- Fulltext.ExportFormat format = Fulltext.ExportFormat.elasticsearch;
- final String fname = post.get("format", "full-elasticsearch");
- final boolean dom = fname.startsWith("dom"); // if dom== false complete urls are exported, otherwise only the domain
- final boolean text = fname.startsWith("text");
- if (fname.endsWith("rss")) format = Fulltext.ExportFormat.rss;
- if (fname.endsWith("solr")) format = Fulltext.ExportFormat.solr;
- if (fname.endsWith("elasticsearch")) format = Fulltext.ExportFormat.elasticsearch;
-
- final String filter = post.get("exportfilter", ".*");
- String query = post.get("exportquery", "*:*");
- final String collection = post.get("collection", "user");
- query += " AND " + CollectionSchema.collection_sxt.getSolrFieldName() + ":\"" + collection + "\"";
-
- // store this call as api call: we do this even if there is a chance that it fails because recurring calls may do not fail
- sb.tables.recordAPICall(post, "IndexPackGenerator_p.html", WorkTables.TABLE_API_TYPE_DUMP, "PackGenerator, q=" + query);
-
- // start the export
- /*
- Tier Tags:
- | Tier | Size | Notes |
- |-----------|-----------|--------------------|
- | common | ≤ 1 GB | IndexPackGenerator |
- | uncommon | 1–5 GB | large web crawls |
- | rare | 5–50 GB | custom parser |
- | epic | 50–200 GB | special infra |
- | legendary | any | human curation |
- */
- final long now = System.currentTimeMillis();
- final long doccount = sb.index.fulltext().getDefaultConnector().getCountByQuery(query);
- if (doccount == 0) throw new IOException("number of exported documents == 0");
- final String category = post.get("category", "scroll"); // core, scroll, codex, gem, fiction, map, echo, spirit, vault
- final String tier = "common"; // common, uncommon, rare, epic, legendary, legendary
- final String origin = "web"; // web, synth,
- String slug = post.get("slug", "export").trim().replaceAll(" ", "-");
- if (slug.isEmpty()) slug = "export";
-
- // if collection is not user, the slug is the collection name
- if (!"user".equals(collection)) {
- slug = collection.trim().replaceAll(" ", "-");
- }
- // we can not construct the file name
- final String filename =
- SwitchboardConstants.YACY_PACK_PREFIX +
- category + "-" + tier + "-" + origin + "_" +
- slug + "_" +
- GenericFormatter.SHORT_DAY_FORMATTER.format(new Date(now));
- // file name schema: YaCyPack_<category>-<tier>-<origin>_<slug>_<YYMMDD>.jsonlist
- // possible storage paths are: hold, load, loaded, unload, live; we use hold here, loaded would also be correct
- } catch (final IOException e) {
- prop.put("lurlexporterror", 1);
- prop.put("lurlexporterror_exportfile", "-no export-");
- prop.put("lurlexporterror_exportfailmsg", e.getMessage());
- return prop;
- }
- }
-
- // insert constants
- prop.putNum("ucount", ucount);
// return rewrite properties
return prop;
}