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
199 lines
4.9 KiB
Go
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
|
|
}
|