Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
135 lines
4.6 KiB
Go
135 lines
4.6 KiB
Go
// Per-tenant configuration for an EXTERNAL, already-running Ollama server
|
|
// (never installed on the archivdms host — the base URL/port comes from the
|
|
// tenant admin). Gates the optional 'ollama' metadata-suggestion provider.
|
|
//
|
|
// GET /api/ollama-config
|
|
// PUT /api/ollama-config
|
|
//
|
|
// Both are admin-only (domain_admin manages its own tenant; superadmin must
|
|
// pass ?tenant_id=), mirroring the LDAP-config and tenant-settings handlers.
|
|
// The base URL is an internal network URL, not a secret, and is returned as-is.
|
|
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"archivdms/internal/audit"
|
|
"archivdms/internal/llm"
|
|
)
|
|
|
|
// handleGetOllamaConfig returns the tenant's Ollama connection config. A tenant
|
|
// that has never configured Ollama gets a zero/default (disabled) config, not a
|
|
// 404 — the frontend always renders an editable form.
|
|
func (s *Server) handleGetOllamaConfig(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
cfg, err := s.store.GetOllamaConfig(r.Context(), tenantID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "load ollama config failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, cfg)
|
|
}
|
|
|
|
// handleListOllamaModels queries an Ollama server for the models actually
|
|
// installed there, so the frontend can offer a picklist instead of a free-text
|
|
// field. Live call, no caching. Prefers the not-yet-saved ?base_url= query
|
|
// param (lets the admin test a URL before hitting "Speichern"); falls back to
|
|
// the persisted config's base_url when the param is absent. 400 if neither is
|
|
// set. When the external Ollama server is unreachable the failure is the
|
|
// external dependency's, not ours → 502 Bad Gateway, not 500.
|
|
func (s *Server) handleListOllamaModels(w http.ResponseWriter, r *http.Request) {
|
|
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
cfg, err := s.store.GetOllamaConfig(r.Context(), tenantID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "load ollama config failed")
|
|
return
|
|
}
|
|
baseURL := r.URL.Query().Get("base_url")
|
|
if baseURL == "" {
|
|
baseURL = cfg.BaseURL
|
|
}
|
|
if baseURL == "" {
|
|
writeError(w, http.StatusBadRequest, "Server-URL muss zuerst eingetragen werden")
|
|
return
|
|
}
|
|
|
|
// Short, listing-specific timeout — independent of the (possibly long)
|
|
// generate timeout. Cap the stored value so a large generate timeout does
|
|
// not make the picklist request hang for minutes.
|
|
timeout := 10 * time.Second
|
|
if cfg.TimeoutSeconds > 0 && cfg.TimeoutSeconds < 10 {
|
|
timeout = time.Duration(cfg.TimeoutSeconds) * time.Second
|
|
}
|
|
|
|
models, err := llm.ListModels(r.Context(), baseURL, timeout)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "ollama nicht erreichbar: "+err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string][]string{"models": models})
|
|
}
|
|
|
|
type upsertOllamaConfigRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
BaseURL string `json:"base_url"`
|
|
Model string `json:"model"`
|
|
TimeoutSeconds int `json:"timeout_seconds"`
|
|
}
|
|
|
|
// handleUpsertOllamaConfig creates or updates the tenant's Ollama connection
|
|
// config. Validation (enabled requires base_url+model, http(s) prefix, timeout
|
|
// range) lives in the store. Every attempt — success or failure — is
|
|
// audit-logged (GoBD).
|
|
func (s *Server) handleUpsertOllamaConfig(w http.ResponseWriter, r *http.Request) {
|
|
sess := sessionFromCtx(r.Context())
|
|
|
|
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
logFail := func(detail string) {
|
|
tid := tenantID
|
|
s.audlog.Log(audit.Entry{
|
|
EventType: audit.EventOllamaConfigUpdate, Username: sess.Username,
|
|
TenantID: &tid, Success: false, Detail: detail,
|
|
})
|
|
}
|
|
|
|
var req upsertOllamaConfigRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
logFail("ollama_config invalid_body")
|
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
if err := s.store.UpsertOllamaConfig(r.Context(), tenantID, req.Enabled, req.BaseURL, req.Model, req.TimeoutSeconds); err != nil {
|
|
logFail("ollama_config upsert_failed:" + err.Error())
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
tid := tenantID
|
|
s.audlog.Log(audit.Entry{
|
|
EventType: audit.EventOllamaConfigUpdate, Username: sess.Username,
|
|
TenantID: &tid, Success: true, Detail: "ollama_config_saved",
|
|
})
|
|
|
|
// Reload so the response reflects the persisted (normalised) state.
|
|
cfg, err := s.store.GetOllamaConfig(r.Context(), tenantID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "load ollama config failed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, cfg)
|
|
}
|