FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user