-
Notifications
You must be signed in to change notification settings - Fork 3.6k
v0.6.95: data enrichment block, nullable workflow description fix #4786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| --- | ||
| title: Enrichment | ||
| description: Enrich data with a Sim enrichment | ||
| --- | ||
|
|
||
| import { BlockInfoCard } from "@/components/ui/block-info-card" | ||
|
|
||
| <BlockInfoCard | ||
| type="enrichment" | ||
| color="#9333EA" | ||
| /> | ||
|
|
||
| ## Usage Instructions | ||
|
|
||
| Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments. | ||
|
|
||
|
|
||
|
|
||
| ## Tools | ||
|
|
||
| ### `enrichment_run` | ||
|
|
||
| Run a Sim enrichment (e.g. Work Email, Phone Number) and return its outputs | ||
|
|
||
| #### Input | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| | --------- | ---- | -------- | ----------- | | ||
| | `enrichmentId` | string | Yes | Registry enrichment id \(e.g. "work-email"\) | | ||
| | `inputs` | json | Yes | Map of the enrichment's input ids to values | | ||
|
|
||
| #### Output | ||
|
|
||
| The exact fields depend on which enrichment ran. `matched` and `provider` are always present. | ||
|
|
||
| | Parameter | Type | Description | | ||
| | --------- | ---- | ----------- | | ||
| | `matched` | boolean | Whether the enrichment found a result | | ||
| | `provider` | string | Provider whose result was returned (e.g. "Hunter", "People Data Labs"); `null` on no match | | ||
| | `email` | string | Work email address (Work Email enrichment) | | ||
| | `phone` | string | Phone number (Phone Number enrichment) | | ||
| | `domain` | string | Website domain (Company Domain enrichment) | | ||
| | `industry` | string | Industry (Company Info enrichment) | | ||
| | `employeeCount` | number | Employee count (Company Info enrichment) | | ||
| | `foundedYear` | number | Founded year (Company Info enrichment) | | ||
| | `description` | string | Company description (Company Info enrichment) | | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ | |
| "elevenlabs", | ||
| "emailbison", | ||
| "enrich", | ||
| "enrichment", | ||
| "evernote", | ||
| "exa", | ||
| "extend", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import type { NextRequest } from 'next/server' | ||
| import { NextResponse } from 'next/server' | ||
| import { runEnrichmentContract } from '@/lib/api/contracts/tools/enrichment' | ||
| import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' | ||
| import { checkInternalAuth } from '@/lib/auth/hybrid' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { getEnrichment } from '@/enrichments/registry' | ||
| import { runEnrichment } from '@/enrichments/run' | ||
|
|
||
| const logger = createLogger('EnrichmentRunAPI') | ||
|
|
||
| /** | ||
| * POST /api/tools/enrichment/run | ||
| * | ||
| * Runs a registry enrichment's provider cascade and returns its outputs. Backs | ||
| * the Enrichment workflow block; called server-to-server by the executor, so it | ||
| * authenticates with the internal token. The cascade injects the workspace's | ||
| * BYOK / hosted key via `executeTool` using `workspaceId`. | ||
| */ | ||
| export const POST = withRouteHandler(async (request: NextRequest) => { | ||
| const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) | ||
| if (!authResult.success) { | ||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) | ||
| } | ||
|
|
||
| const parsed = await parseRequest( | ||
| runEnrichmentContract, | ||
| request, | ||
| {}, | ||
| { | ||
| validationErrorResponse: (error) => | ||
| NextResponse.json( | ||
| { error: getValidationErrorMessage(error, 'Invalid request') }, | ||
| { | ||
| status: 400, | ||
| } | ||
| ), | ||
| } | ||
| ) | ||
| if (!parsed.success) return parsed.response | ||
|
|
||
| const { enrichmentId, inputs, workspaceId } = parsed.data.body | ||
| const enrichment = getEnrichment(enrichmentId) | ||
| if (!enrichment) { | ||
| return NextResponse.json({ error: `Unknown enrichment "${enrichmentId}"` }, { status: 400 }) | ||
| } | ||
|
|
||
| const { result, cost, error, provider } = await runEnrichment(enrichment, inputs, { | ||
| workspaceId, | ||
| signal: request.signal, | ||
| }) | ||
|
|
||
| logger.info('Enrichment block run', { | ||
| enrichmentId, | ||
| matched: Object.keys(result).length > 0, | ||
| provider, | ||
| }) | ||
| return NextResponse.json({ | ||
| matched: Object.keys(result).length > 0, | ||
| result, | ||
| cost, | ||
| error, | ||
| provider, | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { EnrichmentIcon } from '@/components/icons' | ||
| import type { BlockConfig, OutputFieldDefinition, ParamType } from '@/blocks/types' | ||
| import { IntegrationType } from '@/blocks/types' | ||
| import { ALL_ENRICHMENTS, getEnrichment } from '@/enrichments' | ||
| import { mapFieldType } from '@/enrichments/providers' | ||
| import type { EnrichmentOutputField } from '@/enrichments/types' | ||
| import type { EnrichmentRunResponse } from '@/tools/enrichment/types' | ||
|
|
||
| /** Stable subBlock id for an enrichment input (unique across enrichments). */ | ||
| const inputFieldId = (enrichmentId: string, inputId: string) => `${enrichmentId}__${inputId}` | ||
|
|
||
| // One input field per (enrichment, input), shown only for its enrichment. | ||
| const inputSubBlocks = ALL_ENRICHMENTS.flatMap((enrichment) => | ||
| enrichment.inputs.map((input) => ({ | ||
| id: inputFieldId(enrichment.id, input.id), | ||
| title: input.name, | ||
| type: 'short-input' as const, | ||
| placeholder: input.description ?? `Enter ${input.name.toLowerCase()}`, | ||
| condition: { field: 'operation', value: enrichment.id }, | ||
| required: input.required ? ({ field: 'operation', value: enrichment.id } as const) : undefined, | ||
| })) | ||
| ) | ||
|
|
||
| // Block input schema: the operation plus every per-enrichment input field. | ||
| const blockInputs: Record<string, { type: ParamType; description: string }> = { | ||
| operation: { type: 'string', description: 'Enrichment to run' }, | ||
| } | ||
| for (const enrichment of ALL_ENRICHMENTS) { | ||
| for (const input of enrichment.inputs) { | ||
| blockInputs[inputFieldId(enrichment.id, input.id)] = { | ||
| type: mapFieldType(input.type), | ||
| description: `${input.name} (for ${enrichment.name})`, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Union of all enrichment outputs, each shown only for the enrichment(s) that | ||
| // produce it. | ||
| const outputProducers = new Map<string, { field: EnrichmentOutputField; operations: string[] }>() | ||
| for (const enrichment of ALL_ENRICHMENTS) { | ||
| for (const output of enrichment.outputs) { | ||
| const entry = outputProducers.get(output.id) ?? { field: output, operations: [] } | ||
| entry.operations.push(enrichment.id) | ||
| outputProducers.set(output.id, entry) | ||
| } | ||
| } | ||
| // Seed the enrichment outputs first so the reserved `matched` / `provider` | ||
| // keys (assigned below) always win if a future enrichment ever declares an | ||
| // output id that collides with them. | ||
| const blockOutputs: Record<string, OutputFieldDefinition> = {} | ||
| for (const [id, { field, operations }] of outputProducers) { | ||
| blockOutputs[id] = { | ||
| type: mapFieldType(field.type), | ||
| description: field.name, | ||
| condition: { field: 'operation', value: operations }, | ||
| } | ||
| } | ||
| blockOutputs.matched = { | ||
| type: 'boolean', | ||
| description: 'Whether the enrichment found a result', | ||
| } | ||
| blockOutputs.provider = { | ||
| type: 'string', | ||
| description: 'Provider whose result was returned (e.g. "Hunter", "People Data Labs")', | ||
| } | ||
|
|
||
| /** | ||
| * Enrichment block — runs a code-defined Sim enrichment (Work Email, Phone | ||
| * Number, Company Domain, Company Info, …) and returns its outputs. Generated | ||
| * from the enrichment registry, so new enrichments appear automatically. Runs | ||
| * on the workspace's hosted / BYOK key (injected server-side); no credential. | ||
| */ | ||
| export const EnrichmentBlock: BlockConfig<EnrichmentRunResponse> = { | ||
| type: 'enrichment', | ||
| name: 'Data Enrichment', | ||
| description: 'Enrich data with a Sim enrichment', | ||
| longDescription: | ||
| 'Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.', | ||
| docsLink: 'https://docs.sim.ai/tools/enrichment', | ||
| category: 'tools', | ||
| integrationType: IntegrationType.Sales, | ||
| tags: ['enrichment'], | ||
| bgColor: '#9333EA', | ||
| icon: EnrichmentIcon, | ||
|
|
||
| subBlocks: [ | ||
| { | ||
| id: 'operation', | ||
| title: 'Enrichment', | ||
| type: 'dropdown', | ||
| options: ALL_ENRICHMENTS.map((e) => ({ label: e.name, id: e.id })), | ||
| value: () => ALL_ENRICHMENTS[0]?.id ?? '', | ||
| }, | ||
| ...inputSubBlocks, | ||
| ], | ||
|
|
||
| tools: { | ||
| access: ['enrichment_run'], | ||
| config: { | ||
| tool: () => 'enrichment_run', | ||
| params: (params) => { | ||
| const enrichment = getEnrichment(params.operation) | ||
| const inputs: Record<string, unknown> = {} | ||
| if (enrichment) { | ||
| for (const input of enrichment.inputs) { | ||
| const value = params[inputFieldId(enrichment.id, input.id)] | ||
| if (value !== undefined && value !== '') inputs[input.id] = value | ||
| } | ||
| } | ||
| return { enrichmentId: params.operation, inputs } | ||
| }, | ||
| }, | ||
| }, | ||
|
|
||
| inputs: blockInputs, | ||
| outputs: blockOutputs, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { z } from 'zod' | ||
| import { defineRouteContract } from '@/lib/api/contracts/types' | ||
|
|
||
| export const runEnrichmentBodySchema = z.object({ | ||
| enrichmentId: z.string().min(1, 'enrichmentId is required'), | ||
| /** Per-enrichment input map: enrichment input id → mapped value. */ | ||
| inputs: z.record(z.string(), z.unknown()).default({}), | ||
| workspaceId: z.string().min(1, 'workspaceId is required'), | ||
| }) | ||
|
|
||
| const runEnrichmentResponseSchema = z.object({ | ||
| matched: z.boolean(), | ||
| // untyped-response: per-enrichment output map — keys and value types vary by enrichment | ||
| result: z.record(z.string(), z.unknown()), | ||
| cost: z.number(), | ||
| error: z.string().nullable(), | ||
| /** Label of the provider whose result was returned, null on no match. */ | ||
| provider: z.string().nullable(), | ||
| }) | ||
|
|
||
| export const runEnrichmentContract = defineRouteContract({ | ||
| method: 'POST', | ||
| path: '/api/tools/enrichment/run', | ||
| body: runEnrichmentBodySchema, | ||
| response: { mode: 'json', schema: runEnrichmentResponseSchema }, | ||
| }) | ||
|
|
||
| export type RunEnrichmentBody = z.input<typeof runEnrichmentBodySchema> | ||
| export type RunEnrichmentResponse = z.output<typeof runEnrichmentResponseSchema> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { enrichmentRunTool } from './run' |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
matchedflag is computed fromObject.keys(result).length > 0, but the cascade'shasResultguard (inrun.ts) requires at least one non-null, non-empty value before accepting a provider result. The two checks are currently equivalent in practice (the cascade guaranteesresultis either{}or has at least one non-empty value), but aligning the route to the same guard makes the intent explicit and prevents drift if the cascade logic changes.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!