complete DB as source of truth — FTS knowledge surface
Plan #5 done. Wiki already archived to archive/knowledge/. seeds/knowledge.yaml has 24 docs + 6 investigations + 3 runbooks. - MCP search_knowledge: upgraded from ILIKE to PostgreSQL ts_rank/ts_headline - MCP get_entity_knowledge: new tool, walks relationship edges to return all docs/investigations/runbooks linked to an entity - HTTP endpoints (SearchKnowledge, GetEntityKnowledge) already used full FTS - Plan index + audit cross-reference updated
This commit is contained in:
@@ -137,16 +137,51 @@ func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
|
||||
ORDER BY ts DESC LIMIT 50`, nStr(args["entity_id"])), nil
|
||||
})
|
||||
|
||||
register(&mcp.Tool{Name: "search_knowledge", Description: "Full-text search across documentation",
|
||||
register(&mcp.Tool{Name: "search_knowledge", Description: "Full-text search across documentation (PostgreSQL FTS with ts_rank ranking)",
|
||||
InputSchema: objSchema(prop{"query", "string", "Search terms"}),
|
||||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := argsMap(req)
|
||||
q := nStr(args["query"])
|
||||
return queryRows(ctx, pool, `
|
||||
SELECT id, title, LEFT(content, 500) AS preview
|
||||
FROM knowledge_entities
|
||||
WHERE ($1::text IS NULL OR title ILIKE '%'||$1||'%' OR content ILIKE '%'||$1||'%')
|
||||
ORDER BY title LIMIT 20`, q), nil
|
||||
SELECT ke.title, e.slug,
|
||||
ts_rank(ke.search, plainto_tsquery('english', $1)) AS rank,
|
||||
ts_headline('english', ke.content, plainto_tsquery('english', $1),
|
||||
'MaxWords=40, MinWords=15, ShortWord=3, MaxFragments=3,
|
||||
FragmentDelimiter=" ... "') AS snippet,
|
||||
ke.source, ke.tags
|
||||
FROM knowledge_entities ke
|
||||
JOIN entities e ON e.id = ke.entity_id
|
||||
WHERE ke.search @@ plainto_tsquery('english', $1)
|
||||
ORDER BY rank DESC
|
||||
LIMIT 20`, q), nil
|
||||
})
|
||||
|
||||
register(&mcp.Tool{Name: "get_entity_knowledge", Description: "All documents, investigations, and runbooks linked to an entity",
|
||||
InputSchema: objSchema(prop{"entity_slug", "string", "Entity slug (e.g. lxc:jellyfin, service:caddy)"}),
|
||||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := argsMap(req)
|
||||
slug, _ := args["entity_slug"].(string)
|
||||
return queryRows(ctx, pool, `
|
||||
SELECT ke.title, ke.source, e.type AS kind, e.slug,
|
||||
ts_headline('english', ke.content, plainto_tsquery('english', '')) AS headline
|
||||
FROM knowledge_entities ke
|
||||
JOIN entities e ON e.id = ke.entity_id
|
||||
JOIN relationships r ON r.source_id = ke.entity_id
|
||||
JOIN entities target ON target.id = r.target_id
|
||||
WHERE target.slug = $1
|
||||
AND r.valid_to IS NULL
|
||||
AND r.type IN ('documents', 'about')
|
||||
UNION
|
||||
SELECT ke.title, ke.source, e.type AS kind, e.slug,
|
||||
ts_headline('english', ke.content, plainto_tsquery('english', '')) AS headline
|
||||
FROM knowledge_entities ke
|
||||
JOIN entities e ON e.id = ke.entity_id
|
||||
JOIN relationships r ON r.source_id = ke.entity_id
|
||||
JOIN entity_types target_type ON target_type.name = (SELECT type FROM entities WHERE slug = $1)
|
||||
JOIN entities ent ON ent.type = target_type.name AND ent.slug = $1
|
||||
WHERE r.valid_to IS NULL
|
||||
AND r.type = 'procedure-for'
|
||||
ORDER BY 1`, slug), nil
|
||||
})
|
||||
|
||||
register(&mcp.Tool{Name: "query_metrics", Description: "Query time-series metrics",
|
||||
|
||||
Reference in New Issue
Block a user