Skip to content
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

wip #3821

Closed
wants to merge 66 commits into from
Closed

wip #3821

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
905a90f
scaffolding
lgrammel Nov 20, 2024
805057f
Merge branch 'main' into lg/server
lgrammel Nov 20, 2024
c82a19b
Merge remote-tracking branch 'origin/main' into lg/server
lgrammel Nov 21, 2024
f403a5a
add cli
lgrammel Nov 21, 2024
373d1e2
fx
lgrammel Nov 21, 2024
dc56b3d
server
lgrammel Nov 21, 2024
688d2ee
no test
lgrammel Nov 21, 2024
47b2b3a
build
lgrammel Nov 21, 2024
6ef7796
rename
lgrammel Nov 21, 2024
d91b49e
tweak
lgrammel Nov 21, 2024
f660aa0
tsup
lgrammel Nov 21, 2024
3eac6b9
clean
lgrammel Nov 21, 2024
6573b79
types
lgrammel Nov 21, 2024
1aed598
no module
lgrammel Nov 21, 2024
0cbf11f
start
lgrammel Nov 21, 2024
ec460f2
invoke
lgrammel Nov 21, 2024
57e9888
draft
lgrammel Nov 21, 2024
c4dde5d
cotnext
lgrammel Nov 21, 2024
a2c9178
parse
lgrammel Nov 21, 2024
ae8df53
jsdoc
lgrammel Nov 21, 2024
1e7bdb6
state
lgrammel Nov 21, 2024
353e55e
initial state
lgrammel Nov 21, 2024
a4433b5
doc
lgrammel Nov 21, 2024
451e414
refactor
lgrammel Nov 21, 2024
0f47560
Merge branch 'main' into lg/server
lgrammel Nov 22, 2024
00f30b1
pkg
lgrammel Nov 22, 2024
20c67a5
.agents
lgrammel Nov 22, 2024
d8e0dd5
refact
lgrammel Nov 22, 2024
ad1f16f
note
lgrammel Nov 22, 2024
a884e58
cache busting
lgrammel Nov 22, 2024
c957517
jobs
lgrammel Nov 22, 2024
b416275
Merge remote-tracking branch 'origin/main' into lg/server
lgrammel Nov 22, 2024
b2570f4
utils
lgrammel Nov 22, 2024
6560f54
run id
lgrammel Nov 22, 2024
3697acf
run manager
lgrammel Nov 22, 2024
575f762
imports
lgrammel Nov 22, 2024
8d49b88
tweak
lgrammel Nov 22, 2024
d6c6135
run id
lgrammel Nov 22, 2024
15b892d
datastore
lgrammel Nov 22, 2024
ada6e92
submit
lgrammel Nov 22, 2024
1b94791
job queue
lgrammel Nov 22, 2024
5701a76
comment
lgrammel Nov 22, 2024
4102545
notes
lgrammel Nov 22, 2024
ab523ae
extract, note
lgrammel Nov 22, 2024
86c3f0c
get run state
lgrammel Nov 22, 2024
07aabed
mv
lgrammel Nov 22, 2024
6e4bb1a
module loader
lgrammel Nov 22, 2024
6087414
load state module
lgrammel Nov 22, 2024
ef8905d
tweak
lgrammel Nov 22, 2024
d2f3fcd
streams
lgrammel Nov 22, 2024
90cf669
end correctly
lgrammel Nov 22, 2024
8acfe7b
header
lgrammel Nov 22, 2024
2abdf29
headers
lgrammel Nov 22, 2024
4002e69
routing
lgrammel Nov 22, 2024
9ab6070
tweak
lgrammel Nov 22, 2024
7a65b33
simplify
lgrammel Nov 22, 2024
01ea04b
update
lgrammel Nov 22, 2024
cc2cdd7
logs
lgrammel Nov 22, 2024
2fc58a0
fx
lgrammel Nov 22, 2024
b607840
notes
lgrammel Nov 22, 2024
1ef4ef0
example
lgrammel Nov 22, 2024
b02d4ce
toagentstream
lgrammel Nov 22, 2024
82675c2
note
lgrammel Nov 22, 2024
ea94244
simpler
lgrammel Nov 22, 2024
8292382
inline
lgrammel Nov 22, 2024
54eebf3
tweako
lgrammel Nov 22, 2024
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
17 changes: 17 additions & 0 deletions examples/agent-server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ANTHROPIC_API_KEY=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_REGION=""
AZURE_API_KEY=""
AZURE_RESOURCE_NAME=""
COHERE_API_KEY=""
FIREWORKS_API_KEY=""
GOOGLE_GENERATIVE_AI_API_KEY=""
GOOGLE_VERTEX_LOCATION=""
GOOGLE_VERTEX_PROJECT=""
GROQ_API_KEY=""
MISTRAL_API_KEY=""
OPENAI_API_KEY=""
PERPLEXITY_API_KEY=""
TOGETHER_AI_API_KEY=""
XAI_API_KEY=""
2 changes: 2 additions & 0 deletions examples/agent-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.agents
.data
9 changes: 9 additions & 0 deletions examples/agent-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Agent Server Examples

