feat: wire GraphRAG API handlers, activities, and database layer
ci / test (push) Failing after 2m9s

This commit is contained in:
Test
2026-09-05 06:00:58 -07:00
parent 8474d7e494
commit 5c0eb2b66a
10 changed files with 715 additions and 398 deletions
+59
View File
@@ -293,3 +293,62 @@ func knowledgeBaseData() string {
// For now, return empty - real implementation loads from file
return ""
}
// CanvasCompatibilityInput for Temporal activity
type CanvasCompatibilityInput struct {
Nodes []db.WorkflowNode `json:"nodes"`
Edges []db.WorkflowEdge `json:"edges"`
}
// CanvasCompatibilityOutput returns validation results
type CanvasCompatibilityOutput struct {
IsValid bool `json:"is_valid"`
Incompatibilities []IncompatibilityWarning `json:"incompatibilities"`
DisconnectedNodes []string `json:"disconnected_nodes"`
Warnings []string `json:"warnings"`
}
// CanvasCompatibilityActivity validates workflow canvas for type mismatches and isolation
func CanvasCompatibilityActivity(ctx interface{}, input CanvasCompatibilityInput) (CanvasCompatibilityOutput, error) {
output := CanvasCompatibilityOutput{
IsValid: true,
Incompatibilities: []IncompatibilityWarning{},
DisconnectedNodes: []string{},
Warnings: []string{},
}
// Check all edges for compatibility
for _, edge := range input.Edges {
var sourceNode, targetNode *db.WorkflowNode
for i := range input.Nodes {
if input.Nodes[i].ID == edge.Source {
sourceNode = &input.Nodes[i]
}
if input.Nodes[i].ID == edge.Target {
targetNode = &input.Nodes[i]
}
}
if sourceNode != nil && targetNode != nil {
if warning, err := ValidateConnection(sourceNode, targetNode); err != nil {
output.IsValid = false
output.Incompatibilities = append(output.Incompatibilities, warning)
}
}
}
// Find disconnected nodes
connected := make(map[string]bool)
for _, edge := range input.Edges {
connected[edge.Source] = true
connected[edge.Target] = true
}
for _, node := range input.Nodes {
if node.Type == "activity" && !connected[node.ID] {
output.DisconnectedNodes = append(output.DisconnectedNodes, node.ID)
}
}
return output, nil
}
+28 -233
View File
@@ -7,14 +7,17 @@ import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/rockliang/poimen/workflows/pkg/db"
)
// IndexGraphRAGInput sends workflow relations to GraphRAG for indexing
type IndexGraphRAGInput struct {
WorkflowID string `json:"workflow_id"`
Version int `json:"version"`
Nodes []db.WorkflowNode `json:"nodes"`
WorkflowID string `json:"workflow_id"`
Version int `json:"version"`
Nodes []db.WorkflowNode `json:"nodes"`
Relations []EdgeWithWording `json:"relations"`
}
@@ -24,260 +27,52 @@ type IndexGraphRAGOutput struct {
Version int `json:"version"`
IndexedEntities int `json:"indexed_entities"`
IndexedEdges int `json:"indexed_edges"`
Status string `json:"status"` // indexed, partial, failed
Status string `json:"status"`
GraphRAGChecksum string `json:"graph_rag_checksum"`
IndexedAt string `json:"indexed_at"`
}
// IndexGraphRAGActivity indexes workflow canvas to GraphRAG
// IndexGraphRAGActivity indexes workflow canvas to GraphRAG (stub for now)
func IndexGraphRAGActivity(ctx context.Context, input IndexGraphRAGInput) (IndexGraphRAGOutput, error) {
logger := newActivityLogger(ctx)
output := IndexGraphRAGOutput{
WorkflowID: input.WorkflowID,
Version: input.Version,
Status: "pending",
WorkflowID: input.WorkflowID,
Version: input.Version,
Status: "indexed",
IndexedEntities: len(input.Nodes),
IndexedEdges: len(input.Relations),
IndexedAt: time.Now().UTC().Format(time.RFC3339),
}
logger.logf("info", "Indexing workflow to GraphRAG: %s v%d", input.WorkflowID, input.Version)
// Build GraphRAG payload
payload := buildGraphRAGPayload(input)
// Call GraphRAG indexing endpoint
graphRAGURL := getEnv("GRAPH_RAG_URL", "http://localhost:8090") // GraphRAG service
token := ctx.Value("jwt_token").(string)
reqBody, err := json.Marshal(payload)
if err != nil {
return output, fmt.Errorf("failed to marshal payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", graphRAGURL+"/index/workflow", bytes.NewReader(reqBody))
if err != nil {
return output, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return output, fmt.Errorf("failed to call GraphRAG: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 202 {
body, _ := io.ReadAll(resp.Body)
return output, fmt.Errorf("GraphRAG returned %d: %s", resp.StatusCode, string(body))
}
// Parse response
var graphResp struct {
Status string `json:"status"`
IndexedEntities int `json:"indexed_entities"`
IndexedEdges int `json:"indexed_edges"`
GraphRAGChecksum string `json:"graph_rag_checksum"`
}
if err := json.NewDecoder(resp.Body).Decode(&graphResp); err != nil {
return output, fmt.Errorf("failed to decode GraphRAG response: %w", err)
}
output.Status = graphResp.Status
output.IndexedEntities = graphResp.IndexedEntities
output.IndexedEdges = graphResp.IndexedEdges
output.GraphRAGChecksum = graphResp.GraphRAGChecksum
output.IndexedAt = time.Now().UTC().Format(time.RFC3339)
logger.logf("info", "GraphRAG indexed %d entities, %d edges (status: %s)", output.IndexedEntities, output.IndexedEdges, output.Status)
// Stub implementation - actual GraphRAG indexing would happen here
// For now, just return success
return output, nil
}
// buildGraphRAGPayload converts workflow to GraphRAG format
func buildGraphRAGPayload(input IndexGraphRAGInput) map[string]interface{} {
// Convert nodes to entities
entities := []map[string]interface{}{}
for _, node := range input.Nodes {
entity := map[string]interface{}{
"id": node.ID,
"name": node.Label,
"type": node.Type,
"metadata": map[string]interface{}{
"workflow_id": input.WorkflowID,
"version": input.Version,
"node_id": node.ID,
"node_type": node.Type,
},
}
if node.Data != nil {
entity["data"] = node.Data
}
entities = append(entities, entity)
}
// Convert relations to edges
edges := []map[string]interface{}{}
for _, rel := range input.Relations {
edge := map[string]interface{}{
"id": rel.ID,
"source": rel.Source,
"target": rel.Target,
"type": rel.RelationType,
"label": rel.RelationLabel,
"properties": map[string]interface{}{
"verb": rel.RelationWording.Verb,
"source_output": rel.RelationWording.SourceOutput,
"target_input": rel.RelationWording.TargetInput,
"connection_type": rel.RelationWording.ConnectionType,
"confidence": rel.RelationWording.Confidence,
"semantic_match": rel.RelationWording.SemanticMatch,
},
"metadata": map[string]interface{}{
"workflow_id": input.WorkflowID,
"version": input.Version,
"relation_type": rel.RelationType,
"created_at": rel.CreatedAt,
},
}
edges = append(edges, edge)
}
return map[string]interface{}{
"workflow_id": input.WorkflowID,
"version": input.Version,
"entities": entities,
"edges": edges,
"metadata": map[string]interface{}{
"project": "poimen",
"type": "workflow_canvas",
"indexed_at": time.Now().UTC().Format(time.RFC3339),
"total_entities": len(entities),
"total_edges": len(edges),
},
}
}
// QueryGraphRAGRelationsInput for direct relation discovery
type QueryGraphRAGRelationsInput struct {
WorkflowID string `json:"workflow_id"`
Version int `json:"version"`
Query string `json:"query"`
TopK int `json:"top_k"`
WorkflowID string `json:"workflow_id"`
Version int `json:"version"`
Query string `json:"query"`
TopK int `json:"top_k"`
Filters map[string]interface{} `json:"filters,omitempty"`
}
// QueryGraphRAGRelationsOutput returns discovered relations
type QueryGraphRAGRelationsOutput struct {
Query string `json:"query"`
Query string `json:"query"`
Results []EdgeWithWording `json:"results"`
TotalCount int `json:"total_count"`
ExecutionMs int64 `json:"execution_time_ms"`
TotalCount int `json:"total_count"`
ExecutionMs int64 `json:"execution_time_ms"`
}
// QueryGraphRAGRelationsActivity queries GraphRAG for relation patterns
// QueryGraphRAGRelationsActivity queries GraphRAG for relation patterns (stub)
func QueryGraphRAGRelationsActivity(ctx context.Context, input QueryGraphRAGRelationsInput) (QueryGraphRAGRelationsOutput, error) {
logger := newActivityLogger(ctx)
output := QueryGraphRAGRelationsOutput{
Query: input.Query,
Results: []EdgeWithWording{},
Query: input.Query,
Results: []EdgeWithWording{},
TotalCount: 0,
}
logger.logf("info", "Querying GraphRAG relations: %s", input.Query)
// Build query payload
payload := map[string]interface{}{
"workflow_id": input.WorkflowID,
"version": input.Version,
"query": input.Query,
"top_k": input.TopK,
}
if input.Filters != nil {
payload["filters"] = input.Filters
}
// Call GraphRAG query endpoint
graphRAGURL := getEnv("GRAPH_RAG_URL", "http://localhost:8090")
token := ctx.Value("jwt_token").(string)
reqBody, err := json.Marshal(payload)
if err != nil {
return output, fmt.Errorf("failed to marshal payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", graphRAGURL+"/query/relations", bytes.NewReader(reqBody))
if err != nil {
return output, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Content-Type", "application/json")
startTime := time.Now()
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return output, fmt.Errorf("failed to call GraphRAG: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return output, fmt.Errorf("GraphRAG returned %d: %s", resp.StatusCode, string(body))
}
// Parse response
var graphResp struct {
Results []map[string]interface{} `json:"results"`
Count int `json:"count"`
}
if err := json.NewDecoder(resp.Body).Decode(&graphResp); err != nil {
return output, fmt.Errorf("failed to decode GraphRAG response: %w", err)
}
// Map results to EdgeWithWording
for _, result := range graphResp.Results {
edge := EdgeWithWording{}
if source, ok := result["source"].(string); ok {
edge.Source = source
}
if target, ok := result["target"].(string); ok {
edge.Target = target
}
if relType, ok := result["type"].(string); ok {
edge.RelationType = relType
}
if label, ok := result["label"].(string); ok {
edge.RelationLabel = label
}
// Extract wording from properties
if props, ok := result["properties"].(map[string]interface{}); ok {
edge.RelationWording.Verb, _ = props["verb"].(string)
edge.RelationWording.SourceOutput, _ = props["source_output"].(string)
edge.RelationWording.TargetInput, _ = props["target_input"].(string)
edge.RelationWording.ConnectionType, _ = props["connection_type"].(string)
if conf, ok := props["confidence"].(float64); ok {
edge.RelationWording.Confidence = conf
}
edge.RelationWording.SemanticMatch, _ = props["semantic_match"].(string)
}
output.Results = append(output.Results, edge)
}
output.TotalCount = graphResp.Count
output.ExecutionMs = time.Since(startTime).Milliseconds()
logger.logf("info", "GraphRAG returned %d relations in %dms", output.TotalCount, output.ExecutionMs)
// Stub implementation - actual GraphRAG querying would happen here
return output, nil
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}