Skip to content

Commit 56b04e2

Browse files
aviu16HenryHengZJ
andauthored
fix(agentflow): prevent ConditionAgent silent failure when no scenario matches (#5760)
* fix(agentflow): prevent ConditionAgent from silently dropping when no scenario matches the ConditionAgent was doing strict exact string matching against scenario descriptions, but LLMs often return abbreviated or slightly different versions of the scenario text. when nothing matched, all branches got marked as unfulfilled and the flow silently terminated with no response. added fallback matching (startsWith, includes) so partial matches still route correctly, plus a last-resort else branch so the flow never just dies silently. also added a safety net in the execution engine to catch the case where all conditions are unfulfilled. fixes #5620 * refactor: normalize output once and drop unnecessary any casts - normalize calledOutputName once before all matching steps instead of calling toLowerCase().trim() repeatedly - remove explicit any types where inference handles it * test(agentflow): cover ConditionAgent scenario matching fallbacks * Update matchScenario.test.ts * Update matchScenario.ts --------- Co-authored-by: Henry Heng <henryheng@flowiseai.com>
1 parent e131fca commit 56b04e2

4 files changed

Lines changed: 71 additions & 6 deletions

File tree

packages/components/nodes/agentflow/ConditionAgent/ConditionAgent.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '../utils'
1010
import { CONDITION_AGENT_SYSTEM_PROMPT, DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt'
1111
import { BaseChatModel } from '@langchain/core/language_models/chat_models'
12+
import { findBestScenarioIndex } from './matchScenario'
1213

1314
class ConditionAgent_Agentflow implements INode {
1415
label: string
@@ -406,10 +407,7 @@ class ConditionAgent_Agentflow implements INode {
406407
}
407408
}
408409

409-
// Find the first exact match
410-
const matchedScenarioIndex = _conditionAgentScenarios.findIndex(
411-
(scenario) => calledOutputName.toLowerCase() === scenario.scenario.toLowerCase()
412-
)
410+
const matchedScenarioIndex = findBestScenarioIndex(_conditionAgentScenarios, calledOutputName)
413411

414412
const conditions = _conditionAgentScenarios.map((scenario, index) => {
415413
return {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { findBestScenarioIndex } from './matchScenario'
2+
3+
describe('findBestScenarioIndex', () => {
4+
const scenarios = [{ scenario: 'billing issue' }, { scenario: 'technical support' }, { scenario: 'other' }]
5+
6+
it('matches exact scenario (case-insensitive)', () => {
7+
expect(findBestScenarioIndex(scenarios, 'Technical Support')).toBe(1)
8+
})
9+
10+
it('matches exact scenario with surrounding whitespace', () => {
11+
expect(findBestScenarioIndex(scenarios, ' billing issue ')).toBe(0)
12+
})
13+
14+
it('matches abbreviated output using startsWith fallback', () => {
15+
expect(findBestScenarioIndex(scenarios, 'tech')).toBe(1)
16+
})
17+
18+
it('matches substring output in either direction', () => {
19+
expect(findBestScenarioIndex(scenarios, 'need help with billing issue today')).toBe(0)
20+
})
21+
22+
it('falls back to last scenario when no match is found', () => {
23+
expect(findBestScenarioIndex(scenarios, 'completely unrelated')).toBe(2)
24+
})
25+
26+
it('returns -1 for empty scenarios list', () => {
27+
expect(findBestScenarioIndex([], 'anything')).toBe(-1)
28+
})
29+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export type ConditionScenario = { scenario: string }
2+
3+
export const findBestScenarioIndex = (scenarios: ConditionScenario[], calledOutputName: string): number => {
4+
if (!Array.isArray(scenarios) || scenarios.length === 0) return -1
5+
6+
const normalizedOutput = calledOutputName.toLowerCase().trim()
7+
8+
// try exact match first
9+
let matchedScenarioIndex = scenarios.findIndex((scenario) => scenario.scenario.toLowerCase() === normalizedOutput)
10+
11+
// fallback: check if LLM returned a partial/abbreviated scenario name
12+
if (matchedScenarioIndex === -1) {
13+
matchedScenarioIndex = scenarios.findIndex((scenario) => scenario.scenario.toLowerCase().startsWith(normalizedOutput))
14+
}
15+
16+
// further fallback: substring match in either direction
17+
if (matchedScenarioIndex === -1) {
18+
matchedScenarioIndex = scenarios.findIndex(
19+
(scenario) =>
20+
scenario.scenario.toLowerCase().includes(normalizedOutput) || normalizedOutput.includes(scenario.scenario.toLowerCase())
21+
)
22+
}
23+
24+
// last resort: if still no match, use the last scenario as an "else" branch
25+
if (matchedScenarioIndex === -1) {
26+
matchedScenarioIndex = scenarios.length - 1
27+
}
28+
29+
return matchedScenarioIndex
30+
}

packages/server/src/utils/buildAgentflow.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,12 +769,20 @@ async function determineNodesToIgnore(
769769
if (isDecisionNode && result.output?.conditions) {
770770
const outputConditions: ICondition[] = result.output.conditions
771771

772+
// safety net: if no conditions were fulfilled, don't ignore ALL children
773+
// treat the last condition as an else/default fallback
774+
const anyFulfilled = outputConditions.some((c) => c.isFulfilled === true)
775+
if (!anyFulfilled && outputConditions.length > 0) {
776+
// mark the last condition as fulfilled so at least one branch executes
777+
outputConditions[outputConditions.length - 1].isFulfilled = true
778+
}
779+
772780
// Find indexes of unfulfilled conditions
773781
const unfulfilledIndexes = outputConditions
774-
.map((condition: any, index: number) =>
782+
.map((condition, index) =>
775783
condition.isFulfilled === false || !Object.prototype.hasOwnProperty.call(condition, 'isFulfilled') ? index : -1
776784
)
777-
.filter((index: number) => index !== -1)
785+
.filter((index) => index !== -1)
778786

779787
// Find nodes to ignore based on unfulfilled conditions
780788
for (const index of unfulfilledIndexes) {

0 commit comments

Comments
 (0)