mirror of
https://github.com/saymrwulf/KnowledgeRefinery.git
synced 2026-06-17 01:23:48 +00:00
macOS app for corpus ingestion, semantic search, and concept universe visualization powered by local LLMs via LM Studio. Architecture: - Go daemon (17MB single binary, zero dependencies) - chi router, pure-Go SQLite, tiktoken tokenizer - 6-stage pipeline: scan → extract → chunk → embed → annotate → conceptualize - Brute-force cosine vector search in memory - 89 tests across 8 packages - SwiftUI app (macOS 15+) - Multi-workspace management with auto-start daemons - Live pipeline progress, search, concept browser - WebGPU 3D universe renderer with Canvas2D fallback - Custom crystal app icon
143 lines
3.5 KiB
Go
143 lines
3.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/oho/knowledge-refinery-daemon/internal/lmstudio"
|
|
"github.com/oho/knowledge-refinery-daemon/internal/storage"
|
|
)
|
|
|
|
type searchRequest struct {
|
|
Query string `json:"query"`
|
|
Limit int `json:"limit"`
|
|
FilterAssetType *string `json:"filter_asset_type"`
|
|
}
|
|
|
|
type searchResultItem struct {
|
|
ChunkID string `json:"chunk_id"`
|
|
Score float64 `json:"score"`
|
|
Text string `json:"text"`
|
|
AssetID string `json:"asset_id"`
|
|
AssetPath string `json:"asset_path"`
|
|
EvidenceAnchor string `json:"evidence_anchor"`
|
|
Topics *string `json:"topics"`
|
|
Summary *string `json:"summary"`
|
|
Sentiment *string `json:"sentiment"`
|
|
Entities []string `json:"entities"`
|
|
}
|
|
|
|
func SearchRouter(lm *lmstudio.Client, vs *storage.VectorStore, db *storage.Database) chi.Router {
|
|
r := chi.NewRouter()
|
|
|
|
doSearch := func(query string, limit int) ([]searchResultItem, error) {
|
|
rawVec, err := lm.EmbedSingle(query, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert float64 to float32
|
|
queryVec := make([]float32, len(rawVec))
|
|
for i, v := range rawVec {
|
|
queryVec[i] = float32(v)
|
|
}
|
|
|
|
results := vs.Search(queryVec, limit)
|
|
|
|
items := make([]searchResultItem, len(results))
|
|
for i, res := range results {
|
|
item := searchResultItem{
|
|
ChunkID: res.ID,
|
|
Score: res.Distance,
|
|
Text: res.Text,
|
|
AssetID: res.AssetID,
|
|
AssetPath: res.AssetPath,
|
|
EvidenceAnchor: res.EvidenceAnchor,
|
|
}
|
|
|
|
if res.Topics != "" {
|
|
item.Topics = &res.Topics
|
|
}
|
|
|
|
// Enrich with annotation
|
|
ann, _ := db.GetCurrentAnnotation(res.ID)
|
|
if ann != nil {
|
|
if ann.TopicsJSON != nil {
|
|
var topics []string
|
|
json.Unmarshal([]byte(*ann.TopicsJSON), &topics)
|
|
if len(topics) > 0 {
|
|
joined := ""
|
|
for i, t := range topics {
|
|
if i > 0 {
|
|
joined += ", "
|
|
}
|
|
joined += t
|
|
}
|
|
item.Topics = &joined
|
|
}
|
|
}
|
|
item.Summary = ann.Summary
|
|
item.Sentiment = ann.SentimentLabel
|
|
if ann.EntitiesJSON != nil {
|
|
var entities []map[string]string
|
|
json.Unmarshal([]byte(*ann.EntitiesJSON), &entities)
|
|
for _, e := range entities {
|
|
if name, ok := e["name"]; ok {
|
|
item.Entities = append(item.Entities, name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
items[i] = item
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
var req searchRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Limit <= 0 {
|
|
req.Limit = 20
|
|
}
|
|
|
|
items, err := doSearch(req.Query, req.Limit)
|
|
if err != nil {
|
|
http.Error(w, "Failed to embed query: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(items)
|
|
})
|
|
|
|
r.Get("/quick", func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query().Get("q")
|
|
if q == "" {
|
|
http.Error(w, "q parameter required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
limit := 10
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
|
limit = n
|
|
}
|
|
}
|
|
|
|
items, err := doSearch(q, limit)
|
|
if err != nil {
|
|
http.Error(w, "Failed to embed query: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(items)
|
|
})
|
|
|
|
return r
|
|
}
|