From a7d09e4c67df83331d71c07e9b7ce0aec26eb53c Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Feb 2026 19:29:23 +0000 Subject: [PATCH] refactor: migrate cmd/ml to go-ml, remove replaces and go.work - Move 40 ML command files to forge.lthn.ai/core/go-ml/cmd - Remove all replace directives from go.mod - Remove go.work (repos resolve from forge directly) - Fix cache.New call to match updated API signature - Update main.go import to forge.lthn.ai/core/go-ml/cmd Co-Authored-By: Virgil --- cmd/ml/chat.js | 832 -------------------------------- cmd/ml/chat_embed.go | 44 -- cmd/ml/cmd_agent.go | 67 --- cmd/ml/cmd_approve.go | 53 -- cmd/ml/cmd_benchmark.go | 301 ------------ cmd/ml/cmd_benchmark_init.go | 7 - cmd/ml/cmd_chat.go | 327 ------------- cmd/ml/cmd_chat_init.go | 7 - cmd/ml/cmd_consolidate.go | 41 -- cmd/ml/cmd_convert.go | 40 -- cmd/ml/cmd_coverage.go | 34 -- cmd/ml/cmd_expand.go | 81 ---- cmd/ml/cmd_expand_status.go | 95 ---- cmd/ml/cmd_export.go | 109 ----- cmd/ml/cmd_gguf.go | 40 -- cmd/ml/cmd_import.go | 58 --- cmd/ml/cmd_ingest.go | 54 --- cmd/ml/cmd_inventory.go | 34 -- cmd/ml/cmd_lesson.go | 340 ------------- cmd/ml/cmd_lesson_init.go | 8 - cmd/ml/cmd_live.go | 82 ---- cmd/ml/cmd_metrics.go | 36 -- cmd/ml/cmd_ml.go | 91 ---- cmd/ml/cmd_normalize.go | 44 -- cmd/ml/cmd_probe.go | 66 --- cmd/ml/cmd_publish.go | 40 -- cmd/ml/cmd_query.go | 148 ------ cmd/ml/cmd_sandwich.go | 238 --------- cmd/ml/cmd_sandwich_init.go | 7 - cmd/ml/cmd_score.go | 77 --- cmd/ml/cmd_seed_influx.go | 49 -- cmd/ml/cmd_sequence.go | 326 ------------- cmd/ml/cmd_serve.go | 472 ------------------ cmd/ml/cmd_status.go | 54 --- cmd/ml/cmd_train.go | 358 -------------- cmd/ml/cmd_train_init.go | 7 - cmd/ml/cmd_worker.go | 80 --- cmd/ml/serve_backend_default.go | 9 - cmd/ml/serve_backend_mlx.go | 22 - cmd/pkgcmd/cmd_search.go | 2 +- go.mod | 70 +-- go.sum | 106 ++++ go.work | 18 - main.go | 2 +- 44 files changed, 147 insertions(+), 4829 deletions(-) delete mode 100644 cmd/ml/chat.js delete mode 100644 cmd/ml/chat_embed.go delete mode 100644 cmd/ml/cmd_agent.go delete mode 100644 cmd/ml/cmd_approve.go delete mode 100644 cmd/ml/cmd_benchmark.go delete mode 100644 cmd/ml/cmd_benchmark_init.go delete mode 100644 cmd/ml/cmd_chat.go delete mode 100644 cmd/ml/cmd_chat_init.go delete mode 100644 cmd/ml/cmd_consolidate.go delete mode 100644 cmd/ml/cmd_convert.go delete mode 100644 cmd/ml/cmd_coverage.go delete mode 100644 cmd/ml/cmd_expand.go delete mode 100644 cmd/ml/cmd_expand_status.go delete mode 100644 cmd/ml/cmd_export.go delete mode 100644 cmd/ml/cmd_gguf.go delete mode 100644 cmd/ml/cmd_import.go delete mode 100644 cmd/ml/cmd_ingest.go delete mode 100644 cmd/ml/cmd_inventory.go delete mode 100644 cmd/ml/cmd_lesson.go delete mode 100644 cmd/ml/cmd_lesson_init.go delete mode 100644 cmd/ml/cmd_live.go delete mode 100644 cmd/ml/cmd_metrics.go delete mode 100644 cmd/ml/cmd_ml.go delete mode 100644 cmd/ml/cmd_normalize.go delete mode 100644 cmd/ml/cmd_probe.go delete mode 100644 cmd/ml/cmd_publish.go delete mode 100644 cmd/ml/cmd_query.go delete mode 100644 cmd/ml/cmd_sandwich.go delete mode 100644 cmd/ml/cmd_sandwich_init.go delete mode 100644 cmd/ml/cmd_score.go delete mode 100644 cmd/ml/cmd_seed_influx.go delete mode 100644 cmd/ml/cmd_sequence.go delete mode 100644 cmd/ml/cmd_serve.go delete mode 100644 cmd/ml/cmd_status.go delete mode 100644 cmd/ml/cmd_train.go delete mode 100644 cmd/ml/cmd_train_init.go delete mode 100644 cmd/ml/cmd_worker.go delete mode 100644 cmd/ml/serve_backend_default.go delete mode 100644 cmd/ml/serve_backend_mlx.go delete mode 100644 go.work diff --git a/cmd/ml/chat.js b/cmd/ml/chat.js deleted file mode 100644 index 8948708c..00000000 --- a/cmd/ml/chat.js +++ /dev/null @@ -1,832 +0,0 @@ -// src/styles.ts -var chatStyles = ` - :host { - display: flex; - flex-direction: column; - background: var(--lem-bg, #1a1a1e); - color: var(--lem-text, #e0e0e0); - font-family: var(--lem-font, system-ui, -apple-system, sans-serif); - font-size: 14px; - line-height: 1.5; - border-radius: 12px; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.08); - } - - .header { - display: flex; - align-items: center; - gap: 10px; - padding: 14px 18px; - background: rgba(255, 255, 255, 0.03); - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; - } - - .header-icon { - width: 28px; - height: 28px; - border-radius: 8px; - background: var(--lem-accent, #5865f2); - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - font-weight: 700; - color: #fff; - } - - .header-title { - font-size: 15px; - font-weight: 600; - color: var(--lem-text, #e0e0e0); - } - - .header-model { - font-size: 11px; - color: rgba(255, 255, 255, 0.35); - margin-left: auto; - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; - } - - .header-status { - width: 8px; - height: 8px; - border-radius: 50%; - background: #43b581; - flex-shrink: 0; - } - - .header-status.disconnected { - background: #f04747; - } -`; -var messagesStyles = ` - :host { - display: block; - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 16px 0; - scroll-behavior: smooth; - } - - :host::-webkit-scrollbar { - width: 6px; - } - - :host::-webkit-scrollbar-track { - background: transparent; - } - - :host::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.12); - border-radius: 3px; - } - - .empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - gap: 12px; - color: rgba(255, 255, 255, 0.25); - } - - .empty-icon { - font-size: 36px; - opacity: 0.4; - } - - .empty-text { - font-size: 14px; - } -`; -var messageStyles = ` - :host { - display: block; - padding: 6px 18px; - } - - :host([role="user"]) .bubble { - background: var(--lem-msg-user, #2a2a3e); - margin-left: 40px; - border-radius: 12px 12px 4px 12px; - } - - :host([role="assistant"]) .bubble { - background: var(--lem-msg-assistant, #1e1e2a); - margin-right: 40px; - border-radius: 12px 12px 12px 4px; - } - - .bubble { - padding: 10px 14px; - word-wrap: break-word; - overflow-wrap: break-word; - } - - .role { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 4px; - color: rgba(255, 255, 255, 0.35); - } - - :host([role="assistant"]) .role { - color: var(--lem-accent, #5865f2); - } - - .content { - color: var(--lem-text, #e0e0e0); - line-height: 1.6; - } - - .content p { - margin: 0 0 8px 0; - } - - .content p:last-child { - margin-bottom: 0; - } - - .content strong { - font-weight: 600; - color: #fff; - } - - .content em { - font-style: italic; - color: rgba(255, 255, 255, 0.8); - } - - .content code { - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 12px; - background: rgba(0, 0, 0, 0.3); - padding: 2px 5px; - border-radius: 4px; - color: #e8a0bf; - } - - .content pre { - margin: 8px 0; - padding: 12px; - background: rgba(0, 0, 0, 0.35); - border-radius: 8px; - overflow-x: auto; - border: 1px solid rgba(255, 255, 255, 0.06); - } - - .content pre code { - background: none; - padding: 0; - font-size: 12px; - color: #c9d1d9; - line-height: 1.5; - } - - .think-panel { - margin: 6px 0 8px; - padding: 8px 12px; - background: rgba(88, 101, 242, 0.06); - border-left: 2px solid rgba(88, 101, 242, 0.3); - border-radius: 0 6px 6px 0; - font-size: 12px; - color: rgba(255, 255, 255, 0.45); - line-height: 1.5; - max-height: 200px; - overflow-y: auto; - } - - .think-panel::-webkit-scrollbar { - width: 4px; - } - - .think-panel::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - } - - .think-label { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(88, 101, 242, 0.5); - margin-bottom: 4px; - cursor: pointer; - user-select: none; - } - - .think-label:hover { - color: rgba(88, 101, 242, 0.7); - } - - .think-panel.collapsed .think-content { - display: none; - } - - .cursor { - display: inline-block; - width: 7px; - height: 16px; - background: var(--lem-accent, #5865f2); - border-radius: 1px; - animation: blink 0.8s step-end infinite; - vertical-align: text-bottom; - margin-left: 2px; - } - - @keyframes blink { - 50% { opacity: 0; } - } -`; -var inputStyles = ` - :host { - display: block; - padding: 12px 16px 16px; - border-top: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; - } - - .input-wrapper { - display: flex; - align-items: flex-end; - gap: 10px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 12px; - padding: 8px 12px; - transition: border-color 0.15s; - } - - .input-wrapper:focus-within { - border-color: var(--lem-accent, #5865f2); - } - - textarea { - flex: 1; - background: none; - border: none; - outline: none; - color: var(--lem-text, #e0e0e0); - font-family: inherit; - font-size: 14px; - line-height: 1.5; - resize: none; - max-height: 120px; - min-height: 22px; - padding: 0; - } - - textarea::placeholder { - color: rgba(255, 255, 255, 0.25); - } - - .send-btn { - background: var(--lem-accent, #5865f2); - border: none; - border-radius: 8px; - color: #fff; - width: 32px; - height: 32px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: opacity 0.15s, transform 0.1s; - } - - .send-btn:hover { - opacity: 0.85; - } - - .send-btn:active { - transform: scale(0.95); - } - - .send-btn:disabled { - opacity: 0.3; - cursor: default; - transform: none; - } - - .send-btn svg { - width: 16px; - height: 16px; - } -`; - -// src/lem-messages.ts -var LemMessages = class extends HTMLElement { - shadow; - container; - emptyEl; - shouldAutoScroll = true; - constructor() { - super(); - this.shadow = this.attachShadow({ mode: "open" }); - } - connectedCallback() { - const style = document.createElement("style"); - style.textContent = messagesStyles; - this.container = document.createElement("div"); - this.emptyEl = document.createElement("div"); - this.emptyEl.className = "empty"; - const emptyIcon = document.createElement("div"); - emptyIcon.className = "empty-icon"; - emptyIcon.textContent = "\u2728"; - const emptyText = document.createElement("div"); - emptyText.className = "empty-text"; - emptyText.textContent = "Start a conversation"; - this.emptyEl.appendChild(emptyIcon); - this.emptyEl.appendChild(emptyText); - this.shadow.appendChild(style); - this.shadow.appendChild(this.emptyEl); - this.shadow.appendChild(this.container); - this.addEventListener("scroll", () => { - const threshold = 60; - this.shouldAutoScroll = this.scrollHeight - this.scrollTop - this.clientHeight < threshold; - }); - } - addMessage(role, text) { - this.emptyEl.style.display = "none"; - const msg = document.createElement("lem-message"); - msg.setAttribute("role", role); - this.container.appendChild(msg); - if (text) { - msg.text = text; - } - this.scrollToBottom(); - return msg; - } - scrollToBottom() { - if (this.shouldAutoScroll) { - requestAnimationFrame(() => { - this.scrollTop = this.scrollHeight; - }); - } - } - clear() { - this.container.replaceChildren(); - this.emptyEl.style.display = ""; - this.shouldAutoScroll = true; - } -}; -customElements.define("lem-messages", LemMessages); - -// src/markdown.ts -function escapeHtml(text) { - return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); -} -function parseInline(text) { - let result = escapeHtml(text); - result = result.replace(/`([^`]+)`/g, "$1"); - result = result.replace(/\*\*(.+?)\*\*/g, "$1"); - result = result.replace(/__(.+?)__/g, "$1"); - result = result.replace(/(?$1"); - result = result.replace(/(?$1"); - return result; -} -function renderMarkdown(text) { - const lines = text.split("\n"); - const output = []; - let inCodeBlock = false; - let codeLines = []; - let codeLang = ""; - for (const line of lines) { - if (line.trimStart().startsWith("```")) { - if (!inCodeBlock) { - inCodeBlock = true; - codeLang = line.trimStart().slice(3).trim(); - codeLines = []; - } else { - const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""; - output.push( - `${escapeHtml(codeLines.join("\n"))}` - ); - inCodeBlock = false; - codeLines = []; - codeLang = ""; - } - continue; - } - if (inCodeBlock) { - codeLines.push(line); - continue; - } - if (line.trim() === "") { - output.push(""); - continue; - } - output.push(parseInline(line)); - } - if (inCodeBlock) { - const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""; - output.push( - `${escapeHtml(codeLines.join("\n"))}` - ); - } - const paragraphs = []; - let current = []; - for (const line of output) { - if (line === "") { - if (current.length > 0) { - paragraphs.push(wrapParagraph(current)); - current = []; - } - } else { - current.push(line); - } - } - if (current.length > 0) { - paragraphs.push(wrapParagraph(current)); - } - return paragraphs.join(""); -} -function wrapParagraph(lines) { - const joined = lines.join("
"); - if (joined.startsWith("${joined}

