Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .scripts/list-of-samples.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"activities-cancellation-heartbeating",
"activities-dependency-injection",
"activities-examples",
"ai-sdk",
"child-workflows",
"continue-as-new",
"cron-workflows",
Expand Down
3 changes: 3 additions & 0 deletions ai-sdk/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
lib
.eslintrc.js
48 changes: 48 additions & 0 deletions ai-sdk/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { builtinModules } = require('module');

const ALLOWED_NODE_BUILTINS = new Set(['assert']);

module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'deprecation'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
// recommended for safety
'@typescript-eslint/no-floating-promises': 'error', // forgetting to await Activities and Workflow APIs is bad
'deprecation/deprecation': 'warn',

// code style preference
'object-shorthand': ['error', 'always'],

// relaxed rules, for convenience
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off',
},
overrides: [
{
files: ['src/workflows.ts', 'src/workflows-*.ts', 'src/workflows/*.ts'],
rules: {
'no-restricted-imports': [
'error',
...builtinModules.filter((m) => !ALLOWED_NODE_BUILTINS.has(m)).flatMap((m) => [m, `node:${m}`]),
],
},
},
],
};
2 changes: 2 additions & 0 deletions ai-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
lib
node_modules
1 change: 1 addition & 0 deletions ai-sdk/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions ai-sdk/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
18 changes: 18 additions & 0 deletions ai-sdk/.post-create
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
To begin development, install the Temporal CLI:

Mac: {cyan brew install temporal}
Other: Download and extract the latest release from https://github.com/temporalio/cli/releases/latest

Start Temporal Server:

{cyan temporal server start-dev}

Use Node version 18+ (v22.x is recommended):

Mac: {cyan brew install node@22}
Other: https://nodejs.org/en/download/

Then, in the project directory, using two other shells, run these commands:

{cyan npm run start.watch}
{cyan npm run workflow}
1 change: 1 addition & 0 deletions ai-sdk/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib
2 changes: 2 additions & 0 deletions ai-sdk/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
printWidth: 120
singleQuote: true
17 changes: 17 additions & 0 deletions ai-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# AI Sdk

This project demonstrates some uses of the AI SDK inside Temporal.

### Setup

1. `temporal server start-dev` to start [Temporal Server](https://github.com/temporalio/cli/#installation).
1. `npm install` to install dependencies.
1. `export OPENAI_API_KEY=<KEY>`
1. `npm run start.watch` to start the Worker.

### Run the samples

1. `npm run workflow haiku`
1. `npm run workflow tools`
1. `npm run workflow mcp`
1. `npm run workflow middleware`
56 changes: 56 additions & 0 deletions ai-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "temporal-hello-world",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc --build",
"build.watch": "tsc --build --watch",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"start": "ts-node src/worker.ts",
"start.watch": "nodemon src/worker.ts",
"workflow": "ts-node src/client.ts",
"test": "mocha --exit --require ts-node/register --require source-map-support/register src/mocha/*.test.ts"
},
"nodemonConfig": {
"execMap": {
"ts": "ts-node"
},
"ext": "ts",
"watch": [
"src"
]
},
"dependencies": {
"@ai-sdk/openai": "^2.0.28",
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/mcp": "^0.0.8",
"@modelcontextprotocol/sdk": "^1.10.2",
"@temporalio/activity": "^1.14.0",
"@temporalio/ai-sdk": "^1.14.0",
"@temporalio/client": "^1.14.0",
"@temporalio/envconfig": "^1.14.0",
"@temporalio/worker": "^1.14.0",
"@temporalio/workflow": "^1.14.0",
"ai": "^5.0.91",
"nanoid": "3.x",
"zod": "^3.25.76"
},
"devDependencies": {
"@temporalio/testing": "^1.14.0",
"@tsconfig/node18": "^18.2.4",
"@types/mocha": "8.x",
"@types/node": "^22.9.1",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"mocha": "8.x",
"nodemon": "^3.1.7",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}
12 changes: 12 additions & 0 deletions ai-sdk/src/activities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @@@SNIPSTART typescript-vercel-ai-sdk-weather-activity
export async function getWeather(input: {
location: string;
}): Promise<{ city: string; temperatureRange: string; conditions: string }> {
console.log('Activity execution');
return {
city: input.location,
temperatureRange: '14-20C',
conditions: 'Sunny with wind.',
};
}
// @@@SNIPEND
57 changes: 57 additions & 0 deletions ai-sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Connection, Client } from '@temporalio/client';
import { loadClientConnectConfig } from '@temporalio/envconfig';
import { haikuAgent, mcpAgent, middlewareAgent, toolsAgent } from './workflows';
import { nanoid } from 'nanoid';

