From 5f870912a35eb5fcb46339e6d5e641b1d874918d Mon Sep 17 00:00:00 2001 From: jbergner Date: Tue, 4 Aug 2026 21:14:40 +0200 Subject: [PATCH] Neural-Brain integriert --- cmd/server/app.go | 11 +++- internal/brainactivity/client.go | 90 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 internal/brainactivity/client.go diff --git a/cmd/server/app.go b/cmd/server/app.go index 8f3cf6f..a85413d 100644 --- a/cmd/server/app.go +++ b/cmd/server/app.go @@ -11,8 +11,10 @@ import ( "os" "strconv" "strings" + "time" "kb-editor/internal/aifallback" + "kb-editor/internal/brainactivity" "kb-editor/internal/staging" "kb-editor/internal/store" ) @@ -131,8 +133,15 @@ func (a *app) handleList(w http.ResponseWriter, r *http.Request) { } func (a *app) handleSearch(w http.ResponseWriter, r *http.Request) { + startedAt := time.Now() q := queryFromURL(r) - writeJSON(w, http.StatusOK, a.store.Search(q)) + result := a.store.Search(q) + hits := make([]brainactivity.Hit, 0, len(result.Items)) + for _, hit := range result.Items { + hits = append(hits, brainactivity.Hit{ID: hit.ID, Score: float64(hit.Score) / 100}) + } + brainactivity.EmitSearch("knowledgebase", q.Q, hits, time.Since(startedAt)) + writeJSON(w, http.StatusOK, result) } func (a *app) handleFacets(w http.ResponseWriter, r *http.Request) { diff --git a/internal/brainactivity/client.go b/internal/brainactivity/client.go new file mode 100644 index 0000000..fb16e0a --- /dev/null +++ b/internal/brainactivity/client.go @@ -0,0 +1,90 @@ +package brainactivity + +import ( + "bytes" + "encoding/json" + "net/http" + "os" + "strings" + "sync" + "time" +) + +type Hit struct { + ID string `json:"id"` + Score float64 `json:"score,omitempty"` +} + +type event struct { + Type string `json:"type"` + Source string `json:"source"` + Query string `json:"query,omitempty"` + Message string `json:"message,omitempty"` + Hits []Hit `json:"hits,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` +} + +var sender = newSender() + +type asyncSender struct { + once sync.Once + url string + key string + ch chan event + http *http.Client +} + +func newSender() *asyncSender { + return &asyncSender{ch: make(chan event, 128), http: &http.Client{Timeout: 3 * time.Second}} +} + +// EmitSearch is fail-open and has no effect unless BRAIN_ACTIVITY_URL is set. +// It never blocks the ticket-processing path and silently drops telemetry when +// the optional visualization is unavailable or the local queue is full. +func EmitSearch(source, query string, hits []Hit, duration time.Duration) { + sender.once.Do(sender.start) + if sender.url == "" { + return + } + query = strings.TrimSpace(query) + if len([]rune(query)) > 4000 { + query = string([]rune(query)[:4000]) + } + e := event{ + Type: "knowledge.search", Source: source, Query: query, + Message: "Wissenssuche aus " + source, + Hits: hits, Metadata: map[string]any{"duration_ms": duration.Milliseconds(), "result_count": len(hits)}, + } + select { + case sender.ch <- e: + default: + } +} + +func (s *asyncSender) start() { + s.url = strings.TrimSpace(os.Getenv("BRAIN_ACTIVITY_URL")) + s.key = strings.TrimSpace(os.Getenv("BRAIN_ACTIVITY_API_KEY")) + if s.url == "" { + return + } + go func() { + for e := range s.ch { + b, err := json.Marshal(e) + if err != nil { + continue + } + req, err := http.NewRequest(http.MethodPost, s.url, bytes.NewReader(b)) + if err != nil { + continue + } + req.Header.Set("Content-Type", "application/json") + if s.key != "" { + req.Header.Set("Authorization", "Bearer "+s.key) + } + resp, err := s.http.Do(req) + if err == nil { + _ = resp.Body.Close() + } + } + }() +}