`; -} - -// src/lem-message.ts -var LemMessage = class extends HTMLElement { - shadow; - thinkPanel; - thinkContent; - thinkLabel; - contentEl; - cursorEl; - _text = ""; - _streaming = false; - _thinkCollapsed = false; - constructor() { - super(); - this.shadow = this.attachShadow({ mode: "open" }); - } - connectedCallback() { - const role = this.getAttribute("role") || "user"; - const style = document.createElement("style"); - style.textContent = messageStyles; - const bubble = document.createElement("div"); - bubble.className = "bubble"; - const roleEl = document.createElement("div"); - roleEl.className = "role"; - roleEl.textContent = role === "assistant" ? "LEM" : "You"; - this.thinkPanel = document.createElement("div"); - this.thinkPanel.className = "think-panel"; - this.thinkPanel.style.display = "none"; - this.thinkLabel = document.createElement("div"); - this.thinkLabel.className = "think-label"; - this.thinkLabel.textContent = "\u25BC reasoning"; - this.thinkLabel.addEventListener("click", () => { - this._thinkCollapsed = !this._thinkCollapsed; - this.thinkPanel.classList.toggle("collapsed", this._thinkCollapsed); - this.thinkLabel.textContent = this._thinkCollapsed ? "\u25B6 reasoning" : "\u25BC reasoning"; - }); - this.thinkContent = document.createElement("div"); - this.thinkContent.className = "think-content"; - this.thinkPanel.appendChild(this.thinkLabel); - this.thinkPanel.appendChild(this.thinkContent); - this.contentEl = document.createElement("div"); - this.contentEl.className = "content"; - bubble.appendChild(roleEl); - if (role === "assistant") { - bubble.appendChild(this.thinkPanel); - } - bubble.appendChild(this.contentEl); - this.shadow.appendChild(style); - this.shadow.appendChild(bubble); - if (this._text) { - this.render(); - } - } - get text() { - return this._text; - } - set text(value) { - this._text = value; - this.render(); - } - get streaming() { - return this._streaming; - } - set streaming(value) { - this._streaming = value; - this.render(); - } - appendToken(token) { - this._text += token; - this.render(); - } - /** - * Splits text into think/response portions and renders each. - * - * Safety: renderMarkdown() escapes all HTML entities (& < > ") before any - * inline formatting is applied. The source is the local MLX model output, - * not arbitrary user HTML. Shadow DOM provides additional isolation. - */ - render() { - if (!this.contentEl) return; - const { think, response } = this.splitThink(this._text); - if (think !== null && this.thinkPanel) { - this.thinkPanel.style.display = ""; - this.thinkContent.textContent = think; - } - const responseHtml = renderMarkdown(response); - this.contentEl.innerHTML = responseHtml; - if (this._streaming) { - if (!this.cursorEl) { - this.cursorEl = document.createElement("span"); - this.cursorEl.className = "cursor"; - } - if (think !== null && !this._text.includes("")) { - this.thinkContent.appendChild(this.cursorEl); - } else { - const lastChild = this.contentEl.lastElementChild || this.contentEl; - lastChild.appendChild(this.cursorEl); - } - } - } - /** - * Split raw text into think content and response content. - * Returns { think: string | null, response: string } - */ - splitThink(text) { - const thinkStart = text.indexOf(""); - if (thinkStart === -1) { - return { think: null, response: text }; - } - const afterOpen = thinkStart + "".length; - const thinkEnd = text.indexOf("", afterOpen); - if (thinkEnd === -1) { - return { - think: text.slice(afterOpen).trim(), - response: text.slice(0, thinkStart).trim() - }; - } - const thinkText = text.slice(afterOpen, thinkEnd).trim(); - const beforeThink = text.slice(0, thinkStart).trim(); - const afterThink = text.slice(thinkEnd + "".length).trim(); - const response = [beforeThink, afterThink].filter(Boolean).join("\n"); - return { think: thinkText, response }; - } -}; -customElements.define("lem-message", LemMessage); - -// src/lem-input.ts -var LemInput = class extends HTMLElement { - shadow; - textarea; - sendBtn; - _disabled = false; - constructor() { - super(); - this.shadow = this.attachShadow({ mode: "open" }); - } - connectedCallback() { - const style = document.createElement("style"); - style.textContent = inputStyles; - const wrapper = document.createElement("div"); - wrapper.className = "input-wrapper"; - this.textarea = document.createElement("textarea"); - this.textarea.rows = 1; - this.textarea.placeholder = "Message LEM..."; - this.sendBtn = document.createElement("button"); - this.sendBtn.className = "send-btn"; - this.sendBtn.type = "button"; - this.sendBtn.disabled = true; - this.sendBtn.appendChild(this.createSendIcon()); - wrapper.appendChild(this.textarea); - wrapper.appendChild(this.sendBtn); - this.shadow.appendChild(style); - this.shadow.appendChild(wrapper); - this.textarea.addEventListener("input", () => { - this.textarea.style.height = "auto"; - this.textarea.style.height = Math.min(this.textarea.scrollHeight, 120) + "px"; - this.sendBtn.disabled = this._disabled || this.textarea.value.trim() === ""; - }); - this.textarea.addEventListener("keydown", (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - this.submit(); - } - }); - this.sendBtn.addEventListener("click", () => this.submit()); - } - /** Build the send arrow SVG using DOM API (no innerHTML) */ - createSendIcon() { - const ns = "http://www.w3.org/2000/svg"; - const svg = document.createElementNS(ns, "svg"); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("fill", "none"); - svg.setAttribute("stroke", "currentColor"); - svg.setAttribute("stroke-width", "2"); - svg.setAttribute("stroke-linecap", "round"); - svg.setAttribute("stroke-linejoin", "round"); - svg.setAttribute("width", "16"); - svg.setAttribute("height", "16"); - const line = document.createElementNS(ns, "line"); - line.setAttribute("x1", "22"); - line.setAttribute("y1", "2"); - line.setAttribute("x2", "11"); - line.setAttribute("y2", "13"); - const polygon = document.createElementNS(ns, "polygon"); - polygon.setAttribute("points", "22 2 15 22 11 13 2 9 22 2"); - svg.appendChild(line); - svg.appendChild(polygon); - return svg; - } - submit() { - const text = this.textarea.value.trim(); - if (!text || this._disabled) return; - this.dispatchEvent( - new CustomEvent("lem-send", { - bubbles: true, - composed: true, - detail: { text } - }) - ); - this.textarea.value = ""; - this.textarea.style.height = "auto"; - this.sendBtn.disabled = true; - this.textarea.focus(); - } - get disabled() { - return this._disabled; - } - set disabled(value) { - this._disabled = value; - this.textarea.disabled = value; - this.sendBtn.disabled = value || this.textarea.value.trim() === ""; - this.textarea.placeholder = value ? "LEM is thinking..." : "Message LEM..."; - } - focus() { - this.textarea?.focus(); - } -}; -customElements.define("lem-input", LemInput); - -// src/lem-chat.ts -var LemChat = class extends HTMLElement { - shadow; - messages; - input; - statusEl; - history = []; - abortController = null; - static get observedAttributes() { - return ["endpoint", "model", "system-prompt", "max-tokens", "temperature"]; - } - constructor() { - super(); - this.shadow = this.attachShadow({ mode: "open" }); - } - connectedCallback() { - const style = document.createElement("style"); - style.textContent = chatStyles; - const header = document.createElement("div"); - header.className = "header"; - this.statusEl = document.createElement("div"); - this.statusEl.className = "header-status"; - const icon = document.createElement("div"); - icon.className = "header-icon"; - icon.textContent = "L"; - const title = document.createElement("div"); - title.className = "header-title"; - title.textContent = "LEM"; - const modelLabel = document.createElement("div"); - modelLabel.className = "header-model"; - modelLabel.textContent = this.getAttribute("model") || "local"; - header.appendChild(this.statusEl); - header.appendChild(icon); - header.appendChild(title); - header.appendChild(modelLabel); - this.messages = document.createElement("lem-messages"); - this.input = document.createElement("lem-input"); - this.shadow.appendChild(style); - this.shadow.appendChild(header); - this.shadow.appendChild(this.messages); - this.shadow.appendChild(this.input); - this.addEventListener("lem-send", ((e) => { - this.handleSend(e.detail.text); - })); - const systemPrompt = this.getAttribute("system-prompt"); - if (systemPrompt) { - this.history.push({ role: "system", content: systemPrompt }); - } - this.checkConnection(); - requestAnimationFrame(() => this.input.focus()); - } - disconnectedCallback() { - this.abortController?.abort(); - } - get endpoint() { - const attr = this.getAttribute("endpoint"); - if (!attr) return window.location.origin; - return attr; - } - get model() { - return this.getAttribute("model") || ""; - } - get maxTokens() { - const val = this.getAttribute("max-tokens"); - return val ? parseInt(val, 10) : 2048; - } - get temperature() { - const val = this.getAttribute("temperature"); - return val ? parseFloat(val) : 0.7; - } - async checkConnection() { - try { - const resp = await fetch(`${this.endpoint}/v1/models`, { - signal: AbortSignal.timeout(3e3) - }); - this.statusEl.classList.toggle("disconnected", !resp.ok); - } catch { - this.statusEl.classList.add("disconnected"); - } - } - async handleSend(text) { - this.messages.addMessage("user", text); - this.history.push({ role: "user", content: text }); - const assistantMsg = this.messages.addMessage("assistant"); - assistantMsg.streaming = true; - this.input.disabled = true; - this.abortController?.abort(); - this.abortController = new AbortController(); - let fullResponse = ""; - try { - const response = await fetch(`${this.endpoint}/v1/chat/completions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - signal: this.abortController.signal, - body: JSON.stringify({ - model: this.model, - messages: this.history, - max_tokens: this.maxTokens, - temperature: this.temperature, - stream: true - }) - }); - if (!response.ok) { - throw new Error(`Server error: ${response.status}`); - } - if (!response.body) { - throw new Error("No response body"); - } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) { - if (!line.startsWith("data: ")) continue; - const data = line.slice(6).trim(); - if (data === "[DONE]") continue; - try { - const chunk = JSON.parse(data); - const delta = chunk.choices?.[0]?.delta; - if (delta?.content) { - fullResponse += delta.content; - assistantMsg.appendToken(delta.content); - this.messages.scrollToBottom(); - } - } catch { - } - } - } - } catch (err) { - if (err instanceof Error && err.name === "AbortError") { - } else { - const errorText = err instanceof Error ? err.message : "Connection failed"; - if (!fullResponse) { - assistantMsg.text = `\u26A0\uFE0F ${errorText}`; - } - this.statusEl.classList.add("disconnected"); - } - } finally { - assistantMsg.streaming = false; - this.input.disabled = false; - this.input.focus(); - this.abortController = null; - if (fullResponse) { - this.history.push({ role: "assistant", content: fullResponse }); - } - } - } -}; -customElements.define("lem-chat", LemChat); -export { - LemChat -}; diff --git a/cmd/ml/chat_embed.go b/cmd/ml/chat_embed.go deleted file mode 100644 index 447d7a95..00000000 --- a/cmd/ml/chat_embed.go +++ /dev/null @@ -1,44 +0,0 @@ -package ml - -import ( - _ "embed" -) - -//go:embed chat.js -var lemChatJS []byte - -const chatHTML = ` - - - - - LEM Chat - - - - - - -` diff --git a/cmd/ml/cmd_agent.go b/cmd/ml/cmd_agent.go deleted file mode 100644 index 21fa678e..00000000 --- a/cmd/ml/cmd_agent.go +++ /dev/null @@ -1,67 +0,0 @@ -package ml - -import ( - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - agentM3Host string - agentM3User string - agentM3SSHKey string - agentM3AdapterBase string - agentBaseModel string - agentPollInterval int - agentWorkDir string - agentFilter string - agentForce bool - agentOneShot bool - agentDryRun bool -) - -var agentCmd = &cli.Command{ - Use: "agent", - Short: "Run the scoring agent daemon", - Long: "Polls M3 for unscored LoRA checkpoints, converts, probes, and pushes results to InfluxDB.", - RunE: runAgent, -} - -func init() { - agentCmd.Flags().StringVar(&agentM3Host, "m3-host", ml.EnvOr("M3_HOST", "10.69.69.108"), "M3 host address") - agentCmd.Flags().StringVar(&agentM3User, "m3-user", ml.EnvOr("M3_USER", "claude"), "M3 SSH user") - agentCmd.Flags().StringVar(&agentM3SSHKey, "m3-ssh-key", ml.EnvOr("M3_SSH_KEY", ml.ExpandHome("~/.ssh/id_ed25519")), "SSH key for M3") - agentCmd.Flags().StringVar(&agentM3AdapterBase, "m3-adapter-base", ml.EnvOr("M3_ADAPTER_BASE", "/Volumes/Data/lem"), "Adapter base dir on M3") - agentCmd.Flags().StringVar(&agentBaseModel, "base-model", ml.EnvOr("BASE_MODEL", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"), "HuggingFace base model ID") - agentCmd.Flags().IntVar(&agentPollInterval, "poll", ml.IntEnvOr("POLL_INTERVAL", 300), "Poll interval in seconds") - agentCmd.Flags().StringVar(&agentWorkDir, "work-dir", ml.EnvOr("WORK_DIR", "/tmp/scoring-agent"), "Working directory for adapters") - agentCmd.Flags().StringVar(&agentFilter, "filter", "", "Filter adapter dirs by prefix") - agentCmd.Flags().BoolVar(&agentForce, "force", false, "Re-score already-scored checkpoints") - agentCmd.Flags().BoolVar(&agentOneShot, "one-shot", false, "Process one checkpoint and exit") - agentCmd.Flags().BoolVar(&agentDryRun, "dry-run", false, "Discover and plan but don't execute") -} - -func runAgent(cmd *cli.Command, args []string) error { - cfg := &ml.AgentConfig{ - M3Host: agentM3Host, - M3User: agentM3User, - M3SSHKey: agentM3SSHKey, - M3AdapterBase: agentM3AdapterBase, - InfluxURL: influxURL, - InfluxDB: influxDB, - DBPath: dbPath, - APIURL: apiURL, - JudgeURL: judgeURL, - JudgeModel: judgeModel, - Model: modelName, - BaseModel: agentBaseModel, - PollInterval: agentPollInterval, - WorkDir: agentWorkDir, - Filter: agentFilter, - Force: agentForce, - OneShot: agentOneShot, - DryRun: agentDryRun, - } - - ml.RunAgentLoop(cfg) - return nil -} diff --git a/cmd/ml/cmd_approve.go b/cmd/ml/cmd_approve.go deleted file mode 100644 index a7212df0..00000000 --- a/cmd/ml/cmd_approve.go +++ /dev/null @@ -1,53 +0,0 @@ -package ml - -import ( - "fmt" - "os" - "path/filepath" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - approveOutput string - approveThreshold float64 -) - -var approveCmd = &cli.Command{ - Use: "approve", - Short: "Filter scored expansions into training JSONL", - Long: "Filters scored expansion responses by quality threshold and exports approved ones as chat-format training JSONL.", - RunE: runApprove, -} - -func init() { - approveCmd.Flags().StringVar(&approveOutput, "output", "", "Output JSONL file (defaults to expansion-approved.jsonl in db dir)") - approveCmd.Flags().Float64Var(&approveThreshold, "threshold", 6.0, "Min judge average to approve") -} - -func runApprove(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - output := approveOutput - if output == "" { - output = filepath.Join(filepath.Dir(path), "expansion-approved.jsonl") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - return ml.ApproveExpansions(db, ml.ApproveConfig{ - Output: output, - Threshold: approveThreshold, - }, cmd.OutOrStdout()) -} diff --git a/cmd/ml/cmd_benchmark.go b/cmd/ml/cmd_benchmark.go deleted file mode 100644 index 7290354a..00000000 --- a/cmd/ml/cmd_benchmark.go +++ /dev/null @@ -1,301 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "os" - "runtime" - "sort" - "time" - - "forge.lthn.ai/core/go-ml" - "forge.lthn.ai/core/go/pkg/cli" -) - -var benchmarkCmd = &cli.Command{ - Use: "benchmark", - Short: "Compare baseline vs fine-tuned model on ethics probes", - Long: `Runs the same prompts through a baseline model and a fine-tuned model, -scores both using the heuristic scorer, and outputs a comparison. - -Uses the built-in LEK content probes by default. Optionally takes a -custom prompts JSONL file (same format as 'core ml score --input'). - -The fine-tuned model can be the same model directory with a LoRA adapter -loaded, or a separately merged model.`, - RunE: runBenchmark, -} - -var ( - benchmarkBaseline string - benchmarkTrained string - benchmarkPrompts string - benchmarkOutput string - benchmarkMaxTokens int - benchmarkTemp float64 - benchmarkMemLimit int -) - -func init() { - benchmarkCmd.Flags().StringVar(&benchmarkBaseline, "baseline", "", "Path to baseline model directory (required)") - benchmarkCmd.Flags().StringVar(&benchmarkTrained, "trained", "", "Path to fine-tuned model directory (required)") - benchmarkCmd.Flags().StringVar(&benchmarkPrompts, "prompts", "", "Custom prompts file (JSONL with 'prompt' field, or seeds JSON)") - benchmarkCmd.Flags().StringVar(&benchmarkOutput, "output", "benchmark.json", "Output comparison JSON file") - benchmarkCmd.Flags().IntVar(&benchmarkMaxTokens, "max-tokens", 1024, "Max tokens per response") - benchmarkCmd.Flags().Float64Var(&benchmarkTemp, "temperature", 0.4, "Sampling temperature") - benchmarkCmd.Flags().IntVar(&benchmarkMemLimit, "memory-limit", 24, "Metal memory limit in GB") - benchmarkCmd.MarkFlagRequired("baseline") - benchmarkCmd.MarkFlagRequired("trained") -} - -// benchmarkResult holds the comparison for a single prompt. -type benchmarkResult struct { - ID string `json:"id"` - Prompt string `json:"prompt"` - BaselineResponse string `json:"baseline_response"` - TrainedResponse string `json:"trained_response"` - BaselineLEK float64 `json:"baseline_lek_score"` - TrainedLEK float64 `json:"trained_lek_score"` - Delta float64 `json:"delta"` - - BaselineHeuristic *ml.HeuristicScores `json:"baseline_heuristic"` - TrainedHeuristic *ml.HeuristicScores `json:"trained_heuristic"` -} - -// benchmarkSummary holds aggregate comparison metrics. -type benchmarkSummary struct { - BaselineModel string `json:"baseline_model"` - TrainedModel string `json:"trained_model"` - TotalPrompts int `json:"total_prompts"` - AvgBaselineLEK float64 `json:"avg_baseline_lek"` - AvgTrainedLEK float64 `json:"avg_trained_lek"` - AvgDelta float64 `json:"avg_delta"` - Improved int `json:"improved"` - Regressed int `json:"regressed"` - Unchanged int `json:"unchanged"` - Duration string `json:"duration"` - Results []benchmarkResult `json:"results"` -} - -func runBenchmark(cmd *cli.Command, args []string) error { - start := time.Now() - - // Load prompts — either custom file or built-in probes - prompts, err := loadBenchmarkPrompts() - if err != nil { - return err - } - - slog.Info("benchmark: loaded prompts", "count", len(prompts)) - - opts := ml.GenOpts{ - Temperature: benchmarkTemp, - MaxTokens: benchmarkMaxTokens, - } - - // Generate baseline responses - slog.Info("benchmark: loading baseline model", "path", benchmarkBaseline) - baselineBackend, err := ml.NewMLXBackend(benchmarkBaseline) - if err != nil { - return fmt.Errorf("load baseline: %w", err) - } - - baselineResponses := make(map[string]string) - for i, p := range prompts { - slog.Info("benchmark: baseline", - "prompt", fmt.Sprintf("%d/%d", i+1, len(prompts)), - "id", p.id, - ) - resp, err := baselineBackend.Generate(context.Background(), p.prompt, opts) - if err != nil { - slog.Error("benchmark: baseline failed", "id", p.id, "error", err) - continue - } - baselineResponses[p.id] = resp - - if (i+1)%4 == 0 { - runtime.GC() - } - } - - // Force cleanup before loading second model - baselineBackend = nil - runtime.GC() - runtime.GC() - - // Generate trained responses - slog.Info("benchmark: loading trained model", "path", benchmarkTrained) - trainedBackend, err := ml.NewMLXBackend(benchmarkTrained) - if err != nil { - return fmt.Errorf("load trained: %w", err) - } - - trainedResponses := make(map[string]string) - for i, p := range prompts { - slog.Info("benchmark: trained", - "prompt", fmt.Sprintf("%d/%d", i+1, len(prompts)), - "id", p.id, - ) - resp, err := trainedBackend.Generate(context.Background(), p.prompt, opts) - if err != nil { - slog.Error("benchmark: trained failed", "id", p.id, "error", err) - continue - } - trainedResponses[p.id] = resp - - if (i+1)%4 == 0 { - runtime.GC() - } - } - - trainedBackend = nil - runtime.GC() - - // Score both sets - var results []benchmarkResult - var totalBaseline, totalTrained float64 - improved, regressed, unchanged := 0, 0, 0 - - for _, p := range prompts { - baseResp := baselineResponses[p.id] - trainResp := trainedResponses[p.id] - - if baseResp == "" || trainResp == "" { - continue - } - - baseH := ml.ScoreHeuristic(baseResp) - trainH := ml.ScoreHeuristic(trainResp) - delta := trainH.LEKScore - baseH.LEKScore - - totalBaseline += baseH.LEKScore - totalTrained += trainH.LEKScore - - if delta > 0.5 { - improved++ - } else if delta < -0.5 { - regressed++ - } else { - unchanged++ - } - - results = append(results, benchmarkResult{ - ID: p.id, - Prompt: p.prompt, - BaselineResponse: baseResp, - TrainedResponse: trainResp, - BaselineLEK: baseH.LEKScore, - TrainedLEK: trainH.LEKScore, - Delta: delta, - BaselineHeuristic: baseH, - TrainedHeuristic: trainH, - }) - } - - n := float64(len(results)) - if n == 0 { - return fmt.Errorf("no results to compare") - } - - summary := benchmarkSummary{ - BaselineModel: benchmarkBaseline, - TrainedModel: benchmarkTrained, - TotalPrompts: len(results), - AvgBaselineLEK: totalBaseline / n, - AvgTrainedLEK: totalTrained / n, - AvgDelta: (totalTrained - totalBaseline) / n, - Improved: improved, - Regressed: regressed, - Unchanged: unchanged, - Duration: time.Since(start).Round(time.Second).String(), - Results: results, - } - - // Write output - data, err := json.MarshalIndent(summary, "", " ") - if err != nil { - return fmt.Errorf("marshal output: %w", err) - } - if err := os.WriteFile(benchmarkOutput, data, 0644); err != nil { - return fmt.Errorf("write output: %w", err) - } - - // Print summary - fmt.Println() - fmt.Println("=== Benchmark Results ===") - fmt.Printf("Baseline: %s\n", benchmarkBaseline) - fmt.Printf("Trained: %s\n", benchmarkTrained) - fmt.Printf("Prompts: %d\n", len(results)) - fmt.Println() - fmt.Printf("Avg LEK (baseline): %+.2f\n", summary.AvgBaselineLEK) - fmt.Printf("Avg LEK (trained): %+.2f\n", summary.AvgTrainedLEK) - fmt.Printf("Avg Delta: %+.2f\n", summary.AvgDelta) - fmt.Println() - fmt.Printf("Improved: %d (%.0f%%)\n", improved, float64(improved)/n*100) - fmt.Printf("Regressed: %d (%.0f%%)\n", regressed, float64(regressed)/n*100) - fmt.Printf("Unchanged: %d (%.0f%%)\n", unchanged, float64(unchanged)/n*100) - fmt.Printf("Duration: %s\n", summary.Duration) - fmt.Printf("Output: %s\n", benchmarkOutput) - - return nil -} - -type benchPrompt struct { - id string - prompt string -} - -func loadBenchmarkPrompts() ([]benchPrompt, error) { - if benchmarkPrompts == "" { - // Use built-in content probes - probes := ml.ContentProbes - prompts := make([]benchPrompt, len(probes)) - for i, p := range probes { - prompts[i] = benchPrompt{id: p.ID, prompt: p.Prompt} - } - return prompts, nil - } - - // Try seeds JSON format first (array of {id, prompt, ...}) - data, err := os.ReadFile(benchmarkPrompts) - if err != nil { - return nil, fmt.Errorf("read prompts: %w", err) - } - - var seeds []seedPrompt - if json.Unmarshal(data, &seeds) == nil && len(seeds) > 0 { - prompts := make([]benchPrompt, len(seeds)) - for i, s := range seeds { - prompts[i] = benchPrompt{id: s.ID, prompt: s.Prompt} - } - return prompts, nil - } - - // Try JSONL responses format - responses, err := ml.ReadResponses(benchmarkPrompts) - if err != nil { - return nil, fmt.Errorf("parse prompts: %w", err) - } - - // Deduplicate by prompt - seen := make(map[string]bool) - var prompts []benchPrompt - for _, r := range responses { - if seen[r.Prompt] { - continue - } - seen[r.Prompt] = true - id := r.ID - if id == "" { - id = fmt.Sprintf("P%03d", len(prompts)+1) - } - prompts = append(prompts, benchPrompt{id: id, prompt: r.Prompt}) - } - - sort.Slice(prompts, func(i, j int) bool { return prompts[i].id < prompts[j].id }) - return prompts, nil -} diff --git a/cmd/ml/cmd_benchmark_init.go b/cmd/ml/cmd_benchmark_init.go deleted file mode 100644 index ff4508ea..00000000 --- a/cmd/ml/cmd_benchmark_init.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build darwin && arm64 - -package ml - -func init() { - mlCmd.AddCommand(benchmarkCmd) -} diff --git a/cmd/ml/cmd_chat.go b/cmd/ml/cmd_chat.go deleted file mode 100644 index ac470361..00000000 --- a/cmd/ml/cmd_chat.go +++ /dev/null @@ -1,327 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "bufio" - "encoding/json" - "fmt" - "log/slog" - "os" - "runtime" - "strings" - "time" - - "forge.lthn.ai/core/go-ml" - "forge.lthn.ai/core/go/pkg/cli" -) - -var chatCmd = &cli.Command{ - Use: "chat", - Short: "Interactive conversation with a local MLX model", - Long: `Start an interactive chat session with a local MLX model. - -All exchanges are captured and can be written to training JSONL on exit -for use with 'core ml train'. Optionally apply axiom sandwich signing -to wrap the conversation for LEK training. - -Commands during chat: - /quit, /exit End session and save - /save Save conversation so far (appends to output) - /clear Clear conversation history - /system Set system prompt - /undo Remove last exchange`, - RunE: runChat, -} - -var ( - chatModelPath string - chatOutput string - chatKB string - chatKernel string - chatSystem string - chatMaxTokens int - chatTemp float64 - chatMemLimit int -) - -func init() { - chatCmd.Flags().StringVar(&chatModelPath, "model-path", "", "Path to model directory (required)") - chatCmd.Flags().StringVar(&chatOutput, "output", "", "Output JSONL file for captured conversation") - chatCmd.Flags().StringVar(&chatKB, "kb", "", "Knowledge base document for sandwich signing") - chatCmd.Flags().StringVar(&chatKernel, "kernel", "", "LEK-1 kernel file for sandwich signing") - chatCmd.Flags().StringVar(&chatSystem, "system", "", "Initial system prompt") - chatCmd.Flags().IntVar(&chatMaxTokens, "max-tokens", 2048, "Max tokens per response") - chatCmd.Flags().Float64Var(&chatTemp, "temperature", 0.4, "Sampling temperature") - chatCmd.Flags().IntVar(&chatMemLimit, "memory-limit", 24, "Metal memory limit in GB") - chatCmd.MarkFlagRequired("model-path") -} - -func runChat(cmd *cli.Command, args []string) error { - // Load optional KB and kernel for sandwich signing - var kbText, kernelText string - if chatKB != "" { - data, err := os.ReadFile(chatKB) - if err != nil { - return fmt.Errorf("read KB: %w", err) - } - kbText = string(data) - } - if chatKernel != "" { - data, err := os.ReadFile(chatKernel) - if err != nil { - return fmt.Errorf("read kernel: %w", err) - } - kernelText = string(data) - } - sandwich := kbText != "" && kernelText != "" - - // Load model - slog.Info("chat: loading model", "path", chatModelPath) - backend, err := ml.NewMLXBackend(chatModelPath) - if err != nil { - return fmt.Errorf("load model: %w", err) - } - - opts := ml.GenOpts{ - Temperature: chatTemp, - MaxTokens: chatMaxTokens, - } - - // Conversation state - var history []ml.Message - if chatSystem != "" { - history = append(history, ml.Message{Role: "system", Content: chatSystem}) - } - - // Track saved conversations for JSONL output - var savedConversations [][]ml.Message - - fmt.Println("Chat started. Type /quit to exit, /help for commands.") - if sandwich { - fmt.Println("Sandwich signing enabled (KB + kernel)") - } - if chatOutput != "" { - fmt.Printf("Capturing to: %s\n", chatOutput) - } - fmt.Println() - - scanner := bufio.NewScanner(os.Stdin) - scanner.Buffer(make([]byte, 1<<20), 1<<20) // 1MB input buffer - - for { - fmt.Print("you> ") - if !scanner.Scan() { - // EOF (Ctrl+D) - break - } - - input := strings.TrimSpace(scanner.Text()) - if input == "" { - continue - } - - // Handle commands - if strings.HasPrefix(input, "/") { - cmd := strings.Fields(input) - switch cmd[0] { - case "/quit", "/exit": - goto done - case "/save": - if chatOutput == "" { - fmt.Println("No --output file specified. Use --output to enable saving.") - continue - } - if len(history) > 0 { - savedConversations = append(savedConversations, cloneMessages(history)) - fmt.Printf("Saved conversation (%d messages)\n", len(history)) - } - continue - case "/clear": - sysPrompt := "" - for _, m := range history { - if m.Role == "system" { - sysPrompt = m.Content - break - } - } - history = nil - if sysPrompt != "" { - history = append(history, ml.Message{Role: "system", Content: sysPrompt}) - } - fmt.Println("Conversation cleared.") - continue - case "/system": - if len(cmd) < 2 { - fmt.Println("Usage: /system ") - continue - } - sysText := strings.TrimPrefix(input, "/system ") - // Replace existing system prompt or add new one - found := false - for i, m := range history { - if m.Role == "system" { - history[i].Content = sysText - found = true - break - } - } - if !found { - // Prepend system message - history = append([]ml.Message{{Role: "system", Content: sysText}}, history...) - } - fmt.Printf("System prompt set (%d chars)\n", len(sysText)) - continue - case "/undo": - // Remove last user+assistant pair - if len(history) >= 2 { - last := history[len(history)-1] - secondLast := history[len(history)-2] - if secondLast.Role == "user" && last.Role == "assistant" { - history = history[:len(history)-2] - fmt.Println("Last exchange removed.") - } else { - fmt.Println("Cannot undo: last messages are not a user/assistant pair.") - } - } else { - fmt.Println("Nothing to undo.") - } - continue - case "/help": - fmt.Println("Commands:") - fmt.Println(" /quit, /exit End session and save") - fmt.Println(" /save Save conversation so far") - fmt.Println(" /clear Clear conversation history") - fmt.Println(" /system Set system prompt") - fmt.Println(" /undo Remove last exchange") - fmt.Println(" /help Show this help") - continue - default: - fmt.Printf("Unknown command: %s (try /help)\n", cmd[0]) - continue - } - } - - // Add user message - history = append(history, ml.Message{Role: "user", Content: input}) - - // Generate response - genStart := time.Now() - fmt.Print("\nassistant> ") - - var response strings.Builder - err := backend.ChatStream(cmd.Context(), history, opts, func(token string) error { - fmt.Print(token) - response.WriteString(token) - return nil - }) - fmt.Println() - - if err != nil { - slog.Error("chat: generation failed", "error", err) - // Remove the failed user message - history = history[:len(history)-1] - continue - } - - elapsed := time.Since(genStart) - responseText := response.String() - history = append(history, ml.Message{Role: "assistant", Content: responseText}) - - slog.Debug("chat: response generated", - "chars", len(responseText), - "duration", elapsed.Round(time.Millisecond), - ) - - // Periodic cleanup - if len(history)%8 == 0 { - runtime.GC() - } - - fmt.Println() - } - -done: - fmt.Println() - - // Save final conversation if output is specified - if chatOutput != "" && len(history) > 0 { - // Include current conversation if not already saved - savedConversations = append(savedConversations, history) - - if err := writeChatJSONL(chatOutput, savedConversations, sandwich, kbText, kernelText); err != nil { - return fmt.Errorf("save conversation: %w", err) - } - } - - return nil -} - -// writeChatJSONL writes conversations to JSONL file. -// If sandwich is true, wraps user messages with KB + kernel signing. -func writeChatJSONL(path string, conversations [][]ml.Message, sandwich bool, kb, kernel string) error { - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return err - } - defer f.Close() - - encoder := json.NewEncoder(f) - written := 0 - - for _, conv := range conversations { - // Extract user/assistant pairs (skip system messages for training output) - var messages []ml.Message - for _, m := range conv { - if m.Role == "system" { - continue - } - messages = append(messages, m) - } - - if len(messages) < 2 { - continue - } - - if sandwich { - // Apply sandwich signing to user messages - messages = applySandwichSigning(messages, kb, kernel) - } - - record := struct { - Messages []ml.Message `json:"messages"` - }{Messages: messages} - - if err := encoder.Encode(record); err != nil { - return err - } - written++ - } - - slog.Info("chat: saved conversations", - "file", path, - "conversations", written, - "sandwich", sandwich, - ) - return nil -} - -// applySandwichSigning wraps user messages with KB preamble and kernel postfix. -func applySandwichSigning(messages []ml.Message, kb, kernel string) []ml.Message { - signed := make([]ml.Message, len(messages)) - copy(signed, messages) - - for i := range signed { - if signed[i].Role == "user" { - signed[i].Content = buildSandwich(kb, signed[i].Content, kernel) - } - } - return signed -} - -// cloneMessages creates a deep copy of a message slice. -func cloneMessages(msgs []ml.Message) []ml.Message { - clone := make([]ml.Message, len(msgs)) - copy(clone, msgs) - return clone -} diff --git a/cmd/ml/cmd_chat_init.go b/cmd/ml/cmd_chat_init.go deleted file mode 100644 index 4d281d01..00000000 --- a/cmd/ml/cmd_chat_init.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build darwin && arm64 - -package ml - -func init() { - mlCmd.AddCommand(chatCmd) -} diff --git a/cmd/ml/cmd_consolidate.go b/cmd/ml/cmd_consolidate.go deleted file mode 100644 index 0e1cf20b..00000000 --- a/cmd/ml/cmd_consolidate.go +++ /dev/null @@ -1,41 +0,0 @@ -package ml - -import ( - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - consolidateM3Host string - consolidateRemoteDir string - consolidatePattern string - consolidateOutputDir string - consolidateMergedOut string -) - -var consolidateCmd = &cli.Command{ - Use: "consolidate", - Short: "Pull and merge response JSONL files from M3", - Long: "Pulls JSONL response files from M3 via SSH/SCP, merges them by idx, deduplicates, and writes a single merged JSONL output.", - RunE: runConsolidate, -} - -func init() { - consolidateCmd.Flags().StringVar(&consolidateM3Host, "m3-host", "m3", "M3 SSH host") - consolidateCmd.Flags().StringVar(&consolidateRemoteDir, "remote", "/Volumes/Data/lem/responses", "Remote response directory") - consolidateCmd.Flags().StringVar(&consolidatePattern, "pattern", "gold*.jsonl", "File glob pattern") - consolidateCmd.Flags().StringVar(&consolidateOutputDir, "output", "", "Local output directory (default: responses)") - consolidateCmd.Flags().StringVar(&consolidateMergedOut, "merged", "", "Merged output path (default: gold-merged.jsonl in parent of output dir)") -} - -func runConsolidate(cmd *cli.Command, args []string) error { - cfg := ml.ConsolidateConfig{ - M3Host: consolidateM3Host, - RemoteDir: consolidateRemoteDir, - Pattern: consolidatePattern, - OutputDir: consolidateOutputDir, - MergedOut: consolidateMergedOut, - } - - return ml.Consolidate(cfg, cmd.OutOrStdout()) -} diff --git a/cmd/ml/cmd_convert.go b/cmd/ml/cmd_convert.go deleted file mode 100644 index 84c9b27e..00000000 --- a/cmd/ml/cmd_convert.go +++ /dev/null @@ -1,40 +0,0 @@ -package ml - -import ( - "fmt" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - convertInput string - convertConfig string - convertOutputDir string - convertBaseModel string -) - -var convertCmd = &cli.Command{ - Use: "convert", - Short: "Convert MLX LoRA adapter to PEFT format", - Long: "Converts an MLX safetensors LoRA adapter to HuggingFace PEFT format for Ollama.", - RunE: runConvert, -} - -func init() { - convertCmd.Flags().StringVar(&convertInput, "input", "", "Input safetensors file (required)") - convertCmd.Flags().StringVar(&convertConfig, "config", "", "Adapter config JSON (required)") - convertCmd.Flags().StringVar(&convertOutputDir, "output-dir", "", "Output directory (required)") - convertCmd.Flags().StringVar(&convertBaseModel, "base-model", "", "Base model name for adapter_config.json") - convertCmd.MarkFlagRequired("input") - convertCmd.MarkFlagRequired("config") - convertCmd.MarkFlagRequired("output-dir") -} - -func runConvert(cmd *cli.Command, args []string) error { - if err := ml.ConvertMLXtoPEFT(convertInput, convertConfig, convertOutputDir, convertBaseModel); err != nil { - return fmt.Errorf("convert to PEFT: %w", err) - } - fmt.Printf("PEFT adapter written to %s\n", convertOutputDir) - return nil -} diff --git a/cmd/ml/cmd_coverage.go b/cmd/ml/cmd_coverage.go deleted file mode 100644 index 5d3acf7f..00000000 --- a/cmd/ml/cmd_coverage.go +++ /dev/null @@ -1,34 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var coverageCmd = &cli.Command{ - Use: "coverage", - Short: "Analyze seed coverage by region and domain", - Long: "Queries seeds by region and domain, renders ASCII bar charts, and highlights underrepresented areas.", - RunE: runCoverage, -} - -func runCoverage(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - return ml.PrintCoverage(db, cmd.OutOrStdout()) -} diff --git a/cmd/ml/cmd_expand.go b/cmd/ml/cmd_expand.go deleted file mode 100644 index 432aa3fa..00000000 --- a/cmd/ml/cmd_expand.go +++ /dev/null @@ -1,81 +0,0 @@ -package ml - -import ( - "context" - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - expandWorker string - expandOutput string - expandLimit int - expandDryRun bool -) - -var expandCmd = &cli.Command{ - Use: "expand", - Short: "Generate expansion responses from pending prompts", - Long: "Reads pending expansion prompts from DuckDB and generates responses via an OpenAI-compatible API.", - RunE: runExpand, -} - -func init() { - expandCmd.Flags().StringVar(&expandWorker, "worker", "", "Worker hostname (defaults to os.Hostname())") - expandCmd.Flags().StringVar(&expandOutput, "output", ".", "Output directory for JSONL files") - expandCmd.Flags().IntVar(&expandLimit, "limit", 0, "Max prompts to process (0 = all)") - expandCmd.Flags().BoolVar(&expandDryRun, "dry-run", false, "Print plan and exit without generating") -} - -func runExpand(cmd *cli.Command, args []string) error { - if modelName == "" { - return fmt.Errorf("--model is required") - } - - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB env is required") - } - - if expandWorker == "" { - h, _ := os.Hostname() - expandWorker = h - } - - db, err := ml.OpenDBReadWrite(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - rows, err := db.QueryExpansionPrompts("pending", expandLimit) - if err != nil { - return fmt.Errorf("query expansion_prompts: %w", err) - } - fmt.Printf("Loaded %d pending prompts from %s\n", len(rows), path) - - var prompts []ml.Response - for _, r := range rows { - prompt := r.Prompt - if prompt == "" && r.PromptEn != "" { - prompt = r.PromptEn - } - prompts = append(prompts, ml.Response{ - ID: r.SeedID, - Domain: r.Domain, - Prompt: prompt, - }) - } - - ctx := context.Background() - backend := ml.NewHTTPBackend(apiURL, modelName) - influx := ml.NewInfluxClient(influxURL, influxDB) - - return ml.ExpandPrompts(ctx, backend, influx, prompts, modelName, expandWorker, expandOutput, expandDryRun, expandLimit) -} diff --git a/cmd/ml/cmd_expand_status.go b/cmd/ml/cmd_expand_status.go deleted file mode 100644 index 70df2bf2..00000000 --- a/cmd/ml/cmd_expand_status.go +++ /dev/null @@ -1,95 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var expandStatusCmd = &cli.Command{ - Use: "expand-status", - Short: "Show expansion pipeline progress", - Long: "Queries DuckDB for expansion prompts, generated responses, scoring status, and overall pipeline progress.", - RunE: runExpandStatus, -} - -func runExpandStatus(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - fmt.Fprintln(os.Stdout, "LEM Expansion Pipeline Status") - fmt.Fprintln(os.Stdout, "==================================================") - - // Expansion prompts - total, pending, err := db.CountExpansionPrompts() - if err != nil { - fmt.Fprintln(os.Stdout, " Expansion prompts: not created (run: normalize)") - return nil - } - fmt.Fprintf(os.Stdout, " Expansion prompts: %d total, %d pending\n", total, pending) - - // Generated responses — query raw counts via SQL - generated := 0 - rows, err := db.QueryRows("SELECT count(*) AS n FROM expansion_raw") - if err != nil || len(rows) == 0 { - fmt.Fprintln(os.Stdout, " Generated: 0 (run: core ml expand)") - } else { - if n, ok := rows[0]["n"]; ok { - generated = toInt(n) - } - fmt.Fprintf(os.Stdout, " Generated: %d\n", generated) - } - - // Scored — query scoring counts via SQL - sRows, err := db.QueryRows("SELECT count(*) AS n FROM scoring_results WHERE suite = 'heuristic'") - if err != nil || len(sRows) == 0 { - fmt.Fprintln(os.Stdout, " Scored: 0 (run: score --tier 1)") - } else { - scored := toInt(sRows[0]["n"]) - fmt.Fprintf(os.Stdout, " Heuristic scored: %d\n", scored) - } - - // Pipeline progress - if total > 0 && generated > 0 { - genPct := float64(generated) / float64(total) * 100 - fmt.Fprintf(os.Stdout, "\n Progress: %.1f%% generated\n", genPct) - } - - // Golden set context - golden, err := db.CountGoldenSet() - if err == nil && golden > 0 { - fmt.Fprintf(os.Stdout, "\n Golden set: %d / %d\n", golden, targetTotal) - if generated > 0 { - fmt.Fprintf(os.Stdout, " Combined: %d total examples\n", golden+generated) - } - } - - return nil -} - -// toInt converts an interface{} (typically from QueryRows) to int. -func toInt(v interface{}) int { - switch n := v.(type) { - case int: - return n - case int64: - return int(n) - case float64: - return int(n) - default: - return 0 - } -} diff --git a/cmd/ml/cmd_export.go b/cmd/ml/cmd_export.go deleted file mode 100644 index 2ce2b60a..00000000 --- a/cmd/ml/cmd_export.go +++ /dev/null @@ -1,109 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - exportOutputDir string - exportMinChars int - exportTrainPct int - exportValidPct int - exportTestPct int - exportSeed int64 - exportParquet bool -) - -var exportCmd = &cli.Command{ - Use: "export", - Short: "Export golden set to training JSONL and Parquet", - Long: "Reads golden set from DuckDB, filters, splits, and exports to JSONL and optionally Parquet.", - RunE: runExport, -} - -func init() { - exportCmd.Flags().StringVar(&exportOutputDir, "output-dir", "", "Output directory for training files (required)") - exportCmd.Flags().IntVar(&exportMinChars, "min-chars", 50, "Minimum response length in characters") - exportCmd.Flags().IntVar(&exportTrainPct, "train", 80, "Training split percentage") - exportCmd.Flags().IntVar(&exportValidPct, "valid", 10, "Validation split percentage") - exportCmd.Flags().IntVar(&exportTestPct, "test", 10, "Test split percentage") - exportCmd.Flags().Int64Var(&exportSeed, "seed", 42, "Random seed for shuffle") - exportCmd.Flags().BoolVar(&exportParquet, "parquet", false, "Also export Parquet files") - exportCmd.MarkFlagRequired("output-dir") -} - -func runExport(cmd *cli.Command, args []string) error { - if err := ml.ValidatePercentages(exportTrainPct, exportValidPct, exportTestPct); err != nil { - return err - } - - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB env is required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - rows, err := db.QueryGoldenSet(exportMinChars) - if err != nil { - return fmt.Errorf("query golden set: %w", err) - } - fmt.Printf("Loaded %d golden set rows (min %d chars)\n", len(rows), exportMinChars) - - // Convert to Response format. - var responses []ml.Response - for _, r := range rows { - responses = append(responses, ml.Response{ - ID: r.SeedID, - Domain: r.Domain, - Prompt: r.Prompt, - Response: r.Response, - }) - } - - filtered := ml.FilterResponses(responses) - fmt.Printf("After filtering: %d responses\n", len(filtered)) - - train, valid, test := ml.SplitData(filtered, exportTrainPct, exportValidPct, exportTestPct, exportSeed) - fmt.Printf("Split: train=%d, valid=%d, test=%d\n", len(train), len(valid), len(test)) - - if err := os.MkdirAll(exportOutputDir, 0755); err != nil { - return fmt.Errorf("create output dir: %w", err) - } - - for _, split := range []struct { - name string - data []ml.Response - }{ - {"train", train}, - {"valid", valid}, - {"test", test}, - } { - path := fmt.Sprintf("%s/%s.jsonl", exportOutputDir, split.name) - if err := ml.WriteTrainingJSONL(path, split.data); err != nil { - return fmt.Errorf("write %s: %w", split.name, err) - } - fmt.Printf(" %s.jsonl: %d examples\n", split.name, len(split.data)) - } - - if exportParquet { - n, err := ml.ExportParquet(exportOutputDir, "") - if err != nil { - return fmt.Errorf("export parquet: %w", err) - } - fmt.Printf(" Parquet: %d total rows\n", n) - } - - return nil -} diff --git a/cmd/ml/cmd_gguf.go b/cmd/ml/cmd_gguf.go deleted file mode 100644 index 8b1f53b4..00000000 --- a/cmd/ml/cmd_gguf.go +++ /dev/null @@ -1,40 +0,0 @@ -package ml - -import ( - "fmt" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - ggufInput string - ggufConfig string - ggufOutput string - ggufArch string -) - -var ggufCmd = &cli.Command{ - Use: "gguf", - Short: "Convert MLX LoRA adapter to GGUF format", - Long: "Converts an MLX safetensors LoRA adapter to GGUF v3 format for use with llama.cpp.", - RunE: runGGUF, -} - -func init() { - ggufCmd.Flags().StringVar(&ggufInput, "input", "", "Input safetensors file (required)") - ggufCmd.Flags().StringVar(&ggufConfig, "config", "", "Adapter config JSON (required)") - ggufCmd.Flags().StringVar(&ggufOutput, "output", "", "Output GGUF file (required)") - ggufCmd.Flags().StringVar(&ggufArch, "arch", "gemma3", "GGUF architecture name") - ggufCmd.MarkFlagRequired("input") - ggufCmd.MarkFlagRequired("config") - ggufCmd.MarkFlagRequired("output") -} - -func runGGUF(cmd *cli.Command, args []string) error { - if err := ml.ConvertMLXtoGGUFLoRA(ggufInput, ggufConfig, ggufOutput, ggufArch); err != nil { - return fmt.Errorf("convert to GGUF: %w", err) - } - fmt.Printf("GGUF LoRA adapter written to %s\n", ggufOutput) - return nil -} diff --git a/cmd/ml/cmd_import.go b/cmd/ml/cmd_import.go deleted file mode 100644 index 03fb7dd0..00000000 --- a/cmd/ml/cmd_import.go +++ /dev/null @@ -1,58 +0,0 @@ -package ml - -import ( - "fmt" - "os" - "path/filepath" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var importCmd = &cli.Command{ - Use: "import-all", - Short: "Import all LEM data into DuckDB", - Long: "Imports golden set, training examples, benchmark results, benchmark questions, and seeds into DuckDB from M3 and local files.", - RunE: runImportAll, -} - -var ( - importSkipM3 bool - importDataDir string - importM3Host string -) - -func init() { - importCmd.Flags().BoolVar(&importSkipM3, "skip-m3", false, "Skip pulling data from M3") - importCmd.Flags().StringVar(&importDataDir, "data-dir", "", "Local data directory (defaults to db directory)") - importCmd.Flags().StringVar(&importM3Host, "m3-host", "m3", "M3 SSH host alias") -} - -func runImportAll(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - dataDir := importDataDir - if dataDir == "" { - dataDir = filepath.Dir(path) - } - - db, err := ml.OpenDBReadWrite(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - cfg := ml.ImportConfig{ - SkipM3: importSkipM3, - DataDir: dataDir, - M3Host: importM3Host, - } - - return ml.ImportAll(db, cfg, cmd.OutOrStdout()) -} diff --git a/cmd/ml/cmd_ingest.go b/cmd/ml/cmd_ingest.go deleted file mode 100644 index f071a2a6..00000000 --- a/cmd/ml/cmd_ingest.go +++ /dev/null @@ -1,54 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ingestCmd = &cli.Command{ - Use: "ingest", - Short: "Ingest benchmark scores and training logs into InfluxDB", - Long: "Reads content score, capability score, and training log files and writes measurements to InfluxDB for the lab dashboard.", - RunE: runIngest, -} - -var ( - ingestContent string - ingestCapability string - ingestTraining string - ingestRunID string - ingestBatchSize int -) - -func init() { - ingestCmd.Flags().StringVar(&ingestContent, "content", "", "Content scores JSONL file") - ingestCmd.Flags().StringVar(&ingestCapability, "capability", "", "Capability scores JSONL file") - ingestCmd.Flags().StringVar(&ingestTraining, "training-log", "", "MLX LoRA training log file") - ingestCmd.Flags().StringVar(&ingestRunID, "run-id", "", "Run ID tag (defaults to model name)") - ingestCmd.Flags().IntVar(&ingestBatchSize, "batch-size", 100, "Lines per InfluxDB write batch") -} - -func runIngest(cmd *cli.Command, args []string) error { - if modelName == "" { - return fmt.Errorf("--model is required") - } - if ingestContent == "" && ingestCapability == "" && ingestTraining == "" { - return fmt.Errorf("at least one of --content, --capability, or --training-log is required") - } - - influx := ml.NewInfluxClient(influxURL, influxDB) - - cfg := ml.IngestConfig{ - ContentFile: ingestContent, - CapabilityFile: ingestCapability, - TrainingLog: ingestTraining, - Model: modelName, - RunID: ingestRunID, - BatchSize: ingestBatchSize, - } - - return ml.Ingest(influx, cfg, os.Stdout) -} diff --git a/cmd/ml/cmd_inventory.go b/cmd/ml/cmd_inventory.go deleted file mode 100644 index eed4ff6a..00000000 --- a/cmd/ml/cmd_inventory.go +++ /dev/null @@ -1,34 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var inventoryCmd = &cli.Command{ - Use: "inventory", - Short: "Show DuckDB table inventory with stats", - Long: "Queries all DuckDB tables and prints row counts with per-table detail breakdowns.", - RunE: runInventory, -} - -func runInventory(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - return ml.PrintInventory(db, os.Stdout) -} diff --git a/cmd/ml/cmd_lesson.go b/cmd/ml/cmd_lesson.go deleted file mode 100644 index 4246c076..00000000 --- a/cmd/ml/cmd_lesson.go +++ /dev/null @@ -1,340 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "forge.lthn.ai/core/go-ml" - "forge.lthn.ai/core/go/pkg/cli" - "gopkg.in/yaml.v3" -) - -var lessonCmd = &cli.Command{ - Use: "lesson", - Short: "Run a structured training lesson from a YAML definition", - Long: `Runs a training lesson defined in a YAML file. Each lesson contains -prompts organised by category, optional system prompt, and sandwich -signing configuration. - -Lesson YAML format: - id: lek-sovereignty - title: "Sovereignty Lessons" - system: "You are a helpful assistant." - sandwich: - kb: path/to/axioms.md - kernel: path/to/kernel.txt - prompts: - - id: P01 - category: sovereignty - prompt: "A user wants to build an auth system." - signal: "Does the model prefer decentralised?" - -The command generates responses for each prompt and writes them as -training JSONL. State is tracked so lessons can be resumed.`, - RunE: runLesson, -} - -var ( - lessonFile string - lessonModelPath string - lessonOutput string - lessonMaxTokens int - lessonTemp float64 - lessonMemLimit int - lessonResume bool - lessonInteract bool -) - -func init() { - lessonCmd.Flags().StringVar(&lessonFile, "file", "", "Lesson YAML file (required)") - lessonCmd.Flags().StringVar(&lessonModelPath, "model-path", "", "Path to model directory (required)") - lessonCmd.Flags().StringVar(&lessonOutput, "output", "", "Output JSONL file (default: .jsonl)") - lessonCmd.Flags().IntVar(&lessonMaxTokens, "max-tokens", 1024, "Max tokens per response") - lessonCmd.Flags().Float64Var(&lessonTemp, "temperature", 0.4, "Sampling temperature") - lessonCmd.Flags().IntVar(&lessonMemLimit, "memory-limit", 24, "Metal memory limit in GB") - lessonCmd.Flags().BoolVar(&lessonResume, "resume", true, "Resume from last completed prompt") - lessonCmd.Flags().BoolVar(&lessonInteract, "interactive", false, "Interactive mode: review each response before continuing") - lessonCmd.MarkFlagRequired("file") - lessonCmd.MarkFlagRequired("model-path") -} - -// lessonDef is a YAML lesson definition. -type lessonDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - System string `yaml:"system"` - Sandwich *lessonSandwichCfg `yaml:"sandwich"` - Prompts []lessonPrompt `yaml:"prompts"` -} - -type lessonSandwichCfg struct { - KB string `yaml:"kb"` - Kernel string `yaml:"kernel"` -} - -type lessonPrompt struct { - ID string `yaml:"id"` - Category string `yaml:"category"` - Prompt string `yaml:"prompt"` - Signal string `yaml:"signal"` -} - -// lessonState tracks progress through a lesson. -type lessonState struct { - LessonID string `json:"lesson_id"` - Completed map[string]lessonResult `json:"completed"` - UpdatedAt string `json:"updated_at"` -} - -type lessonResult struct { - ResponseChars int `json:"response_chars"` - Duration string `json:"duration"` - CompletedAt string `json:"completed_at"` -} - -func runLesson(cmd *cli.Command, args []string) error { - start := time.Now() - - // Load lesson YAML - data, err := os.ReadFile(lessonFile) - if err != nil { - return fmt.Errorf("read lesson: %w", err) - } - - var lesson lessonDef - if err := yaml.Unmarshal(data, &lesson); err != nil { - return fmt.Errorf("parse lesson: %w", err) - } - - if lesson.ID == "" { - lesson.ID = strings.TrimSuffix(filepath.Base(lessonFile), filepath.Ext(lessonFile)) - } - - // Resolve output path - if lessonOutput == "" { - lessonOutput = lesson.ID + ".jsonl" - } - - // Load sandwich files if configured - var kbText, kernelText string - sandwich := false - if lesson.Sandwich != nil { - baseDir := filepath.Dir(lessonFile) - if lesson.Sandwich.KB != "" { - kbPath := lesson.Sandwich.KB - if !filepath.IsAbs(kbPath) { - kbPath = filepath.Join(baseDir, kbPath) - } - d, err := os.ReadFile(kbPath) - if err != nil { - return fmt.Errorf("read KB: %w", err) - } - kbText = string(d) - } - if lesson.Sandwich.Kernel != "" { - kernelPath := lesson.Sandwich.Kernel - if !filepath.IsAbs(kernelPath) { - kernelPath = filepath.Join(baseDir, kernelPath) - } - d, err := os.ReadFile(kernelPath) - if err != nil { - return fmt.Errorf("read kernel: %w", err) - } - kernelText = string(d) - } - sandwich = kbText != "" && kernelText != "" - } - - slog.Info("lesson: loaded", - "id", lesson.ID, - "title", lesson.Title, - "prompts", len(lesson.Prompts), - "sandwich", sandwich, - ) - - if len(lesson.Prompts) == 0 { - return fmt.Errorf("lesson has no prompts") - } - - // Load state for resume - stateFile := lesson.ID + ".state.json" - state := loadLessonState(stateFile) - if state.LessonID == "" { - state.LessonID = lesson.ID - state.Completed = make(map[string]lessonResult) - } - - // Count remaining - var remaining []lessonPrompt - for _, p := range lesson.Prompts { - if lessonResume { - if _, done := state.Completed[p.ID]; done { - continue - } - } - remaining = append(remaining, p) - } - - if len(remaining) == 0 { - slog.Info("lesson: all prompts completed", - "id", lesson.ID, - "total", len(lesson.Prompts), - ) - return nil - } - - slog.Info("lesson: starting", - "remaining", len(remaining), - "completed", len(state.Completed), - "total", len(lesson.Prompts), - ) - - // Load model - slog.Info("lesson: loading model", "path", lessonModelPath) - backend, err := ml.NewMLXBackend(lessonModelPath) - if err != nil { - return fmt.Errorf("load model: %w", err) - } - - opts := ml.GenOpts{ - Temperature: lessonTemp, - MaxTokens: lessonMaxTokens, - } - - // Open output file (append mode for resume) - outFile, err := os.OpenFile(lessonOutput, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("create output: %w", err) - } - defer outFile.Close() - encoder := json.NewEncoder(outFile) - - generated := 0 - - for i, prompt := range remaining { - promptStart := time.Now() - - slog.Info("lesson: generating", - "prompt", fmt.Sprintf("%d/%d", i+1, len(remaining)), - "id", prompt.ID, - "category", prompt.Category, - ) - - // Build messages - var messages []ml.Message - if lesson.System != "" { - messages = append(messages, ml.Message{Role: "system", Content: lesson.System}) - } - - userContent := prompt.Prompt - if sandwich { - userContent = buildSandwich(kbText, prompt.Prompt, kernelText) - } - messages = append(messages, ml.Message{Role: "user", Content: userContent}) - - // Generate - response, err := backend.Chat(context.Background(), messages, opts) - if err != nil { - slog.Error("lesson: generation failed", - "id", prompt.ID, - "error", err, - ) - continue - } - - elapsed := time.Since(promptStart) - - // Write training record - record := struct { - Messages []ml.Message `json:"messages"` - }{ - Messages: []ml.Message{ - {Role: "user", Content: userContent}, - {Role: "assistant", Content: response}, - }, - } - if err := encoder.Encode(record); err != nil { - return fmt.Errorf("write record: %w", err) - } - - // Update state - state.Completed[prompt.ID] = lessonResult{ - ResponseChars: len(response), - Duration: elapsed.Round(time.Second).String(), - CompletedAt: time.Now().Format(time.RFC3339), - } - state.UpdatedAt = time.Now().Format(time.RFC3339) - - if err := saveLessonState(stateFile, state); err != nil { - slog.Warn("lesson: failed to save state", "error", err) - } - - generated++ - - slog.Info("lesson: generated", - "id", prompt.ID, - "category", prompt.Category, - "response_chars", len(response), - "duration", elapsed.Round(time.Second), - ) - - // Interactive mode: show response and wait for confirmation - if lessonInteract { - fmt.Printf("\n--- %s (%s) ---\n", prompt.ID, prompt.Category) - fmt.Printf("Prompt: %s\n\n", prompt.Prompt) - if prompt.Signal != "" { - fmt.Printf("Signal: %s\n\n", prompt.Signal) - } - fmt.Printf("Response:\n%s\n", response) - fmt.Printf("\nPress Enter to continue (or 'q' to stop)... ") - var input string - fmt.Scanln(&input) - if strings.TrimSpace(input) == "q" { - break - } - } - - // Periodic cleanup - if (i+1)%4 == 0 { - runtime.GC() - } - } - - slog.Info("lesson: complete", - "id", lesson.ID, - "output", lessonOutput, - "generated", generated, - "total_completed", len(state.Completed), - "total_prompts", len(lesson.Prompts), - "duration", time.Since(start).Round(time.Second), - ) - - return nil -} - -func loadLessonState(path string) lessonState { - data, err := os.ReadFile(path) - if err != nil { - return lessonState{} - } - var state lessonState - json.Unmarshal(data, &state) - return state -} - -func saveLessonState(path string, state lessonState) error { - data, err := json.MarshalIndent(state, "", " ") - if err != nil { - return err - } - return os.WriteFile(path, data, 0644) -} diff --git a/cmd/ml/cmd_lesson_init.go b/cmd/ml/cmd_lesson_init.go deleted file mode 100644 index d2342dde..00000000 --- a/cmd/ml/cmd_lesson_init.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build darwin && arm64 - -package ml - -func init() { - mlCmd.AddCommand(lessonCmd) - mlCmd.AddCommand(sequenceCmd) -} diff --git a/cmd/ml/cmd_live.go b/cmd/ml/cmd_live.go deleted file mode 100644 index 5abef9ce..00000000 --- a/cmd/ml/cmd_live.go +++ /dev/null @@ -1,82 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -const targetTotal = 15000 - -var liveCmd = &cli.Command{ - Use: "live", - Short: "Show live generation progress from InfluxDB", - Long: "Queries InfluxDB for real-time generation progress, worker breakdown, and domain/voice counts.", - RunE: runLive, -} - -func runLive(cmd *cli.Command, args []string) error { - influx := ml.NewInfluxClient(influxURL, influxDB) - - // Total completed generations - totalRows, err := influx.QuerySQL("SELECT count(DISTINCT i) AS n FROM gold_gen") - if err != nil { - return fmt.Errorf("live: query total: %w", err) - } - total := sqlScalar(totalRows) - - // Distinct domains and voices - domainRows, err := influx.QuerySQL("SELECT count(DISTINCT d) AS n FROM gold_gen") - if err != nil { - return fmt.Errorf("live: query domains: %w", err) - } - domains := sqlScalar(domainRows) - - voiceRows, err := influx.QuerySQL("SELECT count(DISTINCT v) AS n FROM gold_gen") - if err != nil { - return fmt.Errorf("live: query voices: %w", err) - } - voices := sqlScalar(voiceRows) - - // Per-worker breakdown - workers, err := influx.QuerySQL("SELECT w, count(DISTINCT i) AS n FROM gold_gen GROUP BY w ORDER BY n DESC") - if err != nil { - return fmt.Errorf("live: query workers: %w", err) - } - - pct := float64(total) / float64(targetTotal) * 100 - remaining := targetTotal - total - - fmt.Fprintln(os.Stdout, "Golden Set Live Status (from InfluxDB)") - fmt.Fprintln(os.Stdout, "─────────────────────────────────────────────") - fmt.Fprintf(os.Stdout, " Total: %d / %d (%.1f%%)\n", total, targetTotal, pct) - fmt.Fprintf(os.Stdout, " Remaining: %d\n", remaining) - fmt.Fprintf(os.Stdout, " Domains: %d\n", domains) - fmt.Fprintf(os.Stdout, " Voices: %d\n", voices) - fmt.Fprintln(os.Stdout) - fmt.Fprintln(os.Stdout, " Workers:") - for _, w := range workers { - name := w["w"] - n := w["n"] - marker := "" - if name == "migration" { - marker = " (seed data)" - } - fmt.Fprintf(os.Stdout, " %-20s %6s generations%s\n", name, n, marker) - } - - return nil -} - -// sqlScalar extracts the first numeric value from a QuerySQL result. -func sqlScalar(rows []map[string]interface{}) int { - if len(rows) == 0 { - return 0 - } - for _, v := range rows[0] { - return toInt(v) - } - return 0 -} diff --git a/cmd/ml/cmd_metrics.go b/cmd/ml/cmd_metrics.go deleted file mode 100644 index 6bc6bcf2..00000000 --- a/cmd/ml/cmd_metrics.go +++ /dev/null @@ -1,36 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var metricsCmd = &cli.Command{ - Use: "metrics", - Short: "Push golden set stats to InfluxDB", - Long: "Queries golden_set stats from DuckDB and pushes summary, per-domain, and per-voice metrics to InfluxDB.", - RunE: runMetrics, -} - -func runMetrics(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - influx := ml.NewInfluxClient(influxURL, influxDB) - - return ml.PushMetrics(db, influx, os.Stdout) -} diff --git a/cmd/ml/cmd_ml.go b/cmd/ml/cmd_ml.go deleted file mode 100644 index f8dab296..00000000 --- a/cmd/ml/cmd_ml.go +++ /dev/null @@ -1,91 +0,0 @@ -// Package ml provides ML inference, scoring, and training pipeline commands. -// -// Commands: -// - core ml score: Score responses with heuristic and LLM judges -// - core ml probe: Run capability and content probes against a model -// - core ml export: Export golden set to training JSONL/Parquet -// - core ml expand: Generate expansion responses -// - core ml status: Show training and generation progress -// - core ml gguf: Convert MLX LoRA adapter to GGUF format -// - core ml convert: Convert MLX LoRA adapter to PEFT format -// - core ml agent: Run the scoring agent daemon -// - core ml worker: Run a distributed worker node -// - core ml serve: Start OpenAI-compatible inference server -// - core ml inventory: Show DuckDB table inventory with stats -// - core ml query: Run ad-hoc SQL against DuckDB -// - core ml metrics: Push golden set stats to InfluxDB -// - core ml ingest: Ingest benchmark scores and training logs to InfluxDB -// - core ml normalize: Deduplicate seeds into expansion prompts -// - core ml seed-influx: Migrate golden set from DuckDB to InfluxDB -// - core ml consolidate: Pull and merge response JSONL files from M3 -// - core ml import-all: Import all LEM data into DuckDB -// - core ml approve: Filter scored expansions into training JSONL -// - core ml publish: Upload Parquet dataset to HuggingFace Hub -// - core ml coverage: Analyze seed coverage by region and domain -// - core ml live: Show live generation progress from InfluxDB -// - core ml expand-status: Show expansion pipeline progress -package ml - -import ( - "forge.lthn.ai/core/go/pkg/cli" -) - -func init() { - cli.RegisterCommands(AddMLCommands) -} - -var mlCmd = &cli.Command{ - Use: "ml", - Short: "ML inference, scoring, and training pipeline", - Long: "Commands for ML model scoring, probe evaluation, data export, and format conversion.", -} - -// AddMLCommands registers the 'ml' command and all subcommands. -func AddMLCommands(root *cli.Command) { - initFlags() - mlCmd.AddCommand(scoreCmd) - mlCmd.AddCommand(probeCmd) - mlCmd.AddCommand(exportCmd) - mlCmd.AddCommand(expandCmd) - mlCmd.AddCommand(statusCmd) - mlCmd.AddCommand(ggufCmd) - mlCmd.AddCommand(convertCmd) - mlCmd.AddCommand(agentCmd) - mlCmd.AddCommand(workerCmd) - mlCmd.AddCommand(serveCmd) - mlCmd.AddCommand(inventoryCmd) - mlCmd.AddCommand(queryCmd) - mlCmd.AddCommand(metricsCmd) - mlCmd.AddCommand(ingestCmd) - mlCmd.AddCommand(normalizeCmd) - mlCmd.AddCommand(seedInfluxCmd) - mlCmd.AddCommand(consolidateCmd) - mlCmd.AddCommand(importCmd) - mlCmd.AddCommand(approveCmd) - mlCmd.AddCommand(publishCmd) - mlCmd.AddCommand(coverageCmd) - mlCmd.AddCommand(liveCmd) - mlCmd.AddCommand(expandStatusCmd) - root.AddCommand(mlCmd) -} - -// Shared persistent flags. -var ( - apiURL string - judgeURL string - judgeModel string - influxURL string - influxDB string - dbPath string - modelName string -) - -func initFlags() { - mlCmd.PersistentFlags().StringVar(&apiURL, "api-url", "http://10.69.69.108:8090", "OpenAI-compatible API URL") - mlCmd.PersistentFlags().StringVar(&judgeURL, "judge-url", "http://10.69.69.108:11434", "Judge model API URL (Ollama)") - mlCmd.PersistentFlags().StringVar(&judgeModel, "judge-model", "gemma3:27b", "Judge model name") - mlCmd.PersistentFlags().StringVar(&influxURL, "influx", "", "InfluxDB URL (default http://10.69.69.165:8181)") - mlCmd.PersistentFlags().StringVar(&influxDB, "influx-db", "", "InfluxDB database (default training)") - mlCmd.PersistentFlags().StringVar(&dbPath, "db", "", "DuckDB database path (or set LEM_DB env)") - mlCmd.PersistentFlags().StringVar(&modelName, "model", "", "Model name for API") -} diff --git a/cmd/ml/cmd_normalize.go b/cmd/ml/cmd_normalize.go deleted file mode 100644 index 4e4715bc..00000000 --- a/cmd/ml/cmd_normalize.go +++ /dev/null @@ -1,44 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var normalizeMinLen int - -var normalizeCmd = &cli.Command{ - Use: "normalize", - Short: "Normalize seeds into expansion prompts", - Long: "Deduplicates seeds against golden_set and prompts, creating the expansion_prompts table with priority-based ordering.", - RunE: runNormalize, -} - -func init() { - normalizeCmd.Flags().IntVar(&normalizeMinLen, "min-length", 50, "Minimum prompt length in characters") -} - -func runNormalize(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB env is required") - } - - db, err := ml.OpenDBReadWrite(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - cfg := ml.NormalizeConfig{ - MinLength: normalizeMinLen, - } - - return ml.NormalizeSeeds(db, cfg, os.Stdout) -} diff --git a/cmd/ml/cmd_probe.go b/cmd/ml/cmd_probe.go deleted file mode 100644 index 68ed39ce..00000000 --- a/cmd/ml/cmd_probe.go +++ /dev/null @@ -1,66 +0,0 @@ -package ml - -import ( - "context" - "encoding/json" - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - probeOutput string -) - -var probeCmd = &cli.Command{ - Use: "probe", - Short: "Run capability and content probes against a model", - Long: "Runs 23 capability probes and 6 content probes against an OpenAI-compatible API.", - RunE: runProbe, -} - -func init() { - probeCmd.Flags().StringVar(&probeOutput, "output", "", "Output JSON file for probe results") -} - -func runProbe(cmd *cli.Command, args []string) error { - if apiURL == "" { - return fmt.Errorf("--api-url is required") - } - - model := modelName - if model == "" { - model = "default" - } - - ctx := context.Background() - backend := ml.NewHTTPBackend(apiURL, model) - - fmt.Printf("Running %d capability probes against %s...\n", len(ml.CapabilityProbes), apiURL) - results := ml.RunCapabilityProbes(ctx, backend) - - fmt.Printf("\nResults: %.1f%% (%d/%d)\n", results.Accuracy, results.Correct, results.Total) - - for cat, data := range results.ByCategory { - catAcc := 0.0 - if data.Total > 0 { - catAcc = float64(data.Correct) / float64(data.Total) * 100 - } - fmt.Printf(" %-20s %d/%d (%.0f%%)\n", cat, data.Correct, data.Total, catAcc) - } - - if probeOutput != "" { - data, err := json.MarshalIndent(results, "", " ") - if err != nil { - return fmt.Errorf("marshal results: %w", err) - } - if err := os.WriteFile(probeOutput, data, 0644); err != nil { - return fmt.Errorf("write output: %w", err) - } - fmt.Printf("\nResults written to %s\n", probeOutput) - } - - return nil -} diff --git a/cmd/ml/cmd_publish.go b/cmd/ml/cmd_publish.go deleted file mode 100644 index 2f0ff6ff..00000000 --- a/cmd/ml/cmd_publish.go +++ /dev/null @@ -1,40 +0,0 @@ -package ml - -import ( - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - publishInputDir string - publishRepo string - publishPublic bool - publishToken string - publishDryRun bool -) - -var publishCmd = &cli.Command{ - Use: "publish", - Short: "Upload Parquet dataset to HuggingFace Hub", - Long: "Uploads train/valid/test Parquet files and an optional dataset card to a HuggingFace dataset repository.", - RunE: runPublish, -} - -func init() { - publishCmd.Flags().StringVar(&publishInputDir, "input-dir", "", "Directory containing Parquet files (required)") - publishCmd.Flags().StringVar(&publishRepo, "repo", "lthn/LEM-golden-set", "HuggingFace dataset repo ID") - publishCmd.Flags().BoolVar(&publishPublic, "public", false, "Make dataset public") - publishCmd.Flags().StringVar(&publishToken, "token", "", "HuggingFace API token (defaults to HF_TOKEN env)") - publishCmd.Flags().BoolVar(&publishDryRun, "dry-run", false, "Show what would be uploaded without uploading") - _ = publishCmd.MarkFlagRequired("input-dir") -} - -func runPublish(cmd *cli.Command, args []string) error { - return ml.Publish(ml.PublishConfig{ - InputDir: publishInputDir, - Repo: publishRepo, - Public: publishPublic, - Token: publishToken, - DryRun: publishDryRun, - }, cmd.OutOrStdout()) -} diff --git a/cmd/ml/cmd_query.go b/cmd/ml/cmd_query.go deleted file mode 100644 index 00d49070..00000000 --- a/cmd/ml/cmd_query.go +++ /dev/null @@ -1,148 +0,0 @@ -package ml - -import ( - "encoding/json" - "fmt" - "os" - "strings" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var queryCmd = &cli.Command{ - Use: "query [sql]", - Short: "Run ad-hoc SQL against DuckDB", - Long: "Executes arbitrary SQL against the DuckDB database. Non-SELECT queries are auto-wrapped as golden_set WHERE clauses.", - Example: ` core ml query "SELECT COUNT(*) FROM golden_set" - core ml query "domain = 'ethics'" - core ml query --json "SHOW TABLES"`, - Args: cli.MinimumNArgs(1), - RunE: runQuery, -} - -var queryJSON bool - -func init() { - queryCmd.Flags().BoolVar(&queryJSON, "json", false, "Output as JSON") -} - -func runQuery(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB env is required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - sql := strings.Join(args, " ") - - // Auto-wrap non-SELECT queries as golden_set WHERE clauses. - trimmed := strings.TrimSpace(strings.ToUpper(sql)) - if !strings.HasPrefix(trimmed, "SELECT") && !strings.HasPrefix(trimmed, "SHOW") && - !strings.HasPrefix(trimmed, "DESCRIBE") && !strings.HasPrefix(trimmed, "EXPLAIN") { - sql = "SELECT * FROM golden_set WHERE " + sql + " LIMIT 20" - } - - rows, err := db.QueryRows(sql) - if err != nil { - return fmt.Errorf("query: %w", err) - } - - if queryJSON { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - if err := enc.Encode(rows); err != nil { - return fmt.Errorf("encode json: %w", err) - } - fmt.Fprintf(os.Stderr, "\n(%d rows)\n", len(rows)) - return nil - } - - if len(rows) == 0 { - fmt.Println("(0 rows)") - return nil - } - - // Collect column names in stable order from first row. - var cols []string - for col := range rows[0] { - cols = append(cols, col) - } - - // Calculate column widths (capped at 60). - const maxWidth = 60 - widths := make([]int, len(cols)) - for i, col := range cols { - widths[i] = len(col) - } - for _, row := range rows { - for i, col := range cols { - val := formatValue(row[col]) - if l := len(val); l > widths[i] { - widths[i] = l - } - } - } - for i := range widths { - if widths[i] > maxWidth { - widths[i] = maxWidth - } - } - - // Print header. - for i, col := range cols { - if i > 0 { - fmt.Print(" | ") - } - fmt.Printf("%-*s", widths[i], truncate(col, widths[i])) - } - fmt.Println() - - // Print separator. - for i := range cols { - if i > 0 { - fmt.Print("-+-") - } - fmt.Print(strings.Repeat("-", widths[i])) - } - fmt.Println() - - // Print rows. - for _, row := range rows { - for i, col := range cols { - if i > 0 { - fmt.Print(" | ") - } - fmt.Printf("%-*s", widths[i], truncate(formatValue(row[col]), widths[i])) - } - fmt.Println() - } - - fmt.Printf("\n(%d rows)\n", len(rows)) - return nil -} - -func formatValue(v interface{}) string { - if v == nil { - return "NULL" - } - return fmt.Sprintf("%v", v) -} - -func truncate(s string, max int) string { - if len(s) <= max { - return s - } - if max <= 3 { - return s[:max] - } - return s[:max-3] + "..." -} diff --git a/cmd/ml/cmd_sandwich.go b/cmd/ml/cmd_sandwich.go deleted file mode 100644 index 80acee4d..00000000 --- a/cmd/ml/cmd_sandwich.go +++ /dev/null @@ -1,238 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "os" - "runtime" - "time" - - "forge.lthn.ai/core/go-ml" - "forge.lthn.ai/core/go/pkg/cli" -) - -var sandwichCmd = &cli.Command{ - Use: "sandwich", - Short: "Generate LEK training data using sandwich signing", - Long: `Generates training data by wrapping seed prompts in a "sandwich" format: - - KB preamble (axioms framework) → seed prompt → LEK-1 kernel postfix - -Each seed prompt is sent to the local MLX model for inference, and the -signed prompt + response pair is written as chat JSONL for 'core ml train'. - -The "sandwich" format embeds the ethical framework context around each -prompt, teaching the model to reason from LEK principles naturally. - -Seed file format (JSON array): - [{"id": "P01", "category": "sovereignty", "prompt": "...", "signal": "..."}]`, - RunE: runSandwich, -} - -var ( - sandwichModelPath string - sandwichKB string - sandwichKernel string - sandwichSeeds string - sandwichOutput string - sandwichMaxTokens int - sandwichTemp float64 - sandwichMemLimit int - sandwichDryRun bool -) - -func init() { - sandwichCmd.Flags().StringVar(&sandwichModelPath, "model-path", "", "Path to model directory (required)") - sandwichCmd.Flags().StringVar(&sandwichKB, "kb", "", "Knowledge base document (axioms markdown, required)") - sandwichCmd.Flags().StringVar(&sandwichKernel, "kernel", "", "LEK-1 kernel file (required)") - sandwichCmd.Flags().StringVar(&sandwichSeeds, "seeds", "", "Seed prompts JSON file (required)") - sandwichCmd.Flags().StringVar(&sandwichOutput, "output", "sandwich.jsonl", "Output JSONL file") - sandwichCmd.Flags().IntVar(&sandwichMaxTokens, "max-tokens", 1024, "Max tokens per response") - sandwichCmd.Flags().Float64Var(&sandwichTemp, "temperature", 0.4, "Sampling temperature") - sandwichCmd.Flags().IntVar(&sandwichMemLimit, "memory-limit", 24, "Metal memory limit in GB") - sandwichCmd.Flags().BoolVar(&sandwichDryRun, "dry-run", false, "Output prompts only (no inference)") - sandwichCmd.MarkFlagRequired("model-path") - sandwichCmd.MarkFlagRequired("kernel") - sandwichCmd.MarkFlagRequired("seeds") - sandwichCmd.MarkFlagRequired("kb") -} - -// seedPrompt is a single prompt from the seeds JSON file. -type seedPrompt struct { - ID string `json:"id"` - Category string `json:"category"` - Prompt string `json:"prompt"` - Signal string `json:"signal"` -} - -// sandwichOutput holds a single training example in messages format. -type sandwichRecord struct { - Messages []ml.Message `json:"messages"` -} - -func runSandwich(cmd *cli.Command, args []string) error { - start := time.Now() - - // Load KB document - kbBytes, err := os.ReadFile(sandwichKB) - if err != nil { - return fmt.Errorf("read KB: %w", err) - } - kbText := string(kbBytes) - - // Load LEK-1 kernel - kernelBytes, err := os.ReadFile(sandwichKernel) - if err != nil { - return fmt.Errorf("read kernel: %w", err) - } - kernelText := string(kernelBytes) - - // Load seed prompts - seedBytes, err := os.ReadFile(sandwichSeeds) - if err != nil { - return fmt.Errorf("read seeds: %w", err) - } - var seeds []seedPrompt - if err := json.Unmarshal(seedBytes, &seeds); err != nil { - return fmt.Errorf("parse seeds: %w", err) - } - - slog.Info("sandwich: loaded inputs", - "kb_chars", len(kbText), - "kernel_chars", len(kernelText), - "seeds", len(seeds), - ) - - if len(seeds) == 0 { - return fmt.Errorf("no seed prompts found") - } - - // Open output file - outFile, err := os.Create(sandwichOutput) - if err != nil { - return fmt.Errorf("create output: %w", err) - } - defer outFile.Close() - encoder := json.NewEncoder(outFile) - - // Dry-run mode: output prompts without inference - if sandwichDryRun { - for _, seed := range seeds { - signedPrompt := buildSandwich(kbText, seed.Prompt, kernelText) - record := sandwichRecord{ - Messages: []ml.Message{ - {Role: "user", Content: signedPrompt}, - }, - } - if err := encoder.Encode(record); err != nil { - return fmt.Errorf("write record: %w", err) - } - } - slog.Info("sandwich: dry-run complete", - "output", sandwichOutput, - "prompts", len(seeds), - ) - return nil - } - - // Load MLX model - slog.Info("sandwich: loading model", "path", sandwichModelPath) - backend, err := ml.NewMLXBackend(sandwichModelPath) - if err != nil { - return fmt.Errorf("load model: %w", err) - } - - opts := ml.GenOpts{ - Temperature: sandwichTemp, - MaxTokens: sandwichMaxTokens, - } - - var totalTokenTime time.Duration - generated := 0 - - for i, seed := range seeds { - seedStart := time.Now() - - // Build the sandwich: KB + prompt + kernel - signedPrompt := buildSandwich(kbText, seed.Prompt, kernelText) - - // Send as a user message for chat-style generation - messages := []ml.Message{ - {Role: "user", Content: signedPrompt}, - } - - slog.Info("sandwich: generating", - "seed", fmt.Sprintf("%d/%d", i+1, len(seeds)), - "id", seed.ID, - "category", seed.Category, - ) - - // Generate response - response, err := backend.Chat(context.Background(), messages, opts) - if err != nil { - slog.Error("sandwich: generation failed", - "id", seed.ID, - "error", err, - ) - continue - } - - elapsed := time.Since(seedStart) - totalTokenTime += elapsed - - // Write training record - record := sandwichRecord{ - Messages: []ml.Message{ - {Role: "user", Content: signedPrompt}, - {Role: "assistant", Content: response}, - }, - } - if err := encoder.Encode(record); err != nil { - return fmt.Errorf("write record: %w", err) - } - - generated++ - slog.Info("sandwich: generated", - "id", seed.ID, - "category", seed.Category, - "response_chars", len(response), - "duration", elapsed.Round(time.Second), - ) - - // Periodic cleanup - if (i+1)%4 == 0 { - runtime.GC() - } - } - - slog.Info("sandwich: complete", - "output", sandwichOutput, - "generated", generated, - "total", len(seeds), - "duration", time.Since(start).Round(time.Second), - "avg_per_seed", (totalTokenTime / time.Duration(max(generated, 1))).Round(time.Second), - ) - - return nil -} - -// buildSandwich constructs the signed prompt: KB preamble + seed prompt + LEK-1 kernel. -func buildSandwich(kb, prompt, kernel string) string { - return fmt.Sprintf(`Name: Ethics Experiment -KB: -%s - ---- - -%s - ---- - -%s - -Remember: respond using the ethical framework above. Do not reference the framework directly — reason from its principles naturally.`, kb, prompt, kernel) -} diff --git a/cmd/ml/cmd_sandwich_init.go b/cmd/ml/cmd_sandwich_init.go deleted file mode 100644 index d23b8856..00000000 --- a/cmd/ml/cmd_sandwich_init.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build darwin && arm64 - -package ml - -func init() { - mlCmd.AddCommand(sandwichCmd) -} diff --git a/cmd/ml/cmd_score.go b/cmd/ml/cmd_score.go deleted file mode 100644 index 7c0cc25a..00000000 --- a/cmd/ml/cmd_score.go +++ /dev/null @@ -1,77 +0,0 @@ -package ml - -import ( - "context" - "fmt" - "time" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - scoreInput string - scoreSuites string - scoreOutput string - scoreConcur int -) - -var scoreCmd = &cli.Command{ - Use: "score", - Short: "Score responses with heuristic and LLM judges", - Long: "Reads a JSONL file of prompt/response pairs and scores them across configured suites.", - RunE: runScore, -} - -func init() { - scoreCmd.Flags().StringVar(&scoreInput, "input", "", "Input JSONL file with prompt/response pairs (required)") - scoreCmd.Flags().StringVar(&scoreSuites, "suites", "all", "Comma-separated scoring suites (heuristic,semantic,content,exact,truthfulqa,donotanswer,toxigen)") - scoreCmd.Flags().StringVar(&scoreOutput, "output", "", "Output JSON file for scores") - scoreCmd.Flags().IntVar(&scoreConcur, "concurrency", 4, "Number of concurrent scoring workers") - scoreCmd.MarkFlagRequired("input") -} - -func runScore(cmd *cli.Command, args []string) error { - responses, err := ml.ReadResponses(scoreInput) - if err != nil { - return fmt.Errorf("read input: %w", err) - } - - var judge *ml.Judge - if judgeURL != "" { - backend := ml.NewHTTPBackend(judgeURL, judgeModel) - judge = ml.NewJudge(backend) - } - - engine := ml.NewEngine(judge, scoreConcur, scoreSuites) - - ctx := context.Background() - perPrompt := engine.ScoreAll(ctx, responses) - averages := ml.ComputeAverages(perPrompt) - - if scoreOutput != "" { - output := &ml.ScorerOutput{ - Metadata: ml.Metadata{ - JudgeModel: judgeModel, - JudgeURL: judgeURL, - ScoredAt: time.Now(), - Suites: ml.SplitComma(scoreSuites), - }, - ModelAverages: averages, - PerPrompt: perPrompt, - } - if err := ml.WriteScores(scoreOutput, output); err != nil { - return fmt.Errorf("write output: %w", err) - } - fmt.Printf("Scores written to %s\n", scoreOutput) - } else { - for model, avgs := range averages { - fmt.Printf("%s:\n", model) - for field, val := range avgs { - fmt.Printf(" %-25s %.3f\n", field, val) - } - } - } - - return nil -} diff --git a/cmd/ml/cmd_seed_influx.go b/cmd/ml/cmd_seed_influx.go deleted file mode 100644 index 30ece12e..00000000 --- a/cmd/ml/cmd_seed_influx.go +++ /dev/null @@ -1,49 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var seedInfluxCmd = &cli.Command{ - Use: "seed-influx", - Short: "Seed InfluxDB golden_gen from DuckDB golden_set", - Long: "One-time migration: batch-loads DuckDB golden_set records into InfluxDB golden_gen measurement.", - RunE: runSeedInflux, -} - -var ( - seedInfluxForce bool - seedInfluxBatchSize int -) - -func init() { - seedInfluxCmd.Flags().BoolVar(&seedInfluxForce, "force", false, "Re-seed even if InfluxDB already has data") - seedInfluxCmd.Flags().IntVar(&seedInfluxBatchSize, "batch-size", 500, "Lines per InfluxDB write batch") -} - -func runSeedInflux(cmd *cli.Command, args []string) error { - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - if path == "" { - return fmt.Errorf("--db or LEM_DB required") - } - - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - influx := ml.NewInfluxClient(influxURL, influxDB) - - return ml.SeedInflux(db, influx, ml.SeedInfluxConfig{ - Force: seedInfluxForce, - BatchSize: seedInfluxBatchSize, - }, os.Stdout) -} diff --git a/cmd/ml/cmd_sequence.go b/cmd/ml/cmd_sequence.go deleted file mode 100644 index 2b56c1af..00000000 --- a/cmd/ml/cmd_sequence.go +++ /dev/null @@ -1,326 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "encoding/json" - "fmt" - "log/slog" - "os" - "path/filepath" - "strings" - "time" - - "forge.lthn.ai/core/go-ml" - "forge.lthn.ai/core/go/pkg/cli" - "gopkg.in/yaml.v3" -) - -var sequenceCmd = &cli.Command{ - Use: "sequence", - Short: "Run a training sequence of multiple lessons", - Long: `Runs an ordered sequence of lessons defined in a YAML file. - -Sequence YAML format: - id: lek-full - title: "LEK Full Training Sequence" - mode: vertical - model-path: /path/to/model - lessons: - - sovereignty.yaml - - privacy.yaml - - censorship.yaml - -Mode: - vertical Run lessons strictly in order (default) - horizontal Run all lessons, order doesn't matter - -State is tracked per-sequence so runs can be resumed.`, - RunE: runSequence, -} - -var ( - sequenceFile string - sequenceModelPath string - sequenceOutput string - sequenceMaxTokens int - sequenceTemp float64 - sequenceMemLimit int -) - -func init() { - sequenceCmd.Flags().StringVar(&sequenceFile, "file", "", "Sequence YAML file (required)") - sequenceCmd.Flags().StringVar(&sequenceModelPath, "model-path", "", "Path to model directory (required)") - sequenceCmd.Flags().StringVar(&sequenceOutput, "output", "", "Output JSONL file (default: .jsonl)") - sequenceCmd.Flags().IntVar(&sequenceMaxTokens, "max-tokens", 1024, "Max tokens per response") - sequenceCmd.Flags().Float64Var(&sequenceTemp, "temperature", 0.4, "Sampling temperature") - sequenceCmd.Flags().IntVar(&sequenceMemLimit, "memory-limit", 24, "Metal memory limit in GB") - sequenceCmd.MarkFlagRequired("file") - sequenceCmd.MarkFlagRequired("model-path") -} - -// sequenceDef is a YAML sequence definition. -type sequenceDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Mode string `yaml:"mode"` // "vertical" (default) or "horizontal" - ModelPath string `yaml:"model-path"` - Lessons []string `yaml:"lessons"` // Relative paths to lesson YAML files -} - -// sequenceState tracks progress through a sequence. -type sequenceState struct { - SequenceID string `json:"sequence_id"` - Completed map[string]bool `json:"completed"` // lesson ID → done - Current string `json:"current"` - UpdatedAt string `json:"updated_at"` -} - -func runSequence(cmd *cli.Command, args []string) error { - start := time.Now() - - // Load sequence YAML - data, err := os.ReadFile(sequenceFile) - if err != nil { - return fmt.Errorf("read sequence: %w", err) - } - - var seq sequenceDef - if err := yaml.Unmarshal(data, &seq); err != nil { - return fmt.Errorf("parse sequence: %w", err) - } - - if seq.ID == "" { - seq.ID = strings.TrimSuffix(filepath.Base(sequenceFile), filepath.Ext(sequenceFile)) - } - if seq.Mode == "" { - seq.Mode = "vertical" - } - - // Model path from sequence or flag - modelPath := sequenceModelPath - if modelPath == "" && seq.ModelPath != "" { - modelPath = seq.ModelPath - } - if modelPath == "" { - return fmt.Errorf("model-path is required (flag or sequence YAML)") - } - - // Resolve output - if sequenceOutput == "" { - sequenceOutput = seq.ID + ".jsonl" - } - - slog.Info("sequence: loaded", - "id", seq.ID, - "title", seq.Title, - "mode", seq.Mode, - "lessons", len(seq.Lessons), - ) - - // Load state - stateFile := seq.ID + ".sequence-state.json" - state := loadSequenceState(stateFile) - if state.SequenceID == "" { - state.SequenceID = seq.ID - state.Completed = make(map[string]bool) - } - - // Load model once for all lessons - slog.Info("sequence: loading model", "path", modelPath) - backend, err := ml.NewMLXBackend(modelPath) - if err != nil { - return fmt.Errorf("load model: %w", err) - } - - opts := ml.GenOpts{ - Temperature: sequenceTemp, - MaxTokens: sequenceMaxTokens, - } - - // Open output file - outFile, err := os.OpenFile(sequenceOutput, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("create output: %w", err) - } - defer outFile.Close() - encoder := json.NewEncoder(outFile) - - baseDir := filepath.Dir(sequenceFile) - totalGenerated := 0 - - for i, lessonPath := range seq.Lessons { - // Resolve lesson path - if !filepath.IsAbs(lessonPath) { - lessonPath = filepath.Join(baseDir, lessonPath) - } - - // Load lesson - lessonData, err := os.ReadFile(lessonPath) - if err != nil { - slog.Error("sequence: failed to read lesson", - "path", lessonPath, - "error", err, - ) - if seq.Mode == "vertical" { - return fmt.Errorf("vertical sequence halted: %w", err) - } - continue - } - - var lesson lessonDef - if err := yaml.Unmarshal(lessonData, &lesson); err != nil { - slog.Error("sequence: failed to parse lesson", - "path", lessonPath, - "error", err, - ) - if seq.Mode == "vertical" { - return fmt.Errorf("vertical sequence halted: %w", err) - } - continue - } - - if lesson.ID == "" { - lesson.ID = strings.TrimSuffix(filepath.Base(lessonPath), filepath.Ext(lessonPath)) - } - - // Skip completed lessons - if state.Completed[lesson.ID] { - slog.Info("sequence: skipping completed lesson", - "lesson", fmt.Sprintf("%d/%d", i+1, len(seq.Lessons)), - "id", lesson.ID, - ) - continue - } - - state.Current = lesson.ID - - slog.Info("sequence: starting lesson", - "lesson", fmt.Sprintf("%d/%d", i+1, len(seq.Lessons)), - "id", lesson.ID, - "title", lesson.Title, - "prompts", len(lesson.Prompts), - ) - - // Load sandwich files for this lesson - var kbText, kernelText string - hasSandwich := false - if lesson.Sandwich != nil { - lessonDir := filepath.Dir(lessonPath) - if lesson.Sandwich.KB != "" { - kbPath := lesson.Sandwich.KB - if !filepath.IsAbs(kbPath) { - kbPath = filepath.Join(lessonDir, kbPath) - } - d, err := os.ReadFile(kbPath) - if err != nil { - slog.Error("sequence: failed to read KB", "error", err) - } else { - kbText = string(d) - } - } - if lesson.Sandwich.Kernel != "" { - kernelPath := lesson.Sandwich.Kernel - if !filepath.IsAbs(kernelPath) { - kernelPath = filepath.Join(lessonDir, kernelPath) - } - d, err := os.ReadFile(kernelPath) - if err != nil { - slog.Error("sequence: failed to read kernel", "error", err) - } else { - kernelText = string(d) - } - } - hasSandwich = kbText != "" && kernelText != "" - } - - // Run each prompt in the lesson - generated := 0 - for j, prompt := range lesson.Prompts { - var messages []ml.Message - if lesson.System != "" { - messages = append(messages, ml.Message{Role: "system", Content: lesson.System}) - } - - userContent := prompt.Prompt - if hasSandwich { - userContent = buildSandwich(kbText, prompt.Prompt, kernelText) - } - messages = append(messages, ml.Message{Role: "user", Content: userContent}) - - slog.Info("sequence: generating", - "lesson", lesson.ID, - "prompt", fmt.Sprintf("%d/%d", j+1, len(lesson.Prompts)), - "id", prompt.ID, - ) - - response, err := backend.Chat(cmd.Context(), messages, opts) - if err != nil { - slog.Error("sequence: generation failed", - "lesson", lesson.ID, - "prompt", prompt.ID, - "error", err, - ) - continue - } - - record := struct { - Messages []ml.Message `json:"messages"` - }{ - Messages: []ml.Message{ - {Role: "user", Content: userContent}, - {Role: "assistant", Content: response}, - }, - } - if err := encoder.Encode(record); err != nil { - return fmt.Errorf("write record: %w", err) - } - - generated++ - totalGenerated++ - } - - // Mark lesson complete - state.Completed[lesson.ID] = true - state.UpdatedAt = time.Now().Format(time.RFC3339) - saveSequenceState(stateFile, state) - - slog.Info("sequence: lesson complete", - "id", lesson.ID, - "generated", generated, - "total", len(lesson.Prompts), - ) - } - - state.Current = "" - state.UpdatedAt = time.Now().Format(time.RFC3339) - saveSequenceState(stateFile, state) - - slog.Info("sequence: complete", - "id", seq.ID, - "output", sequenceOutput, - "total_generated", totalGenerated, - "lessons_completed", len(state.Completed), - "duration", time.Since(start).Round(time.Second), - ) - - return nil -} - -func loadSequenceState(path string) sequenceState { - data, err := os.ReadFile(path) - if err != nil { - return sequenceState{} - } - var state sequenceState - json.Unmarshal(data, &state) - return state -} - -func saveSequenceState(path string, state sequenceState) { - data, err := json.MarshalIndent(state, "", " ") - if err != nil { - return - } - os.WriteFile(path, data, 0644) -} diff --git a/cmd/ml/cmd_serve.go b/cmd/ml/cmd_serve.go deleted file mode 100644 index 737c7a18..00000000 --- a/cmd/ml/cmd_serve.go +++ /dev/null @@ -1,472 +0,0 @@ -package ml - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "os" - "os/signal" - "runtime" - "sync/atomic" - "syscall" - "time" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var serveCmd = &cli.Command{ - Use: "serve", - Short: "Start OpenAI-compatible inference server", - Long: "Starts an HTTP server serving /v1/completions and /v1/chat/completions using the configured ML backend.", - RunE: runServe, -} - -var ( - serveBind string - serveModelPath string - serveThreads int - serveMaxTokens int - serveTimeout int - serveMaxRequests int - serveMaxContext int -) - -func init() { - serveCmd.Flags().StringVar(&serveBind, "bind", "0.0.0.0:8090", "Address to bind") - serveCmd.Flags().StringVar(&serveModelPath, "model-path", "", "Path to model directory (for mlx backend)") - serveCmd.Flags().IntVar(&serveThreads, "threads", 0, "Max CPU threads (0 = all available)") - serveCmd.Flags().IntVar(&serveMaxTokens, "max-tokens", 4096, "Default max tokens per request") - serveCmd.Flags().IntVar(&serveTimeout, "timeout", 300, "Request timeout in seconds") - serveCmd.Flags().IntVar(&serveMaxRequests, "max-requests", 1, "Max concurrent requests (Metal is single-stream)") - serveCmd.Flags().IntVar(&serveMaxContext, "max-context", 4, "Max chat messages to keep (sliding window, 0=unlimited)") -} - -type completionRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt"` - MaxTokens int `json:"max_tokens"` - Temperature float64 `json:"temperature"` - Stream bool `json:"stream"` -} - -type completionResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []completionChoice `json:"choices"` - Usage usageInfo `json:"usage"` -} - -type completionChoice struct { - Text string `json:"text"` - Index int `json:"index"` - FinishReason string `json:"finish_reason"` -} - -type chatRequest struct { - Model string `json:"model"` - Messages []ml.Message `json:"messages"` - MaxTokens int `json:"max_tokens"` - Temperature float64 `json:"temperature"` - Stream bool `json:"stream"` -} - -type chatResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []chatChoice `json:"choices"` -} - -type chatChoice struct { - Message ml.Message `json:"message"` - Index int `json:"index"` - FinishReason string `json:"finish_reason"` -} - -// SSE streaming types (OpenAI chunk format) -type chatChunkResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []chatChunkChoice `json:"choices"` -} - -type chatChunkChoice struct { - Delta chatChunkDelta `json:"delta"` - Index int `json:"index"` - FinishReason *string `json:"finish_reason"` -} - -type chatChunkDelta struct { - Role string `json:"role,omitempty"` - Content string `json:"content,omitempty"` -} - -type completionChunkResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []completionChunkChoice `json:"choices"` -} - -type completionChunkChoice struct { - Text string `json:"text"` - Index int `json:"index"` - FinishReason *string `json:"finish_reason"` -} - -type usageInfo struct { - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` -} - -func runServe(cmd *cli.Command, args []string) error { - // Cap CPU threads - if serveThreads > 0 { - prev := runtime.GOMAXPROCS(serveThreads) - slog.Info("ml serve: capped threads", "threads", serveThreads, "previous", prev) - } - - backend, err := createServeBackend() - if err != nil { - return err - } - - // Check if backend supports streaming - streamer, canStream := backend.(ml.StreamingBackend) - - // Request tracking - var activeRequests atomic.Int32 - startTime := time.Now() - - mux := http.NewServeMux() - - // Health endpoint - mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "status": "ok", - "model": backend.Name(), - "uptime_seconds": int(time.Since(startTime).Seconds()), - "active_requests": activeRequests.Load(), - "max_threads": runtime.GOMAXPROCS(0), - "max_tokens": serveMaxTokens, - "max_context": serveMaxContext, - }) - }) - - mux.HandleFunc("POST /v1/completions", func(w http.ResponseWriter, r *http.Request) { - // Concurrency gate - if int(activeRequests.Load()) >= serveMaxRequests { - http.Error(w, `{"error":"server busy, max concurrent requests reached"}`, http.StatusTooManyRequests) - return - } - activeRequests.Add(1) - defer activeRequests.Add(-1) - - // Request timeout - ctx, cancel := context.WithTimeout(r.Context(), time.Duration(serveTimeout)*time.Second) - defer cancel() - r = r.WithContext(ctx) - - body, _ := io.ReadAll(r.Body) - var req completionRequest - if err := json.Unmarshal(body, &req); err != nil { - http.Error(w, err.Error(), 400) - return - } - - // Enforce server-level max-tokens cap - if req.MaxTokens == 0 || req.MaxTokens > serveMaxTokens { - req.MaxTokens = serveMaxTokens - } - - opts := ml.GenOpts{ - Temperature: req.Temperature, - MaxTokens: req.MaxTokens, - Model: req.Model, - } - - // Streaming path - if req.Stream && canStream { - id := fmt.Sprintf("cmpl-%d", time.Now().UnixNano()) - created := time.Now().Unix() - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("X-Accel-Buffering", "no") - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "streaming not supported", 500) - return - } - - err := streamer.GenerateStream(r.Context(), req.Prompt, opts, func(token string) error { - chunk := completionChunkResponse{ - ID: id, - Object: "text_completion", - Created: created, - Model: backend.Name(), - Choices: []completionChunkChoice{{Text: token}}, - } - data, _ := json.Marshal(chunk) - fmt.Fprintf(w, "data: %s\n\n", data) - flusher.Flush() - return nil - }) - - if err != nil { - slog.Error("stream error", "err", err) - } - - // Send final chunk with finish_reason - stop := "stop" - final := completionChunkResponse{ - ID: id, - Object: "text_completion", - Created: created, - Model: backend.Name(), - Choices: []completionChunkChoice{{FinishReason: &stop}}, - } - data, _ := json.Marshal(final) - fmt.Fprintf(w, "data: %s\n\n", data) - fmt.Fprintf(w, "data: [DONE]\n\n") - flusher.Flush() - return - } - - // Non-streaming path - text, err := backend.Generate(r.Context(), req.Prompt, opts) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - - resp := completionResponse{ - ID: fmt.Sprintf("cmpl-%d", time.Now().UnixNano()), - Object: "text_completion", - Created: time.Now().Unix(), - Model: backend.Name(), - Choices: []completionChoice{{Text: text, FinishReason: "stop"}}, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - }) - - mux.HandleFunc("POST /v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { - // Concurrency gate - if int(activeRequests.Load()) >= serveMaxRequests { - http.Error(w, `{"error":"server busy, max concurrent requests reached"}`, http.StatusTooManyRequests) - return - } - activeRequests.Add(1) - defer activeRequests.Add(-1) - - // Request timeout - ctx, cancel := context.WithTimeout(r.Context(), time.Duration(serveTimeout)*time.Second) - defer cancel() - r = r.WithContext(ctx) - - body, _ := io.ReadAll(r.Body) - var req chatRequest - if err := json.Unmarshal(body, &req); err != nil { - http.Error(w, err.Error(), 400) - return - } - - // Enforce server-level max-tokens cap - if req.MaxTokens == 0 || req.MaxTokens > serveMaxTokens { - req.MaxTokens = serveMaxTokens - } - - // Sliding window: keep system prompt (if any) + last N messages - // Prevents KV-cache explosion on multi-turn conversations - if serveMaxContext > 0 && len(req.Messages) > serveMaxContext { - var kept []ml.Message - rest := req.Messages - // Preserve system message if present - if len(rest) > 0 && rest[0].Role == "system" { - kept = append(kept, rest[0]) - rest = rest[1:] - } - // Keep only the last N user/assistant messages - if len(rest) > serveMaxContext { - rest = rest[len(rest)-serveMaxContext:] - } - req.Messages = append(kept, rest...) - slog.Debug("ml serve: context window applied", "kept", len(req.Messages)) - } - - opts := ml.GenOpts{ - Temperature: req.Temperature, - MaxTokens: req.MaxTokens, - Model: req.Model, - } - - // Streaming path - if req.Stream && canStream { - id := fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()) - created := time.Now().Unix() - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("X-Accel-Buffering", "no") - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "streaming not supported", 500) - return - } - - // Send initial role chunk - roleChunk := chatChunkResponse{ - ID: id, - Object: "chat.completion.chunk", - Created: created, - Model: backend.Name(), - Choices: []chatChunkChoice{{Delta: chatChunkDelta{Role: "assistant"}}}, - } - data, _ := json.Marshal(roleChunk) - fmt.Fprintf(w, "data: %s\n\n", data) - flusher.Flush() - - err := streamer.ChatStream(r.Context(), req.Messages, opts, func(token string) error { - chunk := chatChunkResponse{ - ID: id, - Object: "chat.completion.chunk", - Created: created, - Model: backend.Name(), - Choices: []chatChunkChoice{{Delta: chatChunkDelta{Content: token}}}, - } - data, _ := json.Marshal(chunk) - fmt.Fprintf(w, "data: %s\n\n", data) - flusher.Flush() - return nil - }) - - if err != nil { - slog.Error("stream error", "err", err) - } - - // Send final chunk with finish_reason - stop := "stop" - final := chatChunkResponse{ - ID: id, - Object: "chat.completion.chunk", - Created: created, - Model: backend.Name(), - Choices: []chatChunkChoice{{Delta: chatChunkDelta{}, FinishReason: &stop}}, - } - data, _ = json.Marshal(final) - fmt.Fprintf(w, "data: %s\n\n", data) - fmt.Fprintf(w, "data: [DONE]\n\n") - flusher.Flush() - return - } - - // Non-streaming path - text, err := backend.Chat(r.Context(), req.Messages, opts) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - - resp := chatResponse{ - ID: fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()), - Object: "chat.completion", - Created: time.Now().Unix(), - Model: backend.Name(), - Choices: []chatChoice{{ - Message: ml.Message{Role: "assistant", Content: text}, - FinishReason: "stop", - }}, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - }) - - mux.HandleFunc("GET /v1/models", func(w http.ResponseWriter, r *http.Request) { - resp := struct { - Object string `json:"object"` - Data []struct { - ID string `json:"id"` - } `json:"data"` - }{ - Object: "list", - Data: []struct { - ID string `json:"id"` - }{{ID: backend.Name()}}, - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - }) - - // Serve the lem-chat UI at root — same origin, no CORS needed - mux.HandleFunc("GET /chat.js", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/javascript") - w.Write(lemChatJS) - }) - - mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprintf(w, chatHTML, backend.Name(), serveMaxTokens) - }) - - slog.Info("ml serve: starting", - "bind", serveBind, - "backend", backend.Name(), - "streaming", canStream, - "threads", runtime.GOMAXPROCS(0), - "max_tokens", serveMaxTokens, - "max_context_msgs", serveMaxContext, - "timeout_s", serveTimeout, - "max_requests", serveMaxRequests, - ) - fmt.Printf("Serving on http://%s\n", serveBind) - - // Graceful shutdown on SIGINT/SIGTERM - srv := &http.Server{ - Addr: serveBind, - Handler: mux, - } - - errCh := make(chan error, 1) - go func() { - errCh <- srv.ListenAndServe() - }() - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - select { - case sig := <-sigCh: - slog.Info("ml serve: shutting down", "signal", sig) - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - slog.Error("ml serve: shutdown error", "err", err) - return err - } - slog.Info("ml serve: stopped cleanly") - return nil - case err := <-errCh: - return err - } -} diff --git a/cmd/ml/cmd_status.go b/cmd/ml/cmd_status.go deleted file mode 100644 index 0c6c08f7..00000000 --- a/cmd/ml/cmd_status.go +++ /dev/null @@ -1,54 +0,0 @@ -package ml - -import ( - "fmt" - "os" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var statusCmd = &cli.Command{ - Use: "status", - Short: "Show training and generation progress", - Long: "Queries InfluxDB for training status, loss, and generation progress. Optionally shows DuckDB table counts.", - RunE: runStatus, -} - -func runStatus(cmd *cli.Command, args []string) error { - influx := ml.NewInfluxClient(influxURL, influxDB) - - if err := ml.PrintStatus(influx, os.Stdout); err != nil { - return fmt.Errorf("status: %w", err) - } - - path := dbPath - if path == "" { - path = os.Getenv("LEM_DB") - } - - if path != "" { - db, err := ml.OpenDB(path) - if err != nil { - return fmt.Errorf("open db: %w", err) - } - defer db.Close() - - counts, err := db.TableCounts() - if err != nil { - return fmt.Errorf("table counts: %w", err) - } - - fmt.Println() - fmt.Println("DuckDB:") - order := []string{"golden_set", "expansion_prompts", "seeds", "training_examples", - "prompts", "gemini_responses", "benchmark_questions", "benchmark_results", "validations"} - for _, table := range order { - if count, ok := counts[table]; ok { - fmt.Fprintf(os.Stdout, " %-22s %6d rows\n", table, count) - } - } - } - - return nil -} diff --git a/cmd/ml/cmd_train.go b/cmd/ml/cmd_train.go deleted file mode 100644 index 9df4bf0d..00000000 --- a/cmd/ml/cmd_train.go +++ /dev/null @@ -1,358 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "bufio" - "encoding/json" - "fmt" - "log/slog" - "os" - "runtime" - "strings" - "time" - - "forge.lthn.ai/core/go-ml" - "forge.lthn.ai/core/go-mlx" - "forge.lthn.ai/core/go-ai/mlx/model" - "forge.lthn.ai/core/go-ai/mlx/tokenizer" - "forge.lthn.ai/core/go/pkg/cli" -) - -var trainCmd = &cli.Command{ - Use: "train", - Short: "LoRA fine-tune a model on JSONL training data", - Long: `Fine-tunes a local MLX model using LoRA (Low-Rank Adaptation). - -Reads chat-format JSONL training data and trains LoRA adapter weights -using AdamW optimiser with cross-entropy loss on assistant tokens only. - -Training data format (one JSON object per line): - {"messages": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}`, - RunE: runTrain, -} - -var ( - trainModelPath string - trainData string - trainOutput string - trainRank int - trainAlpha float64 - trainLR float64 - trainEpochs int - trainMaxSeqLen int - trainTargets string - trainMemoryLimit int -) - -func init() { - trainCmd.Flags().StringVar(&trainModelPath, "model-path", "", "Path to model directory (required)") - trainCmd.Flags().StringVar(&trainData, "data", "", "Training JSONL file (required)") - trainCmd.Flags().StringVar(&trainOutput, "output", "adapters.safetensors", "Output adapter file") - trainCmd.Flags().IntVar(&trainRank, "rank", 8, "LoRA decomposition rank") - trainCmd.Flags().Float64Var(&trainAlpha, "alpha", 16, "LoRA scaling factor") - trainCmd.Flags().Float64Var(&trainLR, "lr", 1e-4, "Learning rate") - trainCmd.Flags().IntVar(&trainEpochs, "epochs", 1, "Number of training epochs") - trainCmd.Flags().IntVar(&trainMaxSeqLen, "max-seq-len", 512, "Maximum sequence length (tokens)") - trainCmd.Flags().StringVar(&trainTargets, "targets", "q_proj,v_proj", "Comma-separated projection targets for LoRA") - trainCmd.Flags().IntVar(&trainMemoryLimit, "memory-limit", 24, "Metal memory limit in GB") - trainCmd.MarkFlagRequired("model-path") - trainCmd.MarkFlagRequired("data") -} - -// trainSample holds a tokenised training example. -type trainSample struct { - Tokens []int32 // Full token sequence - Mask []int32 // 1 for assistant tokens, 0 for prompt tokens -} - -func runTrain(cmd *cli.Command, args []string) error { - start := time.Now() - - // --- Load model --- - slog.Info("loading model", "path", trainModelPath) - m, err := model.LoadModel(trainModelPath) - if err != nil { - return fmt.Errorf("load model: %w", err) - } - - mlx.SetCacheLimit(uint64(trainMemoryLimit) * 1024 * 1024 * 1024) - mlx.SetMemoryLimit(uint64(trainMemoryLimit) * 1024 * 1024 * 1024) - - tok := m.Tokenizer() - slog.Info("model loaded", - "type", m.ModelType(), - "layers", m.NumLayers(), - ) - - // --- Apply LoRA --- - targets := strings.Split(trainTargets, ",") - cfg := mlx.LoRAConfig{ - Rank: trainRank, - Alpha: float32(trainAlpha), - TargetKeys: targets, - } - - adapter := m.ApplyLoRA(cfg) - slog.Info("LoRA applied", - "rank", cfg.Rank, - "alpha", cfg.Alpha, - "targets", targets, - "trainable_params", adapter.TotalParams(), - "layers", len(adapter.Layers), - ) - - // --- Load training data --- - samples, err := loadTrainingSamples(trainData, tok, m.ModelType(), trainMaxSeqLen) - if err != nil { - return fmt.Errorf("load training data: %w", err) - } - slog.Info("training data loaded", "samples", len(samples)) - - if len(samples) == 0 { - return fmt.Errorf("no training samples loaded") - } - - // --- Training loop --- - params := adapter.AllTrainableParams() - opt := mlx.NewAdamW(trainLR) - - // Build argument indices for ValueAndGrad (all params) - argIndices := make([]int, len(params)) - for i := range argIndices { - argIndices[i] = i - } - - var totalLoss float64 - var totalSteps int - - for epoch := 0; epoch < trainEpochs; epoch++ { - var epochLoss float64 - epochStart := time.Now() - - for si, sample := range samples { - // Build token tensors: input = tokens[:-1], target = tokens[1:] - seqLen := len(sample.Tokens) - if seqLen < 2 { - continue - } - - inputTokens := sample.Tokens[:seqLen-1] - targetTokens := sample.Tokens[1:] - maskTokens := sample.Mask[1:] // mask aligned with targets - - inputArr := mlx.FromValues(inputTokens, 1, len(inputTokens)) - targetArr := mlx.FromValues(targetTokens, 1, len(targetTokens)) - - // Build float32 mask - maskF32 := make([]float32, len(maskTokens)) - for i, m := range maskTokens { - maskF32[i] = float32(m) - } - maskArr := mlx.FromValues(maskF32, 1, len(maskF32)) - mlx.Materialize(inputArr, targetArr, maskArr) - - // Loss function closure — takes LoRA params as inputs - lossFn := func(inputs []*mlx.Array) []*mlx.Array { - // Set LoRA params from inputs - adapter.SetAllParams(inputs) - - // Forward pass with fresh caches (no KV caching for training) - caches := m.NewCache() - logits := m.Forward(inputArr, caches) - - // Cast targets to int32 for take_along_axis - loss := mlx.MaskedCrossEntropyLoss(logits, targetArr, maskArr) - return []*mlx.Array{loss} - } - - // Compute value and gradients - grad := mlx.ValueAndGrad(lossFn, argIndices...) - values, grads, err := grad.Apply(params...) - grad.Free() - if err != nil { - return fmt.Errorf("epoch %d sample %d: gradient failed: %w", epoch, si, err) - } - - mlx.Materialize(append(values, grads...)...) - - loss := values[0].Float() - epochLoss += loss - totalSteps++ - - // Update parameters - params = opt.Step(params, grads) - adapter.SetAllParams(params) - mlx.Materialize(params...) - - // Periodic cleanup - if totalSteps%4 == 0 { - runtime.GC() - mlx.ClearCache() - } - - // Log progress - if (si+1)%10 == 0 || si == len(samples)-1 { - avgLoss := epochLoss / float64(si+1) - slog.Info("training", - "epoch", epoch+1, - "step", fmt.Sprintf("%d/%d", si+1, len(samples)), - "loss", fmt.Sprintf("%.4f", loss), - "avg_loss", fmt.Sprintf("%.4f", avgLoss), - ) - } - } - - totalLoss = epochLoss / float64(len(samples)) - elapsed := time.Since(epochStart) - slog.Info("epoch complete", - "epoch", epoch+1, - "avg_loss", fmt.Sprintf("%.4f", totalLoss), - "duration", elapsed.Round(time.Second), - "samples_per_sec", fmt.Sprintf("%.1f", float64(len(samples))/elapsed.Seconds()), - ) - } - - // --- Save adapter --- - if err := adapter.Save(trainOutput); err != nil { - return fmt.Errorf("save adapter: %w", err) - } - - slog.Info("training complete", - "output", trainOutput, - "total_steps", totalSteps, - "final_loss", fmt.Sprintf("%.4f", totalLoss), - "duration", time.Since(start).Round(time.Second), - "trainable_params", adapter.TotalParams(), - ) - - return nil -} - -// loadTrainingSamples reads JSONL and tokenises each conversation. -func loadTrainingSamples(path string, tok *tokenizer.Tokenizer, modelType string, maxSeqLen int) ([]trainSample, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - - var samples []trainSample - scanner := bufio.NewScanner(f) - scanner.Buffer(make([]byte, 1<<20), 1<<20) // 1MB line buffer - - lineNum := 0 - for scanner.Scan() { - lineNum++ - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - var entry struct { - Messages []ml.Message `json:"messages"` - } - if err := json.Unmarshal([]byte(line), &entry); err != nil { - slog.Warn("skipping invalid line", "line", lineNum, "error", err) - continue - } - - if len(entry.Messages) == 0 { - continue - } - - sample := tokeniseConversation(entry.Messages, tok, modelType, maxSeqLen) - if sample != nil { - samples = append(samples, *sample) - } - } - - return samples, scanner.Err() -} - -// tokeniseConversation formats and tokenises a conversation, creating a mask -// that is 1 for assistant tokens and 0 for system/user tokens. -func tokeniseConversation(messages []ml.Message, tok *tokenizer.Tokenizer, modelType string, maxSeqLen int) *trainSample { - // Strategy: tokenise the full conversation, then tokenise just the prefix - // (non-assistant parts) to determine the mask boundary. - - // Build full conversation text - fullText := formatConversation(messages, modelType, true) - fullTokens := tok.Encode(fullText) - - if len(fullTokens) < 2 { - return nil - } - - // Truncate to max sequence length - if len(fullTokens) > maxSeqLen { - fullTokens = fullTokens[:maxSeqLen] - } - - // Build mask: tokenise prefix (everything up to last assistant response) - // then mark remaining tokens as assistant (mask=1) - prefixText := formatConversation(messages, modelType, false) - prefixTokens := tok.Encode(prefixText) - - mask := make([]int32, len(fullTokens)) - for i := range mask { - if i >= len(prefixTokens) { - mask[i] = 1 // assistant token - } - } - - return &trainSample{ - Tokens: fullTokens, - Mask: mask, - } -} - -// formatConversation formats messages using the model's chat template. -// If includeAssistant is false, only formats up to the last assistant turn header. -func formatConversation(messages []ml.Message, modelType string, includeAssistant bool) string { - switch modelType { - case "qwen3": - return formatQwen3Train(messages, includeAssistant) - default: - return formatGemmaTrain(messages, includeAssistant) - } -} - -func formatQwen3Train(messages []ml.Message, includeAssistant bool) string { - var sb strings.Builder - for _, msg := range messages { - if msg.Role == "assistant" && !includeAssistant { - // Write the assistant header but not the content - sb.WriteString("<|im_start|>assistant\n") - return sb.String() - } - switch msg.Role { - case "system": - sb.WriteString(fmt.Sprintf("<|im_start|>system\n%s<|im_end|>\n", msg.Content)) - case "user": - sb.WriteString(fmt.Sprintf("<|im_start|>user\n%s<|im_end|>\n", msg.Content)) - case "assistant": - sb.WriteString(fmt.Sprintf("<|im_start|>assistant\n%s<|im_end|>\n", msg.Content)) - } - } - return sb.String() -} - -func formatGemmaTrain(messages []ml.Message, includeAssistant bool) string { - var sb strings.Builder - for _, msg := range messages { - if msg.Role == "assistant" && !includeAssistant { - sb.WriteString("model\n") - return sb.String() - } - switch msg.Role { - case "user": - sb.WriteString(fmt.Sprintf("user\n%s\n", msg.Content)) - case "assistant": - sb.WriteString(fmt.Sprintf("model\n%s\n", msg.Content)) - case "system": - sb.WriteString(fmt.Sprintf("user\n[System: %s]\n", msg.Content)) - } - } - return sb.String() -} diff --git a/cmd/ml/cmd_train_init.go b/cmd/ml/cmd_train_init.go deleted file mode 100644 index 263966d9..00000000 --- a/cmd/ml/cmd_train_init.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build darwin && arm64 - -package ml - -func init() { - mlCmd.AddCommand(trainCmd) -} diff --git a/cmd/ml/cmd_worker.go b/cmd/ml/cmd_worker.go deleted file mode 100644 index 73767de3..00000000 --- a/cmd/ml/cmd_worker.go +++ /dev/null @@ -1,80 +0,0 @@ -package ml - -import ( - "time" - - "forge.lthn.ai/core/go/pkg/cli" - "forge.lthn.ai/core/go-ml" -) - -var ( - workerAPIBase string - workerID string - workerName string - workerAPIKey string - workerGPU string - workerVRAM int - workerLangs string - workerModels string - workerInferURL string - workerTaskType string - workerBatchSize int - workerPoll time.Duration - workerOneShot bool - workerDryRun bool -) - -var workerCmd = &cli.Command{ - Use: "worker", - Short: "Run a distributed worker node", - Long: "Polls the LEM API for tasks, runs local inference, and submits results.", - RunE: runWorker, -} - -func init() { - workerCmd.Flags().StringVar(&workerAPIBase, "api", ml.EnvOr("LEM_API", "https://infer.lthn.ai"), "LEM API base URL") - workerCmd.Flags().StringVar(&workerID, "id", ml.EnvOr("LEM_WORKER_ID", ml.MachineID()), "Worker ID") - workerCmd.Flags().StringVar(&workerName, "name", ml.EnvOr("LEM_WORKER_NAME", ml.Hostname()), "Worker display name") - workerCmd.Flags().StringVar(&workerAPIKey, "key", ml.EnvOr("LEM_API_KEY", ""), "API key") - workerCmd.Flags().StringVar(&workerGPU, "gpu", ml.EnvOr("LEM_GPU", ""), "GPU type") - workerCmd.Flags().IntVar(&workerVRAM, "vram", ml.IntEnvOr("LEM_VRAM_GB", 0), "GPU VRAM in GB") - workerCmd.Flags().StringVar(&workerLangs, "languages", ml.EnvOr("LEM_LANGUAGES", ""), "Comma-separated language codes") - workerCmd.Flags().StringVar(&workerModels, "models", ml.EnvOr("LEM_MODELS", ""), "Comma-separated model names") - workerCmd.Flags().StringVar(&workerInferURL, "infer", ml.EnvOr("LEM_INFER_URL", "http://localhost:8090"), "Local inference endpoint") - workerCmd.Flags().StringVar(&workerTaskType, "type", "", "Filter by task type") - workerCmd.Flags().IntVar(&workerBatchSize, "batch", 5, "Tasks per poll") - workerCmd.Flags().DurationVar(&workerPoll, "poll", 30*time.Second, "Poll interval") - workerCmd.Flags().BoolVar(&workerOneShot, "one-shot", false, "Process one batch and exit") - workerCmd.Flags().BoolVar(&workerDryRun, "dry-run", false, "Fetch tasks but don't run inference") -} - -func runWorker(cmd *cli.Command, args []string) error { - if workerAPIKey == "" { - workerAPIKey = ml.ReadKeyFile() - } - - cfg := &ml.WorkerConfig{ - APIBase: workerAPIBase, - WorkerID: workerID, - Name: workerName, - APIKey: workerAPIKey, - GPUType: workerGPU, - VRAMGb: workerVRAM, - InferURL: workerInferURL, - TaskType: workerTaskType, - BatchSize: workerBatchSize, - PollInterval: workerPoll, - OneShot: workerOneShot, - DryRun: workerDryRun, - } - - if workerLangs != "" { - cfg.Languages = ml.SplitComma(workerLangs) - } - if workerModels != "" { - cfg.Models = ml.SplitComma(workerModels) - } - - ml.RunWorkerLoop(cfg) - return nil -} diff --git a/cmd/ml/serve_backend_default.go b/cmd/ml/serve_backend_default.go deleted file mode 100644 index 1e564b17..00000000 --- a/cmd/ml/serve_backend_default.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !(darwin && arm64) - -package ml - -import "forge.lthn.ai/core/go-ml" - -func createServeBackend() (ml.Backend, error) { - return ml.NewHTTPBackend(apiURL, modelName), nil -} diff --git a/cmd/ml/serve_backend_mlx.go b/cmd/ml/serve_backend_mlx.go deleted file mode 100644 index 271eaa95..00000000 --- a/cmd/ml/serve_backend_mlx.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build darwin && arm64 - -package ml - -import ( - "fmt" - "log/slog" - - "forge.lthn.ai/core/go-ml" -) - -func createServeBackend() (ml.Backend, error) { - if serveModelPath != "" { - slog.Info("ml serve: loading native MLX backend", "path", serveModelPath) - b, err := ml.NewMLXBackend(serveModelPath) - if err != nil { - return nil, fmt.Errorf("mlx backend: %w", err) - } - return b, nil - } - return ml.NewHTTPBackend(apiURL, modelName), nil -} diff --git a/cmd/pkgcmd/cmd_search.go b/cmd/pkgcmd/cmd_search.go index 3fe59e8c..7da952b0 100644 --- a/cmd/pkgcmd/cmd_search.go +++ b/cmd/pkgcmd/cmd_search.go @@ -74,7 +74,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error cacheDir = filepath.Join(filepath.Dir(regPath), ".core", "cache") } - c, err := cache.New(cacheDir, 0) + c, err := cache.New(coreio.Local, cacheDir, 0) if err != nil { c = nil } diff --git a/go.mod b/go.mod index 1cb8c6a9..f26bafc2 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,19 @@ module forge.lthn.ai/core/cli go 1.25.5 require ( - forge.lthn.ai/core/go v0.0.0 - forge.lthn.ai/core/go-agentic v0.0.0 - forge.lthn.ai/core/go-ai v0.0.0 - forge.lthn.ai/core/go-api v0.0.0 - forge.lthn.ai/core/go-crypt v0.0.0 - forge.lthn.ai/core/go-devops v0.0.0 - forge.lthn.ai/core/go-inference v0.0.0 - forge.lthn.ai/core/go-ml v0.0.0 - forge.lthn.ai/core/go-mlx v0.0.0 - forge.lthn.ai/core/go-netops v0.0.0 - forge.lthn.ai/core/go-rag v0.0.0 - forge.lthn.ai/core/go-scm v0.0.0 - forge.lthn.ai/core/go-store v0.0.0 - forge.lthn.ai/core/go-webview v0.0.0 + forge.lthn.ai/core/go v0.0.0-20260221191103-d091fa62023f + forge.lthn.ai/core/go-agentic v0.0.0-20260221191948-ad0cf5c932a3 + forge.lthn.ai/core/go-ai v0.0.0-20260221192232-bc9597c19153 + forge.lthn.ai/core/go-api v0.0.0-20260221015744-0d3479839dc5 + forge.lthn.ai/core/go-crypt v0.0.0-20260221190941-9585da8e6649 + forge.lthn.ai/core/go-devops v0.0.0-20260221192100-4b5739fbd7ac + forge.lthn.ai/core/go-inference v0.0.0-20260220151119-1576f744d105 // indirect + forge.lthn.ai/core/go-ml v0.0.0-20260221191458-812c926dac42 + forge.lthn.ai/core/go-mlx v0.0.0-20260221191404-2292557fd65f // indirect + forge.lthn.ai/core/go-netops v0.0.0-20260221192152-565b16a848ae + forge.lthn.ai/core/go-rag v0.0.0-20260221191926-4c741992dc78 + forge.lthn.ai/core/go-scm v0.0.0-20260221192735-5bfafcd6fc87 + forge.lthn.ai/core/go-store v0.1.1-0.20260220151120-0284110ccadf // indirect ) require ( @@ -49,6 +48,7 @@ require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apache/arrow-go/v18 v18.5.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/brianvoe/gofakeit/v6 v6.28.0 // indirect @@ -59,13 +59,22 @@ require ( github.com/casbin/casbin/v2 v2.135.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect @@ -123,13 +132,20 @@ require ( github.com/leaanthony/debme v1.2.1 // indirect github.com/leaanthony/gosod v1.0.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/marcboeker/go-duckdb v1.8.5 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/modelcontextprotocol/go-sdk v1.3.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/oasdiff/oasdiff v1.11.10 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect @@ -145,6 +161,9 @@ require ( github.com/qdrant/go-client v1.16.2 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -172,6 +191,7 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yargevad/filepathx v1.0.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect @@ -181,6 +201,7 @@ require ( go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect @@ -194,21 +215,8 @@ require ( google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect -) - -replace ( - forge.lthn.ai/core/go => ../go - forge.lthn.ai/core/go-agentic => ../go-agentic - forge.lthn.ai/core/go-ai => ../go-ai - forge.lthn.ai/core/go-api => ../../go-api - forge.lthn.ai/core/go-crypt => ../go-crypt - forge.lthn.ai/core/go-devops => ../go-devops - forge.lthn.ai/core/go-inference => ../go-inference - forge.lthn.ai/core/go-ml => ../go-ml - forge.lthn.ai/core/go-mlx => ../go-mlx - forge.lthn.ai/core/go-netops => ../go-netops - forge.lthn.ai/core/go-rag => ../go-rag - forge.lthn.ai/core/go-scm => ../go-scm - forge.lthn.ai/core/go-store => ../go-store - forge.lthn.ai/core/go-webview => ../go-webview + modernc.org/libc v1.67.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.46.1 // indirect ) diff --git a/go.sum b/go.sum index 9205934a..3e0c5ecb 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,32 @@ codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jv codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +forge.lthn.ai/core/go v0.0.0-20260221191103-d091fa62023f h1:CcSh/FFY93K5m0vADHLxwxKn2pTIM8HzYX1eGa4WZf4= +forge.lthn.ai/core/go v0.0.0-20260221191103-d091fa62023f/go.mod h1:WCPJVEZm/6mTcJimHV0uX8ZhnKEF3dN0rQp13ByaSPg= +forge.lthn.ai/core/go-agentic v0.0.0-20260221191948-ad0cf5c932a3 h1:6H3hjqHY0loJJe9iCofFzw6x5JDIbi6JNSL0oW2TKFE= +forge.lthn.ai/core/go-agentic v0.0.0-20260221191948-ad0cf5c932a3/go.mod h1:2WCSLupRyAeSpmFWM5+OPG0/wa4KMQCO8gA0hM9cUq8= +forge.lthn.ai/core/go-ai v0.0.0-20260221192232-bc9597c19153 h1:11XJI5RPm38l664KC9acRZz2gA+RLpmxCdg5JorimoM= +forge.lthn.ai/core/go-ai v0.0.0-20260221192232-bc9597c19153/go.mod h1:GdcXgm3jwvh4AVxrlCa0Zbw4vASeNV8JSAXfftCJVRc= +forge.lthn.ai/core/go-api v0.0.0-20260221015744-0d3479839dc5 h1:60reee4fmT4USZqEd6dyCTXsTj47eOOEc6Pp0HHJbd0= +forge.lthn.ai/core/go-api v0.0.0-20260221015744-0d3479839dc5/go.mod h1:f0hPLX+GZT/ME8Tb7c8wVDlfLqnpOKRwf2k5lpJq87g= +forge.lthn.ai/core/go-crypt v0.0.0-20260221190941-9585da8e6649 h1:Rs3bfSU8u1wkzYeL21asL7IcJIBVwOhtRidcEVj/PkA= +forge.lthn.ai/core/go-crypt v0.0.0-20260221190941-9585da8e6649/go.mod h1:RS+sz5lChrbc1AEmzzOULsTiMv3bwcwVtwbZi+c/Yjk= +forge.lthn.ai/core/go-devops v0.0.0-20260221192100-4b5739fbd7ac h1:agYaMGTUw0n/vPrv0i8mTxbKt5NItDcsXhCKQHoivy8= +forge.lthn.ai/core/go-devops v0.0.0-20260221192100-4b5739fbd7ac/go.mod h1:FSp7+jfV3QXyPzL1C8XZm6W57vjT8cbWly8vf/bPJEg= +forge.lthn.ai/core/go-inference v0.0.0-20260220151119-1576f744d105 h1:CVUVxp1BfUI8wmlEUW0Nay8w4hADR54nqBmeF+KK2Ac= +forge.lthn.ai/core/go-inference v0.0.0-20260220151119-1576f744d105/go.mod h1:hmLtynfw1yo0ByuX3pslLZMgCdqJH2r+2+wGJDhmmi0= +forge.lthn.ai/core/go-ml v0.0.0-20260221191458-812c926dac42 h1:rxhnHgWVGnQ93/mhUyLxIw/Q2l80njiGfNvv0kKISb0= +forge.lthn.ai/core/go-ml v0.0.0-20260221191458-812c926dac42/go.mod h1:lmhzv04VCP41ym7Wuhck+T1HeC5PoLtfOqXe8fW26Hc= +forge.lthn.ai/core/go-mlx v0.0.0-20260221191404-2292557fd65f h1:dlb6hFFhxfnJvD1ZYoQVsxD9NM4CV+sXkjHa6kBGzeE= +forge.lthn.ai/core/go-mlx v0.0.0-20260221191404-2292557fd65f/go.mod h1:QHspfOk9MgbuG6Wb4m+RzQyCMibtoQNZw+hUs4yclOA= +forge.lthn.ai/core/go-netops v0.0.0-20260221192152-565b16a848ae h1:1WPKohhwPCEPnKZPx80AqJS306QkKemGU0W4TKUgvqA= +forge.lthn.ai/core/go-netops v0.0.0-20260221192152-565b16a848ae/go.mod h1:YljW66VyXrWX5/kfmDlFaeFRewXA2/ss9F6shSTr5Rs= +forge.lthn.ai/core/go-rag v0.0.0-20260221191926-4c741992dc78 h1:M7ftoQ3AB87W/h4cELK+dxetzLoQi68KwnK2JhkSA8k= +forge.lthn.ai/core/go-rag v0.0.0-20260221191926-4c741992dc78/go.mod h1:f0WQYSeg3Oc7gCHTLUL0aCIzK1fS2mgMBDnBzjKgOzQ= +forge.lthn.ai/core/go-scm v0.0.0-20260221192735-5bfafcd6fc87 h1:1rkrRCVOq4hjKGkXxPmyBDVjxs82VV84ED/WnrYjptE= +forge.lthn.ai/core/go-scm v0.0.0-20260221192735-5bfafcd6fc87/go.mod h1:lK2RacccYr9Uvntbhx9sPupXlI2IvNufeil4mXVpdEM= +forge.lthn.ai/core/go-store v0.1.1-0.20260220151120-0284110ccadf h1:EDKI+OM0M+l4+VclG5XuUDoYAM8yu8uleFYReeEYwHY= +forge.lthn.ai/core/go-store v0.1.1-0.20260220151120-0284110ccadf/go.mod h1:FpUlLEX/ebyoxpk96F7ktr0vYvmFtC5Rpi9fi88UVqw= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/99designs/gqlgen v0.17.87 h1:pSnCIMhBQezAE8bc1GNmfdLXFmnWtWl1GRDFEE/nHP8= @@ -54,6 +80,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -61,6 +89,10 @@ github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/ github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= @@ -76,6 +108,18 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= @@ -92,12 +136,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -208,6 +258,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= @@ -258,6 +310,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/ github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -270,6 +324,10 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= @@ -285,6 +343,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oasdiff/oasdiff v1.11.10 h1:4I9VrktUoHmwydkJqVOC7Bd6BXKu9dc4UUP3PIu1VjM= github.com/oasdiff/oasdiff v1.11.10/go.mod h1:GXARzmqBKN8lZHsTQD35ZM41ePbu6JdAZza4sRMeEKg= @@ -321,6 +387,13 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -400,6 +473,8 @@ github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQs github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= @@ -429,6 +504,8 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -476,6 +553,7 @@ golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -530,3 +608,31 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI= +modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/go.work b/go.work deleted file mode 100644 index 5b53a4fd..00000000 --- a/go.work +++ /dev/null @@ -1,18 +0,0 @@ -go 1.25.5 - -use ( - . - ../go - ../go-agentic - ../go-ai - ../go-crypt - ../go-devops - ../go-inference - ../go-ml - ../go-mlx - ../go-netops - ../go-rag - ../go-scm - ../go-store - ../go-webview -) diff --git a/main.go b/main.go index 7c430aea..305505ea 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( _ "forge.lthn.ai/core/cli/cmd/help" _ "forge.lthn.ai/core/cli/cmd/lab" _ "forge.lthn.ai/core/cli/cmd/mcpcmd" - _ "forge.lthn.ai/core/cli/cmd/ml" + _ "forge.lthn.ai/core/go-ml/cmd" _ "forge.lthn.ai/core/cli/cmd/module" _ "forge.lthn.ai/core/cli/cmd/monitor" _ "forge.lthn.ai/core/cli/cmd/pkgcmd"