mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-14 17:37:55 -05:00
Compare commits
2 Commits
feat/reord
...
feat/langs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7544557841 | ||
|
|
3f1dccd6aa |
@@ -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
|
||||
|
||||
@@ -226,17 +226,26 @@ export function {Service}Icon(props: SVGProps<SVGSVGElement>) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* SVG paths from brand assets */}
|
||||
{/* SVG paths from user-provided SVG */}
|
||||
</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
|
||||
|
||||
@@ -1853,6 +1853,14 @@ export function LinearIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function LangsmithIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'>
|
||||
<circle cx='12' cy='12' r='10' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function LemlistIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 181' fill='none'>
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
KalshiIcon,
|
||||
LangsmithIcon,
|
||||
LemlistIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
@@ -180,6 +181,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
kalshi: KalshiIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
langsmith: LangsmithIcon,
|
||||
lemlist: LemlistIcon,
|
||||
linear: LinearIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
@@ -231,7 +233,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
supabase: SupabaseIcon,
|
||||
tavily: TavilyIcon,
|
||||
telegram: TelegramIcon,
|
||||
thinking: BrainIcon,
|
||||
tinybird: TinybirdIcon,
|
||||
translate: TranslateIcon,
|
||||
trello: TrelloIcon,
|
||||
|
||||
59
apps/docs/content/docs/en/tools/langsmith.mdx
Normal file
59
apps/docs/content/docs/en/tools/langsmith.mdx
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: LangSmith
|
||||
description: Forward workflow runs to LangSmith for observability
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="langsmith"
|
||||
color="#1B5DFF"
|
||||
/>
|
||||
|
||||
## 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 |
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"jira_service_management",
|
||||
"kalshi",
|
||||
"knowledge",
|
||||
"langsmith",
|
||||
"lemlist",
|
||||
"linear",
|
||||
"linkedin",
|
||||
@@ -103,7 +104,6 @@
|
||||
"supabase",
|
||||
"tavily",
|
||||
"telegram",
|
||||
"thinking",
|
||||
"tinybird",
|
||||
"translate",
|
||||
"trello",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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<string, unknown> = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<HTMLTextAreaElement | HTMLInputElement>
|
||||
}
|
||||
|
||||
interface TagComputationResult {
|
||||
tags: string[]
|
||||
variableInfoMap: Record<string, { type: string; id: string }>
|
||||
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<string, any>, 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<string, any>,
|
||||
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<TagDropdownProps> = ({
|
||||
[inputValue, cursorPosition]
|
||||
)
|
||||
|
||||
const emptyVariableInfoMap: Record<string, { type: string; id: string }> = {}
|
||||
|
||||
/**
|
||||
* Computes tags, variable info, and block tag groups
|
||||
*/
|
||||
const { tags, variableInfoMap, blockTagGroups } = useMemo(() => {
|
||||
const { tags, variableInfoMap, blockTagGroups } = useMemo<TagComputationResult>(() => {
|
||||
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<TagDropdownProps> = ({
|
||||
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<TagDropdownProps> = ({
|
||||
},
|
||||
]
|
||||
|
||||
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<TagDropdownProps> = ({
|
||||
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<TagDropdownProps> = ({
|
||||
},
|
||||
]
|
||||
|
||||
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<TagDropdownProps> = ({
|
||||
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<TagDropdownProps> = ({
|
||||
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<TagDropdownProps> = ({
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -172,7 +172,7 @@ async function executeWebhookJobInternal(
|
||||
const workflowVariables = (wfRows[0]?.variables as Record<string, any>) || {}
|
||||
|
||||
// Merge subblock states (matching workflow-execution pattern)
|
||||
const mergedStates = mergeSubblockState(blocks, {})
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
|
||||
// Create serialized workflow
|
||||
const serializer = new Serializer()
|
||||
|
||||
292
apps/sim/blocks/blocks/langsmith.ts
Normal file
292
apps/sim/blocks/blocks/langsmith.ts
Normal file
@@ -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<LangsmithResponse> = {
|
||||
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 <YYYYMMDDTHHMMSSffffff>Z<id>',
|
||||
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' },
|
||||
},
|
||||
}
|
||||
@@ -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<string, BlockConfig> = {
|
||||
jira_service_management: JiraServiceManagementBlock,
|
||||
kalshi: KalshiBlock,
|
||||
knowledge: KnowledgeBlock,
|
||||
langsmith: LangsmithBlock,
|
||||
lemlist: LemlistBlock,
|
||||
linear: LinearBlock,
|
||||
linkedin: LinkedInBlock,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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<string, OutputFieldDefinition>
|
||||
|
||||
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<string, any>, 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<string, any>,
|
||||
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<string, any> {
|
||||
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, any>): 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'
|
||||
}
|
||||
|
||||
80
apps/sim/lib/workflows/subblocks.ts
Normal file
80
apps/sim/lib/workflows/subblocks.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
export const DEFAULT_SUBBLOCK_TYPE = 'short-input'
|
||||
|
||||
/**
|
||||
* Merges subblock values into the provided subblock structures.
|
||||
* Falls back to a default subblock shape when a value has no structure.
|
||||
* @param subBlocks - Existing subblock definitions from the workflow
|
||||
* @param values - Stored subblock values keyed by subblock id
|
||||
* @returns Merged subblock structures with updated values
|
||||
*/
|
||||
export function mergeSubBlockValues(
|
||||
subBlocks: Record<string, unknown> | undefined,
|
||||
values: Record<string, unknown> | undefined
|
||||
): Record<string, unknown> {
|
||||
const merged = { ...(subBlocks || {}) } as Record<string, any>
|
||||
|
||||
if (!values) return merged
|
||||
|
||||
Object.entries(values).forEach(([subBlockId, value]) => {
|
||||
if (merged[subBlockId] && typeof merged[subBlockId] === 'object') {
|
||||
merged[subBlockId] = {
|
||||
...(merged[subBlockId] as Record<string, unknown>),
|
||||
value,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
merged[subBlockId] = {
|
||||
id: subBlockId,
|
||||
type: DEFAULT_SUBBLOCK_TYPE,
|
||||
value,
|
||||
}
|
||||
})
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges workflow block states with explicit subblock values while maintaining block structure.
|
||||
* Values that are null or undefined do not override existing subblock values.
|
||||
* @param blocks - Block configurations from workflow state
|
||||
* @param subBlockValues - Subblock values keyed by blockId -> subBlockId -> value
|
||||
* @param blockId - Optional specific block ID to merge (merges all if not provided)
|
||||
* @returns Merged block states with updated subblocks
|
||||
*/
|
||||
export function mergeSubblockStateWithValues(
|
||||
blocks: Record<string, BlockState>,
|
||||
subBlockValues: Record<string, Record<string, unknown>> = {},
|
||||
blockId?: string
|
||||
): Record<string, BlockState> {
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
|
||||
return Object.entries(blocksToProcess).reduce(
|
||||
(acc, [id, block]) => {
|
||||
if (!block) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const blockSubBlocks = block.subBlocks || {}
|
||||
const blockValues = subBlockValues[id] || {}
|
||||
const filteredValues = Object.fromEntries(
|
||||
Object.entries(blockValues).filter(([, value]) => value !== null && value !== undefined)
|
||||
)
|
||||
|
||||
const mergedSubBlocks = mergeSubBlockValues(blockSubBlocks, filteredValues) as Record<
|
||||
string,
|
||||
SubBlockState
|
||||
>
|
||||
|
||||
acc[id] = {
|
||||
...block,
|
||||
subBlocks: mergedSubBlocks,
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, BlockState>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import postgres from 'postgres'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { mergeSubBlockValues } from '@/lib/workflows/subblocks'
|
||||
import {
|
||||
BLOCK_OPERATIONS,
|
||||
BLOCKS_OPERATIONS,
|
||||
@@ -455,7 +456,7 @@ async function handleBlocksOperationTx(
|
||||
}
|
||||
|
||||
case BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS: {
|
||||
const { blocks, edges, loops, parallels } = payload
|
||||
const { blocks, edges, loops, parallels, subBlockValues } = payload
|
||||
|
||||
logger.info(`Batch adding blocks to workflow ${workflowId}`, {
|
||||
blockCount: blocks?.length || 0,
|
||||
@@ -465,22 +466,30 @@ async function handleBlocksOperationTx(
|
||||
})
|
||||
|
||||
if (blocks && blocks.length > 0) {
|
||||
const blockValues = blocks.map((block: Record<string, unknown>) => ({
|
||||
id: block.id as string,
|
||||
workflowId,
|
||||
type: block.type as string,
|
||||
name: block.name as string,
|
||||
positionX: (block.position as { x: number; y: number }).x,
|
||||
positionY: (block.position as { x: number; y: number }).y,
|
||||
data: (block.data as Record<string, unknown>) || {},
|
||||
subBlocks: (block.subBlocks as Record<string, unknown>) || {},
|
||||
outputs: (block.outputs as Record<string, unknown>) || {},
|
||||
enabled: (block.enabled as boolean) ?? true,
|
||||
horizontalHandles: (block.horizontalHandles as boolean) ?? true,
|
||||
advancedMode: (block.advancedMode as boolean) ?? false,
|
||||
triggerMode: (block.triggerMode as boolean) ?? false,
|
||||
height: (block.height as number) || 0,
|
||||
}))
|
||||
const blockValues = blocks.map((block: Record<string, unknown>) => {
|
||||
const blockId = block.id as string
|
||||
const mergedSubBlocks = mergeSubBlockValues(
|
||||
block.subBlocks as Record<string, unknown>,
|
||||
subBlockValues?.[blockId]
|
||||
)
|
||||
|
||||
return {
|
||||
id: blockId,
|
||||
workflowId,
|
||||
type: block.type as string,
|
||||
name: block.name as string,
|
||||
positionX: (block.position as { x: number; y: number }).x,
|
||||
positionY: (block.position as { x: number; y: number }).y,
|
||||
data: (block.data as Record<string, unknown>) || {},
|
||||
subBlocks: mergedSubBlocks,
|
||||
outputs: (block.outputs as Record<string, unknown>) || {},
|
||||
enabled: (block.enabled as boolean) ?? true,
|
||||
horizontalHandles: (block.horizontalHandles as boolean) ?? true,
|
||||
advancedMode: (block.advancedMode as boolean) ?? false,
|
||||
triggerMode: (block.triggerMode as boolean) ?? false,
|
||||
height: (block.height as number) || 0,
|
||||
}
|
||||
})
|
||||
|
||||
await tx.insert(workflowBlocks).values(blockValues)
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
* or React hooks, making it safe for use in Next.js API routes.
|
||||
*/
|
||||
|
||||
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
|
||||
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
* Server-safe version of mergeSubblockState for API routes
|
||||
@@ -26,72 +27,7 @@ export function mergeSubblockState(
|
||||
subBlockValues: Record<string, Record<string, any>> = {},
|
||||
blockId?: string
|
||||
): Record<string, BlockState> {
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
|
||||
return Object.entries(blocksToProcess).reduce(
|
||||
(acc, [id, block]) => {
|
||||
// Skip if block is undefined
|
||||
if (!block) {
|
||||
return acc
|
||||
}
|
||||
|
||||
// Initialize subBlocks if not present
|
||||
const blockSubBlocks = block.subBlocks || {}
|
||||
|
||||
// Get stored values for this block
|
||||
const blockValues = subBlockValues[id] || {}
|
||||
|
||||
// Create a deep copy of the block's subBlocks to maintain structure
|
||||
const mergedSubBlocks = Object.entries(blockSubBlocks).reduce(
|
||||
(subAcc, [subBlockId, subBlock]) => {
|
||||
// Skip if subBlock is undefined
|
||||
if (!subBlock) {
|
||||
return subAcc
|
||||
}
|
||||
|
||||
// Get the stored value for this subblock
|
||||
const storedValue = blockValues[subBlockId]
|
||||
|
||||
// Create a new subblock object with the same structure but updated value
|
||||
subAcc[subBlockId] = {
|
||||
...subBlock,
|
||||
value: storedValue !== undefined && storedValue !== null ? storedValue : subBlock.value,
|
||||
}
|
||||
|
||||
return subAcc
|
||||
},
|
||||
{} as Record<string, SubBlockState>
|
||||
)
|
||||
|
||||
// Return the full block state with updated subBlocks
|
||||
acc[id] = {
|
||||
...block,
|
||||
subBlocks: mergedSubBlocks,
|
||||
}
|
||||
|
||||
// Add any values that exist in the provided values but aren't in the block structure
|
||||
// This handles cases where block config has been updated but values still exist
|
||||
Object.entries(blockValues).forEach(([subBlockId, value]) => {
|
||||
if (!mergedSubBlocks[subBlockId] && value !== null && value !== undefined) {
|
||||
// Create a minimal subblock structure
|
||||
mergedSubBlocks[subBlockId] = {
|
||||
id: subBlockId,
|
||||
type: 'short-input', // Default type that's safe to use
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Update the block with the final merged subBlocks (including orphaned values)
|
||||
acc[id] = {
|
||||
...block,
|
||||
subBlocks: mergedSubBlocks,
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, BlockState>
|
||||
)
|
||||
return mergeSubblockStateWithValues(blocks, subBlockValues, blockId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
||||
return edgesToAdd.filter((edge) => {
|
||||
if (edge.source === edge.target) return false
|
||||
return !currentEdges.some(
|
||||
(e) =>
|
||||
e.source === edge.source &&
|
||||
e.sourceHandle === edge.sourceHandle &&
|
||||
e.target === edge.target &&
|
||||
e.targetHandle === edge.targetHandle
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
@@ -32,6 +19,19 @@ const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
||||
|
||||
export { normalizeName }
|
||||
|
||||
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
||||
return edgesToAdd.filter((edge) => {
|
||||
if (edge.source === edge.target) return false
|
||||
return !currentEdges.some(
|
||||
(e) =>
|
||||
e.source === edge.source &&
|
||||
e.sourceHandle === edge.sourceHandle &&
|
||||
e.target === edge.target &&
|
||||
e.targetHandle === edge.targetHandle
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export interface RegeneratedState {
|
||||
blocks: Record<string, BlockState>
|
||||
edges: Edge[]
|
||||
@@ -201,27 +201,20 @@ export function prepareDuplicateBlockState(options: PrepareDuplicateBlockStateOp
|
||||
Object.entries(subBlockValues).filter(([key]) => !WEBHOOK_SUBBLOCK_FIELDS.includes(key))
|
||||
)
|
||||
|
||||
const mergedSubBlocks: Record<string, SubBlockState> = sourceBlock.subBlocks
|
||||
const baseSubBlocks: Record<string, SubBlockState> = sourceBlock.subBlocks
|
||||
? JSON.parse(JSON.stringify(sourceBlock.subBlocks))
|
||||
: {}
|
||||
|
||||
WEBHOOK_SUBBLOCK_FIELDS.forEach((field) => {
|
||||
if (field in mergedSubBlocks) {
|
||||
delete mergedSubBlocks[field]
|
||||
if (field in baseSubBlocks) {
|
||||
delete baseSubBlocks[field]
|
||||
}
|
||||
})
|
||||
|
||||
Object.entries(filteredSubBlockValues).forEach(([subblockId, value]) => {
|
||||
if (mergedSubBlocks[subblockId]) {
|
||||
mergedSubBlocks[subblockId].value = value as SubBlockState['value']
|
||||
} else {
|
||||
mergedSubBlocks[subblockId] = {
|
||||
id: subblockId,
|
||||
type: 'short-input',
|
||||
value: value as SubBlockState['value'],
|
||||
}
|
||||
}
|
||||
})
|
||||
const mergedSubBlocks = mergeSubBlockValues(baseSubBlocks, filteredSubBlockValues) as Record<
|
||||
string,
|
||||
SubBlockState
|
||||
>
|
||||
|
||||
const block: BlockState = {
|
||||
id: newId,
|
||||
@@ -256,11 +249,16 @@ export function mergeSubblockState(
|
||||
workflowId?: string,
|
||||
blockId?: string
|
||||
): Record<string, BlockState> {
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
|
||||
const workflowSubblockValues = workflowId ? subBlockStore.workflowValues[workflowId] || {} : {}
|
||||
|
||||
if (workflowId) {
|
||||
return mergeSubblockStateWithValues(blocks, workflowSubblockValues, blockId)
|
||||
}
|
||||
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
|
||||
return Object.entries(blocksToProcess).reduce(
|
||||
(acc, [id, block]) => {
|
||||
if (!block) {
|
||||
@@ -339,9 +337,15 @@ export async function mergeSubblockStateAsync(
|
||||
workflowId?: string,
|
||||
blockId?: string
|
||||
): Promise<Record<string, BlockState>> {
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
const subBlockStore = useSubBlockStore.getState()
|
||||
|
||||
if (workflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[workflowId] || {}
|
||||
return mergeSubblockStateWithValues(blocks, workflowValues, blockId)
|
||||
}
|
||||
|
||||
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
|
||||
|
||||
// Process blocks in parallel for better performance
|
||||
const processedBlockEntries = await Promise.all(
|
||||
Object.entries(blocksToProcess).map(async ([id, block]) => {
|
||||
@@ -358,16 +362,7 @@ export async function mergeSubblockStateAsync(
|
||||
return null
|
||||
}
|
||||
|
||||
let storedValue = null
|
||||
|
||||
if (workflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[workflowId]
|
||||
if (workflowValues?.[id]) {
|
||||
storedValue = workflowValues[id][subBlockId]
|
||||
}
|
||||
} else {
|
||||
storedValue = subBlockStore.getValue(id, subBlockId)
|
||||
}
|
||||
const storedValue = subBlockStore.getValue(id, subBlockId)
|
||||
|
||||
return [
|
||||
subBlockId,
|
||||
@@ -386,23 +381,6 @@ export async function mergeSubblockStateAsync(
|
||||
subBlockEntries.filter((entry): entry is readonly [string, SubBlockState] => entry !== null)
|
||||
) as Record<string, SubBlockState>
|
||||
|
||||
// Add any values that exist in the store but aren't in the block structure
|
||||
// This handles cases where block config has been updated but values still exist
|
||||
// IMPORTANT: This includes runtime subblock IDs like webhookId, triggerPath, etc.
|
||||
if (workflowId) {
|
||||
const workflowValues = subBlockStore.workflowValues[workflowId]
|
||||
const blockValues = workflowValues?.[id] || {}
|
||||
Object.entries(blockValues).forEach(([subBlockId, value]) => {
|
||||
if (!mergedSubBlocks[subBlockId] && value !== null && value !== undefined) {
|
||||
mergedSubBlocks[subBlockId] = {
|
||||
id: subBlockId,
|
||||
type: 'short-input',
|
||||
value: value as SubBlockState['value'],
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Return the full block state with updated subBlocks (including orphaned values)
|
||||
return [
|
||||
id,
|
||||
|
||||
@@ -639,7 +639,8 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
|
||||
const newName = getUniqueBlockName(block.name, get().blocks)
|
||||
|
||||
const mergedBlock = mergeSubblockState(get().blocks, id)[id]
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
const mergedBlock = mergeSubblockState(get().blocks, activeWorkflowId || undefined, id)[id]
|
||||
|
||||
const newSubBlocks = Object.entries(mergedBlock.subBlocks).reduce(
|
||||
(acc, [subId, subBlock]) => ({
|
||||
@@ -668,7 +669,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
parallels: get().generateParallelBlocks(),
|
||||
}
|
||||
|
||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||
if (activeWorkflowId) {
|
||||
const subBlockValues =
|
||||
useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[id] || {}
|
||||
|
||||
188
apps/sim/tools/langsmith/create_run.ts
Normal file
188
apps/sim/tools/langsmith/create_run.ts
Normal file
@@ -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<string, unknown> = {
|
||||
...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<string, unknown>
|
||||
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<string, unknown>)
|
||||
: 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
112
apps/sim/tools/langsmith/create_runs_batch.ts
Normal file
112
apps/sim/tools/langsmith/create_runs_batch.ts
Normal file
@@ -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<string, unknown> = {
|
||||
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<string, unknown>
|
||||
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<string, unknown>).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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
2
apps/sim/tools/langsmith/index.ts
Normal file
2
apps/sim/tools/langsmith/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { langsmithCreateRunTool } from '@/tools/langsmith/create_run'
|
||||
export { langsmithCreateRunsBatchTool } from '@/tools/langsmith/create_runs_batch'
|
||||
59
apps/sim/tools/langsmith/types.ts
Normal file
59
apps/sim/tools/langsmith/types.ts
Normal file
@@ -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<string, unknown>
|
||||
outputs?: Record<string, unknown>
|
||||
extra?: Record<string, unknown>
|
||||
tags?: string[]
|
||||
parent_run_id?: string
|
||||
trace_id?: string
|
||||
session_id?: string
|
||||
session_name?: string
|
||||
status?: string
|
||||
error?: string
|
||||
dotted_order?: string
|
||||
events?: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
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
|
||||
38
apps/sim/tools/langsmith/utils.ts
Normal file
38
apps/sim/tools/langsmith/utils.ts
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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<string, ToolConfig> = {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user