From 54ba04c52a59b7b4a37077ebd761f777108ef2f1 Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Thu, 8 Oct 2026 15:42:40 +0200 Subject: [PATCH] llmgw-kb collections names each collection's owner as people know it Co-Authored-By: Claude Opus 5.5 (1M context) --- cmd/llmgw-kb/main.go | 2 +- internal/kb/kbsync/api.go | 1 + internal/router/kb.go | 15 +++++++++------ internal/router/kb_test.go | 2 +- internal/store/knowledge.go | 24 ++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 8 deletions(-) diff --git a/cmd/llmgw-kb/main.go b/cmd/llmgw-kb/main.go index 0636c7f..7cad7dc 100644 --- a/cmd/llmgw-kb/main.go +++ b/cmd/llmgw-kb/main.go @@ -237,7 +237,7 @@ func collectionsCmd(g *globalFlags) *cobra.Command { if x.Writable { access = "read, write" } - fmt.Fprintf(tw, "%s\t%s\t%s\t%s (%d)\n", x.Name, access, x.Owner, x.EmbedModel, x.Dims) + fmt.Fprintf(tw, "%s\t%s\t%s\t%s (%d)\n", x.Name, access, or(x.OwnerName, x.Owner), x.EmbedModel, x.Dims) } return tw.Flush() }, diff --git a/internal/kb/kbsync/api.go b/internal/kb/kbsync/api.go index 2710d9e..e8a2536 100644 --- a/internal/kb/kbsync/api.go +++ b/internal/kb/kbsync/api.go @@ -59,6 +59,7 @@ type Collection struct { ID string `json:"id"` Name string `json:"name"` Owner string `json:"owner"` + OwnerName string `json:"owner_name"` EmbedModel string `json:"embed_model"` Dims int `json:"dims"` Writable bool `json:"writable"` diff --git a/internal/router/kb.go b/internal/router/kb.go index a75efa3..707bb8c 100644 --- a/internal/router/kb.go +++ b/internal/router/kb.go @@ -43,9 +43,12 @@ const ( // KBCollectionView is a collection as a caller sees it. type KBCollectionView struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id"` + Name string `json:"name"` + // Owner is the owning scope as scope:ID; OwnerName how people know it, + // "Acme / hr". Owner string `json:"owner"` + OwnerName string `json:"owner_name,omitempty"` EmbedModel string `json:"embed_model"` Dims int `json:"dims"` // Writable is whether this caller may add and remove documents. @@ -119,9 +122,9 @@ func (r *Router) kbCaller(w http.ResponseWriter, req *http.Request) (auth.Princi return p, e, true } -func kbView(c storepkg.KBCollection, writable bool) KBCollectionView { +func (r *Router) kbView(ctx context.Context, c storepkg.KBCollection, writable bool) KBCollectionView { return KBCollectionView{ID: c.ID, Name: c.Name, Owner: string(c.OwnerScope) + ":" + c.OwnerID, - EmbedModel: c.EmbedModel, Dims: c.Dims, Writable: writable} + OwnerName: r.store.OwnerName(ctx, c.OwnerScope, c.OwnerID), EmbedModel: c.EmbedModel, Dims: c.Dims, Writable: writable} } // kbCollection finds a collection by id or name among those the caller may @@ -172,7 +175,7 @@ func (r *Router) handleKBCollections(w http.ResponseWriter, req *http.Request) { } out := []KBCollectionView{} for _, c := range read { - out = append(out, kbView(c, writable[c.ID])) + out = append(out, r.kbView(req.Context(), c, writable[c.ID])) } writeJSON(w, http.StatusOK, map[string]any{"object": "list", "data": out}) } @@ -192,7 +195,7 @@ func (r *Router) handleKBManifest(w http.ResponseWriter, req *http.Request) { return } writable, _ := r.store.CollectionsFor(req.Context(), e, true) - m := KBManifest{Collection: kbView(c, containsCollection(writable, c.ID)), Documents: []KBDocumentView{}} + m := KBManifest{Collection: r.kbView(req.Context(), c, containsCollection(writable, c.ID)), Documents: []KBDocumentView{}} for _, d := range docs { m.Documents = append(m.Documents, KBDocumentView{Name: d.Name, Digest: d.Digest, Bytes: d.Bytes, Chunks: d.Chunks, AddedAt: d.AddedAt}) diff --git a/internal/router/kb_test.go b/internal/router/kb_test.go index b82d40b..f8c0e20 100644 --- a/internal/router/kb_test.go +++ b/internal/router/kb_test.go @@ -138,7 +138,7 @@ func TestKnowledgeBasesKeepTenantsApart(t *testing.T) { t.Errorf("read grant, put: %d", code) } if code, _, body := call(t, h, salesKey, http.MethodGet, "/v1/kb/collections", nil); code != http.StatusOK || - !strings.Contains(body, "politikalar") { + !strings.Contains(body, "politikalar") || !strings.Contains(body, `"owner_name":"a-`+sfx+` / t / hr"`) { t.Errorf("read grant, list: %d %s", code, body) } if _, _, body := call(t, h, otherKey, http.MethodGet, "/v1/kb/collections", nil); strings.Contains(body, "politikalar") { diff --git a/internal/store/knowledge.go b/internal/store/knowledge.go index baca380..2bda459 100644 --- a/internal/store/knowledge.go +++ b/internal/store/knowledge.go @@ -893,3 +893,27 @@ func (s *DB) kbDropPartition(ctx context.Context, c conn, scope Scope, id string _, err := c.ExecContext(ctx, `DROP TABLE IF EXISTS `+kbPartition(id)) return err } + +// OwnerName is how people know a collection's owner: "Acme / hr", a +// project's path, a person's name. The ID when it cannot be found. +func (s *DB) OwnerName(ctx context.Context, scope Scope, id string) string { + var q string + switch scope { + case ScopeOrg: + q = `SELECT name FROM organizations WHERE id = ?` + case ScopeTeam: + q = `SELECT o.name || ' / ' || t.name FROM teams t JOIN organizations o ON o.id = t.org_id WHERE t.id = ?` + case ScopeProject: + q = `SELECT o.name || ' / ' || t.name || ' / ' || p.name FROM projects p + JOIN teams t ON t.id = p.team_id JOIN organizations o ON o.id = t.org_id WHERE p.id = ?` + case ScopeUser: + q = `SELECT CASE WHEN name <> '' THEN name ELSE email END FROM users WHERE id = ?` + default: + return id + } + var name string + if err := s.db.QueryRowContext(ctx, q, id).Scan(&name); err != nil { + return id + } + return name +}