Files
jbergner a6bc71fb3a
Some checks failed
release-tag / Resolve release metadata (push) Successful in 30s
release-tag / Build knowledge (push) Failing after 4m51s
release-tag / Build control (push) Failing after 5m0s
release-tag / Build agent (push) Failing after 5m0s
release-tag / Build agent-data-init (push) Failing after 5m5s
release-tag / Build neuroforge-worker (push) Failing after 5m7s
release-tag / Build neuroforge (push) Failing after 5m9s
Init
2026-08-26 18:34:41 +02:00

199 lines
4.9 KiB
Go

package httpapi
import (
"encoding/json"
"errors"
"io"
"net/http"
"path/filepath"
"strconv"
"strings"
"neuroforge/internal/brain"
)
func (s *Server) ingestText(w http.ResponseWriter, r *http.Request) {
var q brain.IngestTextRequest
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
}
out, err := s.brain.IngestText(r.Context(), q)
if err != nil {
s.err(w, 400, err)
return
}
s.json(w, http.StatusCreated, out)
}
func (s *Server) ingestDocument(w http.ResponseWriter, r *http.Request) {
cfg := s.store.Config()
max := cfg.Ingestion.MaxDocumentBytes
if max <= 0 {
max = 25 << 20
}
// requestLimits already caps the full body; this is a second, route-specific
// bound on the actual uploaded file.
if err := r.ParseMultipartForm(max + (1 << 20)); err != nil {
s.err(w, 400, err)
return
}
f, hdr, err := r.FormFile("file")
if err != nil {
s.err(w, 400, errors.New("multipart field 'file' is required"))
return
}
defer f.Close()
data, err := io.ReadAll(io.LimitReader(f, max+1))
if err != nil {
s.err(w, 400, err)
return
}
if int64(len(data)) > max {
s.err(w, http.StatusRequestEntityTooLarge, errors.New("document exceeds configured max_document_bytes"))
return
}
trust, _ := strconv.ParseFloat(strings.TrimSpace(r.FormValue("trust")), 64)
tags := splitCSV(r.FormValue("tags"))
name := filepath.Base(hdr.Filename)
out, err := s.brain.IngestDocument(r.Context(), name, hdr.Header.Get("Content-Type"), r.FormValue("title"), data, tags, trust)
if err != nil {
s.err(w, 400, err)
return
}
s.json(w, http.StatusCreated, out)
}
func (s *Server) sourcesList(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 100
}
if limit > 1000 {
limit = 1000
}
s.json(w, 200, s.store.SourcesSnapshot(limit))
}
func (s *Server) sourceGet(w http.ResponseWriter, r *http.Request) {
x, ok := s.store.GetSource(r.PathValue("id"))
if !ok {
s.err(w, 404, errors.New("source not found"))
return
}
s.json(w, 200, x)
}
func (s *Server) researchSearch(w http.ResponseWriter, r *http.Request) {
var q brain.ResearchRequest
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
}
out, err := s.brain.Research(r.Context(), q)
if err != nil {
s.err(w, 502, err)
return
}
s.json(w, 200, out)
}
func (s *Server) adminResearchGet(w http.ResponseWriter, r *http.Request) {
c := s.store.Config()
sec := s.store.Secrets()
s.json(w, 200, map[string]any{"research": c.Research, "ingestion": c.Ingestion, "autonomy": map[string]any{"enabled": c.Autonomy.Enabled, "run_on_goal_create": c.Autonomy.RunOnGoalCreate, "default_goal_interval_minutes": c.Autonomy.DefaultGoalIntervalMinutes}, "searxng_auth_configured": strings.TrimSpace(sec.SearXNGAuthHeader) != ""})
}
func (s *Server) adminResearchPut(w http.ResponseWriter, r *http.Request) {
var patch map[string]json.RawMessage
if err := decode(r, &patch); err != nil {
s.err(w, 400, err)
return
}
c := s.store.Config()
base, _ := json.Marshal(c)
var root map[string]json.RawMessage
_ = json.Unmarshal(base, &root)
for _, key := range []string{"research", "ingestion"} {
if v, ok := patch[key]; ok {
root[key] = v
}
}
if raw, ok := patch["autonomy"]; ok {
var a map[string]json.RawMessage
_ = json.Unmarshal(root["autonomy"], &a)
var ap map[string]json.RawMessage
if err := json.Unmarshal(raw, &ap); err != nil {
s.err(w, 400, err)
return
}
for k, v := range ap {
a[k] = v
}
root["autonomy"], _ = json.Marshal(a)
}
merged, _ := json.Marshal(root)
if err := json.Unmarshal(merged, &c); err != nil {
s.err(w, 400, err)
return
}
if err := s.store.ValidateConfig(c); err != nil {
s.err(w, 400, err)
return
}
if err := s.store.UpdateConfig(c); err != nil {
s.err(w, 500, err)
return
}
if raw, ok := patch["searxng_auth_header"]; ok {
var auth string
if json.Unmarshal(raw, &auth) == nil && strings.TrimSpace(auth) != "" {
for _, r := range auth {
if r < 0x20 || r > 0x7e {
s.err(w, 400, errors.New("searxng_auth_header must contain printable ASCII only"))
return
}
}
sec := s.store.Secrets()
sec.SearXNGAuthHeader = auth
if err := s.store.UpdateSecrets(sec); err != nil {
s.err(w, 500, err)
return
}
}
}
s.adminResearchGet(w, r)
}
func (s *Server) adminResearchTest(w http.ResponseWriter, r *http.Request) {
var q struct {
Query string `json:"query"`
}
if r.ContentLength != 0 {
if err := decode(r, &q); err != nil {
s.err(w, 400, err)
return
}
}
if strings.TrimSpace(q.Query) == "" {
q.Query = "NVIDIA GPU CUDA"
}
out, err := s.brain.Research(r.Context(), brain.ResearchRequest{Query: q.Query, Learn: false, FetchPages: false, MaxResults: 5})
if err != nil {
s.err(w, 502, err)
return
}
s.json(w, 200, out)
}
func splitCSV(s string) []string {
var out []string
for _, x := range strings.Split(s, ",") {
x = strings.TrimSpace(x)
if x != "" {
out = append(out, x)
}
}
return out
}