Skip to content

Commit a85133d

Browse files
added AI orchestration , preprocessor options in client, bug fixes, docs updates
- DuckDuckGoSearchTool and HttpNode bug fixes - AI Data Processing Node new AI orchestration functionality - preprocessor options for client - bug fixes - docs updates
1 parent 32f82e0 commit a85133d

32 files changed

Lines changed: 940 additions & 170 deletions

client/src/components/nodes/LlmProcessNode/LlmProcessNode.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
5757
think,
5858
temperatureEnabled,
5959
temperature,
60+
orchestrationMode,
6061
onResultUpdate,
6162
onConfigChange,
6263
onFeedbackSend
@@ -146,6 +147,7 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
146147
ragHandler,
147148
think: think ?? defaultLlmProcessNodeData.think,
148149
temperature: temperatureEnabled ? (temperature ?? defaultLlmProcessNodeData.temperature) : undefined,
150+
orchestrationMode: orchestrationMode ?? defaultLlmProcessNodeData.orchestrationMode,
149151
conversationHistory: {
150152
value: conversationHistory || [],
151153
onChange: (newHistory) => {
@@ -179,6 +181,7 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
179181
ragHandler,
180182
think: think ?? defaultLlmProcessNodeData.think,
181183
temperature: temperatureEnabled ? (temperature ?? defaultLlmProcessNodeData.temperature) : undefined,
184+
orchestrationMode: orchestrationMode ?? defaultLlmProcessNodeData.orchestrationMode,
182185
conversationHistory: {
183186
value: [],
184187
onChange: (newHistory) => {
@@ -209,6 +212,7 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
209212
ragHandler,
210213
think: think ?? defaultLlmProcessNodeData.think,
211214
temperature: temperatureEnabled ? (temperature ?? defaultLlmProcessNodeData.temperature) : undefined,
215+
orchestrationMode: orchestrationMode ?? defaultLlmProcessNodeData.orchestrationMode,
212216
conversationHistory: {
213217
value: [],
214218
onChange: (newHistory) => {
@@ -218,7 +222,7 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
218222
});
219223
}, setIsRunning);
220224
// eslint-disable-next-line max-len
221-
}, [clearError, feedback, format, id, input, maxToolRetries, message, model, onConfigChange, onResultUpdate, processAIRequest, prompt, ragHandler, temperature, temperatureEnabled, think, tools]);
225+
}, [clearError, feedback, format, id, input, maxToolRetries, message, model, onConfigChange, onResultUpdate, processAIRequest, prompt, ragHandler, temperature, temperatureEnabled, think, tools, orchestrationMode]);
222226