### Snippets

```sh
curl -X POST http://127.0.0.1:3000/agent/helloworld-completion/start \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello, world!"}'
```
24 changes: 24 additions & 0 deletions examples/agent-server/agents/helloworld-completion/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Agent } from '@ai-sdk/agent-server';
import { z } from 'zod';

// agent context is available to all states.
// it can be used to pass data between states.
export const contextSchema = z.object({ prompt: z.string() });
export type Context = z.infer<typeof contextSchema>;

export default {
// Explicit start method that receives the request, so that the user
// can perform any data validation, transformation, and loading
// that is required for their agent.
// The goal is to provide the initial context for the agent run.
async start({ request }) {
return {
context: contextSchema.parse(await request.json()),
initialState: 'main',
};
},

// Optional headers. Streams can be anything JSON-serializable,
// so we enable agents to set the headers that are needed.
headers: { 'X-Vercel-AI-Data-Stream': 'v1' },
} satisfies Agent<Context>;
19 changes: 19 additions & 0 deletions examples/agent-server/agents/helloworld-completion/states/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StreamState } from '@ai-sdk/agent-server';
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { Context } from '../agent';

export default {
type: 'stream', // state type, in the future there will be other types
async execute({ context }) {
const result = streamText({
model: openai('gpt-4o'),
prompt: context.prompt,
});

return {
stream: result.toAgentStream(),
nextState: 'END',
};
},
} satisfies StreamState<Context, string>;
18 changes: 18 additions & 0 deletions examples/agent-server/agents/routing-example/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Agent } from '@ai-sdk/agent-server';
import { z } from 'zod';

export const contextSchema = z.object({
prompt: z.string(),
});

export type Context = z.infer<typeof contextSchema>;

export default {
async start({ request }) {
return {
context: contextSchema.parse(await request.json()),
initialState: 'main',
};
},
headers: { 'X-Vercel-AI-Data-Stream': 'v1' },
} satisfies Agent<Context>;
20 changes: 20 additions & 0 deletions examples/agent-server/agents/routing-example/states/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StreamState } from '@ai-sdk/agent-server';
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { Context } from '../agent';

export default {
type: 'stream',
async execute({ context }) {
const result = streamText({
model: openai('gpt-4o'),
system: 'You are a friendly chatbot. Respond briefly and concisely.',
prompt: context.prompt,
});

return {
stream: result.toAgentStream(),
nextState: 'END',
};
},
} satisfies StreamState<Context, string>;
20 changes: 20 additions & 0 deletions examples/agent-server/agents/routing-example/states/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StreamState } from '@ai-sdk/agent-server';
import { StreamData } from 'ai';
import { Context } from '../agent';

export default {
type: 'stream',
async execute({ context }) {
const streamData = new StreamData();
streamData.append({ status: 'selecting route' });
streamData.close();

return {
// will be simplified to streamData.toAgentStream() in the future
stream: streamData.stream.pipeThrough(new TextDecoderStream()),
nextState: context.prompt.toLowerCase().includes('write')
? 'write-blog'
: 'chat',
};
},
} satisfies StreamState<Context, string>;
23 changes: 23 additions & 0 deletions examples/agent-server/agents/routing-example/states/write-blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StreamState } from '@ai-sdk/agent-server';
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { Context } from '../agent';

export default {
type: 'stream',
async execute({ context }) {
const result = streamText({
model: openai('gpt-4o'),
system:
'You are an outstanding writer. ' +
'Write a blog post. ' +
'The blog post MUST BE at least 4 paragraphs long. ',
prompt: context.prompt,
});

return {
stream: result.toAgentStream(),
nextState: 'END',
};
},
} satisfies StreamState<Context, string>;
20 changes: 20 additions & 0 deletions examples/agent-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "agent-server-examples",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"start": "pnpm agent-server --port 3000 --host 127.0.0.1"
},
"devDependencies": {
"@types/node": "22.9.1"
},
"dependencies": {
"@ai-sdk/agent-server": "0.0.0",
"@ai-sdk/openai": "1.0.4",
"ai": "4.0.3",
"zod": "3.23.8"
}
}
18 changes: 18 additions & 0 deletions examples/agent-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"declaration": true,
"sourceMap": true,
"target": "es2022",
"lib": ["es2022", "dom"],
"module": "esnext",
"types": ["node"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"rootDir": "./agents",
"outDir": "./.agents",
"skipLibCheck": true
},
"include": ["agents/**/*.ts"]
}
Empty file.
3 changes: 3 additions & 0 deletions packages/agent-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AI SDK Server

