diff options
| -rw-r--r-- | htroot/AILab.html | 423 | ||||
| -rw-r--r-- | htroot/LLMSelection_p.html | 160 | ||||
| -rw-r--r-- | htroot/env/grafics/AILab_Crawl.png | bin | 0 -> 245006 bytes | |||
| -rw-r--r-- | htroot/env/grafics/AILab_Inference.png | bin | 0 -> 89298 bytes | |||
| -rw-r--r-- | htroot/env/grafics/AILab_Matrix.png | bin | 0 -> 30390 bytes | |||
| -rw-r--r-- | htroot/env/grafics/AILab_RAG.png | bin | 0 -> 39340 bytes | |||
| -rw-r--r-- | htroot/env/grafics/AILab_shield.png | bin | 0 -> 62053 bytes | |||
| -rw-r--r-- | htroot/env/templates/submenuAI.template | 1 | ||||
| -rw-r--r-- | source/net/yacy/htroot/AILab.java | 87 | ||||
| -rw-r--r-- | source/net/yacy/htroot/LLMSelection_p.java | 20 |
10 files changed, 657 insertions, 34 deletions
diff --git a/htroot/AILab.html b/htroot/AILab.html new file mode 100644 index 000000000..09f007ba4 --- /dev/null +++ b/htroot/AILab.html @@ -0,0 +1,423 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>YaCy '#[clientname]#': AI Lab</title> + #%env/templates/metas.template%# + <style type="text/css"> + .ailab-hero { + margin: 26px 0 20px 0; + padding: 24px 24px 18px 24px; + border: 1px solid #dce3f0; + border-radius: 6px; + background: linear-gradient(135deg, #f7fbff 0%, #e8f1ff 40%, #ffffff 100%); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); + } + + .ailab-hero .eyebrow { + font-size: 0.85em; + text-transform: uppercase; + letter-spacing: 0.2em; + color: #2c3e50; + margin: 0 0 6px 0; + } + + .ailab-hero h2 { + margin: 0 0 8px 0; + font-weight: 700; + color: #172b4d; + } + + .ailab-hero p { + margin: 0 0 14px 0; + color: #233142; + } + + .bs-callout { + padding: 20px; + margin: 0 0 6px 0; + border: 1px solid #e8ecf2; + border-left: 6px solid #97a6b9; + border-radius: 4px; + background: #ffffff; + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.05); + transition: transform 0.15s ease, box-shadow 0.15s ease; + } + + .bs-callout:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08); + } + + .bs-callout-mandatory { + border-left-color: #f0ad4e; + } + + .bs-callout-optional { + border-left-color: #5bc0de; + } + + .quest-callout h3 { + margin-top: 0; + margin-bottom: 8px; + font-weight: 700; + color: #111827; + } + + .quest-callout p { + margin: 0 0 10px 0; + color: #34495e; + } + + .quest-body { + display: flex; + gap: 14px; + align-items: center; + flex-wrap: wrap; + } + + .quest-visual img { + max-width: 64px; + width: 64px; + height: 64px; + border-radius: 6px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); + object-fit: contain; + } + + .quest-actions .btn { + margin-bottom: 6px; + text-decoration: none; + min-width: 200px; + font-size: 14px; + } + + .quest-actions .btn-primary, + .quest-actions .btn-info, + .quest-actions .btn-success, + .quest-actions .btn-default { + background-color: #5bc0de; + border-color: #46b8da; + color: #ffffff; + } + + .quest-actions .btn-primary:hover, + .quest-actions .btn-info:hover, + .quest-actions .btn-success:hover, + .quest-actions .btn-default:hover { + background-color: #31b0d5; + border-color: #269abc; + color: #ffffff; + } + + .quest-note { + font-size: 0.95em; + color: #4c566a; + } + + .quest-note .count { + font-weight: 700; + } + + .quest-meta { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; + } + + .quest-meta .meta-arrow { + display: inline-flex; + align-items: center; + font-weight: 700; + color: #5bc0de; + padding: 0 4px; + animation: arrowPulse 1.4s ease-in-out infinite; + } + + .quest-meta .label { + display: inline-block; + padding: 4px 12px; + font-size: 0.85em; + font-weight: 700; + line-height: 1.4; + border-radius: 999px; + } + + .status-pill { + display: inline-block; + padding: 4px 12px; + border-radius: 999px; + font-weight: 700; + font-size: 0.85em; + letter-spacing: 0.05em; + text-transform: uppercase; + background: #f0ad4e; + color: #ffffff; + } + + @keyframes arrowPulse { + 0% { transform: translateX(0); opacity: 0.8; } + 50% { transform: translateX(3px); opacity: 1; } + 100% { transform: translateX(0); opacity: 0.8; } + } + + .status-ready .status-pill { + background: #5cb85c; + } + + .status-locked .status-pill { + background: #b0b7c3; + } + + .status-beta .status-pill { + background: #5bc0de; + } + + .status-pending .status-pill { + background: #f0ad4e; + } + + .lab-progress { + margin-top: 10px; + } + + .lab-progress .progress { + margin-bottom: 0; + } + + @media (max-width: 767px) { + .quest-callout { + margin-bottom: 16px; + } + } + + .quest-grid { + display: flex; + flex-wrap: wrap; + gap: 18px; + align-items: stretch; + } + + .quest-grid .quest-callout { + flex: 1 1 48%; + min-width: 320px; + } + + .quest-locked { + opacity: 0.55; + filter: grayscale(0.2); + pointer-events: none; + } + </style> + </head> + <body id="AILab" data-ailab-inference-configured="#[ailab_inference_configured]#"> + #%env/templates/header.template%# + #%env/templates/submenuAI.template%# + + <div class="container-fluid" style="padding-left:0;padding-right:0;"> + <div class="ailab-hero"> + <div class="eyebrow">AI Lab Build System</div> + <h2>Craft your AI toolkit</h2> + <p>Complete the quests below to unlock YaCy's AI sidekick: bind an inference engine, load production models, feed it with your index, then wire RAG and shields.</p> + <div class="lab-progress"> + <div class="progress"> + <div id="labProgressBar" class="progress-bar progress-bar-success" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" style="width:0%">0 / 5 unlocked</div> + </div> + </div> + </div> + + <div class="quest-grid"> + <div class="bs-callout bs-callout-mandatory quest-callout status-pending" data-quest="inference" data-status="#[ailab_inference_status]#"> + <div class="quest-meta"> + <span class="label label-warning">Mandatory</span> + <span class="meta-arrow" aria-hidden="true">→</span> + <span class="status-pill">Needs setup</span> + </div> + <h3>Bind an inference engine</h3> + <p>Pick your host (Ollama, LM Studio, OpenAI-compatible) and give YaCy a place to send prompts.</p> + <div class="quest-body"> + <div class="quest-visual"> + <img src="env/grafics/AILab_Inference.png" alt="Inference engine setup" width="128" height="128" /> + </div> + <div class="quest-actions"> + <a class="btn btn-info btn-sm" href="LLMSelection_p.html">Open engine setup</a><br /> + <span class="quest-note">Set hoststub, API keys, and defaults to unlock downloads.</span> + </div> + </div> + </div> + + <div class="bs-callout bs-callout-mandatory quest-callout status-pending" data-quest="model" data-status="#[ailab_model_status]#"> + <div class="quest-meta"> + <span class="label label-warning">Mandatory</span> + <span class="meta-arrow" aria-hidden="true">→</span> + <span class="status-pill">Needs setup</span> + </div> + <h3>Populate the Production Models Matrix</h3> + <p>Assign models for chat, search, translation, and more. This is your loadout bench.</p> + <div class="quest-body"> + <div class="quest-visual"> + <img src="env/grafics/AILab_Matrix.png" alt="Model assignment preview" width="128" height="128" /> + </div> + <div class="quest-actions"> + <a class="btn btn-info btn-sm" href="LLMSelection_p.html#availableModels">Go to Production Models Matrix</a><br /> + <span class="quest-note">Deploy at least one model, then assign capabilities (chat, search-query, tooling, vision).</span> + </div> + </div> + </div> + + <div class="bs-callout bs-callout-optional quest-callout status-pending" data-quest="index" data-status="#[ailab_index_status]#"> + <div class="quest-meta"> + <span class="label label-info">Optional</span> + <span class="meta-arrow" aria-hidden="true">→</span> + <span class="status-pill">Needs setup</span> + </div> + <h3>Grow a search index</h3> + <p>Create a local index for grounding: crawl a site or import a pack to give your AI facts to cite.</p> + <div class="quest-body"> + <div class="quest-visual"> + <img src="env/grafics/AILab_Crawl.png" alt="Index creation" width="128" height="128" /> + </div> + <div class="quest-actions"> + <a class="btn btn-info btn-sm" href="CrawlStartSite.html">Start a crawl</a> + <a class="btn btn-info btn-sm" href="IndexPackDownloader_p.html">Import an index pack</a><br /> + <span class="quest-note">Indexed documents: <span class="count">#[ailab_index_count]#</span> / <span class="count">#[ailab_index_needed]#</span> required to unlock (need at least 1000 documents).</span> + </div> + </div> + </div> + + <div class="bs-callout bs-callout-optional quest-callout status-pending" data-quest="rag" data-status="#[ailab_rag_status]#"> + <div class="quest-meta"> + <span class="label label-info">Optional</span> + <span class="meta-arrow" aria-hidden="true">→</span> + <span class="status-pill">Needs setup</span> + </div> + <h3>Wire RAG retrieval</h3> + <p>Map which production models answer search-query and Q/A pairs so the RAG proxy can mix search with chat.</p> + <div class="quest-body"> + <div class="quest-visual"> + <img src="env/grafics/AILab_RAG.png" alt="RAG configuration" width="128" height="128" /> + </div> + <div class="quest-actions"> + <a class="btn btn-info btn-sm" href="LLMSelection_p.html#productionModelsTable">Configure RAG roles</a> + <a class="btn btn-info btn-sm" href="yacychat.html">Test in Chat</a><br /> + <span class="quest-note">Set the search-query and qapairs columns to connect retrieval to your chat flow.</span> + </div> + </div> + </div> + + <div class="bs-callout bs-callout-optional quest-callout status-pending" data-quest="shield" data-status="#[ailab_shield_status]#"> + <div class="quest-meta"> + <span class="label label-info">Optional</span> + <span class="meta-arrow" aria-hidden="true">→</span> + <span class="status-pill">Needs setup</span> + </div> + <h3>Define a shield</h3> + <p>Add guardrails: moderation prompts, content filters, or safety rules that wrap every request.</p> + <div class="quest-body"> + <div class="quest-visual"> + <img src="env/grafics/AILab_shield.png" alt="Shield definition" width="128" height="128" /> + </div> + <div class="quest-actions"> + <a class="btn btn-info btn-sm" href="ConfigProperties_p.html">Open shield settings</a><br /> + <span class="quest-note">Store your shield directives (system prompts, stop words) as properties, then exercise them in chat.</span> + </div> + </div> + </div> + </div> + </div> + + <script type="text/javascript"> + //<![CDATA[ + (function() { + const statusLabels = { + ready: "Unlocked", + complete: "Unlocked", + completed: "Unlocked", + beta: "Beta", + optional: "Optional", + pending: "Needs setup", + todo: "Needs setup" + }; + + const callouts = Array.prototype.slice.call(document.querySelectorAll(".quest-callout")); + let readyCount = 0; + const statusMap = {}; + const indexNeeded = parseInt("#[ailab_index_needed]#", 10) || 1000; + const indexCount = parseInt("#[ailab_index_count]#", 10) || 0; + + callouts.forEach(callout => { + const raw = (callout.getAttribute("data-status") || "").toLowerCase(); + let status = statusLabels[raw] ? raw : "pending"; + if (status === "complete" || status === "completed") { + status = "ready"; + } + callout.classList.remove("status-ready", "status-pending", "status-beta"); + callout.classList.add("status-" + status); + + if (status === "ready") { + readyCount++; + } + + const pill = callout.querySelector(".status-pill"); + if (pill) { + // special display for index quest showing counts while locked/pending + const questName = callout.getAttribute("data-quest"); + if (questName === "index" && status !== "ready") { + pill.textContent = indexCount + " / " + indexNeeded; + } else { + pill.textContent = statusLabels[status] || statusLabels.pending; + } + } + + const q = callout.getAttribute("data-quest"); + if (q) { + statusMap[q] = status; + } + }); + + // Gating: enforce build order + const questOrder = ["inference", "model", "index", "rag", "shield"]; + let prerequisitesMet = true; + questOrder.forEach(function(name) { + const callout = document.querySelector('.quest-callout[data-quest="' + name + '"]'); + if (!callout) return; + const s = statusMap[name] || "pending"; + + // Only gate by prior quests; index stays clickable even while filling up. + let locked = !prerequisitesMet; + if (name === "model") { + const inferenceConfigured = document.body.getAttribute("data-ailab-inference-configured") === "1"; + locked = locked || !inferenceConfigured; + } + + if (locked) { + callout.classList.add("quest-locked", "status-locked"); + const pill = callout.querySelector(".status-pill"); + if (pill) { + pill.textContent = name === "index" ? (indexCount + " / " + indexNeeded) : "Locked"; + } + } else { + callout.classList.remove("quest-locked", "status-locked"); + } + + // Block subsequent quests until current one is ready (index must reach threshold to release RAG and later steps). + if (s !== "ready" || (name === "index" && indexCount < indexNeeded)) { + prerequisitesMet = false; + } + }); + + const progressBar = document.getElementById("labProgressBar"); + if (progressBar && callouts.length > 0) { + const percent = Math.round((readyCount / callouts.length) * 100); + progressBar.style.width = percent + "%"; + progressBar.setAttribute("aria-valuenow", percent.toString()); + progressBar.textContent = readyCount + " / " + callouts.length + " unlocked"; + } + })(); + //]]> + </script> + + #%env/templates/footer.template%# + </body> +</html> diff --git a/htroot/LLMSelection_p.html b/htroot/LLMSelection_p.html index ae607fafa..0209aba8d 100644 --- a/htroot/LLMSelection_p.html +++ b/htroot/LLMSelection_p.html @@ -5,7 +5,7 @@ <title>YaCy '#[clientname]#': LLM Selection</title> #%env/templates/metas.template%# </head> - <body id="IndexControl"> + <body id="IndexControl" data-llm-service="#[llm_service]#" data-llm-hoststub="#[llm_hoststub]#" data-llm-apikey="#[llm_apikey]#"> #%env/templates/header.template%# #%env/templates/submenuAI.template%# <script> @@ -17,6 +17,17 @@ event.preventDefault(); event.returnValue = "Model downloads are still running. Please wait until they finish."; }; + // Localization-friendly test strings and hints (translate/tune as needed) + const TEST_STRINGS = { + toolingEndpointPath: "/v1/chat/completions", // Endpoint used to probe tooling capability on OpenAI-compatible APIs + toolingSystemMessage: "You are a home assistant.", // System prompt for tooling capability test + toolingUserMessage: "Switch on the light", // User prompt for tooling capability test + visionSystemMessage: "you read out images", // System prompt for vision capability test + visionUserMessage: "what is in the image?", // User prompt for vision capability test + visionExpectedText: "42", // Expected mention in LLM response when reading the test image + visionTestImagePath: "env/grafics/llmtest.png" // Image used for the vision capability test + }; + const PRODUCTION_MODEL_TOTAL_COLUMNS = 15; const PRODUCTION_MODEL_MODEL_COLUMN_INDEX = 1; const PRODUCTION_MODEL_USAGE_COLUMN_START = 5; @@ -45,14 +56,7 @@ "vision" ]; const PRODUCTION_MODEL_SUBMIT_URL = "LLMSelection_p.html"; - const TOOLING_TEST_ENDPOINT_PATH = "/v1/chat/completions"; const TOOLING_EXPECTED_FUNCTION_NAME = "lightswitch"; - const TOOLING_TEST_SYSTEM_MESSAGE = "You are a home assistant."; - const TOOLING_TEST_USER_MESSAGE = "Switch on the light"; - const VISION_TEST_SYSTEM_MESSAGE = "you read out images"; - const VISION_TEST_USER_MESSAGE = "what is in the image?"; - const VISION_TEST_EXPECTED_TEXT = "42"; - const VISION_TEST_IMAGE_PATH = "env/grafics/llmtest.png"; let cachedVisionTestImageBase64 = null; let cachedVisionTestImagePromise = null; @@ -152,7 +156,7 @@ } } - async function loadModelList() { + async function loadModelList(fromPreset = false) { availableModels = []; const service = document.getElementById("service").value; const hoststub = document.getElementById("hoststub").value; @@ -168,8 +172,12 @@ loadModelContainer.innerHTML = ""; loadModelContainer.style.display = "none"; } + persistInferenceSystem(); } catch (error) { handleModelLoadError(service, error); + if (fromPreset) { + console.warn("Auto-load of model list failed for preset inference system."); + } } } @@ -285,6 +293,27 @@ } } + function applyPresetInference() { + const serviceSelect = document.getElementById("service"); + const hoststubInput = document.getElementById("hoststub"); + const apikeyInput = document.getElementById("apikey"); + const body = document.body; + const presetService = (body.getAttribute("data-llm-service") || "").trim(); + const presetHoststub = (body.getAttribute("data-llm-hoststub") || "").trim(); + const presetApikey = (body.getAttribute("data-llm-apikey") || "").trim(); + if (serviceSelect && presetService) { + serviceSelect.value = presetService; + } + setHoststub(); + if (hoststubInput && presetHoststub) { + hoststubInput.value = presetHoststub; + } + if (apikeyInput && presetApikey) { + apikeyInput.disabled = false; + apikeyInput.value = presetApikey; + } + } + async function handleModelDelete(modelName, deleteButton) { if (!modelName) return; if (getProductionModelNames().has(modelName)) { @@ -606,6 +635,23 @@ updateAvailableModelButtons(); } + function persistInferenceSystem() { + const hoststubInput = document.getElementById("hoststub"); + const apikeyInput = document.getElementById("apikey"); + const serviceSelect = document.getElementById("service"); + const inference_system = { + service: serviceSelect ? serviceSelect.value : "", + hoststub: hoststubInput ? hoststubInput.value : "", + api_key: apikeyInput ? apikeyInput.value : "" + }; + fetch(PRODUCTION_MODEL_SUBMIT_URL, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ inference_system }) + }).catch(err => { + console.error("Failed to persist inference system", err); + }); + } + function ensureProductionRowUsageCells(row, defaultChecked) { if (!row) return; // ensure existence of checkboxes @@ -669,6 +715,8 @@ updateUndeployButtonState(row); } }); + } else { + ensureFeatureAssignedToAnotherModel(columnIndex, currentRow); } if (currentRow) { @@ -691,6 +739,15 @@ undeployBtn.dataset.action = "undeploy"; styleActionButton(undeployBtn); undeployBtn.addEventListener("click", () => { + // reassign any active features before removing this row + for (let col = PRODUCTION_MODEL_USAGE_COLUMN_START; col <= PRODUCTION_MODEL_USAGE_COLUMN_END; col += 1) { + const cell = row.cells[col]; + if (!cell) continue; + const checkbox = cell.querySelector('input[type="checkbox"]'); + if (checkbox && checkbox.checked) { + ensureFeatureAssignedToAnotherModel(col, row); + } + } row.remove(); persistProductionModels(); }); @@ -702,18 +759,32 @@ if (!row) return; const button = row.querySelector('button[data-action="undeploy"]'); if (!button) return; - hasFeatureChecked = false; - for (let col = PRODUCTION_MODEL_USAGE_COLUMN_START; col <= PRODUCTION_MODEL_USAGE_COLUMN_END; col += 1) { - const cell = row.cells[col]; - if (!cell) continue; - const checkbox = cell.querySelector('input[type="checkbox"]'); - if (checkbox && checkbox.checked) { - hasFeatureChecked = true; - break; - } + button.disabled = false; + button.title = "Remove this model (features will be reassigned if possible)."; + } + + function ensureFeatureAssignedToAnotherModel(columnIndex, sourceRow) { + const tbody = getProductionTableBody(); + if (!tbody) return; + const rows = Array.from(tbody.querySelectorAll("tr")); + if (rows.length <= 1) return; // nothing to reassign to + // If any other row already has the feature, keep it. + const othersHave = rows.some(r => { + if (r === sourceRow) return false; + const cell = r.cells[columnIndex]; + const cb = cell ? cell.querySelector('input[type="checkbox"]') : null; + return cb && cb.checked; + }); + if (othersHave) return; + // pick the first other row and assign + const target = rows.find(r => r !== sourceRow); + if (!target) return; + const targetCell = target.cells[columnIndex]; + const targetCb = targetCell ? targetCell.querySelector('input[type="checkbox"]') : null; + if (targetCb) { + targetCb.checked = true; + updateUndeployButtonState(target); } - button.disabled = hasFeatureChecked; - button.title = hasFeatureChecked ? "Disable the assigned features before undeploying." : ""; } function persistProductionModels() { @@ -741,9 +812,18 @@ }); // push to server + const hoststubInput = document.getElementById("hoststub"); + const apikeyInput = document.getElementById("apikey"); + const serviceSelect = document.getElementById("service"); + const inference_system = { + service: serviceSelect ? serviceSelect.value : "", + hoststub: hoststubInput ? hoststubInput.value : "", + api_key: apikeyInput ? apikeyInput.value : "" + }; + fetch(PRODUCTION_MODEL_SUBMIT_URL, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ production_models: production_models_table }) + body: JSON.stringify({ production_models: production_models_table, inference_system }) }).catch(err => { console.error("Failed to persist production models", err); }); @@ -785,7 +865,7 @@ if (!endpointBase) { return false; } - const targetUrl = `${endpointBase}${TOOLING_TEST_ENDPOINT_PATH}`; + const targetUrl = `${endpointBase}${TEST_STRINGS.toolingEndpointPath}`; const headers = { "Content-Type": "application/json" }; if (apikey) { headers.Authorization = `Bearer ${apikey}`; @@ -809,8 +889,8 @@ temperature: 0.1, max_tokens: 1024, messages: [ - { role: "system", content: TOOLING_TEST_SYSTEM_MESSAGE }, - { role: "user", content: TOOLING_TEST_USER_MESSAGE } + { role: "system", content: TEST_STRINGS.toolingSystemMessage }, + { role: "user", content: TEST_STRINGS.toolingUserMessage } ], tools: [{ type: "function", @@ -911,7 +991,7 @@ if (!endpointBase) { return false; } - const targetUrl = `${endpointBase}${TOOLING_TEST_ENDPOINT_PATH}`; + const targetUrl = `${endpointBase}${TEST_STRINGS.toolingEndpointPath}`; const headers = { "Content-Type": "application/json" }; if (apikey) { headers.Authorization = `Bearer ${apikey}`; @@ -936,11 +1016,11 @@ temperature: 0.1, max_tokens: 512, messages: [ - { role: "system", content: VISION_TEST_SYSTEM_MESSAGE }, + { role: "system", content: TEST_STRINGS.visionSystemMessage }, { role: "user", content: [ - { type: "text", text: VISION_TEST_USER_MESSAGE }, + { type: "text", text: TEST_STRINGS.visionUserMessage }, { type: "image_url", image_url: { @@ -963,7 +1043,7 @@ if (!normalizedText) { return false; } - return normalizedText.indexOf(VISION_TEST_EXPECTED_TEXT) !== -1; + return normalizedText.indexOf(TEST_STRINGS.visionExpectedText) !== -1; }); } @@ -997,7 +1077,7 @@ if (cachedVisionTestImagePromise) { return cachedVisionTestImagePromise; } - cachedVisionTestImagePromise = fetch(VISION_TEST_IMAGE_PATH) + cachedVisionTestImagePromise = fetch(TEST_STRINGS.visionTestImagePath) .then(response => { if (!response.ok) { throw new Error(`Failed to load test image (${response.status})`); @@ -1078,7 +1158,21 @@ return downloadBtn; } - document.addEventListener("DOMContentLoaded", normalizeProductionModelRows); + document.addEventListener("DOMContentLoaded", () => { + try { + normalizeProductionModelRows(); + applyPresetInference(); + // auto-show available models if a preset inference exists + const body = document.body; + const presetService = (body.getAttribute("data-llm-service") || "").trim(); + const presetHoststub = (body.getAttribute("data-llm-hoststub") || "").trim(); + if (presetService && presetHoststub) { + loadModelList(true); + } + } catch (e) { + console.error("Initialization failed", e); + } + }); </script> @@ -1087,7 +1181,7 @@ <p> 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 + In the "Production Models Matrix" you can then assign each selected model a function inside YaCy </p> <p> <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. @@ -1138,9 +1232,9 @@ <legend>Model Downloads</legend> <div id="downloadActivityList"></div> </fieldset> - <fieldset id="availableModelsContainer" style="display:none"></fieldset> + <fieldset id="availableModelsContainer" style="display:none"><a name="availableModels"></a></fieldset> - <fieldset id="productionModelsContainer" style="display: block;"><legend>Production Models</legend> + <fieldset id="productionModelsContainer" style="display: block;"><a name="productionModels"></a><legend>Production Models Matrix</legend> <table class="table table-striped" id="productionModelsTable"> <thead class="thead-dark"> <tr> diff --git a/htroot/env/grafics/AILab_Crawl.png b/htroot/env/grafics/AILab_Crawl.png Binary files differnew file mode 100644 index 000000000..330918bd5 --- /dev/null +++ b/htroot/env/grafics/AILab_Crawl.png diff --git a/htroot/env/grafics/AILab_Inference.png b/htroot/env/grafics/AILab_Inference.png Binary files differnew file mode 100644 index 000000000..0f4004b54 --- /dev/null +++ b/htroot/env/grafics/AILab_Inference.png diff --git a/htroot/env/grafics/AILab_Matrix.png b/htroot/env/grafics/AILab_Matrix.png Binary files differnew file mode 100644 index 000000000..c1cf0efba --- /dev/null +++ b/htroot/env/grafics/AILab_Matrix.png diff --git a/htroot/env/grafics/AILab_RAG.png b/htroot/env/grafics/AILab_RAG.png Binary files differnew file mode 100644 index 000000000..6e14804ef --- /dev/null +++ b/htroot/env/grafics/AILab_RAG.png diff --git a/htroot/env/grafics/AILab_shield.png b/htroot/env/grafics/AILab_shield.png Binary files differnew file mode 100644 index 000000000..72b4cf0c9 --- /dev/null +++ b/htroot/env/grafics/AILab_shield.png diff --git a/htroot/env/templates/submenuAI.template b/htroot/env/templates/submenuAI.template index c9c933661..a5badb975 100644 --- a/htroot/env/templates/submenuAI.template +++ b/htroot/env/templates/submenuAI.template @@ -1,6 +1,7 @@ <div class="SubMenu"> <h3>AI Lab</h3> <ul class="SubMenu"> + <li><a href="AILab.html" class="MenuItemLink">AI Lab</a></li> <li><a href="LLMSelection_p.html" class="MenuItemLink #(authorized)#lock::unlock#(/authorized)#">LLM Selection</a></li> <li><a href="yacychat.html" class="MenuItemLink">Chat</a></li> </ul> diff --git a/source/net/yacy/htroot/AILab.java b/source/net/yacy/htroot/AILab.java new file mode 100644 index 000000000..461a8c32e --- /dev/null +++ b/source/net/yacy/htroot/AILab.java @@ -0,0 +1,87 @@ +// AILab.java +// ------------- +// (C) 2024 by contributors to the YaCy project +// +// This servlet feeds the AI Lab landing page with completion hints +// derived from existing configuration and index state. + +package net.yacy.htroot; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import net.yacy.cora.protocol.RequestHeader; +import net.yacy.search.Switchboard; +import net.yacy.server.serverObjects; +import net.yacy.server.serverSwitch; + +public class AILab { + + public static serverObjects respond(@SuppressWarnings("unused") final RequestHeader header, final serverObjects post, final serverSwitch env) { + final Switchboard sb = (Switchboard) env; + final serverObjects prop = new serverObjects(); + + boolean hasEngine = false; + boolean hasModel = false; + boolean hasRagRole = false; + + // Parse configured production models to infer readiness of engine/model/RAG quests. + final String productionModelJson = sb.getConfig("ai.production_models", "[]"); + try { + final JSONArray productionModels = new JSONArray(new JSONTokener(productionModelJson)); + for (int i = 0; i < productionModels.length(); i++) { + final org.json.JSONObject row = productionModels.getJSONObject(i); + if (!hasEngine) { + hasEngine = !row.optString("hoststub", "").isEmpty(); + } + if (!hasModel) { + hasModel = !row.optString("model", "").isEmpty(); + } + if (!hasRagRole) { + hasRagRole = row.optBoolean("search", false) || row.optBoolean("query", false) || row.optBoolean("qapairs", false); + } + if (hasEngine && hasModel && hasRagRole) { + break; + } + } + } catch (final JSONException e) { + // ignore malformed configuration; fallback statuses will stay "pending" + } + + // consider explicit inference_system configuration as an engine binding + if (!hasEngine) { + final String inferenceJson = sb.getConfig("ai.inference_system", "{}"); + try { + final JSONObject inference = new JSONObject(new JSONTokener(inferenceJson)); + final String hoststub = inference.optString("hoststub", "").trim(); + if (!hoststub.isEmpty()) { + hasEngine = true; + } + } catch (final JSONException e) { + // ignore malformed inference_system + } + } + + // Index presence check: if there are documents in the default fulltext collection, we consider the quest ready. + final long indexDocs = sb.index.fulltext().collectionSize(); + final long indexNeeded = 1000L; + final boolean hasIndex = indexDocs >= indexNeeded; + + // Shield configuration: treat any non-empty custom value as "ready". + final String shieldDefinition = sb.getConfig("ai.shield.definition", "").trim(); + final boolean hasShield = !shieldDefinition.isEmpty(); + + prop.put("ailab_inference_status", hasEngine ? "ready" : "pending"); + prop.put("ailab_model_status", hasModel ? "ready" : "pending"); + prop.put("ailab_inference_configured", hasEngine ? "1" : "0"); + prop.put("ailab_index_status", hasIndex ? "ready" : "pending"); + prop.putNum("ailab_index_count", indexDocs); + prop.putNum("ailab_index_needed", indexNeeded); + prop.put("ailab_rag_status", hasRagRole ? "ready" : "pending"); + prop.put("ailab_shield_status", hasShield ? "ready" : "pending"); + + return prop; + } +} diff --git a/source/net/yacy/htroot/LLMSelection_p.java b/source/net/yacy/htroot/LLMSelection_p.java index 3877ffdac..305864739 100644 --- a/source/net/yacy/htroot/LLMSelection_p.java +++ b/source/net/yacy/htroot/LLMSelection_p.java @@ -58,6 +58,11 @@ public class LLMSelection_p { //e.printStackTrace(); } } + + JSONObject inferenceSystem = bodyj.optJSONObject("inference_system"); + if (inferenceSystem != null) { + sb.setConfig("ai.inference_system", inferenceSystem.toString()); + } /* {"production_models":[{ "service":"OLLAMA", @@ -104,6 +109,19 @@ public class LLMSelection_p { e.printStackTrace(); } + // prefill inference system configuration if present + final String inferenceJson = sb.getConfig("ai.inference_system", "{}"); + try { + JSONObject inference = new JSONObject(new JSONTokener(inferenceJson)); + prop.put("llm_service", inference.optString("service", "OLLAMA")); + prop.put("llm_hoststub", inference.optString("hoststub", "http://localhost:11434")); + prop.put("llm_apikey", inference.optString("api_key", "")); + } catch (JSONException e) { + prop.put("llm_service", "OLLAMA"); + prop.put("llm_hoststub", "http://localhost:11434"); + prop.put("llm_apikey", ""); + } + if (post == null || env == null) { return prop; // nothing to do } @@ -112,4 +130,4 @@ public class LLMSelection_p { return prop; } -}
\ No newline at end of file +} |
