package action import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) // 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"` Relations []EdgeWithWording `json:"relations"` } // IndexGraphRAGOutput confirms indexing status type IndexGraphRAGOutput struct { WorkflowID string `json:"workflow_id"` Version int `json:"version"` IndexedEntities int `json:"indexed_entities"` IndexedEdges int `json:"indexed_edges"` Status string `json:"status"` // indexed, partial, failed GraphRAGChecksum string `json:"graph_rag_checksum"` IndexedAt string `json:"indexed_at"` } // IndexGraphRAGActivity indexes workflow canvas to GraphRAG func IndexGraphRAGActivity(ctx context.Context, input IndexGraphRAGInput) (IndexGraphRAGOutput, error) { logger := newActivityLogger(ctx) output := IndexGraphRAGOutput{ WorkflowID: input.WorkflowID, Version: input.Version, Status: "pending", } 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) 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"` Filters map[string]interface{} `json:"filters,omitempty"` } // QueryGraphRAGRelationsOutput returns discovered relations type QueryGraphRAGRelationsOutput struct { Query string `json:"query"` Results []EdgeWithWording `json:"results"` TotalCount int `json:"total_count"` ExecutionMs int64 `json:"execution_time_ms"` } // QueryGraphRAGRelationsActivity queries GraphRAG for relation patterns func QueryGraphRAGRelationsActivity(ctx context.Context, input QueryGraphRAGRelationsInput) (QueryGraphRAGRelationsOutput, error) { logger := newActivityLogger(ctx) output := QueryGraphRAGRelationsOutput{ Query: input.Query, Results: []EdgeWithWording{}, } 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) return output, nil } func getEnv(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val } return defaultVal }