Experimental prototype. DO NOT USE.
70 changes: 70 additions & 0 deletions packages/agent-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@ai-sdk/agent-server",
"version": "0.0.0",
"license": "Apache-2.0",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**/*",
"CHANGELOG.md"
],
"bin": {
"agent-server": "./dist/agent-server.js"
},
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"clean": "rm -rf dist",
"lint": "eslint \"./**/*.ts*\"",
"type-check": "tsc --noEmit",
"prettier-check": "prettier --check \"./**/*.ts*\"",
"start": "pnpm tsx src/agent-server.ts --port 3000 --host 127.0.0.1",
"test": "pnpm test:node && pnpm test:edge",
"test:edge": "vitest --config vitest.edge.config.js --run --passWithNoTests",
"test:node": "vitest --config vitest.node.config.js --run --passWithNoTests"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"dependencies": {
"@ai-sdk/provider": "1.0.1",
"@ai-sdk/provider-utils": "2.0.2",
"@hono/node-server": "1.13.7",
"commander": "12.1.0",
"hono": "4.6.11",
"pino": "9.5.0",
"zod": "3.23.8",
"dotenv": "16.4.5"
},
"devDependencies": {
"@types/node": "^18",
"@vercel/ai-tsconfig": "workspace:*",
"tsup": "^8",
"tsx": "4.19.2",
"typescript": "5.6.3"
},
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public"
},
"homepage": "https://sdk.vercel.ai/docs",
"repository": {
"type": "git",
"url": "git+https://github.com/vercel/ai.git"
},
"bugs": {
"url": "https://github.com/vercel/ai/issues"
},
"keywords": [
"ai"
]
}
104 changes: 104 additions & 0 deletions packages/agent-server/src/agent-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env node

import { serve } from '@hono/node-server';
import { Option } from 'commander';
import { Hono } from 'hono';
import { logger as honoLogger } from 'hono/logger';
import { requestId } from 'hono/request-id';
import * as path from 'node:path';
import zod from 'zod';
import { DataStore } from './data-store';
import { ModuleLoader } from './module-loader';
import { RunManager } from './run-manager';
import { JobQueue } from './util/job-queue';
import { startService } from './util/start-service';
import { createWorker } from './worker';
import { StreamManager } from './stream-manager';
import { stream } from 'hono/streaming';
import 'dotenv/config';

startService({
name: '@ai-sdk/server',
configurationOptions: [
new Option('--port <number>', 'port number')
.env('PORT')
.argParser(value => +value)
.makeOptionMandatory(),
new Option('--host <string>', 'host name')
.env('HOST')
.makeOptionMandatory(),
],
configurationSchema: zod.object({
host: zod.string(),
port: zod.number(),
}),
async initialize({ host, port }, logger) {
const moduleLoader = new ModuleLoader({
modulePath: path.join(process.cwd(), '.agents'),
});
const jobs = new JobQueue<{ runId: string }>();
const submitJob = jobs.push.bind(jobs);
const dataStore = new DataStore({
dataPath: path.join(process.cwd(), '.data'),
});
const streamManager = new StreamManager();
const runManager = new RunManager({
dataStore,
moduleLoader,
submitJob,
streamManager,
logger,
});

// setup workers
// the workers run in the same process in this prototype,
// so if they perform CPU-bound tasks, they will block
// TODO multiple workers
jobs.startWorker(
createWorker({
dataStore,
moduleLoader,
streamManager,
submitJob,
logger,
}),
);

// Hono setup
const app = new Hono();
app.use(requestId());
app.use(
honoLogger((message, ...rest) => {
logger.info(message, ...rest);
}),
);

// routes setup
app.post('/agent/:agent/start', async c => {
const { runId, headers } = await runManager.startAgent({
agent: c.req.param('agent'),
request: c.req.raw,
});

const runStream = streamManager.getStream(runId);

// headers
c.header('X-Agent-Run-Id', runId);
Object.entries(headers ?? {}).forEach(([key, value]) => {
c.header(key, value);
});

return stream(c, stream =>
stream.pipe(runStream.pipeThrough(new TextEncoderStream())),
);
});

const server = serve({ fetch: app.fetch, hostname: host, port });

return {
async shutdown() {
server.close(); // wait for requests to be finished
},
};
},
});
Loading
Loading