284 lines
8.8 KiB
Go
284 lines
8.8 KiB
Go
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
|
|
}
|