- Migrations 010 (content_hash) + 011 (search tsvector column) - new: internal/knowledge/seed.go — knowledge seed ingest engine - new: internal/httpapi/knowledge.go — SearchKnowledge + GetEntityKnowledge - wire knowledge ingest into oikos seed pipeline - convert all 36 wiki docs + 6 investigations + 12 runbooks → seeds/knowledge.yaml - archive: knowledge/wiki/→archive/, oikos/cards/→archive/, .hermes/plans/→archive/ - delete: 9 superseded Python kernel files, ledger/, mcp/build_host_files.py - remove empty knowledge/ directory tree
142 lines
3.6 KiB
Go
142 lines
3.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/dtoro/oikos/internal/httpapi/gen"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func (s *Server) SearchKnowledge(ctx context.Context, request gen.SearchKnowledgeRequestObject) (gen.SearchKnowledgeResponseObject, error) {
|
|
q := request.Params.Q
|
|
limit := clampLimit(request.Params.Limit)
|
|
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT e.slug, COALESCE(et.name,''), ke.title, ke.source, ke.tags,
|
|
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
|
|
FROM knowledge_entities ke
|
|
JOIN entities e ON e.id = ke.entity_id
|
|
JOIN entity_types et ON et.name = e.type
|
|
WHERE ke.search @@ plainto_tsquery('english', $1)
|
|
ORDER BY rank DESC
|
|
LIMIT $2`,
|
|
q, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []gen.KnowledgeHit{}
|
|
|
|
for rows.Next() {
|
|
var slug, eType, title, source, tagJSON string
|
|
var rank float32
|
|
var snippet *string
|
|
|
|
if err := rows.Scan(&slug, &eType, &title, &source, &tagJSON, &rank, &snippet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tags []string
|
|
json.Unmarshal([]byte(tagJSON), &tags)
|
|
|
|
hitType := gen.Document
|
|
switch eType {
|
|
case "investigation":
|
|
hitType = gen.Investigation
|
|
case "runbook":
|
|
hitType = gen.Runbook
|
|
}
|
|
|
|
id, _ := uuid.Parse("")
|
|
_ = id // not needed for response since we use slug
|
|
|
|
items = append(items, gen.KnowledgeHit{
|
|
Slug: slug,
|
|
Title: title,
|
|
Type: hitType,
|
|
Rank: &rank,
|
|
Snippet: snippet,
|
|
SourcePath: &source,
|
|
})
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, rows.Err()
|
|
}
|
|
|
|
if items == nil {
|
|
items = []gen.KnowledgeHit{}
|
|
}
|
|
|
|
return gen.SearchKnowledge200JSONResponse{Items: items}, nil
|
|
}
|
|
|
|
func (s *Server) GetEntityKnowledge(ctx context.Context, request gen.GetEntityKnowledgeRequestObject) (gen.GetEntityKnowledgeResponseObject, error) {
|
|
entitySlug := request.EntityId
|
|
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT e.slug, COALESCE(et.name,''), ke.title, ke.source, ke.tags
|
|
FROM knowledge_entities ke
|
|
JOIN entities e ON e.id = ke.entity_id
|
|
JOIN entity_types et ON et.name = e.type
|
|
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 e.slug, COALESCE(et.name,''), ke.title, ke.source, ke.tags
|
|
FROM knowledge_entities ke
|
|
JOIN entities e ON e.id = ke.entity_id
|
|
JOIN entity_types et ON et.name = e.type
|
|
JOIN relationships r ON r.source_id = ke.entity_id
|
|
JOIN entity_types target_type ON target_type.name = r.target_id::text
|
|
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`,
|
|
entitySlug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := []gen.KnowledgeHit{}
|
|
for rows.Next() {
|
|
var slug, eType, title, source, tagJSON string
|
|
var tags []string
|
|
|
|
if err := rows.Scan(&slug, &eType, &title, &source, &tagJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
json.Unmarshal([]byte(tagJSON), &tags)
|
|
|
|
hitType := gen.Document
|
|
switch eType {
|
|
case "investigation":
|
|
hitType = gen.Investigation
|
|
case "runbook":
|
|
hitType = gen.Runbook
|
|
}
|
|
|
|
items = append(items, gen.KnowledgeHit{
|
|
Slug: slug,
|
|
Title: title,
|
|
Type: hitType,
|
|
SourcePath: &source,
|
|
})
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, rows.Err()
|
|
}
|
|
|
|
if items == nil {
|
|
items = []gen.KnowledgeHit{}
|
|
}
|
|
|
|
return gen.GetEntityKnowledge200JSONResponse{Items: items}, nil
|
|
} |