async function run() {
const args = process.argv;
const workflow = args[2] ?? 'haiku';
console.log(`Running ${workflow}`);

const config = loadClientConnectConfig();
const connection = await Connection.connect(config.connectionOptions);
const client = new Client({ connection });

let handle;
switch (workflow) {
case 'middleware':
handle = await client.workflow.start(middlewareAgent, {
taskQueue: 'ai-sdk',
args: ['Middleware'],
workflowId: 'workflow-' + nanoid(),
});
break;
case 'mcp':
handle = await client.workflow.start(mcpAgent, {
taskQueue: 'ai-sdk',
args: ['Tell me about lickitung.'],
workflowId: 'workflow-' + nanoid(),
});
break;
case 'tools':
handle = await client.workflow.start(toolsAgent, {
taskQueue: 'ai-sdk',
args: ['What is the weather in Tokyo?'],
workflowId: 'workflow-' + nanoid(),
});
break;
case 'haiku':
handle = await client.workflow.start(haikuAgent, {
taskQueue: 'ai-sdk',
args: ['Temporal'],
workflowId: 'workflow-' + nanoid(),
});
break;
default:
throw new Error('Unknown workflow type: ' + workflow);
}
console.log(`Started workflow ${handle.workflowId}`);

// optional: wait for workflow result
console.log(await handle.result()); // Hello, Temporal!
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
87 changes: 87 additions & 0 deletions ai-sdk/src/mcp-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const POKE_API_BASE = 'https://pokeapi.co/api/v2';

const server = new McpServer({
name: 'pokemon',
version: '1.0.0',
});

server.registerTool(
'get-pokemon',
{
description: 'Get Pokemon details by name',
inputSchema: {
name: z.string(),
},
},
async ({ name }) => {
const path = `/pokemon/${name.toLowerCase()}`;
const pokemon = await makePokeApiRequest<Pokemon>(path);

if (!pokemon) {
return {
content: [
{
type: 'text',
text: 'Failed to retrieve Pokemon data',
},
],
};
}

return {
content: [
{
type: 'text',
text: formatPokemonData(pokemon),
},
],
};
},
);

async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.log('Pokemon MCP Server running on stdio');
}

main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});

interface PokemonAbility {
id: string;
name: string;
}

interface Pokemon {
id: string;
name: string;
abilities: { ability: PokemonAbility }[];
}

async function makePokeApiRequest<T>(path: string): Promise<T | null> {
try {
const url = `${POKE_API_BASE}${path}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP Error Status: ${response.status}`);
}
return (await response.json()) as T;
} catch (error) {
console.error('[ERROR] Failed to make PokeAPI request:', error);
return null;
}
}

function formatPokemonData(pokemon: Pokemon) {
return [
`Name: ${pokemon.name}`,
`Abilities: ${pokemon.abilities.map((ability) => ability.ability.name).join(', ')}`,
].join('\n');
}
16 changes: 16 additions & 0 deletions ai-sdk/src/mocha/activities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MockActivityEnvironment } from '@temporalio/testing';
import { describe, it } from 'mocha';
import * as activities from '../activities';
import assert from 'assert';

describe('greet activity', async () => {
it('successfully greets the user', async () => {
const env = new MockActivityEnvironment();
const result = await env.run(activities.getWeather, { location: 'Tokyo' });
assert.equal(result, {
city: 'Tokyo',
temperatureRange: '14-20C',
conditions: 'Sunny with wind.',
});
});
});
37 changes: 37 additions & 0 deletions ai-sdk/src/mocha/workflows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { before, describe, it } from 'mocha';
import { Worker } from '@temporalio/worker';
import { haikuAgent } from '../workflows';
import * as activities from '../activities';

describe('Example workflow', () => {
let testEnv: TestWorkflowEnvironment;

before(async () => {
testEnv = await TestWorkflowEnvironment.createLocal();
});

after(async () => {
await testEnv?.teardown();
});

it('successfully completes the Workflow', async () => {
const { client, nativeConnection } = testEnv;
const taskQueue = 'test';

const worker = await Worker.create({
connection: nativeConnection,
taskQueue,
workflowsPath: require.resolve('../workflows'),
activities,
});

const result = await worker.runUntil(
client.workflow.execute(haikuAgent, {
args: ['Temporal'],
workflowId: 'test',
taskQueue,
}),
);
});
});
Loading