Files
goclaw/internal/store/pg/knowledge_graph_traversal.go
T
viettranx 6eb33f9cea feat: decouple memory/KG sharing from workspace folder sharing
Add independent `share_memory` config flag to control memory and
knowledge graph sharing separately from workspace folder isolation.

- Add ShareMemory field to WorkspaceSharingConfig
- Decouple WithSharedMemory(ctx) from shouldShareWorkspace() in loop.go
- Add shouldShareMemory() helper independent of workspace sharing
- Fix KG Traverse CTE to scope user_id in recursive step (pre-existing bug)
- Add memory toggle UI with violet styling in workspace sharing section
- Add i18n translations (en/vi/zh) for new memory sharing controls
- Add unit tests for shouldShareMemory() independence
2026-03-12 18:26:40 +07:00

113 lines
2.7 KiB
Go

package pg
import (
"context"
"encoding/json"
"time"
"github.com/lib/pq"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// Traverse walks the knowledge graph from startEntityID up to maxDepth hops
// using a recursive CTE. Returns all reachable entities (excluding the start node).
// A 5-second statement timeout is applied for safety.
func (s *PGKnowledgeGraphStore) Traverse(ctx context.Context, agentID, userID, startEntityID string, maxDepth int) ([]store.TraversalResult, error) {
if maxDepth <= 0 {
maxDepth = 3
}
aid := mustParseUUID(agentID)
startID := mustParseUUID(startEntityID)
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback() //nolint:errcheck
if _, err := tx.ExecContext(ctx, `SET LOCAL statement_timeout = '5000'`); err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, `
WITH RECURSIVE paths AS (
SELECT
e.id, e.agent_id, e.user_id, e.external_id,
e.name, e.entity_type, e.description,
e.properties, e.source_id, e.confidence,
e.created_at, e.updated_at,
1 AS depth,
ARRAY[e.id::text] AS path,
''::text AS via
FROM kg_entities e
WHERE e.id = $1 AND e.agent_id = $2 AND e.user_id = $3
UNION ALL
SELECT
e.id, e.agent_id, e.user_id, e.external_id,
e.name, e.entity_type, e.description,
e.properties, e.source_id, e.confidence,
e.created_at, e.updated_at,
p.depth + 1,
p.path || e.id::text,
r.relation_type
FROM paths p
JOIN kg_relations r ON p.id = r.source_entity_id AND r.user_id = $3
JOIN kg_entities e ON r.target_entity_id = e.id AND e.user_id = $3
WHERE p.depth < $4
AND NOT e.id::text = ANY(p.path)
)
SELECT
id, agent_id, user_id, external_id,
name, entity_type, description,
properties, source_id, confidence,
created_at, updated_at,
depth, path, via
FROM paths WHERE depth > 1`,
startID, aid, userID, maxDepth,
)
if err != nil {
return nil, err
}
defer rows.Close()
var results []store.TraversalResult
for rows.Next() {
var e store.Entity
var props []byte
var createdAt, updatedAt time.Time
var depth int
var path []string
var via string
if err := rows.Scan(
&e.ID, &e.AgentID, &e.UserID, &e.ExternalID,
&e.Name, &e.EntityType, &e.Description,
&props, &e.SourceID, &e.Confidence,
&createdAt, &updatedAt,
&depth, pq.Array(&path), &via,
); err != nil {
continue
}
if len(props) > 0 {
json.Unmarshal(props, &e.Properties) //nolint:errcheck
}
e.CreatedAt = createdAt.UnixMilli()
e.UpdatedAt = updatedAt.UnixMilli()
results = append(results, store.TraversalResult{
Entity: e,
Depth: depth,
Path: path,
Via: via,
})
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, tx.Commit()
}