Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions server/app/Services/Copilot/Services/LLMService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions server/app/Services/Copilot/Services/SaveWorkflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 {
Expand Down
224 changes: 139 additions & 85 deletions server/app/Services/Copilot/Services/WorkflowGeneration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}