223227
const [systemPrompt, setSystemPrompt] = useDebouncedState({
224228
callback: (value: string) => {
@@ -229,10 +233,10 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
229233
});
230234
const [messagePrefix, setMessagePrefix] = useDebouncedState({
231235
callback: (value: string) => {
232-
onConfigChange(id, {message: {...message, preffix: value}});
236+
onConfigChange(id, {message: {...message, prefix: value}});
233237
},
234238
delay: 300,
235-
initialValue: message?.preffix || ""
239+
initialValue: message?.prefix || ""
236240
});
237241
const [messageSuffix, setMessageSuffix] = useDebouncedState({
238242
callback: (value: string) => {
@@ -369,6 +373,18 @@ export function LlmProcessNode ({data, id}: NodeProps<AppNode>) {
369373
sx={{mb: 2}}
370374
helperText="Maximum number of retry attempts for each required tool"
371375
/>
376+
<Box sx={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1, ml: 1.5}}>
377+
<FormControlLabel
378+
control={
379+
<Switch
380+
size="small"
381+
checked={orchestrationMode ?? defaultLlmProcessNodeData.orchestrationMode}
382+
onChange={e => onConfigChange(id, {orchestrationMode: e.target.checked})}
383+
/>
384+
}
385+
label={<Typography variant="body2">AI Orchestration Mode</Typography>}
386+
/>
387+
</Box>
372388
<Box sx={{display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1, ml: 1.5}}>
373389
<FormControlLabel
374390
control={

client/src/components/nodes/LlmProcessNode/descriptor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const LlmProcessNodeDescriptor: NodeDescriptor<typeof NODE_TYPE, LlmProce
1919
title: TITLE,
2020
assertion: assertIsLlmProcessNodeData,
2121
metadata: {
22-
description: "Sends data to an Ollama LLM with a configurable prompt. Supports tool calling, feedback loops, and conversation history.",
22+
// eslint-disable-next-line max-len
23+
description: "Sends data to an Ollama LLM with a configurable prompt. Supports tool calling, feedback loops, and conversation history. When 'AI Orchestration Mode' is enabled, an AI orchestrator decomposes the input (string or array) into individual sequential agent tasks and synthesizes a final aggregated response.",
2324
ports: {
2425
[NODE_PORT_IDS.FLOW]: {
2526
inputSchema: z.any().describe("Incoming data, tools, and context for prompt execution."),

client/src/components/nodes/LlmProcessNode/hooks/useAIProcessor.ts

Lines changed: 44 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -5,88 +5,19 @@
55
************************************************************************/
66

77
import {useState, useCallback} from 'react';
8-
import {OllamaService} from '../../../../services/ollamaService';
98
import {GenericNodeData} from '../../../../types/workflow';
109
import {Message, MessageRole, SystemUserConfigValues, ToolSchema} from '../../../../types/ollama.types';
11-
import Ajv from "ajv";
1210
import {LlmProcessNodeData} from '../types/workflow';
1311
import {useFetchModels} from '../../../../hooks/useFetchModels';
12+
import {runOrchestration, runSingleCall} from '../utils/aiOrchestration';
13+
import {assertIsSerializedInput, getExpectedOutputType, parseFormat, serializeInput} from '../utils/formatUtils';
1414

1515

1616
export interface UseAIProcessorOptions {
1717
onSuccess?: (result: any) => void;
1818
onError?: (error: string) => void;
1919
}
2020

21-
function getExpectedOutputType (format: any): string {
22-
try {
23-
const schema = typeof format === "string" ? JSON.parse(format) : format;
24-
25-
if (!schema || typeof schema !== "object") return "unknown";
26-
27-
if (schema.type) {
28-
return schema.type;
29-
}
30-
31-
for (const key of ["oneOf", "anyOf", "allOf"]) {
32-
if (Array.isArray(schema[key])) {
33-
for (const subschema of schema[key]) {
34-
const type = getExpectedOutputType(subschema);
35-
36-
if (type !== "unknown") return type;
37-
}
38-
}
39-
}
40-
} catch (e) {
41-
throw new Error(`Invalid format schema: ${e instanceof Error ? e.message : String(e)}`);
42-
}
43-
44-
return "unknown";
45-
}
46-
47-
const serializeInput = (inputData: any): string | undefined => {
48-
if (inputData == undefined) {
49-
return undefined;
50-
}
51-
52-
if (typeof inputData === 'string') {
53-
return inputData;
54-
}
55-
56-
if (typeof inputData === 'object') {
57-
return JSON.stringify(inputData, null, 4);
58-
}
59-
60-
return String(inputData);
61-
};
62-
63-
function assertIsSerializedInput (input: any): asserts input is string {
64-
if (typeof input !== 'string') {
65-
throw new Error(`Expected serialized input to be a string, but got ${typeof input}`);
66-
}
67-
}
68-
69-
const buildUnifiedFormat = (format: {onSuccess?: string; onError?: string;}): string | undefined => {
70-
if (!format) return undefined;
71-
72-
const {onSuccess, onError} = format;
73-
74-
if (onSuccess && onError) {
75-
return JSON.stringify({
76-
oneOf: [
77-
JSON.parse(onSuccess),
78-
JSON.parse(onError)
79-
]
80-
});
81-
}
82-
83-
if (onSuccess) return onSuccess;
84-
85-
// in case of only onError provided, we ignore it because we don't want to return an error as a success response
86-
87-
return undefined;
88-
};
89-
9021
export function useAIProcessor (options: UseAIProcessorOptions = {}) {
9122
const {onSuccess, onError} = options;
9223
const [error, setError] = useState<string[]>([]);
@@ -110,7 +41,8 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
11041
ragHandler,
11142
conversationHistory,
11243
think,
113-
temperature
44+
temperature,
45+
orchestrationMode
11446
}: Pick<GenericNodeData & LlmProcessNodeData, 'input' | 'prompt' | 'message' | 'model' | 'format'> & {
11547
tools?: {
11648
schema: ToolSchema,
@@ -126,6 +58,7 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
12658
};
12759
think?: boolean;
12860
temperature?: number;
61+
orchestrationMode?: boolean;
12962
}) => {
13063
if (!model) {
13164
const errorMsg = "Please select a model first.";
@@ -136,6 +69,32 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
13669
return null;
13770
}
13871

72+
if (orchestrationMode && (Array.isArray(input) || typeof input === "string")) {
73+
setError([]);
74+
75+
const orchestrationResult = await runOrchestration({
76+
input,
77+
prompt,
78+
model,
79+
format,
80+
tools,
81+
maxToolRetries,
82+
think,
83+
temperature,
84+
});
85+
86+
if (!orchestrationResult.success) {
87+
setError(prev => [...prev, orchestrationResult.error]);
88+
onError?.(orchestrationResult.error);
89+
90+
return null;
91+
}
92+
93+
onSuccess?.(orchestrationResult.result);
94+
95+
return orchestrationResult.result;
96+
}
97+
13998
if (!prompt && !message && !input) {
14099
const errorMsg = "Please provide a prompt, message, or input data.";
141100

@@ -166,7 +125,7 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
166125
assertIsSerializedInput(serializedInput);
167126

168127
const baseUserContent = message
169-
? `${message.preffix || ''}${serializedInput}${message.suffix || ''}`
128+
? `${message.prefix || ''}${serializedInput}${message.suffix || ''}`
170129
: serializedInput;
171130

172131
let userContent = baseUserContent;
@@ -217,7 +176,7 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
217176
assertIsSerializedInput(serializedInput);
218177

219178
const userContent = message
220-
? `${message.preffix || ''}${serializedInput}${message.suffix || ''}`
179+
? `${message.prefix || ''}${serializedInput}${message.suffix || ''}`
221180
: serializedInput;
222181

223182
messages.push({
@@ -228,42 +187,21 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
228187
}
229188
}
230189

231-
let parsedFormat;
232-
let onErrorSchema;
190+
let parsedFormat: object | undefined;
233191
let onErrorValidator: ((data: any) => boolean) | undefined;
234192

235-
if (format) {
236-
try {
237-
let unifiedFormat: string | undefined;
238-
239-
if (typeof format === "object" && (format.onSuccess || format.onError)) {
240-
unifiedFormat = buildUnifiedFormat(format);
241-
242-
if (format.onError) {
243-
onErrorSchema = JSON.parse(format.onError);
244-
245-
const ajv = new Ajv();
246-
247-
onErrorValidator = ajv.compile(onErrorSchema);
248-
}
249-
} else if (typeof format === "string") {
250-
unifiedFormat = format;
251-
}
252-
253-
if (unifiedFormat) {
254-
parsedFormat = JSON.parse(unifiedFormat);
255-
}
256-
} catch (parseError) {
257-
const errorMsg = `Invalid format JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`;
193+
try {
194+
({parsedFormat, onErrorValidator} = parseFormat(format));
195+
} catch (parseError) {
196+
const errorMsg = `Invalid format JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`;
258197

259-
setError(prev => [...prev, errorMsg]);
260-
onError?.(errorMsg);
198+
setError(prev => [...prev, errorMsg]);
199+
onError?.(errorMsg);
261200

262-
return null;
263-
}
201+
return null;
264202
}
265203

266-
const response = await OllamaService.getInstance().fetchAIResponse({
204+
const response = await runSingleCall({
267205
messages,
268206
model,
269207
...(parsedFormat ? {format: parsedFormat} : {}),
@@ -293,10 +231,10 @@ export function useAIProcessor (options: UseAIProcessorOptions = {}) {
293231
try {
294232
const expectedOutputType = getExpectedOutputType(parsedFormat);
295233

296-
if (expectedOutputType === "object") {
234+
if (expectedOutputType === "object" || expectedOutputType === "array") {
297235
result = JSON.parse(result);
298236

299-
if (onErrorValidator && onErrorValidator(result)) {
237+
if (expectedOutputType === "object" && onErrorValidator && onErrorValidator(result)) {
300238
const errorMsg = `LLM returned an error response matching onError schema:\n${JSON.stringify(result, null, 4)}`;
301239

302240
setError(prev => [...prev, errorMsg]);

client/src/components/nodes/LlmProcessNode/types/workflow.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* See the LICENSE file in the project root for license details. *
55
************************************************************************/
66

7+
/* eslint-disable max-len */
8+
79
import type {BaseNodeData} from "../../../../types/workflow";
810
import type {Node} from '@xyflow/react';
911
import type {NODE_TYPE} from "../constants";
@@ -19,7 +21,7 @@ export const LlmProcessNodeDataSchema = z.object({
1921
onError: z.string().optional().describe("JSON Schema string for Ollama structured output on error — constrains the LLM reply shape when the node is handling an error"),
2022
}).optional().describe("Structured output schemas passed to Ollama to constrain LLM response format"),
2123
message: z.object({
22-
preffix: z.string().optional().describe("Text prepended to the user message before sending to the LLM"),
24+
prefix: z.string().optional().describe("Text prepended to the user message before sending to the LLM"),
2325
suffix: z.string().optional().describe("Text appended to the user message before sending to the LLM"),
2426
}).optional().describe("Message wrapper applied around the incoming data"),
2527
maxFeedbackLoops: z.number().int().nonnegative().describe("Maximum number of feedback iterations before stopping"),
@@ -31,6 +33,7 @@ export const LlmProcessNodeDataSchema = z.object({
3133
think: z.boolean().optional().describe("Enable thinking mode for supported models (e.g. deepseek-r1)"),
3234
temperatureEnabled: z.boolean().optional().describe("Whether to override the default model temperature"),
3335
temperature: z.number().min(0).max(2).optional().default(0.8).describe("Sampling temperature (0 = deterministic, 2 = very random)"),
36+
orchestrationMode: z.boolean().optional().describe("When enabled, an AI orchestrator decomposes the input (string or array) into individual agent tasks, runs them sequentially, then synthesizes a final aggregated response"),
3437
});
3538

3639
export type LlmProcessNodeData = z.infer<typeof LlmProcessNodeDataSchema>;
@@ -52,4 +55,5 @@ export const defaultLlmProcessNodeData: LlmProcessNodeData = {
5255
think: false,
5356
temperatureEnabled: false,
5457
temperature: 0.8,
58+
orchestrationMode: false,
5559
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/************************************************************************
2+
* Copyright (C) 2025 Code Forge Temple *
3+
* This file is part of agentic-signal project *
4+
* See the LICENSE file in the project root for license details. *
5+
************************************************************************/
6+
7+
export * from "./types";
8+
9+
export {runSingleCall} from "./runSingleCall";
10+
11+
export {runOrchestration} from "./runOrchestration";
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/************************************************************************
2+
* Copyright (C) 2025 Code Forge Temple *
3+
* This file is part of agentic-signal project *
4+
* See the LICENSE file in the project root for license details. *
5+
************************************************************************/
6+
7+
import {AgentTask, AgentTaskResult} from "./types";
8+
9+
/**
10+
* Builds a context block injected into an agent's prompt when it depends on prior task outputs.
11+
* Only backward references (index < taskIndex) are accepted; forward/invalid refs are warned and skipped.
12+
*/
13+
export function buildDependencyContext (
14+
task: AgentTask,
15+
taskIndex: number,
16+
completedResults: AgentTaskResult[]
17+
): string {
18+
if (!task.dependsOn || task.dependsOn.length === 0) return "";
19+
20+
const invalid = task.dependsOn.filter(i => i < 0 || i >= taskIndex);
21+
22+
if (invalid.length > 0) {
23+
console.warn(
24+
`[AI Orchestration] Task ${taskIndex + 1} has invalid dependsOn indices (must be < ${taskIndex}, ignored): ${invalid.join(", ")}`
25+
);
26+
}
27+
28+
const validIndices = task.dependsOn.filter(i => i >= 0 && i < taskIndex);
29+
30+
if (validIndices.length === 0) return "";
31+
32+
const sections = validIndices.map(i =>
33+
`### Output of Task ${i + 1}\n${completedResults[i].response}`
34+
);
35+
36+
return `[CONTEXT FROM PRIOR TASKS]\n${sections.join("\n\n")}\n\n[YOUR TASK]\n`;
37+
}
38+
39+
export function buildAggregationMessage (results: AgentTaskResult[]): string {
40+
const sections = results.map((r, i) =>
41+
`## Task ${i + 1}\n**Request:** ${r.task.content}\n**Response:** ${r.response}`
42+
);
43+
44+
return `The following are the results of individual agent tasks. Synthesize them into a final comprehensive response.\n\n${sections.join("\n\n")}`;
45+
}

0 commit comments

Comments
 (0)