Skip to content

Commit 7d67ae3

Browse files
authored
fix(evaluator): fix evaluator to handle temperature gracefully like router (#1792)
* fix(evaluator): fix evaluator to handle temperature gracefully like router * apps/sim * fix eval-input subblock
1 parent f998086 commit 7d67ae3

File tree

6 files changed

+231
-50
lines changed

6 files changed

+231
-50
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/eval-input.tsx

Lines changed: 169 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { useMemo } from 'react'
1+
import { useMemo, useRef, useState } from 'react'
22
import { Plus, Trash } from 'lucide-react'
33
import { Button } from '@/components/ui/button'
4+
import { formatDisplayText } from '@/components/ui/formatted-text'
45
import { Input } from '@/components/ui/input'
56
import { Label } from '@/components/ui/label'
7+
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
8+
import { Textarea } from '@/components/ui/textarea'
69
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
10+
import { cn } from '@/lib/utils'
711
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
12+
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
813

914
interface EvalMetric {
1015
id: string
@@ -22,6 +27,7 @@ interface EvalInputProps {
2227
isPreview?: boolean
2328
previewValue?: EvalMetric[] | null
2429
disabled?: boolean
30+
isConnecting?: boolean
2531
}
2632

2733
// Default values
@@ -38,17 +44,24 @@ export function EvalInput({
3844
isPreview = false,
3945
previewValue,
4046
disabled = false,
47+
isConnecting = false,
4148
}: EvalInputProps) {
4249
const [storeValue, setStoreValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId)
50+
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
51+
52+
const [showTags, setShowTags] = useState(false)
53+
const [cursorPosition, setCursorPosition] = useState(0)
54+
const [activeMetricId, setActiveMetricId] = useState<string | null>(null)
55+
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
56+
const descriptionInputRefs = useRef<Record<string, HTMLTextAreaElement>>({})
57+
const descriptionOverlayRefs = useRef<Record<string, HTMLDivElement>>({})
58+
const [dragHighlight, setDragHighlight] = useState<Record<string, boolean>>({})
4359

44-
// Use preview value when in preview mode, otherwise use store value
4560
const value = isPreview ? previewValue : storeValue
4661

47-
// State hooks - memoize default metric to prevent key changes
4862
const defaultMetric = useMemo(() => createDefaultMetric(), [])
4963
const metrics: EvalMetric[] = value || [defaultMetric]
5064

51-
// Metric operations
5265
const addMetric = () => {
5366
if (isPreview || disabled) return
5467

@@ -61,7 +74,6 @@ export function EvalInput({
6174
setStoreValue(metrics.filter((metric) => metric.id !== id))
6275
}
6376

64-
// Update handlers
6577
const updateMetric = (id: string, field: keyof EvalMetric, value: any) => {
6678
if (isPreview || disabled) return
6779
setStoreValue(
@@ -86,7 +98,6 @@ export function EvalInput({
8698
)
8799
}
88100

89-
// Validation handlers
90101
const handleRangeBlur = (id: string, field: 'min' | 'max', value: string) => {
91102
const sanitizedValue = value.replace(/[^\d.-]/g, '')
92103
const numValue = Number.parseFloat(sanitizedValue)
@@ -106,7 +117,97 @@ export function EvalInput({
106117
)
107118
}
108119

109-
// Metric header
120+
const handleTagSelect = (tag: string) => {
121+
if (!activeMetricId) return
122+
123+
const metric = metrics.find((m) => m.id === activeMetricId)
124+
if (!metric) return
125+
126+
const currentValue = metric.description || ''
127+
const textBeforeCursor = currentValue.slice(0, cursorPosition)
128+
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
129+
130+
const newValue =
131+
currentValue.slice(0, lastOpenBracket) + tag + currentValue.slice(cursorPosition)
132+
133+
updateMetric(activeMetricId, 'description', newValue)
134+
setShowTags(false)
135+
136+
setTimeout(() => {
137+
const inputEl = descriptionInputRefs.current[activeMetricId]
138+
if (inputEl) {
139+
inputEl.focus()
140+
const newCursorPos = lastOpenBracket + tag.length
141+
inputEl.setSelectionRange(newCursorPos, newCursorPos)
142+
}
143+
}, 10)
144+
}
145+
146+
const handleDescriptionChange = (metricId: string, newValue: string, selectionStart?: number) => {
147+
updateMetric(metricId, 'description', newValue)
148+
149+
if (selectionStart !== undefined) {
150+
setCursorPosition(selectionStart)
151+
setActiveMetricId(metricId)
152+
153+
const shouldShowTags = checkTagTrigger(newValue, selectionStart)
154+
setShowTags(shouldShowTags.show)
155+
156+
if (shouldShowTags.show) {
157+
const textBeforeCursor = newValue.slice(0, selectionStart)
158+
const lastOpenBracket = textBeforeCursor.lastIndexOf('<')
159+
const tagContent = textBeforeCursor.slice(lastOpenBracket + 1)
160+
const dotIndex = tagContent.indexOf('.')
161+
const sourceBlock = dotIndex > 0 ? tagContent.slice(0, dotIndex) : null
162+
setActiveSourceBlockId(sourceBlock)
163+
}
164+
}
165+
}
166+
167+
const handleDrop = (e: React.DragEvent, metricId: string) => {
168+
e.preventDefault()
169+
setDragHighlight((prev) => ({ ...prev, [metricId]: false }))
170+
const input = descriptionInputRefs.current[metricId]
171+
input?.focus()
172+
173+
if (input) {
174+
const metric = metrics.find((m) => m.id === metricId)
175+
const currentValue = metric?.description || ''
176+
const dropPosition = input.selectionStart ?? currentValue.length
177+
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
178+
updateMetric(metricId, 'description', newValue)
179+
setActiveMetricId(metricId)
180+
setCursorPosition(dropPosition + 1)
181+
setShowTags(true)
182+
183+
try {
184+
const data = JSON.parse(e.dataTransfer.getData('application/json'))
185+
if (data?.connectionData?.sourceBlockId) {
186+
setActiveSourceBlockId(data.connectionData.sourceBlockId)
187+
}
188+
} catch {}
189+
190+
setTimeout(() => {
191+
const el = descriptionInputRefs.current[metricId]
192+
if (el) {
193+
el.selectionStart = dropPosition + 1
194+
el.selectionEnd = dropPosition + 1
195+
}
196+
}, 0)
197+
}
198+
}
199+
200+
const handleDragOver = (e: React.DragEvent, metricId: string) => {
201+
e.preventDefault()
202+
e.dataTransfer.dropEffect = 'copy'
203+
setDragHighlight((prev) => ({ ...prev, [metricId]: true }))
204+
}
205+
206+
const handleDragLeave = (e: React.DragEvent, metricId: string) => {
207+
e.preventDefault()
208+
setDragHighlight((prev) => ({ ...prev, [metricId]: false }))
209+
}
210+
110211
const renderMetricHeader = (metric: EvalMetric, index: number) => (
111212
<div className='flex h-10 items-center justify-between rounded-t-lg border-b bg-card px-3'>
112213
<span className='font-medium text-sm'>Metric {index + 1}</span>
@@ -146,7 +247,6 @@ export function EvalInput({
146247
</div>
147248
)
148249

149-
// Main render
150250
return (
151251
<div className='space-y-2'>
152252
{metrics.map((metric, index) => (
@@ -172,13 +272,67 @@ export function EvalInput({
172272

173273
<div key={`description-${metric.id}`} className='space-y-1'>
174274
<Label>Description</Label>
175-
<Input
176-
value={metric.description}
177-
onChange={(e) => updateMetric(metric.id, 'description', e.target.value)}
178-
placeholder='How accurate is the response?'
179-
disabled={isPreview || disabled}
180-
className='placeholder:text-muted-foreground/50'
181-
/>
275+
<div className='relative'>
276+
<Textarea
277+
ref={(el) => {
278+
if (el) descriptionInputRefs.current[metric.id] = el
279+
}}
280+
value={metric.description}
281+
onChange={(e) =>
282+
handleDescriptionChange(
283+
metric.id,
284+
e.target.value,
285+
e.target.selectionStart ?? undefined
286+
)
287+
}
288+
placeholder='How accurate is the response?'
289+
disabled={isPreview || disabled}
290+
className={cn(
291+
'min-h-[80px] border border-input bg-white text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
292+
(isConnecting || dragHighlight[metric.id]) &&
293+
'ring-2 ring-blue-500 ring-offset-2'
294+
)}
295+
style={{
296+
fontFamily: 'inherit',
297+
lineHeight: 'inherit',
298+
wordBreak: 'break-word',
299+
whiteSpace: 'pre-wrap',
300+
}}
301+
rows={3}
302+
onDrop={(e) => handleDrop(e, metric.id)}
303+
onDragOver={(e) => handleDragOver(e, metric.id)}
304+
onDragLeave={(e) => handleDragLeave(e, metric.id)}
305+
/>
306+
<div
307+
ref={(el) => {
308+
if (el) descriptionOverlayRefs.current[metric.id] = el
309+
}}
310+
className='pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 text-sm'
311+
style={{
312+
fontFamily: 'inherit',
313+
lineHeight: 'inherit',
314+
}}
315+
>
316+
<div className='w-full whitespace-pre-wrap break-words'>
317+
{formatDisplayText(metric.description || '', {
318+
accessiblePrefixes,
319+
highlightAll: !accessiblePrefixes,
320+
})}
321+
</div>
322+
</div>
323+
{showTags && activeMetricId === metric.id && (
324+
<TagDropdown
325+
visible={showTags}
326+
onSelect={handleTagSelect}
327+
blockId={blockId}
328+
activeSourceBlockId={activeSourceBlockId}
329+
inputValue={metric.description || ''}
330+
cursorPosition={cursorPosition}
331+
onClose={() => setShowTags(false)}
332+
className='absolute top-full left-0 z-50 mt-1'
333+
/>
334+
)}
335+
</div>
182336
</div>
183337

184338
<div key={`range-${metric.id}`} className='grid grid-cols-2 gap-4'>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/variables-input/variables-input.tsx

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -339,33 +339,58 @@ export function VariablesInput({
339339
<div className='space-y-1.5'>
340340
<Label className='text-xs'>Value</Label>
341341
{assignment.type === 'object' || assignment.type === 'array' ? (
342-
<Textarea
343-
ref={(el) => {
344-
if (el) valueInputRefs.current[assignment.id] = el
345-
}}
346-
value={assignment.value || ''}
347-
onChange={(e) =>
348-
handleValueInputChange(
349-
assignment.id,
350-
e.target.value,
351-
e.target.selectionStart ?? undefined
352-
)
353-
}
354-
placeholder={
355-
assignment.type === 'object'
356-
? '{\n "key": "value"\n}'
357-
: '[\n 1, 2, 3\n]'
358-
}
359-
disabled={isPreview || disabled}
360-
className={cn(
361-
'min-h-[120px] border border-input bg-white font-mono text-sm placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
362-
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2',
363-
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
364-
)}
365-
onDrop={(e) => handleDrop(e, assignment.id)}
366-
onDragOver={(e) => handleDragOver(e, assignment.id)}
367-
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
368-
/>
342+
<div className='relative'>
343+
<Textarea
344+
ref={(el) => {
345+
if (el) valueInputRefs.current[assignment.id] = el
346+
}}
347+
value={assignment.value || ''}
348+
onChange={(e) =>
349+
handleValueInputChange(
350+
assignment.id,
351+
e.target.value,
352+
e.target.selectionStart ?? undefined
353+
)
354+
}
355+
placeholder={
356+
assignment.type === 'object'
357+
? '{\n "key": "value"\n}'
358+
: '[\n 1, 2, 3\n]'
359+
}
360+
disabled={isPreview || disabled}
361+
className={cn(
362+
'min-h-[120px] border border-input bg-white font-mono text-sm text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
363+
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2',
364+
isConnecting && 'ring-2 ring-blue-500 ring-offset-2'
365+
)}
366+
style={{
367+
fontFamily: 'inherit',
368+
lineHeight: 'inherit',
369+
wordBreak: 'break-word',
370+
whiteSpace: 'pre-wrap',
371+
}}
372+
onDrop={(e) => handleDrop(e, assignment.id)}
373+
onDragOver={(e) => handleDragOver(e, assignment.id)}
374+
onDragLeave={(e) => handleDragLeave(e, assignment.id)}
375+
/>
376+
<div
377+
ref={(el) => {
378+
if (el) overlayRefs.current[assignment.id] = el
379+
}}
380+
className='pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 font-mono text-sm'
381+
style={{
382+
fontFamily: 'inherit',
383+
lineHeight: 'inherit',
384+
}}
385+
>
386+
<div className='w-full whitespace-pre-wrap break-words'>
387+
{formatDisplayText(assignment.value || '', {
388+
accessiblePrefixes,
389+
highlightAll: !accessiblePrefixes,
390+
})}
391+
</div>
392+
</div>
393+
</div>
369394
) : (
370395
<div className='relative'>
371396
<Input

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ export const SubBlock = memo(
297297
isPreview={isPreview}
298298
previewValue={previewValue}
299299
disabled={isDisabled}
300+
isConnecting={isConnecting}
300301
/>
301302
)
302303
case 'time-input':

apps/sim/blocks/blocks/evaluator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
174174
{
175175
id: 'content',
176176
title: 'Content',
177-
type: 'short-input',
177+
type: 'long-input',
178178
layout: 'full',
179179
placeholder: 'Enter the content to evaluate',
180180
required: true,
@@ -252,7 +252,6 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
252252
layout: 'half',
253253
min: 0,
254254
max: 2,
255-
value: () => '0.1',
256255
hidden: true,
257256
},
258257
{

apps/sim/executor/handlers/evaluator/evaluator-handler.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ export class EvaluatorBlockHandler implements BlockHandler {
2121
inputs: Record<string, any>,
2222
context: ExecutionContext
2323
): Promise<BlockOutput> {
24-
const model = inputs.model || 'gpt-4o'
25-
const providerId = getProviderFromModel(model)
24+
const evaluatorConfig = {
25+
model: inputs.model || 'gpt-4o',
26+
apiKey: inputs.apiKey,
27+
}
28+
const providerId = getProviderFromModel(evaluatorConfig.model)
2629

2730
// Process the content to ensure it's in a suitable format
2831
let processedContent = ''
@@ -109,7 +112,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
109112
// Make sure we force JSON output in the request
110113
const providerRequest = {
111114
provider: providerId,
112-
model: model,
115+
model: evaluatorConfig.model,
113116
systemPrompt: systemPromptObj.systemPrompt,
114117
responseFormat: systemPromptObj.responseFormat,
115118
context: JSON.stringify([
@@ -119,8 +122,8 @@ export class EvaluatorBlockHandler implements BlockHandler {
119122
'Please evaluate the content provided in the system prompt. Return ONLY a valid JSON with metric scores.',
120123
},
121124
]),
122-
temperature: inputs.temperature || 0,
123-
apiKey: inputs.apiKey,
125+
temperature: 0.1,
126+
apiKey: evaluatorConfig.apiKey,
124127
workflowId: context.workflowId,
125128
}
126129

0 commit comments

Comments
 (0)