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

ArtifactStorage options for JS SDK (untested) #100

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
2,613 changes: 2,289 additions & 324 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
}
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/firebase": "^3.2.1",
"@types/redis": "^4.0.11",
"@openapitools/openapi-generator-cli": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"eslint": "^8.45.0",
Expand All @@ -23,6 +26,15 @@
"prettier-plugin-tailwindcss": "^0.5.11",
"typescript": "^5.1.6"
},
"optionalDependencies": {
"@azure/storage-blob": "^12.17.0",
"@google-cloud/storage": "^7.7.0",
"@supabase/supabase-js": "^2.39.3",
"aws-sdk": "^2.1555.0",
"firebase": "^10.8.0",
"redis": "^4.6.13",
"simple-git": "^3.22.0"
},
"dependencies": {
"express": "^4.18.2",
"express-openapi-validator": "^5.1.5",
Expand Down
9 changes: 8 additions & 1 deletion packages/sdk/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Agent, {
type StepResult,
type TaskInput,
} from 'agent-protocol'
// import S3Storage from 'agent-protocol/storage/S3Storage'

async function taskHandler(taskInput: TaskInput | null): Promise<StepHandler> {
console.log(`task: ${taskInput}`)
Expand All @@ -35,7 +36,13 @@ async function taskHandler(taskInput: TaskInput | null): Promise<StepHandler> {
return stepHandler
}

Agent.handleTask(taskHandler, {}).start()
const config = {
// port: 8000,
// workspace: './workspace',
// Defaults to FileStorage, but other options are available, see also ArtifactStorageFactory
// artifactStorage: new S3Storage(s3, 'my-agent-artifacts'),
}
Agent.handleTask(taskHandler, config).start()
```

See the [https://github.com/AI-Engineer-Foundation/agent-protocol/tree/main/packages/sdk/js/examples](examples folder) for running in serverless environments.
Expand Down
104 changes: 58 additions & 46 deletions packages/sdk/js/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { v4 as uuid } from 'uuid'
import * as fs from 'fs'
import * as path from 'path'

import {
type TaskInput,
Expand All @@ -18,9 +16,13 @@ import {
createApi,
type ApiConfig,
type RouteRegisterFn,
type RouteContext,
} from './api'
import { type Router, type Express } from 'express'
import { FileStorage, type ArtifactStorage } from './artifacts'

export interface RouteContext {
agent: Agent
}

/**
* A function that handles a step in a task.
Expand Down Expand Up @@ -337,39 +339,16 @@ const registerGetArtifacts: RouteRegisterFn = (router: Router) => {
})
}

/**
* Get path of an artifact associated to a task
* @param taskId Task associated with artifact
* @param artifact Artifact associated with the path returned
* @returns Absolute path of the artifact
*/
export const getArtifactPath = (
taskId: string,
workspace: string,
artifact: Artifact
): string => {
const rootDir = path.isAbsolute(workspace)
? workspace
: path.join(process.cwd(), workspace)

return path.join(
rootDir,
taskId,
artifact.relative_path ?? '',
artifact.file_name
)
}

/**
* Creates an artifact for a task
* @param task Task associated with new artifact
* @param file File that will be added as artifact
* @param relativePath Relative path where the artifact might be stored. Can be undefined
*/
export const createArtifact = async (
workspace: string,
task: Task,
file: any,
agent: Agent,
file: Express.Multer.File,
relativePath?: string
): Promise<Artifact> => {
const artifactId = uuid()
Expand All @@ -386,16 +365,16 @@ export const createArtifact = async (
: []
task.artifacts.push(artifact)

const artifactFolderPath = getArtifactPath(task.task_id, workspace, artifact)

// Save file to server's file system
fs.mkdirSync(path.join(artifactFolderPath, '..'), { recursive: true })
fs.writeFileSync(artifactFolderPath, file.buffer)
// Save the file
const [storage, workspace] = agent.getArtifactStorageAndWorkspace(
task.task_id
)
await storage.writeArtifact(task.task_id, workspace, artifact, file)
return artifact
}
const registerCreateArtifact: RouteRegisterFn = (
router: Router,
context: RouteContext
agent: Agent
) => {
router.post('/agent/tasks/:task_id/artifacts', (req, res) => {
void (async () => {
Expand All @@ -414,13 +393,18 @@ const registerCreateArtifact: RouteRegisterFn = (

const files = req.files as Express.Multer.File[]
const file = files.find(({ fieldname }) => fieldname === 'file')
const artifact = await createArtifact(
context.workspace,
task[0],
file,
relativePath
)
res.status(200).json(artifact)

if (file == null) {
res.status(400).json({ message: 'No file found in the request' })
} else {
const artifact = await createArtifact(
task[0],
agent,
file,
relativePath
)
res.status(200).json(artifact)
}
} catch (err: Error | any) {
console.error(err)
res.status(500).json({ error: err.message })
Expand Down Expand Up @@ -450,17 +434,19 @@ export const getTaskArtifact = async (
}
const registerGetTaskArtifact: RouteRegisterFn = (
router: Router,
context: RouteContext
agent: Agent
) => {
router.get('/agent/tasks/:task_id/artifacts/:artifact_id', (req, res) => {
void (async () => {
const taskId = req.params.task_id
const artifactId = req.params.artifact_id
try {
const artifact = await getTaskArtifact(taskId, artifactId)
const artifactPath = getArtifactPath(
const [storage, workspace] =
agent.getArtifactStorageAndWorkspace(taskId)
const artifactPath = storage.getArtifactPath(
taskId,
context.workspace,
workspace,
artifact
)
res.status(200).sendFile(artifactPath)
Expand All @@ -475,18 +461,26 @@ const registerGetTaskArtifact: RouteRegisterFn = (
export interface AgentConfig {
port: number
workspace: string
artifactStorage: ArtifactStorage
}

export const defaultAgentConfig: AgentConfig = {
port: 8000,
workspace: './workspace',
artifactStorage: new FileStorage(),
}

export class Agent {
private workspace: string
private artifactStorage: ArtifactStorage

constructor(
public taskHandler: TaskHandler,
public config: AgentConfig
) {}
) {
this.artifactStorage = config.artifactStorage
this.workspace = config.workspace
}

static handleTask(
_taskHandler: TaskHandler,
Expand All @@ -496,6 +490,8 @@ export class Agent {
return new Agent(_taskHandler, {
workspace: config.workspace ?? defaultAgentConfig.workspace,
port: config.port ?? defaultAgentConfig.port,
artifactStorage:
config.artifactStorage ?? defaultAgentConfig.artifactStorage,
})
}

Expand Down Expand Up @@ -527,8 +523,24 @@ export class Agent {
console.log(`Agent listening at http://localhost:${this.config.port}`)
},
context: {
workspace: this.config.workspace,
agent: this,
},
}
}

