Changeset 0.17.0 (#75)

This commit is contained in:
2026-02-27 16:25:39 +00:00
parent 8bb77710b9
commit c9141a6896
37 changed files with 2778 additions and 968 deletions

View File

@@ -147,9 +147,25 @@ func (h *KnowledgeBaseHandler) CreateKB(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "team_id required for team scope"})
return
}
// TODO: verify user is team admin
// Verify user is team admin (or system admin)
role, _ := c.Get("role")
if role != "admin" {
var teamRole string
err := database.DB.QueryRow(
`SELECT role FROM team_members WHERE team_id = $1 AND user_id = $2`,
req.TeamID, userID).Scan(&teamRole)
if err != nil || teamRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "team admin access required to create team knowledge bases"})
return
}
}
case "global":
// TODO: verify user is admin
// Verify user is system admin
role, _ := c.Get("role")
if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "admin access required to create global knowledge bases"})
return
}
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "scope must be personal, team, or global"})
return
@@ -630,9 +646,7 @@ func (h *KnowledgeBaseHandler) getUserTeamIDs(c *gin.Context, userID string) []s
// updateDocStorageKey sets the storage_key on a document after upload.
func (h *KnowledgeBaseHandler) updateDocStorageKey(c *gin.Context, docID, storageKey string) {
database.DB.ExecContext(c.Request.Context(),
`UPDATE kb_documents SET storage_key = $1 WHERE id = $2`,
storageKey, docID)
h.stores.KnowledgeBases.UpdateDocumentStorageKey(c.Request.Context(), docID, storageKey)
}
// toKBResponse converts a model to the API response type.
@@ -747,6 +761,46 @@ func (h *KnowledgeBaseHandler) SetChannelKBs(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": kbs})
}
// ── Discoverable Management (v0.17.0) ────────────
// SetDiscoverable toggles KB visibility to users.
func (h *KnowledgeBaseHandler) SetDiscoverable(c *gin.Context) {
kbID := c.Param("id")
var req struct {
Discoverable bool `json:"discoverable"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.stores.KnowledgeBases.SetDiscoverable(c.Request.Context(), kbID, req.Discoverable); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update discoverability"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// ListDiscoverableKBs returns KBs the user can see that are marked discoverable.
// Used by the channel KB toggle popup when kb_direct_access is enabled.
func (h *KnowledgeBaseHandler) ListDiscoverableKBs(c *gin.Context) {
userID := getUserID(c)
teamIDs := h.getUserTeamIDs(c, userID)
kbs, err := h.stores.KnowledgeBases.ListDiscoverable(c.Request.Context(), userID, teamIDs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list knowledge bases"})
return
}
if kbs == nil {
kbs = []models.KnowledgeBase{}
}
c.JSON(http.StatusOK, gin.H{"data": kbs})
}
// userCanAccess checks if a user can access a KB based on scope.
func (h *KnowledgeBaseHandler) userCanAccess(kb *models.KnowledgeBase, userID string, teamIDs []string) bool {
switch kb.Scope {
@@ -773,60 +827,57 @@ func (h *KnowledgeBaseHandler) userCanAccess(kb *models.KnowledgeBase, userID st
// BuildKBHint returns a system prompt fragment listing active KBs for a
// channel, or empty string if none are active. Called by the completion
// handler to nudge the LLM to use kb_search.
func BuildKBHint(ctx context.Context, stores store.Stores, channelID, userID string) string {
func BuildKBHint(ctx context.Context, stores store.Stores, channelID, userID, personaID string) string {
teamIDs, _ := stores.Teams.GetUserTeamIDs(ctx, userID)
kbIDs, err := stores.KnowledgeBases.GetActiveKBIDs(ctx, channelID, userID, teamIDs)
if err != nil || len(kbIDs) == 0 {
// Also check personal KBs — always available
personalKBs, err := stores.KnowledgeBases.ListPersonal(ctx, userID)
if err != nil || len(personalKBs) == 0 {
return ""
}
// Only include personal KBs that have chunks
hasContent := false
for _, kb := range personalKBs {
if kb.ChunkCount > 0 {
hasContent = true
break
}
}
if !hasContent {
return ""
}
}
// Collect KB details for the hint
type kbInfo struct {
Name string
DocCount int
}
var kbs []kbInfo
seen := make(map[string]bool)
// Persona-bound KBs (v0.17.0)
if personaID != "" {
personaKBs, err := stores.Personas.GetKBs(ctx, personaID)
if err == nil {
for _, pkb := range personaKBs {
if pkb.ChunkCount > 0 {
kbs = append(kbs, kbInfo{Name: pkb.KBName, DocCount: pkb.DocumentCount})
seen[pkb.KBID] = true
}
}
}
}
// Channel-linked KBs
channelKBs, err := stores.KnowledgeBases.GetChannelKBs(ctx, channelID)
if err == nil {
for _, ckb := range channelKBs {
if ckb.Enabled && ckb.DocumentCount > 0 {
if ckb.Enabled && ckb.DocumentCount > 0 && !seen[ckb.KBID] {
kbs = append(kbs, kbInfo{Name: ckb.KBName, DocCount: ckb.DocumentCount})
seen[ckb.KBID] = true
}
}
}
// Personal KBs (not already linked to channel)
// Personal KBs (not already counted)
personalKBs, err := stores.KnowledgeBases.ListPersonal(ctx, userID)
if err == nil {
linked := make(map[string]bool)
for _, ckb := range channelKBs {
linked[ckb.KBID] = true
}
for _, kb := range personalKBs {
if !linked[kb.ID] && kb.ChunkCount > 0 {
if !seen[kb.ID] && kb.ChunkCount > 0 {
kbs = append(kbs, kbInfo{Name: kb.Name, DocCount: kb.DocumentCount})
seen[kb.ID] = true
}
}
}
// Group-granted KBs are resolved via GetActiveKBIDs — only add names
// for KBs found through channel link or persona. The kb_search tool
// handles the full resolution at search time.
_ = teamIDs // used by GetActiveKBIDsWithPersona at search time
if len(kbs) == 0 {
return ""
}