268 lines
7.1 KiB
Go
268 lines
7.1 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"gskaro-v1/internal/llm"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
type llmLogEntry struct {
|
|
Model string
|
|
Prompt string
|
|
Answer string
|
|
Timestamp time.Time
|
|
PromptTokens int
|
|
AnswerTokens int
|
|
}
|
|
|
|
var llmHistory []llmLogEntry
|
|
|
|
// -----------------------------
|
|
// LLM Console
|
|
// -----------------------------
|
|
|
|
func llmConsoleHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("HX-Request") != "true" {
|
|
fmt.Fprintf(w, `
|
|
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>LLM Console</title>
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
</head>
|
|
<body hx-boost="true">
|
|
<div id="top-bar" style="display:flex; gap:16px; align-items:center; margin-bottom:16px;">
|
|
<div>
|
|
<button hx-get="/api/stats" hx-target="#content">Статистика</button>
|
|
<button hx-get="/api/tasks" hx-target="#content">Задачи</button>
|
|
<button hx-get="/api/notes" hx-target="#content">AI_NOTES.md</button>
|
|
<button hx-get="/api/llm/console" hx-target="#content">LLM Console</button>
|
|
<button hx-get="/api/llm/models" hx-target="#content">LLM Models</button>
|
|
<button hx-get="/api/llm/history" hx-target="#content">LLM History</button>
|
|
</div>
|
|
|
|
<div id="model-badge"
|
|
hx-get="/api/llm/status"
|
|
hx-trigger="load, every 5s">
|
|
Текущая модель: <strong>—</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="content">
|
|
`)
|
|
renderConsole(w)
|
|
fmt.Fprintf(w, `
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`)
|
|
return
|
|
}
|
|
|
|
renderConsole(w)
|
|
}
|
|
|
|
func renderConsole(w http.ResponseWriter) {
|
|
fmt.Fprintf(w, `
|
|
<h2>LLM Console</h2>
|
|
|
|
<p>Текущая модель: <strong>%s</strong></p>
|
|
|
|
<form hx-post="/api/llm/query" hx-target="#llm-output" hx-swap="innerHTML">
|
|
<textarea name="prompt" rows="4" style="width:100%%"></textarea>
|
|
<br>
|
|
<button type="submit">Отправить</button>
|
|
</form>
|
|
|
|
<div id="llm-output" style="margin-top:20px; border:1px solid #ddd; padding:10px;">
|
|
Ожидание ответа...
|
|
</div>
|
|
`, llm.ActiveModel)
|
|
}
|
|
|
|
// -----------------------------
|
|
// Список моделей
|
|
// -----------------------------
|
|
|
|
func llmModelsHandler(w http.ResponseWriter, r *http.Request) {
|
|
host := r.URL.Query().Get("host")
|
|
|
|
if host == "" {
|
|
fmt.Fprintf(w, `
|
|
<h2>Подключение к Ollama</h2>
|
|
<form hx-get="/api/llm/models" hx-target="#content">
|
|
<input type="text" name="host" placeholder="http://localhost:11434" style="width:300px;">
|
|
<button type="submit">Получить модели</button>
|
|
</form>
|
|
`)
|
|
return
|
|
}
|
|
|
|
models, err := llm.GetModelsFromHost(host)
|
|
if err != nil {
|
|
fmt.Fprintf(w, `<p style="color:red;">Ошибка подключения: %s</p>`, err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(w, `<h2>Модели на %s</h2>`, host)
|
|
fmt.Fprintf(w, `<form hx-post="/api/llm/select" hx-target="#content">`)
|
|
fmt.Fprintf(w, `<input type="hidden" name="host" value="%s">`, host)
|
|
fmt.Fprintf(w, `<select name="model">`)
|
|
|
|
for _, m := range models {
|
|
fmt.Fprintf(w, `<option value="%s">%s</option>`, m, m)
|
|
}
|
|
|
|
fmt.Fprintf(w, `</select>`)
|
|
fmt.Fprintf(w, `<button type="submit">Выбрать</button>`)
|
|
fmt.Fprintf(w, `</form>`)
|
|
}
|
|
|
|
// -----------------------------
|
|
// Выбор модели
|
|
// -----------------------------
|
|
|
|
func llmSelectHandler(w http.ResponseWriter, r *http.Request) {
|
|
host := r.FormValue("host")
|
|
model := r.FormValue("model")
|
|
|
|
if host != "" {
|
|
llm.ActiveHost = host
|
|
}
|
|
|
|
if model != "" {
|
|
llm.ActiveModel = model
|
|
}
|
|
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
w.Header().Set("HX-Redirect", "/api/llm/console")
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/api/llm/console", http.StatusSeeOther)
|
|
}
|
|
|
|
// -----------------------------
|
|
// Запрос к модели
|
|
// -----------------------------
|
|
|
|
func llmQueryHandler(w http.ResponseWriter, r *http.Request) {
|
|
prompt := r.FormValue("prompt")
|
|
if prompt == "" {
|
|
fmt.Fprintf(w, "<p style='color:red;'>Пустой запрос.</p>")
|
|
return
|
|
}
|
|
|
|
client := &llm.OllamaClient{
|
|
Host: llm.ActiveHost,
|
|
Model: llm.ActiveModel,
|
|
}
|
|
|
|
var answerBuf bytes.Buffer
|
|
var promptTokens, answerTokens int
|
|
|
|
err := client.Stream(prompt, func(chunk string, meta *llm.OllamaGenerateResponse) {
|
|
answerBuf.WriteString(chunk)
|
|
if meta != nil {
|
|
promptTokens = meta.PromptEvalCount
|
|
answerTokens = meta.EvalCount
|
|
}
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(w, "<p style='color:red;'>Ошибка: %s</p>", err)
|
|
return
|
|
}
|
|
|
|
resp := answerBuf.String()
|
|
|
|
llmHistory = append(llmHistory, llmLogEntry{
|
|
Model: llm.ActiveModel,
|
|
Prompt: prompt,
|
|
Answer: resp,
|
|
Timestamp: time.Now(),
|
|
PromptTokens: promptTokens,
|
|
AnswerTokens: answerTokens,
|
|
})
|
|
|
|
fmt.Fprintf(w, "<pre>%s</pre>", resp)
|
|
}
|
|
|
|
// -----------------------------
|
|
// Статус
|
|
// -----------------------------
|
|
|
|
func llmStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := http.Get(llm.ActiveHost + "/api/tags")
|
|
if err != nil {
|
|
fmt.Fprintf(w, `
|
|
<span style="padding:4px 8px; border-radius:4px; background:#fee; color:#900;">
|
|
Ollama: offline
|
|
</span>
|
|
|
|
<span>Модель: <strong>%s</strong></span>
|
|
`, llm.ActiveModel)
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
|
|
fmt.Fprintf(w, `
|
|
<span style="padding:4px 8px; border-radius:4px; background:#e6ffed; color:#137333;">
|
|
Ollama: online
|
|
</span>
|
|
|
|
<span>Модель: <strong>%s</strong></span>
|
|
`, llm.ActiveModel)
|
|
}
|
|
|
|
// -----------------------------
|
|
// История
|
|
// -----------------------------
|
|
|
|
func llmHistoryHandler(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, "<h2>LLM History</h2>")
|
|
|
|
if len(llmHistory) == 0 {
|
|
fmt.Fprintf(w, "<p>Пока пусто.</p>")
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(w, `<ul style="list-style:none; padding:0;">`)
|
|
|
|
for i := len(llmHistory) - 1; i >= 0; i-- {
|
|
e := llmHistory[i]
|
|
|
|
fmt.Fprintf(w, `
|
|
<li style="margin-bottom:16px; padding:12px; border:1px solid #ddd; border-radius:6px;">
|
|
<div style="color:#666; font-size:13px;">
|
|
🕒 %s
|
|
</div>
|
|
|
|
<div><strong>Модель:</strong> %s</div>
|
|
|
|
<div style="margin-top:8px;"><strong>👤 Запрос:</strong></div>
|
|
<pre style="white-space:pre-wrap; background:#fafafa; padding:8px; border-radius:4px;">%s</pre>
|
|
|
|
<div style="margin-top:8px;"><strong>🤖 Ответ модели:</strong></div>
|
|
<pre style="white-space:pre-wrap; background:#f0f7ff; padding:8px; border-radius:4px;">%s</pre>
|
|
|
|
<div style="margin-top:8px; color:#555;">
|
|
🔢 Токены: prompt=%d, answer=%d, total=%d
|
|
</div>
|
|
</li>
|
|
`,
|
|
e.Timestamp.Format("2006-01-02 15:04:05"),
|
|
e.Model,
|
|
e.Prompt,
|
|
e.Answer,
|
|
e.PromptTokens,
|
|
e.AnswerTokens,
|
|
e.PromptTokens+e.AnswerTokens,
|
|
)
|
|
}
|
|
|
|
fmt.Fprintf(w, `</ul>`)
|
|
}
|