/**
* @param taskId (potentially) POST /agent/tasks { additional_input } could configure the artifactStorage and/or workspace for a Task
*/
getArtifactStorageAndWorkspace(taskId: string): [ArtifactStorage, string] {
return [this.artifactStorage, this.workspace]
}

/** It's easier for Serverless apps to configure artifactStorage after the app has been created */
setArtifactStorage(storage: ArtifactStorage): void {
this.artifactStorage = storage
}

setWorkspace(workspace: string): void {
this.workspace = workspace
}
}
9 changes: 3 additions & 6 deletions packages/sdk/js/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import express, { type Express, Router } from 'express' // <-- Import Router
import type * as core from 'express-serve-static-core'

import spec from '../../../../schemas/openapi.yml'
import { type Agent, type RouteContext } from './agent'

export type ApiApp = core.Express

export interface RouteContext {
workspace: string
}

export type RouteRegisterFn = (app: Router, context: RouteContext) => void
export type RouteRegisterFn = (app: Router, agent: Agent) => void

export interface ApiConfig {
context: RouteContext
Expand Down Expand Up @@ -44,7 +41,7 @@ export const createApi = (config: ApiConfig, start = true): Express => {
const router = Router()

config.routes.forEach((route) => {
route(router, config.context)
route(router, config.context.agent)
})

app.use('/ap/v1', router)
Expand Down
49 changes: 49 additions & 0 deletions packages/sdk/js/src/artifacts/ArtifactStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import path from 'path'
import { type Artifact } from '../models'

/**
* @see ArtifactStorageFactory
*/
export default abstract class ArtifactStorage {
/**
* Save an artifact associated to a task
* @param taskId Task associated with artifact
* @param artifact Artifact associated with the path returned
* @returns Absolute path of the artifact
*/
async writeArtifact(
taskId: string,
workspace: string,
artifact: Artifact,
file: Express.Multer.File
): Promise<void> {
const artifactFolderPath = this.getArtifactPath(taskId, workspace, artifact)
await this.saveFile(artifactFolderPath, file.buffer)
}

/**
* Get path of an artifact associated to a task
* @param taskId Task associated with artifact
* @param workspace The path to the workspace, defaults to './workspace'
* @param artifact Artifact associated with the path returned
* @returns Absolute path of the artifact
*/
getArtifactPath(
taskId: string,
workspace: string,
artifact: Artifact
): string {
const rootDir = path.isAbsolute(workspace)
? workspace
: path.join(process.cwd(), workspace)

return path.join(
rootDir,
taskId,
artifact.relative_path ?? '',
artifact.file_name
)
}

protected async saveFile(artifactPath: string, data: Buffer): Promise<void> {}
}
Loading
Loading