From 75445578413563578533d0c60e4b69ff93ef9488 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 14 Jan 2026 13:59:39 -0800 Subject: [PATCH] feat(langsmith): add langsmith tools for logging, output selector use tool-aware listing --- .claude/commands/add-block.md | 12 + .claude/commands/add-integration.md | 35 ++- apps/docs/components/icons.tsx | 8 + apps/docs/components/ui/icon-mapping.ts | 3 +- apps/docs/content/docs/en/tools/langsmith.mdx | 59 ++++ apps/docs/content/docs/en/tools/meta.json | 2 +- apps/docs/content/docs/en/tools/tinybird.mdx | 5 - .../output-select/output-select.tsx | 12 +- .../components/tag-dropdown/tag-dropdown.tsx | 194 ++---------- apps/sim/blocks/blocks/langsmith.ts | 292 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 21 ++ .../sim/lib/workflows/blocks/block-outputs.ts | 168 ++++++++++ apps/sim/tools/langsmith/create_run.ts | 188 +++++++++++ apps/sim/tools/langsmith/create_runs_batch.ts | 112 +++++++ apps/sim/tools/langsmith/index.ts | 2 + apps/sim/tools/langsmith/types.ts | 59 ++++ apps/sim/tools/langsmith/utils.ts | 38 +++ apps/sim/tools/registry.ts | 3 + 19 files changed, 1030 insertions(+), 185 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/langsmith.mdx create mode 100644 apps/sim/blocks/blocks/langsmith.ts create mode 100644 apps/sim/tools/langsmith/create_run.ts create mode 100644 apps/sim/tools/langsmith/create_runs_batch.ts create mode 100644 apps/sim/tools/langsmith/index.ts create mode 100644 apps/sim/tools/langsmith/types.ts create mode 100644 apps/sim/tools/langsmith/utils.ts diff --git a/.claude/commands/add-block.md b/.claude/commands/add-block.md index 12629d2ce6..a4d6ad0f6f 100644 --- a/.claude/commands/add-block.md +++ b/.claude/commands/add-block.md @@ -577,6 +577,17 @@ export const ServiceBlock: BlockConfig = { See the `/add-trigger` skill for creating triggers. +## Icon Requirement + +If the icon doesn't already exist in `@/components/icons.tsx`, **do NOT search for it yourself**. After completing the block, ask the user to provide the SVG: + +``` +The block is complete, but I need an icon for {Service}. +Please provide the SVG and I'll convert it to a React component. + +You can usually find this in the service's brand/press kit page, or copy it from their website. +``` + ## Checklist Before Finishing - [ ] All subBlocks have `id`, `title` (except switch), and `type` @@ -588,4 +599,5 @@ See the `/add-trigger` skill for creating triggers. - [ ] Tools.config.tool returns correct tool ID - [ ] Outputs match tool outputs - [ ] Block registered in registry.ts +- [ ] If icon missing: asked user to provide SVG - [ ] If triggers exist: `triggers` config set, trigger subBlocks spread diff --git a/.claude/commands/add-integration.md b/.claude/commands/add-integration.md index 9a8e3ca69b..a6d2af5cf5 100644 --- a/.claude/commands/add-integration.md +++ b/.claude/commands/add-integration.md @@ -226,17 +226,26 @@ export function {Service}Icon(props: SVGProps) { fill="none" xmlns="http://www.w3.org/2000/svg" > - {/* SVG paths from brand assets */} + {/* SVG paths from user-provided SVG */} ) } ``` -### Finding Icons -1. Check the service's brand/press kit page -2. Download SVG logo -3. Convert to React component -4. Ensure it accepts and spreads props +### Getting Icons +**Do NOT search for icons yourself.** At the end of implementation, ask the user to provide the SVG: + +``` +I've completed the integration. Before I can add the icon, please provide the SVG for {Service}. +You can usually find this in the service's brand/press kit page, or copy it from their website. + +Paste the SVG code here and I'll convert it to a React component. +``` + +Once the user provides the SVG: +1. Extract the SVG paths/content +2. Create a React component that spreads props +3. Ensure viewBox is preserved from the original SVG ## Step 5: Create Triggers (Optional) @@ -405,6 +414,7 @@ If creating V2 versions (API-aligned outputs): - [ ] If triggers: spread trigger subBlocks with `getTrigger()` ### Icon +- [ ] Asked user to provide SVG - [ ] Added icon to `components/icons.tsx` - [ ] Icon spreads props correctly @@ -433,11 +443,18 @@ You: I'll add the Stripe integration. Let me: 1. First, research the Stripe API using Context7 2. Create the tools for key operations (payments, subscriptions, etc.) 3. Create the block with operation dropdown -4. Add the Stripe icon -5. Register everything -6. Generate docs +4. Register everything +5. Generate docs +6. Ask you for the Stripe icon SVG [Proceed with implementation...] + +[After completing steps 1-5...] + +I've completed the Stripe integration. Before I can add the icon, please provide the SVG for Stripe. +You can usually find this in the service's brand/press kit page, or copy it from their website. + +Paste the SVG code here and I'll convert it to a React component. ``` ## Common Gotchas diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index f47d1b8792..6dc2486bff 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1853,6 +1853,14 @@ export function LinearIcon(props: React.SVGProps) { ) } +export function LangsmithIcon(props: SVGProps) { + return ( + + + + ) +} + export function LemlistIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 549bbc6e70..79cd7c945e 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -55,6 +55,7 @@ import { JiraIcon, JiraServiceManagementIcon, KalshiIcon, + LangsmithIcon, LemlistIcon, LinearIcon, LinkedInIcon, @@ -180,6 +181,7 @@ export const blockTypeToIconMap: Record = { jira_service_management: JiraServiceManagementIcon, kalshi: KalshiIcon, knowledge: PackageSearchIcon, + langsmith: LangsmithIcon, lemlist: LemlistIcon, linear: LinearIcon, linkedin: LinkedInIcon, @@ -231,7 +233,6 @@ export const blockTypeToIconMap: Record = { supabase: SupabaseIcon, tavily: TavilyIcon, telegram: TelegramIcon, - thinking: BrainIcon, tinybird: TinybirdIcon, translate: TranslateIcon, trello: TrelloIcon, diff --git a/apps/docs/content/docs/en/tools/langsmith.mdx b/apps/docs/content/docs/en/tools/langsmith.mdx new file mode 100644 index 0000000000..bade38cc69 --- /dev/null +++ b/apps/docs/content/docs/en/tools/langsmith.mdx @@ -0,0 +1,59 @@ +--- +title: LangSmith +description: Forward workflow runs to LangSmith for observability +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Send run data to LangSmith to trace executions, attach metadata, and monitor workflow performance. + + + +## Tools + +### `langsmith_create_run` + +Forward a single run to LangSmith for ingestion. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | LangSmith API key | +| `runId` | string | Yes | Unique run identifier | +| `name` | string | Yes | Run name | +| `runType` | string | Yes | Run type \(tool, chain, llm, retriever, embedding, prompt, parser\) | +| `startTime` | string | Yes | Run start time in ISO-8601 format | +| `endTime` | string | No | Run end time in ISO-8601 format | + +#### Output + +This tool does not produce any outputs. + +### `langsmith_create_runs_batch` + +Forward multiple runs to LangSmith in a single batch. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | LangSmith API key | +| `post` | json | No | Array of new runs to ingest | +| `patch` | json | No | Array of runs to update/patch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accepted` | boolean | Whether the batch was accepted for ingestion | +| `runIds` | array | Run identifiers provided in the request | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 5d24380339..d1d88a5116 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -52,6 +52,7 @@ "jira_service_management", "kalshi", "knowledge", + "langsmith", "lemlist", "linear", "linkedin", @@ -103,7 +104,6 @@ "supabase", "tavily", "telegram", - "thinking", "tinybird", "translate", "trello", diff --git a/apps/docs/content/docs/en/tools/tinybird.mdx b/apps/docs/content/docs/en/tools/tinybird.mdx index 9da20cce93..e35bddbed3 100644 --- a/apps/docs/content/docs/en/tools/tinybird.mdx +++ b/apps/docs/content/docs/en/tools/tinybird.mdx @@ -63,8 +63,3 @@ Execute SQL queries against Tinybird Pipes and Data Sources using the Query API. | `statistics` | json | Query execution statistics - elapsed time, rows read, bytes read \(only available with FORMAT JSON\) | - -## Notes - -- Category: `tools` -- Type: `tinybird` diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index cf6973216e..ea0406fe11 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -8,6 +8,7 @@ import { extractFieldsFromSchema, parseResponseFormatSafely, } from '@/lib/core/utils/response-format' +import { getToolOutputs } from '@/lib/workflows/blocks/block-outputs' import { getBlock } from '@/blocks' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -128,6 +129,10 @@ export function OutputSelect({ ? baselineWorkflow.blocks?.[block.id]?.subBlocks?.responseFormat?.value : subBlockValues?.[block.id]?.responseFormat const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) + const operationValue = + shouldUseBaseline && baselineWorkflow + ? baselineWorkflow.blocks?.[block.id]?.subBlocks?.operation?.value + : subBlockValues?.[block.id]?.operation let outputsToProcess: Record = {} @@ -141,7 +146,12 @@ export function OutputSelect({ outputsToProcess = blockConfig?.outputs || {} } } else { - outputsToProcess = blockConfig?.outputs || {} + const toolOutputs = + blockConfig && typeof operationValue === 'string' + ? getToolOutputs(blockConfig, operationValue) + : {} + outputsToProcess = + Object.keys(toolOutputs).length > 0 ? toolOutputs : blockConfig?.outputs || {} } if (Object.keys(outputsToProcess).length === 0) return diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index b33427bab9..89a5b4e1e8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -20,7 +20,13 @@ import { extractFieldsFromSchema, parseResponseFormatSafely, } from '@/lib/core/utils/response-format' -import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/blocks/block-outputs' +import { + getBlockOutputPaths, + getBlockOutputType, + getOutputPathsFromSchema, + getToolOutputPaths, + getToolOutputType, +} from '@/lib/workflows/blocks/block-outputs' import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' import { KeyboardNavigationHandler } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler' import type { @@ -38,7 +44,6 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { normalizeName } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { BlockState } from '@/stores/workflows/workflow/types' -import { getTool } from '@/tools/utils' const logger = createLogger('TagDropdown') @@ -68,6 +73,12 @@ interface TagDropdownProps { inputRef?: React.RefObject } +interface TagComputationResult { + tags: string[] + variableInfoMap: Record + blockTagGroups: BlockTagGroup[] +} + /** * Checks if the tag trigger (`<`) should show the tag dropdown. * @@ -218,161 +229,6 @@ const getOutputTypeForPath = ( return 'any' } -/** - * Recursively generates all output paths from an outputs schema. - * - * @remarks - * Traverses nested objects and arrays to build dot-separated paths - * for all leaf values in the schema. - * - * @param outputs - The outputs schema object - * @param prefix - Current path prefix for recursion - * @returns Array of dot-separated paths to all output fields - */ -const generateOutputPaths = (outputs: Record, prefix = ''): string[] => { - const paths: string[] = [] - - for (const [key, value] of Object.entries(outputs)) { - const currentPath = prefix ? `${prefix}.${key}` : key - - if (typeof value === 'string') { - paths.push(currentPath) - } else if (typeof value === 'object' && value !== null) { - if ('type' in value && typeof value.type === 'string') { - const hasNestedProperties = - ((value.type === 'object' || value.type === 'json') && value.properties) || - (value.type === 'array' && value.items?.properties) || - (value.type === 'array' && - value.items && - typeof value.items === 'object' && - !('type' in value.items)) - - if (!hasNestedProperties) { - paths.push(currentPath) - } - - if ((value.type === 'object' || value.type === 'json') && value.properties) { - paths.push(...generateOutputPaths(value.properties, currentPath)) - } else if (value.type === 'array' && value.items?.properties) { - paths.push(...generateOutputPaths(value.items.properties, currentPath)) - } else if ( - value.type === 'array' && - value.items && - typeof value.items === 'object' && - !('type' in value.items) - ) { - paths.push(...generateOutputPaths(value.items, currentPath)) - } - } else { - const subPaths = generateOutputPaths(value, currentPath) - paths.push(...subPaths) - } - } else { - paths.push(currentPath) - } - } - - return paths -} - -/** - * Recursively generates all output paths with their types from an outputs schema. - * - * @remarks - * Similar to generateOutputPaths but also captures the type information - * for each path, useful for displaying type hints in the UI. - * - * @param outputs - The outputs schema object - * @param prefix - Current path prefix for recursion - * @returns Array of objects containing path and type for each output field - */ -const generateOutputPathsWithTypes = ( - outputs: Record, - prefix = '' -): Array<{ path: string; type: string }> => { - const paths: Array<{ path: string; type: string }> = [] - - for (const [key, value] of Object.entries(outputs)) { - const currentPath = prefix ? `${prefix}.${key}` : key - - if (typeof value === 'string') { - paths.push({ path: currentPath, type: value }) - } else if (typeof value === 'object' && value !== null) { - if ('type' in value && typeof value.type === 'string') { - if (value.type === 'array' && value.items?.properties) { - paths.push({ path: currentPath, type: 'array' }) - const subPaths = generateOutputPathsWithTypes(value.items.properties, currentPath) - paths.push(...subPaths) - } else if ((value.type === 'object' || value.type === 'json') && value.properties) { - paths.push({ path: currentPath, type: value.type }) - const subPaths = generateOutputPathsWithTypes(value.properties, currentPath) - paths.push(...subPaths) - } else { - paths.push({ path: currentPath, type: value.type }) - } - } else { - const subPaths = generateOutputPathsWithTypes(value, currentPath) - paths.push(...subPaths) - } - } else { - paths.push({ path: currentPath, type: 'any' }) - } - } - - return paths -} - -/** - * Generates output paths for a tool-based block. - * - * @param blockConfig - The block configuration containing tools config - * @param operation - The selected operation for the tool - * @returns Array of output paths for the tool, or empty array on error - */ -const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): string[] => { - if (!blockConfig?.tools?.config?.tool) return [] - - try { - const toolId = blockConfig.tools.config.tool({ operation }) - if (!toolId) return [] - - const toolConfig = getTool(toolId) - if (!toolConfig?.outputs) return [] - - return generateOutputPaths(toolConfig.outputs) - } catch (error) { - logger.warn('Failed to get tool outputs for operation', { operation, error }) - return [] - } -} - -/** - * Gets the output type for a specific path in a tool's outputs. - * - * @param blockConfig - The block configuration containing tools config - * @param operation - The selected operation for the tool - * @param path - The dot-separated path to the output field - * @returns The type of the output field, or 'any' if not found - */ -const getToolOutputType = (blockConfig: BlockConfig, operation: string, path: string): string => { - if (!blockConfig?.tools?.config?.tool) return 'any' - - try { - const toolId = blockConfig.tools.config.tool({ operation }) - if (!toolId) return 'any' - - const toolConfig = getTool(toolId) - if (!toolConfig?.outputs) return 'any' - - const pathsWithTypes = generateOutputPathsWithTypes(toolConfig.outputs) - const matchingPath = pathsWithTypes.find((p) => p.path === path) - return matchingPath?.type || 'any' - } catch (error) { - logger.warn('Failed to get tool output type for path', { path, error }) - return 'any' - } -} - /** * Calculates the viewport position of the caret in a textarea/input. * @@ -601,14 +457,16 @@ export const TagDropdown: React.FC = ({ [inputValue, cursorPosition] ) + const emptyVariableInfoMap: Record = {} + /** * Computes tags, variable info, and block tag groups */ - const { tags, variableInfoMap, blockTagGroups } = useMemo(() => { + const { tags, variableInfoMap, blockTagGroups } = useMemo(() => { if (activeSourceBlockId) { const sourceBlock = blocks[activeSourceBlockId] if (!sourceBlock) { - return { tags: [], variableInfoMap: {}, blockTagGroups: [] } + return { tags: [], variableInfoMap: emptyVariableInfoMap, blockTagGroups: [] } } const blockConfig = getBlock(sourceBlock.type) @@ -619,7 +477,7 @@ export const TagDropdown: React.FC = ({ const blockName = sourceBlock.name || sourceBlock.type const normalizedBlockName = normalizeName(blockName) - const outputPaths = generateOutputPaths(mockConfig.outputs) + const outputPaths = getOutputPathsFromSchema(mockConfig.outputs) const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) const blockTagGroups: BlockTagGroup[] = [ @@ -632,9 +490,9 @@ export const TagDropdown: React.FC = ({ }, ] - return { tags: blockTags, variableInfoMap: {}, blockTagGroups } + return { tags: blockTags, variableInfoMap: emptyVariableInfoMap, blockTagGroups } } - return { tags: [], variableInfoMap: {}, blockTagGroups: [] } + return { tags: [], variableInfoMap: emptyVariableInfoMap, blockTagGroups: [] } } const blockName = sourceBlock.name || sourceBlock.type @@ -777,7 +635,7 @@ export const TagDropdown: React.FC = ({ const operationValue = mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation') const toolOutputPaths = operationValue - ? generateToolOutputPaths(blockConfig, operationValue) + ? getToolOutputPaths(blockConfig, operationValue) : [] if (toolOutputPaths.length > 0) { @@ -810,12 +668,12 @@ export const TagDropdown: React.FC = ({ }, ] - return { tags: blockTags, variableInfoMap: {}, blockTagGroups } + return { tags: blockTags, variableInfoMap: emptyVariableInfoMap, blockTagGroups } } const hasInvalidBlocks = Object.values(blocks).some((block) => !block || !block.type) if (hasInvalidBlocks) { - return { tags: [], variableInfoMap: {}, blockTagGroups: [] } + return { tags: [], variableInfoMap: emptyVariableInfoMap, blockTagGroups: [] } } const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') @@ -981,7 +839,7 @@ export const TagDropdown: React.FC = ({ const blockName = accessibleBlock.name || accessibleBlock.type const normalizedBlockName = normalizeName(blockName) - const outputPaths = generateOutputPaths(mockConfig.outputs) + const outputPaths = getOutputPathsFromSchema(mockConfig.outputs) let blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) blockTags = ensureRootTag(blockTags, normalizedBlockName) @@ -1109,7 +967,7 @@ export const TagDropdown: React.FC = ({ const operationValue = mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation') const toolOutputPaths = operationValue - ? generateToolOutputPaths(blockConfig, operationValue) + ? getToolOutputPaths(blockConfig, operationValue) : [] if (toolOutputPaths.length > 0) { @@ -1183,7 +1041,7 @@ export const TagDropdown: React.FC = ({ const filteredTags = useMemo(() => { if (!searchTerm) return tags - return tags.filter((tag) => tag.toLowerCase().includes(searchTerm)) + return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm)) }, [tags, searchTerm]) const { variableTags, filteredBlockTagGroups } = useMemo(() => { diff --git a/apps/sim/blocks/blocks/langsmith.ts b/apps/sim/blocks/blocks/langsmith.ts new file mode 100644 index 0000000000..f5d2d9e24a --- /dev/null +++ b/apps/sim/blocks/blocks/langsmith.ts @@ -0,0 +1,292 @@ +import { LangsmithIcon } from '@/components/icons' +import { AuthMode, type BlockConfig } from '@/blocks/types' +import type { LangsmithResponse } from '@/tools/langsmith/types' + +export const LangsmithBlock: BlockConfig = { + type: 'langsmith', + name: 'LangSmith', + description: 'Forward workflow runs to LangSmith for observability', + longDescription: + 'Send run data to LangSmith to trace executions, attach metadata, and monitor workflow performance.', + docsLink: 'https://docs.sim.ai/tools/langsmith', + category: 'tools', + bgColor: '#181C1E', + icon: LangsmithIcon, + authMode: AuthMode.ApiKey, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Run', id: 'create_run' }, + { label: 'Create Runs Batch', id: 'create_runs_batch' }, + ], + value: () => 'create_run', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your LangSmith API key', + password: true, + required: true, + }, + { + id: 'id', + title: 'Run ID', + type: 'short-input', + placeholder: 'Auto-generated if blank', + condition: { field: 'operation', value: 'create_run' }, + }, + { + id: 'name', + title: 'Name', + type: 'short-input', + placeholder: 'Run name', + required: { field: 'operation', value: 'create_run' }, + condition: { field: 'operation', value: 'create_run' }, + }, + { + id: 'run_type', + title: 'Run Type', + type: 'dropdown', + options: [ + { label: 'Chain', id: 'chain' }, + { label: 'Tool', id: 'tool' }, + { label: 'LLM', id: 'llm' }, + { label: 'Retriever', id: 'retriever' }, + { label: 'Embedding', id: 'embedding' }, + { label: 'Prompt', id: 'prompt' }, + { label: 'Parser', id: 'parser' }, + ], + value: () => 'chain', + required: { field: 'operation', value: 'create_run' }, + condition: { field: 'operation', value: 'create_run' }, + }, + { + id: 'start_time', + title: 'Start Time', + type: 'short-input', + placeholder: '2025-01-01T12:00:00Z', + condition: { field: 'operation', value: 'create_run' }, + value: () => new Date().toISOString(), + }, + { + id: 'end_time', + title: 'End Time', + type: 'short-input', + placeholder: '2025-01-01T12:00:30Z', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'inputs', + title: 'Inputs', + type: 'code', + placeholder: '{"input":"value"}', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'outputs', + title: 'Outputs', + type: 'code', + placeholder: '{"output":"value"}', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'extra', + title: 'Metadata', + type: 'code', + placeholder: '{"ls_model":"gpt-4"}', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'tags', + title: 'Tags', + type: 'code', + placeholder: '["production","workflow"]', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'parent_run_id', + title: 'Parent Run ID', + type: 'short-input', + placeholder: 'Parent run identifier', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'trace_id', + title: 'Trace ID', + type: 'short-input', + placeholder: 'Auto-generated if blank', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'session_id', + title: 'Session ID', + type: 'short-input', + placeholder: 'Session identifier', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'session_name', + title: 'Session Name', + type: 'short-input', + placeholder: 'Session name', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'status', + title: 'Status', + type: 'short-input', + placeholder: 'success', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'error', + title: 'Error', + type: 'long-input', + placeholder: 'Error message', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'dotted_order', + title: 'Dotted Order', + type: 'short-input', + placeholder: 'Defaults to Z', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'events', + title: 'Events', + type: 'code', + placeholder: '[{"event":"token","value":1}]', + condition: { field: 'operation', value: 'create_run' }, + mode: 'advanced', + }, + { + id: 'post', + title: 'Post Runs', + type: 'code', + placeholder: '[{"id":"...","name":"...","run_type":"chain","start_time":"..."}]', + condition: { field: 'operation', value: 'create_runs_batch' }, + }, + { + id: 'patch', + title: 'Patch Runs', + type: 'code', + placeholder: '[{"id":"...","name":"...","run_type":"chain","start_time":"..."}]', + condition: { field: 'operation', value: 'create_runs_batch' }, + mode: 'advanced', + }, + ], + tools: { + access: ['langsmith_create_run', 'langsmith_create_runs_batch'], + config: { + tool: (params) => { + switch (params.operation) { + case 'create_runs_batch': + return 'langsmith_create_runs_batch' + case 'create_run': + default: + return 'langsmith_create_run' + } + }, + params: (params) => { + const parseJsonValue = (value: unknown, label: string) => { + if (value === undefined || value === null || value === '') { + return undefined + } + if (typeof value === 'string') { + try { + return JSON.parse(value) + } catch (error) { + throw new Error( + `Invalid JSON for ${label}: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + return value + } + + if (params.operation === 'create_runs_batch') { + const post = parseJsonValue(params.post, 'post runs') + const patch = parseJsonValue(params.patch, 'patch runs') + + if (!post && !patch) { + throw new Error('Provide at least one of post or patch runs') + } + + return { + apiKey: params.apiKey, + post, + patch, + } + } + + return { + apiKey: params.apiKey, + id: params.id, + name: params.name, + run_type: params.run_type, + start_time: params.start_time, + end_time: params.end_time, + inputs: parseJsonValue(params.inputs, 'inputs'), + outputs: parseJsonValue(params.outputs, 'outputs'), + extra: parseJsonValue(params.extra, 'metadata'), + tags: parseJsonValue(params.tags, 'tags'), + parent_run_id: params.parent_run_id, + trace_id: params.trace_id, + session_id: params.session_id, + session_name: params.session_name, + status: params.status, + error: params.error, + dotted_order: params.dotted_order, + events: parseJsonValue(params.events, 'events'), + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'LangSmith API key' }, + id: { type: 'string', description: 'Run identifier' }, + name: { type: 'string', description: 'Run name' }, + run_type: { type: 'string', description: 'Run type' }, + start_time: { type: 'string', description: 'Run start time (ISO)' }, + end_time: { type: 'string', description: 'Run end time (ISO)' }, + inputs: { type: 'json', description: 'Run inputs payload' }, + outputs: { type: 'json', description: 'Run outputs payload' }, + extra: { type: 'json', description: 'Additional metadata (extra)' }, + tags: { type: 'json', description: 'Tags array' }, + parent_run_id: { type: 'string', description: 'Parent run ID' }, + trace_id: { type: 'string', description: 'Trace ID' }, + session_id: { type: 'string', description: 'Session ID' }, + session_name: { type: 'string', description: 'Session name' }, + status: { type: 'string', description: 'Run status' }, + error: { type: 'string', description: 'Error message' }, + dotted_order: { type: 'string', description: 'Dotted order string' }, + events: { type: 'json', description: 'Events array' }, + post: { type: 'json', description: 'Runs to ingest in batch' }, + patch: { type: 'json', description: 'Runs to update in batch' }, + }, + outputs: { + accepted: { type: 'boolean', description: 'Whether ingestion was accepted' }, + runId: { type: 'string', description: 'Run ID for single run' }, + runIds: { type: 'array', description: 'Run IDs for batch ingest' }, + message: { type: 'string', description: 'LangSmith response message' }, + messages: { type: 'array', description: 'Per-run response messages' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f5504a7a16..b6538b241f 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -61,6 +61,7 @@ import { JiraBlock } from '@/blocks/blocks/jira' import { JiraServiceManagementBlock } from '@/blocks/blocks/jira_service_management' import { KalshiBlock } from '@/blocks/blocks/kalshi' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' +import { LangsmithBlock } from '@/blocks/blocks/langsmith' import { LemlistBlock } from '@/blocks/blocks/lemlist' import { LinearBlock } from '@/blocks/blocks/linear' import { LinkedInBlock } from '@/blocks/blocks/linkedin' @@ -217,6 +218,7 @@ export const registry: Record = { jira_service_management: JiraServiceManagementBlock, kalshi: KalshiBlock, knowledge: KnowledgeBlock, + langsmith: LangsmithBlock, lemlist: LemlistBlock, linear: LinearBlock, linkedin: LinkedInBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index f47d1b8792..d8ebc1641e 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1853,6 +1853,27 @@ export function LinearIcon(props: React.SVGProps) { ) } +export function LangsmithIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function LemlistIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index 2fabf9692f..a3a9ec1663 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@sim/logger' import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' import { classifyStartBlockType, @@ -12,8 +13,11 @@ import { } from '@/lib/workflows/types' import { getBlock } from '@/blocks' import type { BlockConfig, OutputCondition, OutputFieldDefinition } from '@/blocks/types' +import { getTool } from '@/tools/utils' import { getTrigger, isTriggerValid } from '@/triggers' +const logger = createLogger('BlockOutputs') + type OutputDefinition = Record interface SubBlockWithValue { @@ -435,3 +439,167 @@ export function getBlockOutputType( const value = traverseOutputPath(outputs, pathParts) return extractType(value) } + +/** + * Recursively generates all output paths from an outputs schema. + * + * @param outputs - The outputs schema object + * @param prefix - Current path prefix for recursion + * @returns Array of dot-separated paths to all output fields + */ +function generateOutputPaths(outputs: Record, prefix = ''): string[] { + const paths: string[] = [] + + for (const [key, value] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'string') { + paths.push(currentPath) + } else if (typeof value === 'object' && value !== null) { + if ('type' in value && typeof value.type === 'string') { + const hasNestedProperties = + ((value.type === 'object' || value.type === 'json') && value.properties) || + (value.type === 'array' && value.items?.properties) || + (value.type === 'array' && + value.items && + typeof value.items === 'object' && + !('type' in value.items)) + + if (!hasNestedProperties) { + paths.push(currentPath) + } + + if ((value.type === 'object' || value.type === 'json') && value.properties) { + paths.push(...generateOutputPaths(value.properties, currentPath)) + } else if (value.type === 'array' && value.items?.properties) { + paths.push(...generateOutputPaths(value.items.properties, currentPath)) + } else if ( + value.type === 'array' && + value.items && + typeof value.items === 'object' && + !('type' in value.items) + ) { + paths.push(...generateOutputPaths(value.items, currentPath)) + } + } else { + const subPaths = generateOutputPaths(value, currentPath) + paths.push(...subPaths) + } + } else { + paths.push(currentPath) + } + } + + return paths +} + +/** + * Recursively generates all output paths with their types from an outputs schema. + * + * @param outputs - The outputs schema object + * @param prefix - Current path prefix for recursion + * @returns Array of objects containing path and type for each output field + */ +function generateOutputPathsWithTypes( + outputs: Record, + prefix = '' +): Array<{ path: string; type: string }> { + const paths: Array<{ path: string; type: string }> = [] + + for (const [key, value] of Object.entries(outputs)) { + const currentPath = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'string') { + paths.push({ path: currentPath, type: value }) + } else if (typeof value === 'object' && value !== null) { + if ('type' in value && typeof value.type === 'string') { + if (value.type === 'array' && value.items?.properties) { + paths.push({ path: currentPath, type: 'array' }) + const subPaths = generateOutputPathsWithTypes(value.items.properties, currentPath) + paths.push(...subPaths) + } else if ((value.type === 'object' || value.type === 'json') && value.properties) { + paths.push({ path: currentPath, type: value.type }) + const subPaths = generateOutputPathsWithTypes(value.properties, currentPath) + paths.push(...subPaths) + } else { + paths.push({ path: currentPath, type: value.type }) + } + } else { + const subPaths = generateOutputPathsWithTypes(value, currentPath) + paths.push(...subPaths) + } + } else { + paths.push({ path: currentPath, type: 'any' }) + } + } + + return paths +} + +/** + * Gets the tool outputs for a block operation. + * + * @param blockConfig - The block configuration containing tools config + * @param operation - The selected operation for the tool + * @returns Outputs schema for the tool, or empty object on error + */ +export function getToolOutputs(blockConfig: BlockConfig, operation: string): Record { + if (!blockConfig?.tools?.config?.tool) return {} + + try { + const toolId = blockConfig.tools.config.tool({ operation }) + if (!toolId) return {} + + const toolConfig = getTool(toolId) + if (!toolConfig?.outputs) return {} + + return toolConfig.outputs + } catch (error) { + logger.warn('Failed to get tool outputs for operation', { operation, error }) + return {} + } +} + +/** + * Generates output paths for a tool-based block. + * + * @param blockConfig - The block configuration containing tools config + * @param operation - The selected operation for the tool + * @returns Array of output paths for the tool, or empty array on error + */ +export function getToolOutputPaths(blockConfig: BlockConfig, operation: string): string[] { + const outputs = getToolOutputs(blockConfig, operation) + if (!outputs || Object.keys(outputs).length === 0) return [] + return generateOutputPaths(outputs) +} + +/** + * Generates output paths from a schema definition. + * + * @param outputs - The outputs schema object + * @returns Array of dot-separated paths to all output fields + */ +export function getOutputPathsFromSchema(outputs: Record): string[] { + return generateOutputPaths(outputs) +} + +/** + * Gets the output type for a specific path in a tool's outputs. + * + * @param blockConfig - The block configuration containing tools config + * @param operation - The selected operation for the tool + * @param path - The dot-separated path to the output field + * @returns The type of the output field, or 'any' if not found + */ +export function getToolOutputType( + blockConfig: BlockConfig, + operation: string, + path: string +): string { + const outputs = getToolOutputs(blockConfig, operation) + if (!outputs || Object.keys(outputs).length === 0) return 'any' + + const pathsWithTypes = generateOutputPathsWithTypes(outputs) + const matchingPath = pathsWithTypes.find((p) => p.path === path) + return matchingPath?.type || 'any' +} diff --git a/apps/sim/tools/langsmith/create_run.ts b/apps/sim/tools/langsmith/create_run.ts new file mode 100644 index 0000000000..6abeb29afe --- /dev/null +++ b/apps/sim/tools/langsmith/create_run.ts @@ -0,0 +1,188 @@ +import type { LangsmithCreateRunParams, LangsmithCreateRunResponse } from '@/tools/langsmith/types' +import { normalizeLangsmithRunPayload } from '@/tools/langsmith/utils' +import type { ToolConfig } from '@/tools/types' + +export const langsmithCreateRunTool: ToolConfig< + LangsmithCreateRunParams, + LangsmithCreateRunResponse +> = { + id: 'langsmith_create_run', + name: 'LangSmith Create Run', + description: 'Forward a single run to LangSmith for ingestion.', + version: '1.0.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LangSmith API key', + }, + id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Unique run identifier', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Run name', + }, + run_type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Run type (tool, chain, llm, retriever, embedding, prompt, parser)', + }, + start_time: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run start time in ISO-8601 format', + }, + end_time: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run end time in ISO-8601 format', + }, + inputs: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Inputs payload', + }, + outputs: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Outputs payload', + }, + extra: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Additional metadata (extra)', + }, + tags: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Array of tag strings', + }, + parent_run_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Parent run ID', + }, + trace_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Trace ID', + }, + session_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Session ID', + }, + session_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Session name', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Run status', + }, + error: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Error details', + }, + dotted_order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Dotted order string', + }, + events: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Structured events array', + }, + }, + request: { + url: () => 'https://api.smith.langchain.com/runs', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const { payload } = normalizeLangsmithRunPayload(params) + const normalizedPayload: Record = { + ...payload, + name: payload.name?.trim(), + inputs: params.inputs, + outputs: params.outputs, + extra: params.extra, + tags: params.tags, + status: params.status, + error: params.error, + events: params.events, + } + + return Object.fromEntries( + Object.entries(normalizedPayload).filter(([, value]) => value !== undefined) + ) + }, + }, + transformResponse: async (response, params) => { + const runId = params ? normalizeLangsmithRunPayload(params).runId : null + const data = (await response.json()) as Record + const directMessage = + typeof (data as { message?: unknown }).message === 'string' + ? (data as { message: string }).message + : null + const nestedPayload = + runId && typeof data[runId] === 'object' && data[runId] !== null + ? (data[runId] as Record) + : null + const nestedMessage = + nestedPayload && typeof nestedPayload.message === 'string' ? nestedPayload.message : null + + return { + success: true, + output: { + accepted: true, + runId: runId ?? null, + message: directMessage ?? nestedMessage ?? null, + }, + } + }, + outputs: { + accepted: { + type: 'boolean', + description: 'Whether the run was accepted for ingestion', + }, + runId: { + type: 'string', + description: 'Run identifier provided in the request', + optional: true, + }, + message: { + type: 'string', + description: 'Response message from LangSmith', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/langsmith/create_runs_batch.ts b/apps/sim/tools/langsmith/create_runs_batch.ts new file mode 100644 index 0000000000..6dbeb56992 --- /dev/null +++ b/apps/sim/tools/langsmith/create_runs_batch.ts @@ -0,0 +1,112 @@ +import type { + LangsmithCreateRunsBatchParams, + LangsmithCreateRunsBatchResponse, + LangsmithRunPayload, +} from '@/tools/langsmith/types' +import { normalizeLangsmithRunPayload } from '@/tools/langsmith/utils' +import type { ToolConfig } from '@/tools/types' + +export const langsmithCreateRunsBatchTool: ToolConfig< + LangsmithCreateRunsBatchParams, + LangsmithCreateRunsBatchResponse +> = { + id: 'langsmith_create_runs_batch', + name: 'LangSmith Create Runs Batch', + description: 'Forward multiple runs to LangSmith in a single batch.', + version: '1.0.0', + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'LangSmith API key', + }, + post: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Array of new runs to ingest', + }, + patch: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Array of runs to update/patch', + }, + }, + request: { + url: () => 'https://api.smith.langchain.com/runs/batch', + method: 'POST', + headers: (params) => ({ + 'X-Api-Key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const payload: Record = { + post: params.post + ? params.post.map((run) => normalizeLangsmithRunPayload(run).payload) + : undefined, + patch: params.patch + ? params.patch.map((run) => normalizeLangsmithRunPayload(run).payload) + : undefined, + } + + return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined)) + }, + }, + transformResponse: async (response, params) => { + const data = (await response.json()) as Record + const directMessage = + typeof (data as { message?: unknown }).message === 'string' + ? (data as { message: string }).message + : null + const messages = Object.values(data) + .map((value) => { + if (typeof value !== 'object' || value === null) { + return null + } + const messageValue = (value as Record).message + return typeof messageValue === 'string' ? messageValue : null + }) + .filter((value): value is string => Boolean(value)) + + const collectRunIds = (runs?: LangsmithRunPayload[]) => + runs?.map((run) => normalizeLangsmithRunPayload(run).runId) ?? [] + + return { + success: true, + output: { + accepted: true, + runIds: [...collectRunIds(params?.post), ...collectRunIds(params?.patch)], + message: directMessage ?? null, + messages: messages.length ? messages : undefined, + }, + } + }, + outputs: { + accepted: { + type: 'boolean', + description: 'Whether the batch was accepted for ingestion', + }, + runIds: { + type: 'array', + description: 'Run identifiers provided in the request', + items: { + type: 'string', + }, + }, + message: { + type: 'string', + description: 'Response message from LangSmith', + optional: true, + }, + messages: { + type: 'array', + description: 'Per-run response messages, when provided', + optional: true, + items: { + type: 'string', + }, + }, + }, +} diff --git a/apps/sim/tools/langsmith/index.ts b/apps/sim/tools/langsmith/index.ts new file mode 100644 index 0000000000..f173b10a21 --- /dev/null +++ b/apps/sim/tools/langsmith/index.ts @@ -0,0 +1,2 @@ +export { langsmithCreateRunTool } from '@/tools/langsmith/create_run' +export { langsmithCreateRunsBatchTool } from '@/tools/langsmith/create_runs_batch' diff --git a/apps/sim/tools/langsmith/types.ts b/apps/sim/tools/langsmith/types.ts new file mode 100644 index 0000000000..b0e287b464 --- /dev/null +++ b/apps/sim/tools/langsmith/types.ts @@ -0,0 +1,59 @@ +import type { ToolResponse } from '@/tools/types' + +export type LangsmithRunType = + | 'tool' + | 'chain' + | 'llm' + | 'retriever' + | 'embedding' + | 'prompt' + | 'parser' + +export interface LangsmithRunPayload { + id?: string + name: string + run_type: LangsmithRunType + start_time?: string + end_time?: string + inputs?: Record + outputs?: Record + extra?: Record + tags?: string[] + parent_run_id?: string + trace_id?: string + session_id?: string + session_name?: string + status?: string + error?: string + dotted_order?: string + events?: Record[] +} + +export interface LangsmithCreateRunParams extends LangsmithRunPayload { + apiKey: string +} + +export interface LangsmithCreateRunsBatchParams { + apiKey: string + post?: LangsmithRunPayload[] + patch?: LangsmithRunPayload[] +} + +export interface LangsmithCreateRunResponse extends ToolResponse { + output: { + accepted: boolean + runId: string | null + message: string | null + } +} + +export interface LangsmithCreateRunsBatchResponse extends ToolResponse { + output: { + accepted: boolean + runIds: string[] + message: string | null + messages?: string[] + } +} + +export type LangsmithResponse = LangsmithCreateRunResponse | LangsmithCreateRunsBatchResponse diff --git a/apps/sim/tools/langsmith/utils.ts b/apps/sim/tools/langsmith/utils.ts new file mode 100644 index 0000000000..cbe3ae8872 --- /dev/null +++ b/apps/sim/tools/langsmith/utils.ts @@ -0,0 +1,38 @@ +import type { LangsmithRunPayload } from '@/tools/langsmith/types' + +interface NormalizedRunPayload { + payload: LangsmithRunPayload + runId: string +} + +const toCompactTimestamp = (startTime?: string): string => { + const parsed = startTime ? new Date(startTime) : new Date() + const date = Number.isNaN(parsed.getTime()) ? new Date() : parsed + const pad = (value: number, length: number) => value.toString().padStart(length, '0') + const year = date.getUTCFullYear() + const month = pad(date.getUTCMonth() + 1, 2) + const day = pad(date.getUTCDate(), 2) + const hours = pad(date.getUTCHours(), 2) + const minutes = pad(date.getUTCMinutes(), 2) + const seconds = pad(date.getUTCSeconds(), 2) + const micros = pad(date.getUTCMilliseconds() * 1000, 6) + return `${year}${month}${day}T${hours}${minutes}${seconds}${micros}` +} + +export const normalizeLangsmithRunPayload = (run: LangsmithRunPayload): NormalizedRunPayload => { + const runId = run.id ?? crypto.randomUUID() + const traceId = run.trace_id ?? runId + const startTime = run.start_time ?? new Date().toISOString() + const dottedOrder = run.dotted_order ?? `${toCompactTimestamp(startTime)}Z${runId}` + + return { + runId, + payload: { + ...run, + id: runId, + trace_id: traceId, + start_time: startTime, + dotted_order: dottedOrder, + }, + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 7d9a6816d0..94987ee430 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -653,6 +653,7 @@ import { knowledgeSearchTool, knowledgeUploadChunkTool, } from '@/tools/knowledge' +import { langsmithCreateRunsBatchTool, langsmithCreateRunTool } from '@/tools/langsmith' import { lemlistGetActivitiesTool, lemlistGetLeadTool, lemlistSendEmailTool } from '@/tools/lemlist' import { linearAddLabelToIssueTool, @@ -2442,6 +2443,8 @@ export const tools: Record = { linear_update_project_status: linearUpdateProjectStatusTool, linear_delete_project_status: linearDeleteProjectStatusTool, linear_list_project_statuses: linearListProjectStatusesTool, + langsmith_create_run: langsmithCreateRunTool, + langsmith_create_runs_batch: langsmithCreateRunsBatchTool, lemlist_get_activities: lemlistGetActivitiesTool, lemlist_get_lead: lemlistGetLeadTool, lemlist_send_email: lemlistSendEmailTool,