diff --git a/server/app/Services/Copilot/Services/LLMService.php b/server/app/Services/Copilot/Services/LLMService.php index 28a6bea..64ea41f 100644 --- a/server/app/Services/Copilot/Services/LLMService.php +++ b/server/app/Services/Copilot/Services/LLMService.php @@ -34,6 +34,10 @@ private static function callOpenAI($prompt){ return $decoded; } + return self::handleAIError($results, $model); + } + + private static function handleAIError($results, $model){ Log::error("RAW CONTENT :" , ["content" => $results]); if(preg_match('/\{.*\}|\[.*\]/s', $results, $m)){// AI may have included some markdown or explanation diff --git a/server/app/Services/Copilot/Services/SaveWorkflow.php b/server/app/Services/Copilot/Services/SaveWorkflow.php index 903ac96..8d8bffc 100644 --- a/server/app/Services/Copilot/Services/SaveWorkflow.php +++ b/server/app/Services/Copilot/Services/SaveWorkflow.php @@ -27,7 +27,17 @@ public static function save($requestForm){ $endpoint = rtrim(env('QDRANT_CLUSTER_ENDPOINT', ''), '/'); - Http::withHeaders([ + self::callQdrant($endpoint, $denseVector, $sparseVector, $payload); + + return $payload; + } catch (\Exception $e) { + Log::error("Failed to store workflow", ['error' => $e->getMessage()]); + throw new Exception("failed to store workflow in qdrant " . $e->getMessage()); + } + } + + private static function callQdrant($endpoint, $denseVector, $sparseVector, $payload){ + Http::withHeaders([ 'api-key' => env('QDRANT_API_KEY'), ])->put( $endpoint . '/collections/n8n_workflows/points?wait=true', @@ -44,12 +54,6 @@ public static function save($requestForm){ ], ] ); - - return $payload; - } catch (\Exception $e) { - Log::error("Failed to store workflow", ['error' => $e->getMessage()]); - throw new Exception("failed to store workflow in qdrant " . $e->getMessage()); - } } private static function buildPayload(array $json , string $question): array { diff --git a/server/app/Services/Copilot/Services/WorkflowGeneration.php b/server/app/Services/Copilot/Services/WorkflowGeneration.php index 2916d7e..5d98cd5 100644 --- a/server/app/Services/Copilot/Services/WorkflowGeneration.php +++ b/server/app/Services/Copilot/Services/WorkflowGeneration.php @@ -2,117 +2,171 @@ namespace App\Services\Copilot\Services; - class WorkflowGeneration{ - + public static function buildWorkflowContext(array $flows): string{ - if(empty($flows)){ + if (empty($flows)) { return ""; } - $out = ""; - $counter = 1; - + $sections = []; + $counter = 1; - foreach ($flows as $flow){ - // if it's not an array now, skip it + foreach ($flows as $flow) { if (!is_array($flow)) { continue; } - $name = $flow["workflow"] ?? "Unknown Workflow"; - $nodes = $flow["nodes_used"] ?? []; - $count = $flow["node_count"] ?? count($nodes); - $raw = $flow["raw"] ?? $flow; + $sections[] = self::formatWorkflowSection($flow, $counter++); + } + + return implode("\n", $sections); + } + + private static function formatWorkflowSection(array $flow, int $index): string{ + $name = $flow["workflow"] ?? "Unknown Workflow"; + $nodes = $flow["nodes_used"] ?? []; + $count = $flow["node_count"] ?? count($nodes); + $raw = $flow["raw"] ?? $flow; + $jsonDump = json_encode($raw, JSON_PRETTY_PRINT); + + return implode("\n", [ + "--- Workflow {$index} ---", + "Name: {$name}", + "Nodes: " . implode(", ", $nodes), + "Node Count: {$count}", + "JSON:", + $jsonDump, + "" + ]); + } + + public static function buildSchemasContext(array $rankedSchemas): string{ + $grouped = self::groupSchemasByNode($rankedSchemas); + self::sortOperationsByScore($grouped); - $out .= "\n--- Workflow {$counter} ---\n"; - $out .= "Name: {$name}\n"; - $out .= "Nodes: " . implode(", ", $nodes) . "\n"; - $out .= "Node Count: {$count}\n"; - $out .= "JSON:\n" . json_encode($raw, JSON_PRETTY_PRINT) . "\n"; + $lines = self::schemaIntroText(); - $counter++; + foreach ($grouped as $node => $operations) { + $lines = array_merge($lines, self::formatNodeSection($node, $operations)); } - return $out; + return implode("\n", $lines); } - public static function buildSchemasContext(array $rankedSchemas): string{ + private static function groupSchemasByNode(array $rankedSchemas): array{ $byNode = []; foreach ($rankedSchemas as $row) { - $s = $row["schema"]; - $node = $s["node"]; - - $byNode[$node][] = [ - "resource" => $s["resource"] ?? "default", - "operation" => $s["operation"] ?? "default", - "display" => $s["display"] ?? "", - "description" => $s["description"] ?? "", - "fields" => $s["fields"] ?? [], - "inputs" => $s["inputs"] ?? [], - "outputs" => $s["outputs"] ?? [], - ]; + $schema = $row["schema"] ?? []; + $node = $schema["node"] ?? "UnknownNode"; + + $byNode[$node][] = self::normalizeOperation($schema, $row["score"] ?? 0); } - foreach ($byNode as &$ops) { - usort($ops, fn($a, $b) => $b["score"] <=> $a["score"]); + return $byNode; + } + + private static function normalizeOperation(array $schema, int $score): array{ + return [ + "resource" => $schema["resource"] ?? "default", + "operation" => $schema["operation"] ?? "default", + "display" => $schema["display"] ?? "", + "description" => $schema["description"] ?? "", + "fields" => $schema["fields"] ?? [], + "inputs" => $schema["inputs"] ?? [], + "outputs" => $schema["outputs"] ?? [], + "score" => $score, + ]; + } + + private static function sortOperationsByScore(array &$grouped): void{ + foreach ($grouped as &$ops) { + usort($ops, fn ($a, $b) => $b["score"] <=> $a["score"]); } + } - $out = []; - $out[] = "You may ONLY use the following n8n node operations.if you don't find one here then use the one you know of."; - $out[] = "Every operation below is valid, ranked, and schema-verified."; - $out[] = "Do NOT invent nodes, resources, operations, or fields."; - $out[] = ""; - - foreach ($byNode as $node => $operations) { - $out[] = "NODE: {$node}"; - $out[] = str_repeat("=", 50); - - foreach ($operations as $op) { - $out[] = "OPERATION: {$op["resource"]} → {$op["operation"]}"; - if ($op["display"]) { - $out[] = "LABEL: {$op["display"]}"; - } - if ($op["description"]) { - $out[] = "DESCRIPTION: {$op["description"]}"; - } - - // Fields - if (!empty($op["fields"])) { - $out[] = "FIELDS:"; - foreach ($op["fields"] as $f) { - $req = !empty($f["required"]) ? "required" : "optional"; - $out[] = "- {$f["name"]} ({$f["type"]}, {$req})"; - } - } else { - $out[] = "FIELDS: none"; - } - - // Inputs - if (!empty($op["inputs"])) { - $out[] = "INPUTS:"; - foreach ($op["inputs"] as $i) { - $out[] = "- {$i["name"]} ({$i["type"]})"; - } - } - - // Outputs - if (!empty($op["outputs"])) { - $out[] = "OUTPUTS:"; - foreach ($op["outputs"] as $o) { - $out[] = "- {$o["name"]} ({$o["type"]})"; - } - } - - $out[] = ""; // spacing between operations - } + private static function schemaIntroText(): array{ + return [ + "You may ONLY use the following n8n node operations. If you don't find one here, use one you already know.", + "Every operation below is valid, ranked, and schema-verified.", + "Do NOT invent nodes, resources, operations, or fields.", + "" + ]; + } + + private static function formatNodeSection(string $node, array $operations): array{ + $lines = [ + "NODE: {$node}", + str_repeat("=", 50), + ]; - $out[] = ""; // spacing between nodes + foreach ($operations as $op) { + $lines = array_merge($lines, self::formatOperation($op)); } - return implode("\n", $out); + $lines[] = ""; // spacing after each node + return $lines; + } + + private static function formatOperation(array $op): array{ + $lines = [ + "OPERATION: {$op["resource"]} → {$op["operation"]}", + ]; + + if ($op["display"]) { + $lines[] = "LABEL: {$op["display"]}"; + } + + if ($op["description"]) { + $lines[] = "DESCRIPTION: {$op["description"]}"; + } + + $lines = array_merge($lines, self::formatFields($op["fields"])); + $lines = array_merge($lines, self::formatInputs($op["inputs"])); + $lines = array_merge($lines, self::formatOutputs($op["outputs"])); + + $lines[] = ""; // spacing between operations + return $lines; } -} + private static function formatFields(array $fields): array{ + if (empty($fields)) { + return ["FIELDS: none"]; + } + + $lines = ["FIELDS:"]; + foreach ($fields as $f) { + $required = !empty($f["required"]) ? "required" : "optional"; + $lines[] = "- {$f["name"]} ({$f["type"]}, {$required})"; + } + return $lines; + } + + private static function formatInputs(array $inputs): array{ + if (empty($inputs)) { + return []; + } + + $lines = ["INPUTS:"]; + foreach ($inputs as $i) { + $lines[] = "- {$i["name"]} ({$i["type"]})"; + } + + return $lines; + } + + private static function formatOutputs(array $outputs): array{ + if (empty($outputs)) { + return []; + } + + $lines = ["OUTPUTS:"]; + foreach ($outputs as $o) { + $lines[] = "- {$o["name"]} ({$o["type"]})"; + } + + return $lines; + } +}