mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(langsmith): add langsmith tools for logging, output selector use tool-aware listing
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
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