Compare commits

...

6 Commits

Author SHA1 Message Date
Vikhyath Mondreti
f2283d37e5 improve router prompt 2026-01-10 11:10:02 -08:00
Vikhyath Mondreti
ac7e93827f Merge staging into fix/router 2026-01-10 11:02:37 -08:00
Vikhyath Mondreti
f1b8745625 router block error path decision 2026-01-10 10:59:35 -08:00
Siddharth Ganesan
d7718e6b84 Fix lint 2026-01-09 17:21:27 -08:00
Siddharth Ganesan
9d7ab37e3d Fix autoconnect edge for router 2026-01-09 17:21:19 -08:00
Siddharth Ganesan
b65a9d33cd Fix router block 2026-01-09 17:18:16 -08:00
7 changed files with 54 additions and 23 deletions

View File

@@ -2,7 +2,6 @@
title: Router
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Image } from '@/components/ui/image'
@@ -102,11 +101,18 @@ Input (Lead) → Router
└── [Self-serve] → Workflow (Automated Onboarding)
```
## Error Handling
When the Router cannot determine an appropriate route for the given context, it will route to the **error path** instead of arbitrarily selecting a route. This happens when:
- The context doesn't clearly match any of the defined route descriptions
- The AI determines that none of the available routes are appropriate
## Best Practices
- **Write clear route descriptions**: Each route description should clearly explain when that route should be selected. Be specific about the criteria.
- **Make routes mutually exclusive**: When possible, ensure route descriptions don't overlap to prevent ambiguous routing decisions.
- **Include an error/fallback route**: Add a catch-all route for unexpected inputs that don't match other routes.
- **Connect an error path**: Handle cases where no route matches by connecting an error handler for graceful fallback behavior.
- **Use descriptive route titles**: Route titles appear in the workflow canvas, so make them meaningful for readability.
- **Test with diverse inputs**: Ensure the Router handles various input types, edge cases, and unexpected content.
- **Monitor routing performance**: Review routing decisions regularly and refine route descriptions based on actual usage patterns.

View File

@@ -654,17 +654,20 @@ export function ConditionInput({
}
const removeBlock = (id: string) => {
if (isPreview || disabled || conditionalBlocks.length <= 2) return
if (isPreview || disabled) return
// Condition mode requires at least 2 blocks (if/else), router mode requires at least 1
const minBlocks = isRouterMode ? 1 : 2
if (conditionalBlocks.length <= minBlocks) return
// Remove any associated edges before removing the block
const handlePrefix = isRouterMode ? `router-${id}` : `condition-${id}`
const edgeIdsToRemove = edges
.filter((edge) => edge.sourceHandle?.startsWith(`condition-${id}`))
.filter((edge) => edge.sourceHandle?.startsWith(handlePrefix))
.map((edge) => edge.id)
if (edgeIdsToRemove.length > 0) {
batchRemoveEdges(edgeIdsToRemove)
}
if (conditionalBlocks.length === 1) return
shouldPersistRef.current = true
setConditionalBlocks((blocks) => updateBlockTitles(blocks.filter((block) => block.id !== id)))
@@ -816,7 +819,9 @@ export function ConditionInput({
<Button
variant='ghost'
onClick={() => removeBlock(block.id)}
disabled={isPreview || disabled || conditionalBlocks.length === 1}
disabled={
isPreview || disabled || conditionalBlocks.length <= (isRouterMode ? 1 : 2)
}
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
>
<Trash className='h-[14px] w-[14px]' />

View File

@@ -863,7 +863,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
return parsed.map((item: unknown, index: number) => {
const routeItem = item as { id?: string; value?: string }
return {
id: routeItem?.id ?? `${id}-route-${index}`,
// Use stable ID format that matches ConditionInput's generateStableId
id: routeItem?.id ?? `${id}-route${index + 1}`,
value: routeItem?.value ?? '',
}
})
@@ -873,7 +874,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
logger.warn('Failed to parse router routes value', { error, blockId: id })
}
return [{ id: `${id}-route-route1`, value: '' }]
// Fallback must match ConditionInput's default: generateStableId(blockId, 'route1') = `${blockId}-route1`
return [{ id: `${id}-route1`, value: '' }]
}, [type, subBlockState, id])
/**

View File

@@ -987,6 +987,14 @@ const WorkflowContent = React.memo(() => {
const handleId = conditionHandles[0].getAttribute('data-handleid')
if (handleId) return handleId
}
} else if (block.type === 'router_v2') {
const routerHandles = document.querySelectorAll(
`[data-nodeid^="${block.id}"][data-handleid^="router-"]`
)
if (routerHandles.length > 0) {
const handleId = routerHandles[0].getAttribute('data-handleid')
if (handleId) return handleId
}
} else if (block.type === 'loop') {
return 'loop-end-source'
} else if (block.type === 'parallel') {

View File

@@ -115,25 +115,26 @@ Description: ${route.value || 'No description provided'}
)
.join('\n')
return `You are an intelligent routing agent. Your task is to analyze the provided context and select the most appropriate route from the available options.
return `You are a DETERMINISTIC routing agent. You MUST select exactly ONE option.
Available Routes:
${routesInfo}
Context to analyze:
Context to route:
${context}
Instructions:
1. Carefully analyze the context against each route's description
2. Select the route that best matches the context's intent and requirements
3. Consider the semantic meaning, not just keyword matching
4. If multiple routes could match, choose the most specific one
ROUTING RULES:
1. ALWAYS prefer selecting a route over NO_MATCH
2. Pick the route whose description BEST matches the context, even if it's not a perfect match
3. If the context is even partially related to a route's description, select that route
4. ONLY output NO_MATCH if the context is completely unrelated to ALL route descriptions
Response Format:
Return ONLY the route ID as a single string, no punctuation, no explanation.
Example: "route-abc123"
OUTPUT FORMAT:
- Output EXACTLY one route ID (copied exactly as shown above) OR "NO_MATCH"
- No explanation, no punctuation, no additional text
- Just the route ID or NO_MATCH
Remember: Your response must be ONLY the route ID - no additional text, formatting, or explanation.`
Your response:`
}
/**

View File

@@ -278,14 +278,24 @@ export class RouterBlockHandler implements BlockHandler {
const result = await response.json()
const chosenRouteId = result.content.trim()
if (chosenRouteId === 'NO_MATCH' || chosenRouteId.toUpperCase() === 'NO_MATCH') {
logger.info('Router determined no route matches the context, routing to error path')
throw new Error('Router could not determine a matching route for the given context')
}
const chosenRoute = routes.find((r) => r.id === chosenRouteId)
// Throw error if LLM returns invalid route ID - this routes through error path
if (!chosenRoute) {
const availableRoutes = routes.map((r) => ({ id: r.id, title: r.title }))
logger.error(
`Invalid routing decision. Response content: "${result.content}", available routes:`,
routes.map((r) => ({ id: r.id, title: r.title }))
`Invalid routing decision. Response content: "${result.content}". Available routes:`,
availableRoutes
)
throw new Error(
`Router could not determine a valid route. LLM response: "${result.content}". Available route IDs: ${routes.map((r) => r.id).join(', ')}`
)
throw new Error(`Invalid routing decision: ${chosenRouteId}`)
}
// Find the target block connected to this route's handle

View File

@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",