@elizaos/plugin-workflow 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/auto-enable.ts +18 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/workflow.d.ts +23 -0
- package/dist/actions/workflow.d.ts.map +1 -0
- package/dist/actions/workflow.js +425 -0
- package/dist/actions/workflow.js.map +1 -0
- package/dist/data/defaultNodes.json +9887 -0
- package/dist/data/schemaIndex.json +1 -0
- package/dist/data/triggerSchemaIndex.json +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +588 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +59 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/automations-builder.d.ts +21 -0
- package/dist/lib/automations-builder.d.ts.map +1 -0
- package/dist/lib/automations-builder.js +557 -0
- package/dist/lib/automations-builder.js.map +1 -0
- package/dist/lib/automations-types.d.ts +153 -0
- package/dist/lib/automations-types.d.ts.map +1 -0
- package/dist/lib/automations-types.js +191 -0
- package/dist/lib/automations-types.js.map +1 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/legacy-task-migration.d.ts +20 -0
- package/dist/lib/legacy-task-migration.d.ts.map +1 -0
- package/dist/lib/legacy-task-migration.js +110 -0
- package/dist/lib/legacy-task-migration.js.map +1 -0
- package/dist/lib/legacy-text-trigger-migration.d.ts +18 -0
- package/dist/lib/legacy-text-trigger-migration.d.ts.map +1 -0
- package/dist/lib/legacy-text-trigger-migration.js +131 -0
- package/dist/lib/legacy-text-trigger-migration.js.map +1 -0
- package/dist/lib/workflow-clarification.d.ts +113 -0
- package/dist/lib/workflow-clarification.d.ts.map +1 -0
- package/dist/lib/workflow-clarification.js +425 -0
- package/dist/lib/workflow-clarification.js.map +1 -0
- package/dist/plugin-routes.d.ts +9 -0
- package/dist/plugin-routes.d.ts.map +1 -0
- package/dist/plugin-routes.js +147 -0
- package/dist/plugin-routes.js.map +1 -0
- package/dist/providers/activeWorkflows.d.ts +11 -0
- package/dist/providers/activeWorkflows.d.ts.map +1 -0
- package/dist/providers/activeWorkflows.js +72 -0
- package/dist/providers/activeWorkflows.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/pendingDraft.d.ts +9 -0
- package/dist/providers/pendingDraft.d.ts.map +1 -0
- package/dist/providers/pendingDraft.js +48 -0
- package/dist/providers/pendingDraft.js.map +1 -0
- package/dist/providers/workflowStatus.d.ts +3 -0
- package/dist/providers/workflowStatus.d.ts.map +1 -0
- package/dist/providers/workflowStatus.js +69 -0
- package/dist/providers/workflowStatus.js.map +1 -0
- package/dist/register-routes.d.ts +2 -0
- package/dist/register-routes.d.ts.map +1 -0
- package/dist/register-routes.js +6 -0
- package/dist/register-routes.js.map +1 -0
- package/dist/routes/_helpers.d.ts +11 -0
- package/dist/routes/_helpers.d.ts.map +1 -0
- package/dist/routes/_helpers.js +22 -0
- package/dist/routes/_helpers.js.map +1 -0
- package/dist/routes/automations.d.ts +19 -0
- package/dist/routes/automations.d.ts.map +1 -0
- package/dist/routes/automations.js +32 -0
- package/dist/routes/automations.js.map +1 -0
- package/dist/routes/embedded-webhooks.d.ts +3 -0
- package/dist/routes/embedded-webhooks.d.ts.map +1 -0
- package/dist/routes/embedded-webhooks.js +47 -0
- package/dist/routes/embedded-webhooks.js.map +1 -0
- package/dist/routes/executions.d.ts +3 -0
- package/dist/routes/executions.d.ts.map +1 -0
- package/dist/routes/executions.js +58 -0
- package/dist/routes/executions.js.map +1 -0
- package/dist/routes/index.d.ts +4 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +14 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/nodes.d.ts +3 -0
- package/dist/routes/nodes.d.ts.map +1 -0
- package/dist/routes/nodes.js +168 -0
- package/dist/routes/nodes.js.map +1 -0
- package/dist/routes/validation.d.ts +3 -0
- package/dist/routes/validation.d.ts.map +1 -0
- package/dist/routes/validation.js +41 -0
- package/dist/routes/validation.js.map +1 -0
- package/dist/routes/workflow-routes.d.ts +27 -0
- package/dist/routes/workflow-routes.d.ts.map +1 -0
- package/dist/routes/workflow-routes.js +326 -0
- package/dist/routes/workflow-routes.js.map +1 -0
- package/dist/routes/workflows.d.ts +3 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +252 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/schemas/draftIntent.d.ts +22 -0
- package/dist/schemas/draftIntent.d.ts.map +1 -0
- package/dist/schemas/draftIntent.js +22 -0
- package/dist/schemas/draftIntent.js.map +1 -0
- package/dist/schemas/feasibility.d.ts +13 -0
- package/dist/schemas/feasibility.d.ts.map +1 -0
- package/dist/schemas/feasibility.js +9 -0
- package/dist/schemas/feasibility.js.map +1 -0
- package/dist/schemas/index.d.ts +5 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +5 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/keywordExtraction.d.ts +14 -0
- package/dist/schemas/keywordExtraction.d.ts.map +1 -0
- package/dist/schemas/keywordExtraction.js +12 -0
- package/dist/schemas/keywordExtraction.js.map +1 -0
- package/dist/schemas/workflowMatching.d.ts +36 -0
- package/dist/schemas/workflowMatching.d.ts.map +1 -0
- package/dist/schemas/workflowMatching.js +30 -0
- package/dist/schemas/workflowMatching.js.map +1 -0
- package/dist/services/embedded-workflow-service.d.ts +106 -0
- package/dist/services/embedded-workflow-service.d.ts.map +1 -0
- package/dist/services/embedded-workflow-service.js +1900 -0
- package/dist/services/embedded-workflow-service.js.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +5 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/workflow-credential-store.d.ts +27 -0
- package/dist/services/workflow-credential-store.d.ts.map +1 -0
- package/dist/services/workflow-credential-store.js +92 -0
- package/dist/services/workflow-credential-store.js.map +1 -0
- package/dist/services/workflow-dispatch.d.ts +41 -0
- package/dist/services/workflow-dispatch.d.ts.map +1 -0
- package/dist/services/workflow-dispatch.js +86 -0
- package/dist/services/workflow-dispatch.js.map +1 -0
- package/dist/services/workflow-service.d.ts +63 -0
- package/dist/services/workflow-service.d.ts.map +1 -0
- package/dist/services/workflow-service.js +492 -0
- package/dist/services/workflow-service.js.map +1 -0
- package/dist/trigger-routes.d.ts +153 -0
- package/dist/trigger-routes.d.ts.map +1 -0
- package/dist/trigger-routes.js +424 -0
- package/dist/trigger-routes.js.map +1 -0
- package/dist/types/index.d.ts +457 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +59 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/catalog.d.ts +16 -0
- package/dist/utils/catalog.d.ts.map +1 -0
- package/dist/utils/catalog.js +211 -0
- package/dist/utils/catalog.js.map +1 -0
- package/dist/utils/clarification.d.ts +17 -0
- package/dist/utils/clarification.d.ts.map +1 -0
- package/dist/utils/clarification.js +46 -0
- package/dist/utils/clarification.js.map +1 -0
- package/dist/utils/context.d.ts +4 -0
- package/dist/utils/context.d.ts.map +1 -0
- package/dist/utils/context.js +18 -0
- package/dist/utils/context.js.map +1 -0
- package/dist/utils/credentialResolver.d.ts +22 -0
- package/dist/utils/credentialResolver.d.ts.map +1 -0
- package/dist/utils/credentialResolver.js +146 -0
- package/dist/utils/credentialResolver.js.map +1 -0
- package/dist/utils/generation.d.ts +36 -0
- package/dist/utils/generation.d.ts.map +1 -0
- package/dist/utils/generation.js +701 -0
- package/dist/utils/generation.js.map +1 -0
- package/dist/utils/host-capabilities.d.ts +27 -0
- package/dist/utils/host-capabilities.d.ts.map +1 -0
- package/dist/utils/host-capabilities.js +59 -0
- package/dist/utils/host-capabilities.js.map +1 -0
- package/dist/utils/inferSyntheticOutputSchema.d.ts +20 -0
- package/dist/utils/inferSyntheticOutputSchema.d.ts.map +1 -0
- package/dist/utils/inferSyntheticOutputSchema.js +151 -0
- package/dist/utils/inferSyntheticOutputSchema.js.map +1 -0
- package/dist/utils/outputSchema.d.ts +26 -0
- package/dist/utils/outputSchema.d.ts.map +1 -0
- package/dist/utils/outputSchema.js +297 -0
- package/dist/utils/outputSchema.js.map +1 -0
- package/dist/utils/validateAndRepair.d.ts +41 -0
- package/dist/utils/validateAndRepair.d.ts.map +1 -0
- package/dist/utils/validateAndRepair.js +483 -0
- package/dist/utils/validateAndRepair.js.map +1 -0
- package/dist/utils/workflow-prompts/actionResponse.d.ts +2 -0
- package/dist/utils/workflow-prompts/actionResponse.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/actionResponse.js +17 -0
- package/dist/utils/workflow-prompts/actionResponse.js.map +1 -0
- package/dist/utils/workflow-prompts/draftIntent.d.ts +2 -0
- package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/draftIntent.js +23 -0
- package/dist/utils/workflow-prompts/draftIntent.js.map +1 -0
- package/dist/utils/workflow-prompts/feasibilityCheck.d.ts +2 -0
- package/dist/utils/workflow-prompts/feasibilityCheck.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/feasibilityCheck.js +21 -0
- package/dist/utils/workflow-prompts/feasibilityCheck.js.map +1 -0
- package/dist/utils/workflow-prompts/fieldCorrection.d.ts +3 -0
- package/dist/utils/workflow-prompts/fieldCorrection.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/fieldCorrection.js +20 -0
- package/dist/utils/workflow-prompts/fieldCorrection.js.map +1 -0
- package/dist/utils/workflow-prompts/index.d.ts +8 -0
- package/dist/utils/workflow-prompts/index.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/index.js +8 -0
- package/dist/utils/workflow-prompts/index.js.map +1 -0
- package/dist/utils/workflow-prompts/keywordExtraction.d.ts +2 -0
- package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/keywordExtraction.js +21 -0
- package/dist/utils/workflow-prompts/keywordExtraction.js.map +1 -0
- package/dist/utils/workflow-prompts/parameterCorrection.d.ts +3 -0
- package/dist/utils/workflow-prompts/parameterCorrection.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/parameterCorrection.js +29 -0
- package/dist/utils/workflow-prompts/parameterCorrection.js.map +1 -0
- package/dist/utils/workflow-prompts/workflowGeneration.d.ts +2 -0
- package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/workflowGeneration.js +529 -0
- package/dist/utils/workflow-prompts/workflowGeneration.js.map +1 -0
- package/dist/utils/workflow-prompts/workflowMatching.d.ts +2 -0
- package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -0
- package/dist/utils/workflow-prompts/workflowMatching.js +23 -0
- package/dist/utils/workflow-prompts/workflowMatching.js.map +1 -0
- package/dist/utils/workflow.d.ts +62 -0
- package/dist/utils/workflow.d.ts.map +1 -0
- package/dist/utils/workflow.js +712 -0
- package/dist/utils/workflow.js.map +1 -0
- package/package.json +87 -0
- package/src/actions/index.ts +1 -0
- package/src/actions/workflow.ts +494 -0
- package/src/data/defaultNodes.json +9887 -0
- package/src/data/schemaIndex.json +1 -0
- package/src/data/triggerSchemaIndex.json +1 -0
- package/src/db/index.ts +8 -0
- package/src/db/schema.ts +94 -0
- package/src/index.ts +179 -0
- package/src/lib/automations-builder.ts +679 -0
- package/src/lib/automations-types.ts +391 -0
- package/src/lib/index.ts +8 -0
- package/src/lib/legacy-task-migration.ts +143 -0
- package/src/lib/legacy-text-trigger-migration.ts +178 -0
- package/src/lib/workflow-clarification.ts +497 -0
- package/src/plugin-routes.ts +164 -0
- package/src/providers/activeWorkflows.ts +81 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/pendingDraft.ts +55 -0
- package/src/providers/workflowStatus.ts +88 -0
- package/src/register-routes.ts +6 -0
- package/src/routes/_helpers.ts +27 -0
- package/src/routes/automations.ts +46 -0
- package/src/routes/embedded-webhooks.ts +64 -0
- package/src/routes/executions.ts +75 -0
- package/src/routes/index.ts +16 -0
- package/src/routes/nodes.ts +211 -0
- package/src/routes/validation.ts +51 -0
- package/src/routes/workflow-routes.ts +469 -0
- package/src/routes/workflows.ts +310 -0
- package/src/schemas/draftIntent.ts +21 -0
- package/src/schemas/feasibility.ts +8 -0
- package/src/schemas/index.ts +4 -0
- package/src/schemas/keywordExtraction.ts +11 -0
- package/src/schemas/workflowMatching.ts +29 -0
- package/src/services/embedded-workflow-service.ts +2224 -0
- package/src/services/index.ts +17 -0
- package/src/services/workflow-credential-store.ts +132 -0
- package/src/services/workflow-dispatch.ts +121 -0
- package/src/services/workflow-service.ts +839 -0
- package/src/trigger-routes.ts +714 -0
- package/src/types/index.ts +562 -0
- package/src/utils/catalog.ts +260 -0
- package/src/utils/clarification.ts +52 -0
- package/src/utils/context.ts +22 -0
- package/src/utils/credentialResolver.ts +234 -0
- package/src/utils/generation.ts +987 -0
- package/src/utils/host-capabilities.ts +81 -0
- package/src/utils/inferSyntheticOutputSchema.ts +163 -0
- package/src/utils/outputSchema.ts +372 -0
- package/src/utils/validateAndRepair.ts +610 -0
- package/src/utils/workflow-prompts/actionResponse.ts +16 -0
- package/src/utils/workflow-prompts/draftIntent.ts +22 -0
- package/src/utils/workflow-prompts/feasibilityCheck.ts +20 -0
- package/src/utils/workflow-prompts/fieldCorrection.ts +20 -0
- package/src/utils/workflow-prompts/index.ts +10 -0
- package/src/utils/workflow-prompts/keywordExtraction.ts +20 -0
- package/src/utils/workflow-prompts/parameterCorrection.ts +29 -0
- package/src/utils/workflow-prompts/workflowGeneration.ts +528 -0
- package/src/utils/workflow-prompts/workflowMatching.ts +22 -0
- package/src/utils/workflow.ts +895 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import type { GenerateTextParams } from '@elizaos/core';
|
|
2
|
+
import { type IAgentRuntime, logger, ModelType } from '@elizaos/core';
|
|
3
|
+
import {
|
|
4
|
+
draftIntentSchema,
|
|
5
|
+
feasibilitySchema,
|
|
6
|
+
keywordExtractionSchema,
|
|
7
|
+
workflowMatchingSchema,
|
|
8
|
+
} from '../schemas/index';
|
|
9
|
+
import type {
|
|
10
|
+
DraftIntentResult,
|
|
11
|
+
FeasibilityResult,
|
|
12
|
+
KeywordExtractionResult,
|
|
13
|
+
NodeDefinition,
|
|
14
|
+
NodeSearchResult,
|
|
15
|
+
OutputRefValidation,
|
|
16
|
+
RuntimeContext,
|
|
17
|
+
WorkflowDefinition,
|
|
18
|
+
WorkflowDraft,
|
|
19
|
+
WorkflowMatchResult,
|
|
20
|
+
} from '../types/index';
|
|
21
|
+
import { getNodeDefinition, simplifyNodeForLLM } from './catalog';
|
|
22
|
+
import {
|
|
23
|
+
formatSchemaForPrompt,
|
|
24
|
+
getAvailableOperations,
|
|
25
|
+
getAvailableResources,
|
|
26
|
+
hasOutputSchema,
|
|
27
|
+
loadOutputSchema,
|
|
28
|
+
} from './outputSchema';
|
|
29
|
+
import type { UnknownParamDetection } from './workflow';
|
|
30
|
+
import {
|
|
31
|
+
ACTION_RESPONSE_SYSTEM_PROMPT,
|
|
32
|
+
DRAFT_INTENT_SYSTEM_PROMPT,
|
|
33
|
+
FEASIBILITY_CHECK_PROMPT,
|
|
34
|
+
FIELD_CORRECTION_SYSTEM_PROMPT,
|
|
35
|
+
FIELD_CORRECTION_USER_PROMPT,
|
|
36
|
+
KEYWORD_EXTRACTION_SYSTEM_PROMPT,
|
|
37
|
+
PARAM_CORRECTION_SYSTEM_PROMPT,
|
|
38
|
+
PARAM_CORRECTION_USER_PROMPT,
|
|
39
|
+
WORKFLOW_GENERATION_SYSTEM_PROMPT,
|
|
40
|
+
} from './workflow-prompts/index';
|
|
41
|
+
import { WORKFLOW_MATCHING_SYSTEM_PROMPT } from './workflow-prompts/workflowMatching';
|
|
42
|
+
|
|
43
|
+
type StructuredModelRunner = {
|
|
44
|
+
useModel<T>(
|
|
45
|
+
modelType: typeof ModelType.TEXT_SMALL,
|
|
46
|
+
params: GenerateTextParams & { responseSchema: unknown },
|
|
47
|
+
provider?: string
|
|
48
|
+
): Promise<T>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function useStructuredModel<T>(
|
|
52
|
+
runtime: IAgentRuntime,
|
|
53
|
+
prompt: string,
|
|
54
|
+
schema: unknown
|
|
55
|
+
): Promise<T> {
|
|
56
|
+
const structuredRuntime = runtime as IAgentRuntime & StructuredModelRunner;
|
|
57
|
+
return (await structuredRuntime.useModel<T>(ModelType.TEXT_SMALL, {
|
|
58
|
+
prompt,
|
|
59
|
+
responseSchema: schema as never,
|
|
60
|
+
})) as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build an optional bias directive that nudges keyword extraction toward
|
|
65
|
+
* providers the host has already declared it can satisfy. The directive is
|
|
66
|
+
* appended to KEYWORD_EXTRACTION_SYSTEM_PROMPT only when a non-empty
|
|
67
|
+
* `preferredProviders` list is supplied — keeps existing baseline behavior
|
|
68
|
+
* for non-host installs.
|
|
69
|
+
*/
|
|
70
|
+
function buildPreferredProvidersDirective(preferredProviders?: string[]): string {
|
|
71
|
+
if (!preferredProviders || preferredProviders.length === 0) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
const list = preferredProviders.map((p) => p.toLowerCase()).join(', ');
|
|
75
|
+
return `\n\nHost-supported providers: ${list}. When the user names a generic concept that maps to one of these (e.g. "my email" with gmail in the list, "my chat" with discord in the list), emit the specific provider keyword (gmail, discord) — NOT a generic fallback (imap, webhook, email). Prefer these provider names over alternative integrations.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function extractKeywords(
|
|
79
|
+
runtime: IAgentRuntime,
|
|
80
|
+
userPrompt: string,
|
|
81
|
+
preferredProviders?: string[]
|
|
82
|
+
): Promise<string[]> {
|
|
83
|
+
let result: KeywordExtractionResult;
|
|
84
|
+
try {
|
|
85
|
+
result = await useStructuredModel<KeywordExtractionResult>(
|
|
86
|
+
runtime,
|
|
87
|
+
`${KEYWORD_EXTRACTION_SYSTEM_PROMPT}${buildPreferredProvidersDirective(preferredProviders)}\n\nUser request: ${userPrompt}`,
|
|
88
|
+
keywordExtractionSchema
|
|
89
|
+
);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
92
|
+
logger.error(
|
|
93
|
+
{ src: 'plugin:workflow:generation:keywords', error: errMsg },
|
|
94
|
+
`Keyword extraction LLM call failed: ${errMsg}`
|
|
95
|
+
);
|
|
96
|
+
throw new Error(`Keyword extraction failed: ${errMsg}`, { cause: error });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Validate structure
|
|
100
|
+
if (!result?.keywords || !Array.isArray(result.keywords)) {
|
|
101
|
+
logger.error(
|
|
102
|
+
{
|
|
103
|
+
src: 'plugin:workflow:generation:keywords',
|
|
104
|
+
result: JSON.stringify(result),
|
|
105
|
+
},
|
|
106
|
+
'Invalid keyword extraction response structure'
|
|
107
|
+
);
|
|
108
|
+
throw new Error('Invalid keyword extraction response: missing or invalid keywords array');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Validate all items are strings
|
|
112
|
+
if (!result.keywords.every((kw) => typeof kw === 'string')) {
|
|
113
|
+
throw new Error('Keywords array contains non-string elements');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Limit to 5 keywords max, filter empty strings
|
|
117
|
+
return result.keywords
|
|
118
|
+
.slice(0, 5)
|
|
119
|
+
.map((kw) => kw.trim())
|
|
120
|
+
.filter((kw) => kw.length > 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function matchWorkflow(
|
|
124
|
+
runtime: IAgentRuntime,
|
|
125
|
+
userRequest: string,
|
|
126
|
+
workflows: WorkflowDefinition[]
|
|
127
|
+
): Promise<WorkflowMatchResult> {
|
|
128
|
+
if (workflows.length === 0) {
|
|
129
|
+
return {
|
|
130
|
+
matchedWorkflowId: null,
|
|
131
|
+
confidence: 'none',
|
|
132
|
+
matches: [],
|
|
133
|
+
reason: 'No workflows available',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Build workflow list for LLM
|
|
139
|
+
const workflowList = workflows
|
|
140
|
+
.map(
|
|
141
|
+
(wf, index) =>
|
|
142
|
+
`${index + 1}. "${wf.name}" (ID: ${wf.id}, Status: ${wf.active ? 'ACTIVE' : 'INACTIVE'})`
|
|
143
|
+
)
|
|
144
|
+
.join('\n');
|
|
145
|
+
|
|
146
|
+
const userPrompt = `${userRequest}
|
|
147
|
+
|
|
148
|
+
Available workflows:
|
|
149
|
+
${workflowList}`;
|
|
150
|
+
|
|
151
|
+
let result: WorkflowMatchResult;
|
|
152
|
+
try {
|
|
153
|
+
result = await useStructuredModel<WorkflowMatchResult>(
|
|
154
|
+
runtime,
|
|
155
|
+
`${WORKFLOW_MATCHING_SYSTEM_PROMPT}\n\n${userPrompt}`,
|
|
156
|
+
workflowMatchingSchema
|
|
157
|
+
);
|
|
158
|
+
} catch (innerError) {
|
|
159
|
+
const errMsg = innerError instanceof Error ? innerError.message : String(innerError);
|
|
160
|
+
logger.error(
|
|
161
|
+
{ src: 'plugin:workflow:generation:matcher', error: errMsg },
|
|
162
|
+
`Workflow matching LLM call failed: ${errMsg}`
|
|
163
|
+
);
|
|
164
|
+
throw innerError;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate the returned ID actually exists in the provided list
|
|
168
|
+
if (result.matchedWorkflowId && !workflows.some((wf) => wf.id === result.matchedWorkflowId)) {
|
|
169
|
+
logger.warn(
|
|
170
|
+
{ src: 'plugin:workflow:generation:matcher' },
|
|
171
|
+
`LLM returned non-existent workflow ID "${result.matchedWorkflowId}" — discarding`
|
|
172
|
+
);
|
|
173
|
+
result.matchedWorkflowId = null;
|
|
174
|
+
result.confidence = 'none';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
logger.debug(
|
|
178
|
+
{ src: 'plugin:workflow:generation:matcher' },
|
|
179
|
+
`Workflow match: ${result.matchedWorkflowId || 'none'} (confidence: ${result.confidence})`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return result;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
185
|
+
logger.error(
|
|
186
|
+
{ src: 'plugin:workflow:generation:matcher' },
|
|
187
|
+
`Workflow matching failed: ${errorMessage}`
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
matchedWorkflowId: null,
|
|
192
|
+
confidence: 'none',
|
|
193
|
+
matches: [],
|
|
194
|
+
reason: `Workflow matching service unavailable: ${errorMessage}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function classifyDraftIntent(
|
|
200
|
+
runtime: IAgentRuntime,
|
|
201
|
+
userMessage: string,
|
|
202
|
+
draft: WorkflowDraft
|
|
203
|
+
): Promise<DraftIntentResult> {
|
|
204
|
+
const draftSummary = `Workflow: "${draft.workflow.name}"
|
|
205
|
+
Nodes: ${draft.workflow.nodes.map((n) => `${n.name} (${n.type})`).join(', ')}
|
|
206
|
+
Original prompt: "${draft.prompt}"`;
|
|
207
|
+
|
|
208
|
+
let result: DraftIntentResult;
|
|
209
|
+
try {
|
|
210
|
+
result = await useStructuredModel<DraftIntentResult>(
|
|
211
|
+
runtime,
|
|
212
|
+
`${DRAFT_INTENT_SYSTEM_PROMPT}
|
|
213
|
+
|
|
214
|
+
## Current Draft
|
|
215
|
+
|
|
216
|
+
${draftSummary}
|
|
217
|
+
|
|
218
|
+
## User Message
|
|
219
|
+
|
|
220
|
+
${userMessage}`,
|
|
221
|
+
draftIntentSchema
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
225
|
+
logger.error(
|
|
226
|
+
{ src: 'plugin:workflow:generation:intent', error: errMsg },
|
|
227
|
+
`classifyDraftIntent LLM call failed: ${errMsg}`
|
|
228
|
+
);
|
|
229
|
+
return {
|
|
230
|
+
intent: 'show_preview',
|
|
231
|
+
reason: `Intent classification failed (${errMsg}) — re-showing preview`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const validIntents = ['confirm', 'cancel', 'modify', 'new'] as const;
|
|
236
|
+
if (!result?.intent || !validIntents.includes(result.intent as (typeof validIntents)[number])) {
|
|
237
|
+
logger.warn(
|
|
238
|
+
{ src: 'plugin:workflow:generation:intent' },
|
|
239
|
+
`Invalid intent from LLM: ${JSON.stringify(result)}, re-showing preview`
|
|
240
|
+
);
|
|
241
|
+
return {
|
|
242
|
+
intent: 'show_preview',
|
|
243
|
+
reason: 'Could not classify intent — re-showing preview',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Layer 3 retry helper (Session 21). When `validateAndRepair` flags errors
|
|
252
|
+
* it can't auto-fix deterministically (e.g. truly unknown output field),
|
|
253
|
+
* send a surgical fix prompt to the LLM listing only the failing items
|
|
254
|
+
* and re-validate. The caller wraps this in a 3-attempt loop.
|
|
255
|
+
*
|
|
256
|
+
* Lives next to `correctFieldReferences` and `correctParameterNames` —
|
|
257
|
+
* those are still the preferred specific-class corrections. This is the
|
|
258
|
+
* generic backstop for any remaining error class.
|
|
259
|
+
*/
|
|
260
|
+
export async function fixWorkflowErrors(
|
|
261
|
+
runtime: IAgentRuntime,
|
|
262
|
+
workflow: WorkflowDefinition,
|
|
263
|
+
errors: Array<{
|
|
264
|
+
kind: string;
|
|
265
|
+
node: string;
|
|
266
|
+
detail: string;
|
|
267
|
+
expression?: string;
|
|
268
|
+
availableFields?: string[];
|
|
269
|
+
}>,
|
|
270
|
+
relevantNodes: NodeDefinition[]
|
|
271
|
+
): Promise<WorkflowDefinition> {
|
|
272
|
+
if (errors.length === 0) {
|
|
273
|
+
return workflow;
|
|
274
|
+
}
|
|
275
|
+
const errorBlock = errors
|
|
276
|
+
.map((e, i) => {
|
|
277
|
+
const av = e.availableFields?.length
|
|
278
|
+
? ` Available fields on the upstream node: ${e.availableFields.join(', ')}.`
|
|
279
|
+
: '';
|
|
280
|
+
const expr = e.expression ? ` Expression: \`${e.expression}\`.` : '';
|
|
281
|
+
return `${i + 1}. Node "${e.node}" — ${e.detail}.${expr}${av}`;
|
|
282
|
+
})
|
|
283
|
+
.join('\n');
|
|
284
|
+
|
|
285
|
+
const simplifiedNodes = relevantNodes.map(simplifyNodeForLLM);
|
|
286
|
+
const fixPrompt = `You are fixing a deterministic-validator-flagged workflow. Apply ONLY the listed fixes — do not refactor anything else.
|
|
287
|
+
|
|
288
|
+
## Errors to fix
|
|
289
|
+
|
|
290
|
+
${errorBlock}
|
|
291
|
+
|
|
292
|
+
## Available node definitions (for reference)
|
|
293
|
+
|
|
294
|
+
${JSON.stringify(simplifiedNodes, null, 2)}
|
|
295
|
+
|
|
296
|
+
## Current workflow JSON
|
|
297
|
+
|
|
298
|
+
${JSON.stringify(workflow, null, 2)}
|
|
299
|
+
|
|
300
|
+
Return the COMPLETE corrected workflow JSON. Preserve every field that was not part of the error list. Only change what is required to fix the listed items.`;
|
|
301
|
+
|
|
302
|
+
let response: string;
|
|
303
|
+
try {
|
|
304
|
+
response = (await runtime.useModel(ModelType.TEXT_LARGE, {
|
|
305
|
+
prompt: fixPrompt,
|
|
306
|
+
temperature: 0,
|
|
307
|
+
responseFormat: { type: 'json_object' },
|
|
308
|
+
})) as string;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
311
|
+
logger.error(
|
|
312
|
+
{ src: 'plugin:workflow:generation:fixErrors', err: errMsg },
|
|
313
|
+
`fixWorkflowErrors LLM call failed: ${errMsg}`
|
|
314
|
+
);
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
return parseWorkflowResponse(response);
|
|
320
|
+
} catch (_err) {
|
|
321
|
+
logger.error(
|
|
322
|
+
{ src: 'plugin:workflow:generation:fixErrors' },
|
|
323
|
+
'fixWorkflowErrors response could not be parsed; keeping original workflow'
|
|
324
|
+
);
|
|
325
|
+
return workflow;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Walk a string starting at every `{` opener, extract the first slice that
|
|
331
|
+
* yields a valid JSON object, and return that parsed value. Tolerates leading
|
|
332
|
+
* and trailing prose around the workflow JSON (which the LLM occasionally
|
|
333
|
+
* emits in spite of `responseFormat: { type: 'json_object' }`).
|
|
334
|
+
*
|
|
335
|
+
* Returns the parsed value rather than the source string so callers don't pay
|
|
336
|
+
* for a second `JSON.parse` on the same bytes.
|
|
337
|
+
*
|
|
338
|
+
* Caveat: returns the *first* slice that round-trips through `JSON.parse`. If
|
|
339
|
+
* the LLM ever prefixes the workflow with a small standalone JSON fragment
|
|
340
|
+
* (e.g. `{"ok":true}\n{…full workflow…}`), the small fragment wins and the
|
|
341
|
+
* downstream `nodes`/`connections` validation throws "missing nodes array".
|
|
342
|
+
* That's an obvious failure mode rather than a silent corruption, so we don't
|
|
343
|
+
* try to second-guess which candidate is the workflow here.
|
|
344
|
+
*/
|
|
345
|
+
function extractFirstBalancedJsonObject(text: string): unknown | null {
|
|
346
|
+
for (let start = 0; start < text.length; start++) {
|
|
347
|
+
if (text[start] !== '{') {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
let depth = 0;
|
|
351
|
+
let inString = false;
|
|
352
|
+
let escaped = false;
|
|
353
|
+
for (let i = start; i < text.length; i++) {
|
|
354
|
+
const ch = text[i];
|
|
355
|
+
if (inString) {
|
|
356
|
+
if (escaped) {
|
|
357
|
+
escaped = false;
|
|
358
|
+
} else if (ch === '\\') {
|
|
359
|
+
escaped = true;
|
|
360
|
+
} else if (ch === '"') {
|
|
361
|
+
inString = false;
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (ch === '"') {
|
|
366
|
+
inString = true;
|
|
367
|
+
} else if (ch === '{') {
|
|
368
|
+
depth++;
|
|
369
|
+
} else if (ch === '}') {
|
|
370
|
+
depth--;
|
|
371
|
+
if (depth === 0) {
|
|
372
|
+
const candidate = text.slice(start, i + 1);
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(candidate);
|
|
375
|
+
} catch {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseWorkflowResponse(response: string): WorkflowDefinition {
|
|
386
|
+
// Strip markdown code fences (handles ```json, ```, with any whitespace/newlines)
|
|
387
|
+
const cleaned = response
|
|
388
|
+
.replace(/^[\s\S]*?```(?:json)?\s*\n?/i, '') // Remove everything up to and including opening fence
|
|
389
|
+
.replace(/\n?```[\s\S]*$/i, '') // Remove closing fence and everything after
|
|
390
|
+
.trim();
|
|
391
|
+
|
|
392
|
+
let workflow: WorkflowDefinition;
|
|
393
|
+
try {
|
|
394
|
+
workflow = JSON.parse(cleaned) as WorkflowDefinition;
|
|
395
|
+
} catch (initialError) {
|
|
396
|
+
// Fence-strip + JSON.parse failed. The LLM may have wrapped the JSON in
|
|
397
|
+
// prose despite responseFormat: { type: 'json_object' }. Walk the cleaned
|
|
398
|
+
// text and extract the first balanced JSON object that parses.
|
|
399
|
+
const extracted = extractFirstBalancedJsonObject(cleaned);
|
|
400
|
+
if (extracted === null) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Failed to parse workflow JSON: ${initialError instanceof Error ? initialError.message : String(initialError)}\n\nRaw response: ${response}`,
|
|
403
|
+
{ cause: initialError }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
workflow = extracted as WorkflowDefinition;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
|
|
410
|
+
throw new Error('Invalid workflow: missing or invalid nodes array');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!workflow.connections || typeof workflow.connections !== 'object') {
|
|
414
|
+
throw new Error('Invalid workflow: missing or invalid connections object');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return workflow;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Build output schema context for relevant nodes so the LLM knows the exact
|
|
422
|
+
* output fields when writing expressions like {{ $json.field }}.
|
|
423
|
+
*/
|
|
424
|
+
function buildOutputSchemaContext(nodes: NodeDefinition[]): string {
|
|
425
|
+
const sections: string[] = [];
|
|
426
|
+
|
|
427
|
+
for (const node of nodes) {
|
|
428
|
+
if (!hasOutputSchema(node.name)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const resources = getAvailableResources(node.name);
|
|
433
|
+
for (const resource of resources) {
|
|
434
|
+
const operations = getAvailableOperations(node.name, resource);
|
|
435
|
+
for (const operation of operations) {
|
|
436
|
+
const result = loadOutputSchema(node.name, resource, operation);
|
|
437
|
+
if (!result) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const formatted = formatSchemaForPrompt(result.schema);
|
|
441
|
+
sections.push(
|
|
442
|
+
`### ${node.name} (resource: "${resource}", operation: "${operation}")\n${formatted}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (sections.length === 0) {
|
|
449
|
+
return '';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return `\n## Node Output Schemas\n\nWhen referencing output data from a previous node using expressions like \`{{ $json.field }}\`, use ONLY the field paths listed below. Do NOT invent field names from your training data.\n\n${sections.join('\n\n')}`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Render the optional host-supplied runtime context as two prompt sections:
|
|
457
|
+
* `## Available Credentials` (which credential types the host can resolve) and
|
|
458
|
+
* `## Runtime Facts` (real values like Discord guild/channel IDs, the user's
|
|
459
|
+
* email). Returns the empty string when the host did not register a provider
|
|
460
|
+
* — preserving exact baseline behavior for non-host installs.
|
|
461
|
+
*/
|
|
462
|
+
function buildRuntimeContextSections(ctx?: RuntimeContext): string {
|
|
463
|
+
if (!ctx) {
|
|
464
|
+
return '';
|
|
465
|
+
}
|
|
466
|
+
const lines: string[] = [];
|
|
467
|
+
if (ctx.supportedCredentials?.length) {
|
|
468
|
+
lines.push('## Available Credentials');
|
|
469
|
+
lines.push(
|
|
470
|
+
'These credential types are pre-resolved by the host. Attach the credentials block to every relevant node — the host injects the real id post-generation.'
|
|
471
|
+
);
|
|
472
|
+
for (const c of ctx.supportedCredentials) {
|
|
473
|
+
lines.push(
|
|
474
|
+
`- ${c.credType}: name "${c.friendlyName}" — applies to: ${c.nodeTypes.join(', ')}`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
lines.push('');
|
|
478
|
+
}
|
|
479
|
+
if (ctx.facts?.length) {
|
|
480
|
+
lines.push('## Runtime Facts');
|
|
481
|
+
lines.push('Use these real values verbatim instead of placeholders.');
|
|
482
|
+
for (const fact of ctx.facts) {
|
|
483
|
+
lines.push(`- ${fact}`);
|
|
484
|
+
}
|
|
485
|
+
lines.push('');
|
|
486
|
+
}
|
|
487
|
+
return lines.length ? `\n${lines.join('\n')}\n` : '';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Run a TEXT_LARGE generation and parse the result as a workflow.
|
|
492
|
+
* If the first response fails parseWorkflowResponse — which happens when the
|
|
493
|
+
* LLM occasionally drops required fields despite responseFormat: json_object —
|
|
494
|
+
* retry once with an explicit corrective nudge before letting the parse error
|
|
495
|
+
* escape. Generation calls are 30-90s of sequential LLM work upstream of this
|
|
496
|
+
* point, so failing closed on a single non-deterministic LLM roll is poor UX.
|
|
497
|
+
*/
|
|
498
|
+
async function callLlmAndParseWorkflow(
|
|
499
|
+
runtime: IAgentRuntime,
|
|
500
|
+
prompt: string,
|
|
501
|
+
context: 'generateWorkflow' | 'modifyWorkflow'
|
|
502
|
+
): Promise<WorkflowDefinition> {
|
|
503
|
+
const callOnce = async (extraInstruction?: string): Promise<string> =>
|
|
504
|
+
(await runtime.useModel(ModelType.TEXT_LARGE, {
|
|
505
|
+
prompt: extraInstruction ? `${prompt}\n\n${extraInstruction}` : prompt,
|
|
506
|
+
temperature: 0,
|
|
507
|
+
responseFormat: { type: 'json_object' },
|
|
508
|
+
})) as string;
|
|
509
|
+
|
|
510
|
+
const firstResponse = await callOnce();
|
|
511
|
+
try {
|
|
512
|
+
return parseWorkflowResponse(firstResponse);
|
|
513
|
+
} catch (firstErr) {
|
|
514
|
+
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
515
|
+
logger.warn(
|
|
516
|
+
{ src: `plugin:workflow:generation:${context}`, err: firstMsg },
|
|
517
|
+
'parseWorkflowResponse failed on first attempt; retrying once'
|
|
518
|
+
);
|
|
519
|
+
const retryResponse = await callOnce(
|
|
520
|
+
'Your previous response was malformed and could not be parsed as a workflow. Return ONLY a single valid JSON object containing the required fields "nodes" (array) and "connections" (object) — no prose, no markdown fences, no explanations.'
|
|
521
|
+
);
|
|
522
|
+
return parseWorkflowResponse(retryResponse);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function generateWorkflow(
|
|
527
|
+
runtime: IAgentRuntime,
|
|
528
|
+
userPrompt: string,
|
|
529
|
+
relevantNodes: NodeDefinition[],
|
|
530
|
+
runtimeContext?: RuntimeContext
|
|
531
|
+
): Promise<WorkflowDefinition> {
|
|
532
|
+
const simplifiedNodes = relevantNodes.map(simplifyNodeForLLM);
|
|
533
|
+
const outputSchemaCtx = buildOutputSchemaContext(relevantNodes);
|
|
534
|
+
const runtimeCtxSections = buildRuntimeContextSections(runtimeContext);
|
|
535
|
+
|
|
536
|
+
const fullPrompt = `${WORKFLOW_GENERATION_SYSTEM_PROMPT}
|
|
537
|
+
|
|
538
|
+
## Relevant Nodes Available
|
|
539
|
+
|
|
540
|
+
${JSON.stringify(simplifiedNodes, null, 2)}
|
|
541
|
+
|
|
542
|
+
Use these node definitions to generate the workflow. Each node's "properties" field defines the available parameters.
|
|
543
|
+
${outputSchemaCtx}${runtimeCtxSections}
|
|
544
|
+
|
|
545
|
+
## User Request
|
|
546
|
+
|
|
547
|
+
${userPrompt}
|
|
548
|
+
|
|
549
|
+
Generate a valid workflow JSON that fulfills this request.`;
|
|
550
|
+
|
|
551
|
+
const workflow = await callLlmAndParseWorkflow(runtime, fullPrompt, 'generateWorkflow');
|
|
552
|
+
|
|
553
|
+
if (!workflow.name) {
|
|
554
|
+
workflow.name = `Workflow - ${userPrompt.slice(0, 50).trim()}`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return workflow;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function modifyWorkflow(
|
|
561
|
+
runtime: IAgentRuntime,
|
|
562
|
+
existingWorkflow: WorkflowDefinition,
|
|
563
|
+
modificationRequest: string,
|
|
564
|
+
relevantNodes: NodeDefinition[],
|
|
565
|
+
runtimeContext?: RuntimeContext
|
|
566
|
+
): Promise<WorkflowDefinition> {
|
|
567
|
+
const { _meta, ...workflowForLLM } = existingWorkflow;
|
|
568
|
+
|
|
569
|
+
const simplifiedNodes = relevantNodes.map(simplifyNodeForLLM);
|
|
570
|
+
const outputSchemaCtx = buildOutputSchemaContext(relevantNodes);
|
|
571
|
+
const runtimeCtxSections = buildRuntimeContextSections(runtimeContext);
|
|
572
|
+
|
|
573
|
+
const fullPrompt = `${WORKFLOW_GENERATION_SYSTEM_PROMPT}
|
|
574
|
+
|
|
575
|
+
## Relevant Nodes Available
|
|
576
|
+
|
|
577
|
+
${JSON.stringify(simplifiedNodes, null, 2)}
|
|
578
|
+
|
|
579
|
+
Use these node definitions to modify the workflow. Each node's "properties" field defines the available parameters.
|
|
580
|
+
${outputSchemaCtx}${runtimeCtxSections}
|
|
581
|
+
|
|
582
|
+
## Existing Workflow (modify this)
|
|
583
|
+
|
|
584
|
+
${JSON.stringify(workflowForLLM, null, 2)}
|
|
585
|
+
|
|
586
|
+
## Modification Request
|
|
587
|
+
|
|
588
|
+
${modificationRequest}
|
|
589
|
+
|
|
590
|
+
Modify the existing workflow according to the request above. Return the COMPLETE modified workflow JSON.
|
|
591
|
+
Keep all unchanged nodes and connections intact. Only add, remove, or change what the user asked for.`;
|
|
592
|
+
|
|
593
|
+
const modified = await callLlmAndParseWorkflow(runtime, fullPrompt, 'modifyWorkflow');
|
|
594
|
+
|
|
595
|
+
// Preserve the original workflow ID for updates (LLM doesn't return it)
|
|
596
|
+
if (existingWorkflow.id) {
|
|
597
|
+
modified.id = existingWorkflow.id;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return modified;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function collectExistingNodeDefinitions(workflow: WorkflowDefinition): NodeDefinition[] {
|
|
604
|
+
const defs: NodeDefinition[] = [];
|
|
605
|
+
const seen = new Set<string>();
|
|
606
|
+
|
|
607
|
+
for (const node of workflow.nodes) {
|
|
608
|
+
if (seen.has(node.type)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
seen.add(node.type);
|
|
612
|
+
|
|
613
|
+
const def = getNodeDefinition(node.type);
|
|
614
|
+
if (def) {
|
|
615
|
+
defs.push(def);
|
|
616
|
+
} else {
|
|
617
|
+
logger.warn(
|
|
618
|
+
{ src: 'plugin:workflow:generation:modify' },
|
|
619
|
+
`No catalog definition found for node type "${node.type}" — LLM will have limited context for this node`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return defs;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export async function formatActionResponse(
|
|
628
|
+
runtime: IAgentRuntime,
|
|
629
|
+
responseType: string,
|
|
630
|
+
data: Record<string, unknown>
|
|
631
|
+
): Promise<string> {
|
|
632
|
+
try {
|
|
633
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
634
|
+
prompt: `${ACTION_RESPONSE_SYSTEM_PROMPT}\n\nType: ${responseType}\n\nData:\n${formatActionDataForPrompt(data)}`,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return (response as string).trim();
|
|
638
|
+
} catch (error) {
|
|
639
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
640
|
+
logger.error(
|
|
641
|
+
{
|
|
642
|
+
src: 'plugin:workflow:generation:format',
|
|
643
|
+
error: errMsg,
|
|
644
|
+
responseType,
|
|
645
|
+
},
|
|
646
|
+
`formatActionResponse LLM call failed: ${errMsg}`
|
|
647
|
+
);
|
|
648
|
+
// Return a fallback message so the action can still communicate with the user
|
|
649
|
+
if (responseType === 'ERROR') {
|
|
650
|
+
return `An error occurred: ${data.error || 'Unknown error'}`;
|
|
651
|
+
}
|
|
652
|
+
return `Operation completed (type: ${responseType})`;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatActionDataForPrompt(value: unknown, indent = 0): string {
|
|
657
|
+
if (value === undefined || value === null) {
|
|
658
|
+
return '';
|
|
659
|
+
}
|
|
660
|
+
if (typeof value === 'string') {
|
|
661
|
+
return value.trim();
|
|
662
|
+
}
|
|
663
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
664
|
+
return String(value);
|
|
665
|
+
}
|
|
666
|
+
const pad = ' '.repeat(indent);
|
|
667
|
+
if (Array.isArray(value)) {
|
|
668
|
+
return value
|
|
669
|
+
.map((item) => {
|
|
670
|
+
const formatted = formatActionDataForPrompt(item, indent + 2);
|
|
671
|
+
return `${pad}- ${formatted.replace(/\n/g, `\n${pad} `)}`;
|
|
672
|
+
})
|
|
673
|
+
.join('\n');
|
|
674
|
+
}
|
|
675
|
+
if (typeof value === 'object') {
|
|
676
|
+
return Object.entries(value as Record<string, unknown>)
|
|
677
|
+
.map(([key, entry]) => {
|
|
678
|
+
const formatted = formatActionDataForPrompt(entry, indent + 2);
|
|
679
|
+
return formatted.includes('\n')
|
|
680
|
+
? `${pad}${key}:\n${formatted}`
|
|
681
|
+
: `${pad}${key}: ${formatted}`;
|
|
682
|
+
})
|
|
683
|
+
.join('\n');
|
|
684
|
+
}
|
|
685
|
+
return String(value);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export async function assessFeasibility(
|
|
689
|
+
runtime: IAgentRuntime,
|
|
690
|
+
userPrompt: string,
|
|
691
|
+
removedNodes: NodeSearchResult[],
|
|
692
|
+
remainingNodes: NodeSearchResult[]
|
|
693
|
+
): Promise<FeasibilityResult> {
|
|
694
|
+
const removedList = removedNodes
|
|
695
|
+
.filter((r) => r.node.credentials?.length)
|
|
696
|
+
.map((r) => r.node.displayName)
|
|
697
|
+
.join(', ');
|
|
698
|
+
|
|
699
|
+
const availableList = remainingNodes
|
|
700
|
+
.filter((r) => r.node.credentials?.length)
|
|
701
|
+
.map((r) => r.node.displayName)
|
|
702
|
+
.join(', ');
|
|
703
|
+
|
|
704
|
+
const utilityList = remainingNodes
|
|
705
|
+
.filter((r) => !r.node.credentials?.length)
|
|
706
|
+
.map((r) => r.node.displayName)
|
|
707
|
+
.join(', ');
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
const result = await useStructuredModel<FeasibilityResult>(
|
|
711
|
+
runtime,
|
|
712
|
+
`${FEASIBILITY_CHECK_PROMPT}\n\n## User Request\n${userPrompt}` +
|
|
713
|
+
`\n\n## Removed Integrations (unavailable)\n${removedList}` +
|
|
714
|
+
`\n\n## Available Service Integrations\n${availableList}` +
|
|
715
|
+
`\n\n## Available Utility Nodes\n${utilityList}`,
|
|
716
|
+
feasibilitySchema
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
return result;
|
|
720
|
+
} catch (error) {
|
|
721
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
722
|
+
logger.error(
|
|
723
|
+
{ src: 'plugin:workflow:generation:feasibility', error: errMsg },
|
|
724
|
+
`Feasibility assessment LLM call failed: ${errMsg}`
|
|
725
|
+
);
|
|
726
|
+
return {
|
|
727
|
+
feasible: false,
|
|
728
|
+
reason: `Feasibility check failed: ${errMsg}`,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Auto-corrects invalid field references in expressions using parallel LLM calls.
|
|
735
|
+
* Returns a new workflow with corrected expressions.
|
|
736
|
+
*/
|
|
737
|
+
export async function correctFieldReferences(
|
|
738
|
+
runtime: IAgentRuntime,
|
|
739
|
+
workflow: WorkflowDefinition,
|
|
740
|
+
invalidRefs: OutputRefValidation[]
|
|
741
|
+
): Promise<WorkflowDefinition> {
|
|
742
|
+
if (invalidRefs.length === 0) {
|
|
743
|
+
return workflow;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
logger.debug(
|
|
747
|
+
{ src: 'plugin:workflow:generation:correction' },
|
|
748
|
+
`Correcting ${invalidRefs.length} invalid field reference(s)`
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
const corrections = await Promise.all(
|
|
752
|
+
invalidRefs.map(async (ref) => {
|
|
753
|
+
try {
|
|
754
|
+
const userPrompt = FIELD_CORRECTION_USER_PROMPT.replace(
|
|
755
|
+
'{expression}',
|
|
756
|
+
ref.expression
|
|
757
|
+
).replace('{availableFields}', ref.availableFields.join('\n'));
|
|
758
|
+
|
|
759
|
+
const corrected = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
760
|
+
prompt: `${FIELD_CORRECTION_SYSTEM_PROMPT}\n\n${userPrompt}`,
|
|
761
|
+
temperature: 0,
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const cleaned = (corrected as string).trim();
|
|
765
|
+
return {
|
|
766
|
+
original: ref.expression,
|
|
767
|
+
corrected: cleaned,
|
|
768
|
+
nodeName: ref.nodeName,
|
|
769
|
+
};
|
|
770
|
+
} catch (error) {
|
|
771
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
772
|
+
logger.warn(
|
|
773
|
+
{ src: 'plugin:workflow:generation:correction', error: errMsg },
|
|
774
|
+
`Failed to correct expression "${ref.expression}": ${errMsg}`
|
|
775
|
+
);
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
})
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
const correctedWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowDefinition;
|
|
782
|
+
|
|
783
|
+
for (const correction of corrections) {
|
|
784
|
+
if (!correction) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const node = correctedWorkflow.nodes.find((n) => n.name === correction.nodeName);
|
|
789
|
+
if (!node?.parameters) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
replaceInObject(node.parameters, correction.original, correction.corrected);
|
|
794
|
+
|
|
795
|
+
logger.debug(
|
|
796
|
+
{ src: 'plugin:workflow:generation:correction' },
|
|
797
|
+
`Corrected "${correction.original}" → "${correction.corrected}" in node "${correction.nodeName}"`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return correctedWorkflow;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function replaceInObject(
|
|
805
|
+
obj: Record<string, unknown>,
|
|
806
|
+
original: string,
|
|
807
|
+
replacement: string
|
|
808
|
+
): void {
|
|
809
|
+
for (const key of Object.keys(obj)) {
|
|
810
|
+
const value = obj[key];
|
|
811
|
+
|
|
812
|
+
if (typeof value === 'string' && value.includes(original)) {
|
|
813
|
+
obj[key] = value.replaceAll(original, replacement);
|
|
814
|
+
} else if (Array.isArray(value)) {
|
|
815
|
+
for (let i = 0; i < value.length; i++) {
|
|
816
|
+
if (typeof value[i] === 'string' && value[i].includes(original)) {
|
|
817
|
+
value[i] = value[i].replaceAll(original, replacement);
|
|
818
|
+
} else if (typeof value[i] === 'object' && value[i] !== null) {
|
|
819
|
+
replaceInObject(value[i] as Record<string, unknown>, original, replacement);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
823
|
+
replaceInObject(value as Record<string, unknown>, original, replacement);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Try to deterministically rename an unknown key to a valid property name.
|
|
830
|
+
* Returns the matching property name or null if no confident match.
|
|
831
|
+
*/
|
|
832
|
+
function fuzzyMatchParam(
|
|
833
|
+
unknownKey: string,
|
|
834
|
+
validProps: { name: string; type: string }[]
|
|
835
|
+
): string | null {
|
|
836
|
+
const lower = unknownKey.toLowerCase();
|
|
837
|
+
|
|
838
|
+
// Substring match: one valid prop contains the unknown key or vice-versa
|
|
839
|
+
// Guard: shorter string must be ≥ 60% of the longer to avoid false positives (e.g. "url" in "curl")
|
|
840
|
+
const substringMatches = validProps.filter((p) => {
|
|
841
|
+
const pLower = p.name.toLowerCase();
|
|
842
|
+
if (!(pLower.includes(lower) || lower.includes(pLower))) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
const ratio = Math.min(lower.length, pLower.length) / Math.max(lower.length, pLower.length);
|
|
846
|
+
return ratio >= 0.6;
|
|
847
|
+
});
|
|
848
|
+
if (substringMatches.length === 1) {
|
|
849
|
+
return substringMatches[0].name;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Deterministic fast path + LLM fallback for parameter name correction. */
|
|
856
|
+
export async function correctParameterNames(
|
|
857
|
+
runtime: IAgentRuntime,
|
|
858
|
+
workflow: WorkflowDefinition,
|
|
859
|
+
detections: UnknownParamDetection[]
|
|
860
|
+
): Promise<WorkflowDefinition> {
|
|
861
|
+
if (detections.length === 0) {
|
|
862
|
+
return workflow;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const correctedWorkflow = JSON.parse(JSON.stringify(workflow)) as WorkflowDefinition;
|
|
866
|
+
|
|
867
|
+
// Phase 1: deterministic renames (no LLM cost)
|
|
868
|
+
const needsLLM: UnknownParamDetection[] = [];
|
|
869
|
+
|
|
870
|
+
for (const detection of detections) {
|
|
871
|
+
const node = correctedWorkflow.nodes.find((n) => n.name === detection.nodeName);
|
|
872
|
+
if (!node) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const remainingUnknowns: string[] = [];
|
|
877
|
+
|
|
878
|
+
for (const key of detection.unknownKeys) {
|
|
879
|
+
const match = fuzzyMatchParam(key, detection.propertyDefs);
|
|
880
|
+
if (match) {
|
|
881
|
+
logger.debug(
|
|
882
|
+
{ src: 'plugin:workflow:generation:paramCorrection' },
|
|
883
|
+
`Node "${detection.nodeName}": ${key} → ${match} (deterministic)`
|
|
884
|
+
);
|
|
885
|
+
node.parameters[match] = node.parameters[key];
|
|
886
|
+
delete node.parameters[key];
|
|
887
|
+
} else {
|
|
888
|
+
remainingUnknowns.push(key);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (remainingUnknowns.length > 0) {
|
|
893
|
+
needsLLM.push({
|
|
894
|
+
...detection,
|
|
895
|
+
unknownKeys: remainingUnknowns,
|
|
896
|
+
currentParams: node.parameters,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (needsLLM.length === 0) {
|
|
902
|
+
return correctedWorkflow;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Phase 2: LLM correction for complex cases (restructuring)
|
|
906
|
+
logger.debug(
|
|
907
|
+
{ src: 'plugin:workflow:generation:paramCorrection' },
|
|
908
|
+
`LLM correction needed for ${needsLLM.length} node(s): ${needsLLM.map((d) => `"${d.nodeName}" (${d.unknownKeys.join(', ')})`).join('; ')}`
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
const corrections = await Promise.all(
|
|
912
|
+
needsLLM.map(async (detection) => {
|
|
913
|
+
try {
|
|
914
|
+
const userPrompt = PARAM_CORRECTION_USER_PROMPT.replace('{nodeType}', detection.nodeType)
|
|
915
|
+
.replace('{currentParams}', JSON.stringify(detection.currentParams, null, 2))
|
|
916
|
+
.replace('{propertyDefs}', JSON.stringify(detection.propertyDefs, null, 2));
|
|
917
|
+
|
|
918
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
919
|
+
prompt: `${PARAM_CORRECTION_SYSTEM_PROMPT}\n\n${userPrompt}`,
|
|
920
|
+
temperature: 0,
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
const cleaned = (response as string)
|
|
924
|
+
.replace(/^[\s\S]*?```(?:json)?\s*\n?/i, '')
|
|
925
|
+
.replace(/\n?```[\s\S]*$/i, '')
|
|
926
|
+
.trim();
|
|
927
|
+
|
|
928
|
+
const correctedParams = JSON.parse(cleaned) as Record<string, unknown>;
|
|
929
|
+
|
|
930
|
+
// Validate: corrected params should only contain valid property names
|
|
931
|
+
const validNames = new Set(detection.propertyDefs.map((p) => p.name));
|
|
932
|
+
validNames.add('resource');
|
|
933
|
+
validNames.add('operation');
|
|
934
|
+
const invalidKeys = Object.keys(correctedParams).filter((k) => !validNames.has(k));
|
|
935
|
+
if (invalidKeys.length > 0) {
|
|
936
|
+
logger.warn(
|
|
937
|
+
{ src: 'plugin:workflow:generation:paramCorrection' },
|
|
938
|
+
`LLM returned invalid keys for "${detection.nodeName}": ${invalidKeys.join(', ')} — dropping them`
|
|
939
|
+
);
|
|
940
|
+
for (const k of invalidKeys) {
|
|
941
|
+
delete correctedParams[k];
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return { nodeName: detection.nodeName, correctedParams };
|
|
946
|
+
} catch (error) {
|
|
947
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
948
|
+
logger.warn(
|
|
949
|
+
{
|
|
950
|
+
src: 'plugin:workflow:generation:paramCorrection',
|
|
951
|
+
error: errMsg,
|
|
952
|
+
},
|
|
953
|
+
`Failed to correct parameters for node "${detection.nodeName}": ${errMsg}`
|
|
954
|
+
);
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
for (const correction of corrections) {
|
|
961
|
+
if (!correction) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const node = correctedWorkflow.nodes.find((n) => n.name === correction.nodeName);
|
|
966
|
+
if (!node) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Preserve resource/operation (already corrected by correctOptionParameters)
|
|
971
|
+
if (node.parameters.resource !== undefined) {
|
|
972
|
+
correction.correctedParams.resource = node.parameters.resource;
|
|
973
|
+
}
|
|
974
|
+
if (node.parameters.operation !== undefined) {
|
|
975
|
+
correction.correctedParams.operation = node.parameters.operation;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
logger.debug(
|
|
979
|
+
{ src: 'plugin:workflow:generation:paramCorrection' },
|
|
980
|
+
`Node "${correction.nodeName}": params corrected via LLM — keys: ${Object.keys(correction.correctedParams).join(', ')}`
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
node.parameters = correction.correctedParams;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return correctedWorkflow;
|
|
987
|
+
}
|