-
Notifications
You must be signed in to change notification settings - Fork 142
Integration with AI SDK #1792
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
Open
tconley1428
wants to merge
36
commits into
main
Choose a base branch
from
ai/initial
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,611
−264
Open
Integration with AI SDK #1792
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
bcb181b
Initial WIP on ai-sdk integration
tconley1428 a04854e
Tools and a basic hello workflow now work
tconley1428 925bd9d
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 d45be90
Leveraging plugin for AI SDK integration
tconley1428 c7af3f3
Some changes
tconley1428 d334684
Add experimental telemetry validation
tconley1428 a285bcc
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 90c745a
Remove AI markdown
tconley1428 fe533f2
Linting and project structure
tconley1428 34fc1ca
Update dependencies
tconley1428 c01f8bb
Linting and project structure
tconley1428 a330d99
Fix build
tconley1428 21bc8d5
Linting
tconley1428 181727c
Docstrings
tconley1428 e7faeed
Merge branch 'main' into ai/initial
tconley1428 844be62
Clean up
tconley1428 843b112
Fix core version
tconley1428 839dabb
Revert lock changes to minimum
tconley1428 89948bd
Adding MCP and activity config support
tconley1428 fd9fbbc
Linting
tconley1428 bd5d9be
Fix error suppression location after lint
tconley1428 613f32e
Test fix of the fetch-esm CI issue
mjameswh 022d814
Bump GHA mac runners to macos15
mjameswh a83c35d
Try using latest mcp server
tconley1428 bae6997
Skip MCP test for now
tconley1428 5a89bd4
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 5396f36
:wqMerge remote-tracking branch 'origin/main' into ai/initial
tconley1428 fafb2ac
Fix pnpm rebase
tconley1428 4a66eb1
PR Feedback
tconley1428 e3f4d70
Update lockfile
tconley1428 78eec0e
PR feedback and multiple MCP servers
tconley1428 fe5a1bb
PR feedback and multiple MCP servers
tconley1428 78db4a7
Merge remote-tracking branch 'origin/main' into ai/initial
tconley1428 d962d74
Lint
tconley1428 26e4753
Fix merge
tconley1428 9763e56
Fix merge
tconley1428 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
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,47 @@ | ||
| { | ||
| "name": "@temporalio/ai-sdk", | ||
| "version": "1.13.2", | ||
| "description": "Temporal AI SDK integration package", | ||
| "main": "lib/index.js", | ||
| "types": "./lib/index.d.ts", | ||
| "keywords": [ | ||
| "temporal", | ||
| "workflow", | ||
| "ai", | ||
| "ai-sdk", | ||
| "llm" | ||
| ], | ||
| "author": "Temporal Technologies Inc. <sdk@temporal.io>", | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "@temporalio/plugin": "workspace:*", | ||
| "@temporalio/workflow": "workspace:*", | ||
| "@ungap/structured-clone": "^1.3.0", | ||
| "headers-polyfill": "^4.0.3", | ||
| "web-streams-polyfill": "^4.2.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "@ai-sdk/provider": "^2.0.0", | ||
| "@ai-sdk/mcp": "^0.0.8", | ||
| "ai": "^5.0.91" | ||
| }, | ||
| "engines": { | ||
| "node": ">= 18.0.0" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/temporalio/sdk-typescript/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/temporalio/sdk-typescript.git", | ||
| "directory": "packages/ai-sdk" | ||
| }, | ||
| "homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/ai-sdk", | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "files": [ | ||
| "src", | ||
| "lib" | ||
| ] | ||
| } |
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,103 @@ | ||
| import { | ||
tconley1428 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| LanguageModelV2CallOptions, | ||
| LanguageModelV2CallWarning, | ||
| LanguageModelV2Content, | ||
| LanguageModelV2FinishReason, | ||
| LanguageModelV2ResponseMetadata, | ||
| LanguageModelV2Usage, | ||
| ProviderV2, | ||
| SharedV2Headers, | ||
| SharedV2ProviderMetadata, | ||
| } from '@ai-sdk/provider'; | ||
| import type { ToolCallOptions } from 'ai'; | ||
| import type { McpClientFactory } from './mcp'; | ||
|
|
||
| export interface InvokeModelArgs { | ||
| modelId: string; | ||
| options: LanguageModelV2CallOptions; | ||
| } | ||
|
|
||
| export interface InvokeModelResult { | ||
| content: Array<LanguageModelV2Content>; | ||
| finishReason: LanguageModelV2FinishReason; | ||
| usage: LanguageModelV2Usage; | ||
| providerMetadata?: SharedV2ProviderMetadata; | ||
| request?: { body?: unknown }; | ||
| response?: LanguageModelV2ResponseMetadata & { headers?: SharedV2Headers; body?: unknown }; | ||
| warnings: Array<LanguageModelV2CallWarning>; | ||
| } | ||
|
|
||
| export interface ListToolResult { | ||
| description?: string; | ||
| inputSchema: any; | ||
| } | ||
|
|
||
| export interface ListToolArgs { | ||
| clientArgs?: any; | ||
| } | ||
|
|
||
| export interface CallToolArgs { | ||
| clientArgs?: any; | ||
| name: string; | ||
| args: any; | ||
| options: ToolCallOptions; | ||
| } | ||
|
|
||
| /** | ||
| * Creates Temporal activities for AI model invocation using the provided AI SDK provider. | ||
| * These activities allow workflows to call AI models while maintaining Temporal's | ||
| * execution guarantees and replay safety. | ||
| * | ||
| * @param provider The AI SDK provider to use for model invocations | ||
| * @param mcpClientFactories A mapping of server names to functions to create mcp clients | ||
| * @returns An object containing the activity functions | ||
| * | ||
| * @experimental The AI SDK integration is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export const createActivities = (provider: ProviderV2, mcpClientFactories?: [string, McpClientFactory][]): object => { | ||
| let activities = { | ||
| async invokeModel(args: InvokeModelArgs): Promise<InvokeModelResult> { | ||
| const model = provider.languageModel(args.modelId); | ||
| return await model.doGenerate(args.options); | ||
| }, | ||
| }; | ||
| if (mcpClientFactories !== undefined) { | ||
| mcpClientFactories.forEach(([name, func]) => { | ||
| activities = { | ||
| ...activities, | ||
| ...activitiesForName(name, func), | ||
| }; | ||
| }); | ||
| } | ||
| return activities; | ||
| }; | ||
|
|
||
| const activitiesForName = (name: string, mcpClientFactory: McpClientFactory): object => { | ||
| const listToolsActivity = async (args: ListToolArgs): Promise<Record<string, ListToolResult>> => { | ||
| const mcpClient = await mcpClientFactory(args.clientArgs); | ||
| const tools = await mcpClient.tools(); | ||
|
|
||
| return Object.fromEntries( | ||
| Object.entries(tools).map(([k, v]) => [ | ||
| k, | ||
| { | ||
| description: v.description, | ||
| inputSchema: v.inputSchema, | ||
| }, | ||
| ]) | ||
| ); | ||
| }; | ||
| const callToolActivity = async (args: CallToolArgs): Promise<any> => { | ||
| const mcpClient = await mcpClientFactory(args.clientArgs); | ||
| const tools = await mcpClient.tools(); | ||
| const tool = tools[args.name]; | ||
| if (tool === undefined) { | ||
| throw new Error(`Tool ${args.name} not found.`); | ||
| } | ||
| return tool.execute(args.args, args.options); | ||
| }; | ||
| return { | ||
| [name + '-listTools']: listToolsActivity, | ||
| [name + '-callTool']: callToolActivity, | ||
| }; | ||
| }; | ||
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,6 @@ | ||
| // eslint-disable-next-line import/no-unassigned-import | ||
| import './load-polyfills'; | ||
|
|
||
| export * from './mcp'; | ||
| export * from './plugin'; | ||
| export * from './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,18 @@ | ||
| import { Headers } from 'headers-polyfill'; | ||
| import { inWorkflowContext } from '@temporalio/workflow'; | ||
|
|
||
| if (inWorkflowContext()) { | ||
| // Apply Headers polyfill | ||
| if (typeof globalThis.Headers === 'undefined') { | ||
| globalThis.Headers = Headers; | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-require-imports,import/no-unassigned-import | ||
| require('web-streams-polyfill/polyfill'); | ||
| // Attach the polyfill as a Global function | ||
| if (!('structuredClone' in globalThis)) { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports,import/no-unassigned-import | ||
| const structuredClone = require('@ungap/structured-clone'); | ||
| globalThis.structuredClone = structuredClone.default; | ||
| } | ||
| } |
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,68 @@ | ||
| import type { ToolSet } from 'ai'; | ||
| import type { experimental_MCPClient as MCPClient } from '@ai-sdk/mcp'; | ||
| import * as workflow from '@temporalio/workflow'; | ||
| import type { ActivityOptions } from '@temporalio/workflow'; | ||
| import type { ListToolResult } from './activities'; | ||
|
|
||
| export type McpClientFactory = (args: unknown) => Promise<MCPClient>; | ||
|
|
||
| /** | ||
| * Options for the Temporal MCP Client | ||
| * | ||
| * @experimental The AI SDK plugin is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export interface TemporalMCPClientOptions { | ||
| readonly name: string; | ||
| // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types | ||
| readonly clientArgs?: any; | ||
| readonly activityOptions?: ActivityOptions; | ||
| } | ||
|
|
||
| /** | ||
| * A Temporal MCP Client which uses activities to execute list tools and call tools. | ||
| * This should match by name an MCPClientFactory registered with the AI SDK plugin to function. | ||
| * | ||
| * @experimental The AI SDK plugin is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export class TemporalMCPClient { | ||
| constructor(readonly options: TemporalMCPClientOptions) {} | ||
|
|
||
| async tools(): Promise<ToolSet> { | ||
| workflow.log.info(`Options: ${this.options.activityOptions}`); | ||
| const activities = workflow.proxyActivities({ startToCloseTimeout: '10 minutes', ...this.options.activityOptions }); | ||
|
|
||
| const listActivity = activities[this.options.name + '-listTools'] | ||
| if (listActivity === undefined) { | ||
| throw new Error(`List Tools activity could not be found for mcp client ${this.options.name}. Has it been registered in the plugin?`) | ||
| } | ||
| const tools: Record<string, ListToolResult> = await listActivity({ clientArgs: this.options.clientArgs }); | ||
| return Object.fromEntries( | ||
| Object.entries(tools).map(([toolName, toolResult]) => [ | ||
| toolName, | ||
| { | ||
| execute: async (args: any, options) => { | ||
| const activities = workflow | ||
| .proxyActivities({ | ||
| summary: toolName, | ||
| startToCloseTimeout: '10 minutes', | ||
| ...this.options, | ||
| }); | ||
| const callActivity = activities[this.options.name + '-callTool']; | ||
| if (callActivity === undefined) { | ||
| throw new Error(`Call Tool activity could not be found for mcp client ${this.options.name}. Has it been registered in the plugin?`) | ||
| } | ||
| return await callActivity({ name: toolName, args, options, clientArgs: this.options.clientArgs }); | ||
| }, | ||
| inputSchema: { | ||
| ...toolResult.inputSchema, | ||
| _type: undefined, | ||
| validate: undefined, | ||
| [Symbol.for('vercel.ai.schema')]: true, | ||
| [Symbol.for('vercel.ai.validator')]: true, | ||
| }, | ||
| type: 'dynamic', | ||
| }, | ||
| ]) | ||
| ); | ||
| } | ||
| } |
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 type { ProviderV2 } from '@ai-sdk/provider'; | ||
| import { SimplePlugin } from '@temporalio/plugin'; | ||
| import { createActivities } from './activities'; | ||
| import type { McpClientFactory } from './mcp'; | ||
|
|
||
| /** | ||
| * Options for the AI SDK plugin | ||
| * | ||
| * @experimental The AI SDK plugin is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export interface AiSdkPluginOptions { | ||
| modelProvider: ProviderV2; | ||
| mcpClientFactories?: [string, McpClientFactory][]; | ||
| } | ||
|
|
||
| /** | ||
| * A Temporal plugin that integrates AI SDK providers for use in workflows. | ||
| * This plugin creates activities that allow workflows to invoke AI models. | ||
| * | ||
| * @experimental The AI SDK plugin is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export class AiSdkPlugin extends SimplePlugin { | ||
| constructor(options: AiSdkPluginOptions) { | ||
| super({ | ||
| name: 'AiSDKPlugin', | ||
| activities: createActivities(options.modelProvider, options.mcpClientFactories), | ||
| }); | ||
| } | ||
| } |
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,81 @@ | ||
| import type { | ||
| EmbeddingModelV2, | ||
| ImageModelV2, | ||
| LanguageModelV2, | ||
| LanguageModelV2CallOptions, | ||
| ProviderV2, | ||
| SharedV2Headers, | ||
| } from '@ai-sdk/provider'; | ||
| import * as workflow from '@temporalio/workflow'; | ||
| import { ActivityOptions } from '@temporalio/workflow'; | ||
| import { InvokeModelResult } from './activities'; | ||
|
|
||
| /** | ||
| * A language model implementation that delegates AI model calls to Temporal activities. | ||
| * This allows workflows to invoke AI models through the Temporal execution model. | ||
| * | ||
| * @experimental The AI SDK integration is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export class TemporalLanguageModel implements LanguageModelV2 { | ||
| readonly specificationVersion = 'v2'; | ||
| readonly provider = 'temporal'; | ||
| readonly supportedUrls = {}; | ||
|
|
||
| constructor( | ||
| readonly modelId: string, | ||
| readonly options?: ActivityOptions | ||
| ) {} | ||
|
|
||
| async doGenerate(options: LanguageModelV2CallOptions): Promise<InvokeModelResult> { | ||
| const activities = workflow.proxyActivities({ startToCloseTimeout: '10 minutes', ...this.options }); | ||
| if (activities.invokeModel === undefined) { | ||
| throw new Error("Unable to find model activity. Is the plugin registered?") | ||
| } | ||
| const result = await activities.invokeModel({modelId: this.modelId, options}); | ||
| if (result === undefined) { | ||
| throw new Error('Received undefined response from model activity.'); | ||
| } | ||
| if (result.response !== undefined) { | ||
| result.response.timestamp = new Date(result.response.timestamp); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| doStream(_options: LanguageModelV2CallOptions): PromiseLike<{ | ||
| stream: any; | ||
| request?: { body?: unknown }; | ||
| response?: { headers?: SharedV2Headers }; | ||
| }> { | ||
| throw new Error('Streaming not supported.'); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A Temporal-specific provider implementation that creates AI models which execute | ||
| * through Temporal activities. This provider integrates AI SDK models with Temporal's | ||
| * execution model to ensure reliable, durable AI model invocations. | ||
| * | ||
| * @experimental The AI SDK integration is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export class TemporalProvider implements ProviderV2 { | ||
| constructor(readonly options?: ActivityOptions) {} | ||
|
|
||
| imageModel(_modelId: string): ImageModelV2 { | ||
| throw new Error('Not implemented'); | ||
| } | ||
|
|
||
| languageModel(modelId: string): LanguageModelV2 { | ||
| return new TemporalLanguageModel(modelId, this.options); | ||
| } | ||
|
|
||
| textEmbeddingModel(_modelId: string): EmbeddingModelV2<string> { | ||
| throw new Error('Not implemented'); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A singleton instance of TemporalProvider for convenient use in applications. | ||
| * | ||
| * @experimental The AI SDK integration is an experimental feature; APIs may change without notice. | ||
| */ | ||
| export const temporalProvider: TemporalProvider = new TemporalProvider(); |
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,9 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "compilerOptions": { | ||
| "outDir": "./lib", | ||
| "rootDir": "./src" | ||
| }, | ||
| "references": [{ "path": "../plugin" }, { "path": "../workflow" }], | ||
| "include": ["./src/**/*.ts"] | ||
| } |
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.
Uh oh!
There was an error while loading. Please reload this page.