This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package statemachine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/workflow"
|
||||
"github.com/rockliang/poimen/workflows/action"
|
||||
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||
)
|
||||
|
||||
type WorkflowGraphQueryInput struct {
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
Query string `json:"query"`
|
||||
SearchType string `json:"search_type"`
|
||||
RelationType string `json:"relation_type"`
|
||||
Version int `json:"version"`
|
||||
ConfidenceFloor float64 `json:"confidence_floor"`
|
||||
TopK int `json:"top_k"`
|
||||
FindPaths bool `json:"find_paths"`
|
||||
TargetNodeID string `json:"target_node_id"`
|
||||
MaxPathDepth int `json:"max_path_depth"`
|
||||
RankingProfile string `json:"ranking_profile"`
|
||||
IncludeReasoning bool `json:"include_reasoning"`
|
||||
}
|
||||
|
||||
type WorkflowGraphQueryOutput struct {
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
Query string `json:"query"`
|
||||
Version int `json:"version"`
|
||||
ExecutionTimeMs int64 `json:"execution_time_ms"`
|
||||
Results []action.EdgeWithWording `json:"results"`
|
||||
Paths []QueryPath `json:"paths"`
|
||||
TotalCount int `json:"total_count"`
|
||||
HasMore bool `json:"has_more"`
|
||||
RankingProfile string `json:"ranking_profile"`
|
||||
}
|
||||
|
||||
type QueryPath struct {
|
||||
SourceID string `json:"source_id"`
|
||||
TargetID string `json:"target_id"`
|
||||
Distance int `json:"distance"`
|
||||
PathCount int `json:"path_count"`
|
||||
NodeIDs []string `json:"node_ids"`
|
||||
Confidence float64 `json:"total_confidence"`
|
||||
}
|
||||
|
||||
func WorkflowGraphQuery(ctx workflow.Context, input WorkflowGraphQueryInput) (WorkflowGraphQueryOutput, error) {
|
||||
startTime := time.Now()
|
||||
output := WorkflowGraphQueryOutput{
|
||||
WorkflowID: input.WorkflowID,
|
||||
Query: input.Query,
|
||||
Version: input.Version,
|
||||
RankingProfile: input.RankingProfile,
|
||||
Results: []action.EdgeWithWording{},
|
||||
Paths: []QueryPath{},
|
||||
}
|
||||
|
||||
opts := workflow.ActivityOptions{
|
||||
StartToCloseTimeout: 120 * time.Second,
|
||||
RetryPolicy: &workflow.RetryPolicy{
|
||||
InitialInterval: 2 * time.Second,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaxInterval: 10 * time.Second,
|
||||
MaxAttempts: 3,
|
||||
},
|
||||
}
|
||||
ctx = workflow.WithActivityOptions(ctx, opts)
|
||||
|
||||
// Fetch canvas + relations
|
||||
var canvasData action.CanvasWithRelationsData
|
||||
err := workflow.ExecuteActivity(ctx, action.FetchCanvasRelationsActivity,
|
||||
action.FetchCanvasRelationsInput{
|
||||
WorkflowID: input.WorkflowID,
|
||||
Version: input.Version,
|
||||
},
|
||||
).Get(ctx, &canvasData)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
// Query Memory System via unified endpoint
|
||||
var graphResults action.GraphRAGQueryOutput
|
||||
err = workflow.ExecuteActivity(ctx, action.QueryGraphRAGActivity,
|
||||
action.GraphRAGQueryInput{
|
||||
WorkflowID: input.WorkflowID,
|
||||
Query: input.Query,
|
||||
SearchType: input.SearchType,
|
||||
RelationType: input.RelationType,
|
||||
ConfidenceFloor: input.ConfidenceFloor,
|
||||
TopK: input.TopK,
|
||||
RankingProfile: input.RankingProfile,
|
||||
Canvas: canvasData,
|
||||
},
|
||||
).Get(ctx, &graphResults)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
|
||||
output.Results = graphResults.Edges
|
||||
output.TotalCount = graphResults.TotalCount
|
||||
output.HasMore = graphResults.HasMore
|
||||
|
||||
output.ExecutionTimeMs = time.Since(startTime).Milliseconds()
|
||||
return output, nil
|
||||
}
|
||||
Reference in New Issue
Block a user