From db7f05ec7264dc84849808c4f7b00cf2335aab93 Mon Sep 17 00:00:00 2001 From: Gregor Adams Date: Thu, 4 May 2023 18:39:48 +0200 Subject: [PATCH] feat: rework the API enables several defaults better config options --- README.md | 10 +- examples/book.ts | 149 +++++++++----------- examples/config.ts | 12 -- examples/dev-team.ts | 131 ----------------- examples/simple.ts | 10 ++ examples/translation.ts | 56 -------- package-lock.json | 30 ++-- package.json | 3 +- packages/core/package.json | 9 +- packages/core/src/agent.ts | 146 ++++++++++--------- packages/core/src/index.ts | 1 + packages/core/src/memory-adapter.ts | 45 ++++++ packages/core/src/types.ts | 39 +++-- packages/core/src/utils.ts | 60 +++++--- packages/openai/package.json | 17 ++- packages/openai/src/config.ts | 12 ++ packages/openai/src/dall-e-model-adapter.ts | 22 +-- packages/openai/src/gpt-model-adapter.ts | 65 +++++++-- packages/openai/src/types.ts | 14 +- packages/store/package.json | 7 +- packages/store/src/index.ts | 1 - packages/store/src/utils.ts | 24 ---- 22 files changed, 397 insertions(+), 466 deletions(-) delete mode 100644 examples/config.ts delete mode 100644 examples/dev-team.ts create mode 100644 examples/simple.ts delete mode 100644 examples/translation.ts create mode 100644 packages/core/src/memory-adapter.ts create mode 100644 packages/openai/src/config.ts delete mode 100644 packages/store/src/utils.ts diff --git a/README.md b/README.md index 0ac63ce..9602bba 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Hyv is a modular software development library centered on AI collaboration that ## Features 🌟 - 🚀 **Streamlined Task Management**: Hyv enhances your projects with efficient task distribution and coordination, simplifying resource utilization. -- 🧩 **Flexible Modular Design**: Hyv's modular architecture allows seamless integration of various tools, models, and adapters, providing a customizable solution. +- 🧩 **Flexible Modular Design**: Hyv's modular architecture allows seamless integration of various sideEffects, models, and adapters, providing a customizable solution. - 🌐 **Broad Compatibility**: Designed for various technologies, Hyv is a versatile option for developers working with diverse platforms and frameworks. - 📚 **Comprehensive Documentation**: Hyv includes detailed documentation and examples, aiding in understanding its features and effective implementation in projects. - 🌱 **Community-Driven**: Hyv is developed and maintained by a devoted community of developers, continually working to refine and extend its capabilities. @@ -23,7 +23,7 @@ npm install "@hyv/core" "@hyv/openai" "@hyv/store" ```typescript import process from "node:process"; -import { Agent, createInstruction, sprint } from "@hyv/core"; +import { Agent, createInstruction, sequence } from "@hyv/core"; import type { ModelMessage } from "@hyv/core"; import { DallEModelAdapter, GPTModelAdapter } from "@hyv/openai"; import type { DallEOptions, GPT3Options } from "@hyv/openai"; @@ -63,7 +63,7 @@ const author = new Agent( ), }, openai), store, - { tools: [fileWriter] } + { sideEffects: [fileWriter] } ); const illustrator = new Agent( @@ -72,12 +72,12 @@ const illustrator = new Agent( n: 1, }, openai), store, - { tools: [imageWriter] } + { sideEffects: [imageWriter] } ); try { const messageId = await store.set(book); - await sprint(messageId, [author, illustrator]); + await sequence(messageId, [author, illustrator]); console.log("Done"); } catch (error) { console.error("Error:", error); diff --git a/examples/book.ts b/examples/book.ts index 2a1f65c..702ae16 100644 --- a/examples/book.ts +++ b/examples/book.ts @@ -1,106 +1,83 @@ +import { Agent, createFileWriter, createInstruction, minify, sequence } from "@hyv/core"; +import { DallEModelAdapter, GPTModelAdapter } from "@hyv/openai"; import slugify from "@sindresorhus/slugify"; -import { Agent } from "../packages/core/src/agent.js"; -import type { ModelMessage } from "../packages/core/src/types.js"; -import type { AgentOptions } from "../packages/core/src/types.js"; -import type { ModelAdapter } from "../packages/core/src/types.js"; -import type { StoreAdapter } from "../packages/core/src/types.js"; -import { createInstruction, minify, sprint } from "../packages/core/src/utils.js"; -import { GPTModelAdapter } from "../packages/openai/src/gpt-model-adapter.js"; -import { DallEModelAdapter } from "../packages/openai/src/index.js"; -import type { GPT4Options } from "../packages/openai/src/types.js"; -import { createFileWriter, FSAdapter } from "../packages/store/src/index.js"; - -import { openai } from "./config.js"; - -const title = "The AIgent"; -const genre = "Science Fiction"; -const illustrationStyle = "watercolor illustration"; -const context = "AGI has become reality"; +const title = "Greg the Hero"; +const genre = "Adventure"; +const illustrationStyle = "flat"; +const context = "Greg writes the best AI libraries"; const dir = `out/stories/${slugify(title)}`; -const store = new FSAdapter(dir); const fileWriter = createFileWriter(dir); const imageWriter = createFileWriter(dir, "base64"); -const book: ModelMessage & { - title: string; - context: string; - genre: string; - imageCount: number; - chapterCount: number; - illustrationStyle: string; -} = { - title, - context, - genre, - imageCount: 3, - chapterCount: 3, - illustrationStyle, -}; +const author = new Agent( + new GPTModelAdapter({ + model: "gpt-4", + }), + { + sideEffects: [fileWriter], + } +); -interface AuthorData extends ModelMessage { - images: [{ path: string; prompt: string }]; - files: [{ path: string; content: string }]; +const illustrator = new Agent(new DallEModelAdapter(), { sideEffects: [imageWriter] }); + +/** + * Estimates reading time for a text + * @param text + */ +function getReadingTime(text: string) { + return text.length / 1_000; } -const options: AgentOptions = { - tools: [fileWriter], - async after(message) { - return { - ...message, - files: message.files.map(file => ({ - ...file, - readingTime: file.content.length / 1000, - })), - } as Message; - }, -}; +/** + * Estimates reading time for a text + * @param text + */ +function getWordCount(text: string) { + return text.split(" ").length; +} -const author = new Agent( - new GPTModelAdapter( - { - model: "gpt-4", - temperature: 0.7, - maxTokens: 4096, - historySize: 1, - systemInstruction: createInstruction( - "Author", - minify` - 1. Write a long bestseller story (500-600 words long)! - 2. Write a Markdown document WITH IMAGE TAGS and short alt text! - 3. INLINE all images (as Markdown) **as part of the story**! +// Give the agent some tools +author.after = async message => ({ + ...message, + files: message.files.map(file => ({ + ...file, + readingTime: getReadingTime(file.content), + words: getWordCount(file.content), + })), +}); + +// Adjust the agent's model +author.model.maxTokens = 1024; +author.model.systemInstruction = createInstruction( + "Author", + minify` + 1. Write a long("approximateWordCount") story! + 2. Write a VALID Markdown document WITH IMAGE TAGS and SHORT alt text! + 3. INLINE all images (as VALID Markdown) **within the story**! 4. All images should be LOCAL FILES! - 5. Add a DETAILED, CLEAR and very DESCRIPTIVE prompt with "illustrationStyle" for each image to be generated. + 5. Add a DETAILED, CLEAR, DESCRIPTIVE prompt("illustrationStyle") for each image to be generated. `, - { - images: [{ path: "string", prompt: "string" }], - files: [{ path: "story.md", content: "markdown" }], - } - ), - }, - openai - ), - store, - options + { + images: [{ path: "path/to/file.jpg", prompt: "string" }], + files: [{ path: "story.md", content: "Markdown" }], + } ); -const illustrator = new Agent( - new DallEModelAdapter( +try { + await sequence( { - size: "256x256", - n: 1, + title, + context, + genre, + approximateWordCount: 100, + imageCount: 2, + chapterCount: 2, + illustrationStyle, }, - openai - ), - store, - { tools: [imageWriter] } -); - -try { - const messageId = await store.set(book); - await sprint, StoreAdapter>(messageId, [author, illustrator]); - console.log("Done"); + [author, illustrator] + ); } catch (error) { console.error("Error:", error); } diff --git a/examples/config.ts b/examples/config.ts deleted file mode 100644 index e93aca0..0000000 --- a/examples/config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import process from "node:process"; - -import { config } from "dotenv"; -import { Configuration, OpenAIApi } from "openai"; - -config(); - -export const configuration = new Configuration({ - apiKey: process.env.OPENAI_API_KEY, -}); - -export const openai = new OpenAIApi(configuration); diff --git a/examples/dev-team.ts b/examples/dev-team.ts deleted file mode 100644 index 32ca87c..0000000 --- a/examples/dev-team.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Agent } from "../packages/core/src/agent.js"; -import type { ModelMessage } from "../packages/core/src/types.js"; -import { createInstruction, getResult, sprint } from "../packages/core/src/utils.js"; -import { GPTModelAdapter } from "../packages/openai/src/gpt-model-adapter.js"; -import type { GPT4Options } from "../packages/openai/src/types.js"; -import { createFileWriter, FSAdapter } from "../packages/store/src/index.js"; - -import { openai } from "./config.js"; - -const dir = "out/dev-team"; -const store = new FSAdapter(dir); - -const fileWriter = createFileWriter(dir); - -const pmAgent = new Agent( - new GPTModelAdapter( - { - model: "gpt-4", - temperature: 0.7, - maxTokens: 2048, - historySize: 1, - systemInstruction: createInstruction( - "Project Manager, A11y expert", - "create a small user story, provide a very detailed cucumber feature (Feature,Background?,Scenario(Given?When,Then))", - { - feature: "string", - userStory: "As as User, I want …, so that …", - cucumber: "string", - } - ), - }, - openai - ), - store -); - -const testAgent = new Agent( - new GPTModelAdapter( - { - model: "gpt-4", - temperature: 0.2, - maxTokens: 2048, - historySize: 2, - systemInstruction: createInstruction( - "Full Stack Test Engineer", - "write cypress step-definitions for cucumber and feature files, use data-test-id, use valid TypeScript", - { - dependencies: "string[]", - files: [{ path: "string", content: "string" }], - } - ), - }, - openai - ), - store, - { tools: [fileWriter] } -); - -const devAgent = new Agent( - new GPTModelAdapter( - { - model: "gpt-4", - temperature: 0.2, - maxTokens: 2048, - historySize: 2, - systemInstruction: createInstruction( - "Full Stack Developer", - "satisfy the tests with a react component, use valid TypeScript", - { - dependencies: "string[]", - files: [{ path: "string", content: "string" }], - } - ), - }, - openai - ), - store, - { tools: [fileWriter] } -); - -export interface ReviewMessage extends ModelMessage { - message: string; - approved: boolean; - changesRequest: boolean; - comments: { line: number; column: number; comment: string }[]; -} - -const reviewAgent = new Agent( - new GPTModelAdapter( - { - model: "gpt-4", - temperature: 0.7, - maxTokens: 2048, - historySize: 1, - systemInstruction: createInstruction( - "Full Stack Code Reviewer, A11y expert", - "review code, be precise and very critical", - { - message: "string", - approved: "boolean", - changes: [ - { path: "string", line: "number", column: "number", comment: "string" }, - ], - } - ), - }, - openai - ), - store, - { - async finally(messageId, message: ReviewMessage) { - return message.approved - ? messageId - : getResult(await getResult(messageId, devAgent), reviewAgent); - }, - } -); - -// Example Process - -const message: ModelMessage & { feature: string } = { - feature: "Counter Component", -}; - -try { - const messageId = await store.set(message); - await sprint(messageId, [pmAgent, testAgent, devAgent]); - console.log("Done"); -} catch (error) { - console.error("Error:", error); -} diff --git a/examples/simple.ts b/examples/simple.ts new file mode 100644 index 0000000..4f1d1d7 --- /dev/null +++ b/examples/simple.ts @@ -0,0 +1,10 @@ +import { Agent, sequence } from "@hyv/core"; +import { GPTModelAdapter } from "@hyv/openai"; + +const agent = new Agent(new GPTModelAdapter()); + +try { + await sequence({ question: "What is life?" }, [agent]); +} catch (error) { + console.error("Error:", error); +} diff --git a/examples/translation.ts b/examples/translation.ts deleted file mode 100644 index c503e55..0000000 --- a/examples/translation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Agent } from "../packages/core/src/agent.js"; -import type { ModelMessage } from "../packages/core/src/types.js"; -import { createInstruction, sprint } from "../packages/core/src/utils.js"; -import { GPTModelAdapter } from "../packages/openai/src/gpt-model-adapter.js"; -import type { GPT3Options } from "../packages/openai/src/types.js"; -import { FSAdapter } from "../packages/store/src/index.js"; - -import { openai } from "./config.js"; - -const sharedStore = new FSAdapter("out/translation"); - -const translate = new Agent( - new GPTModelAdapter( - { - model: "gpt-3.5-turbo", - temperature: 0.5, - maxTokens: 256, - historySize: 1, - systemInstruction: createInstruction("AI", "Translate English text to German.", { - translation: "", - }), - }, - openai - ), - sharedStore -); - -const paraphrase = new Agent( - new GPTModelAdapter( - { - model: "gpt-3.5-turbo", - temperature: 0.5, - maxTokens: 256, - historySize: 1, - systemInstruction: createInstruction("AI", "Paraphrase the summarized text.", { - paraphrase: "", - }), - }, - openai - ), - sharedStore -); - -// Example Process - -const message: ModelMessage & { content: string } = { - content: "My name is Hive. I am easy to use", -}; - -try { - const messageId = await sharedStore.set(message); - await sprint(messageId, [translate, paraphrase]); - console.log("Done"); -} catch (error) { - console.error("Error:", error); -} diff --git a/package-lock.json b/package-lock.json index 8809f83..21b8128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4648,7 +4648,6 @@ "version": "0.26.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dev": true, "dependencies": { "follow-redirects": "^1.14.8" } @@ -11845,7 +11844,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", - "dev": true, "dependencies": { "axios": "^0.26.0", "form-data": "^4.0.0" @@ -14848,21 +14846,29 @@ "packages/core": { "name": "@hyv/core", "version": "0.0.3", - "license": "AGPL" + "license": "AGPL", + "dependencies": { + "lru-cache": "9.1.1", + "nanoid": "4.0.2" + } + }, + "packages/core/node_modules/lru-cache": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", + "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", + "engines": { + "node": "14 || >=16.14" + } }, "packages/openai": { "name": "@hyv/openai", "version": "0.0.3", "license": "AGPL", "dependencies": { - "@hyv/core": "^0.0.3", - "type-fest": "^3.9.0" - }, - "devDependencies": { - "openai": "^3.2.1" - }, - "peerDependencies": { - "openai": "^3.2.1" + "@hyv/core": "0.0.3", + "dotenv": "16.0.3", + "openai": "3.2.1", + "type-fest": "3.9.0" } }, "packages/openai/node_modules/type-fest": { @@ -14881,7 +14887,7 @@ "version": "0.0.3", "license": "AGPL", "dependencies": { - "@hyv/core": "^0.0.3", + "@hyv/core": "0.0.3", "nanoid": "4.0.2" } } diff --git a/package.json b/package.json index d0a5300..84abf1d 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,8 @@ "license": "AGPL", "private": true, "scripts": { - "demo:dev-team": "ts-node-esm examples/dev-team.ts", - "demo:translation": "ts-node-esm examples/translation.ts", "demo:book": "ts-node-esm examples/book.ts", + "demo:simple": "ts-node-esm examples/simple.ts", "prebuild": "npm run clean", "build": "npx lerna run build", "clean": "npx lerna run clean", diff --git a/packages/core/package.json b/packages/core/package.json index 6b94848..021712a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,8 +27,9 @@ "main": "index.js", "module": "dist/index.js", "types": "dist/index.d.ts", - "exports": { - ".": "./index.js" + ".": { + "types": "./dist/index.d.ts", + "default": "./index.js" }, "files": [ "dist" @@ -37,6 +38,10 @@ "build": "rollup -c", "clean": "npx rimraf dist" }, + "dependencies": { + "nanoid": "4.0.2", + "lru-cache": "9.1.1" + }, "publishConfig": { "access": "public" } diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 5cd158c..771bd67 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,15 +1,24 @@ -import type { AgentOptions, ModelAdapter, ModelMessage, StoreAdapter, Tool } from "./types.js"; +import { MemoryAdapter } from "./memory-adapter.js"; +import type { + AgentOptions, + ModelAdapter, + ModelMessage, + StoreAdapter, + SideEffect, +} from "./types.js"; + +export const memoryStore = new MemoryAdapter(); /** - * Represents an agent that manages a model, a store, and a set of tools. - * The agent can assign tasks, find tools, retrieve messages, and return results. + * Represents an agent that manages a model, a store, and a set of sideEffects. + * The agent can assign tasks, find sideEffects, retrieve messages, and return results. * * @template Model - A type that extends ModelAdapter. * @template Store - A type that extends StoreAdapter. * @class Agent * @property {Model} #model - The model instance. * @property {Store} #store - The store instance. - * @property {AgentOptions["tool"]} #tools - An array of tools. + * @property {AgentOptions["sideEffect"]} #sideEffects - An array of sideEffects. * @property {AgentOptions["before"]} #before - A function that runs before the model. * @property {AgentOptions["after"]} #after - A function that runs after the model. * @property {AgentOptions["finally"]} #finally - A function that runs when the process is done. @@ -20,114 +29,119 @@ export class Agent< Store extends StoreAdapter = StoreAdapter > { #model: Model; - #store: Store; - #tools: Tool[] = []; - readonly before: AgentOptions["before"] = async (message): Promise => message; - readonly after: AgentOptions["after"] = async (message): Promise => message; - readonly finally: AgentOptions["finally"] = async messageId => messageId; - #task: Promise; + #store: Store | MemoryAdapter; + #sideEffects: SideEffect[] = []; + #before: AgentOptions["before"] = async (message): Promise => message; + #after: AgentOptions["after"] = async (message): Promise => message; + #finally: AgentOptions["finally"] = async messageId => messageId; /** * Creates an instance of the Agent class. * * @param {Model} model - The model instance. - * @param {Store} store - The store instance. * @param {AgentOptions} options - The configuration for the agent */ - constructor( - model: Model, - store: Store, - options: AgentOptions = {} - ) { + constructor(model: Model, options: AgentOptions = {}) { this.#model = model; - this.#store = store; - if (options.tools) { - this.#tools = options.tools; + this.#store = options.store ?? memoryStore; + + if (options.sideEffects) { + this.#sideEffects = options.sideEffects; } if (options.before) { - this.before = options.before; + this.#before = options.before; } if (options.after) { - this.after = options.after; + this.#after = options.after; } if (options.finally) { - this.finally = options.finally; + this.#finally = options.finally; } } /** - * Assigns a message to the model and runs the appropriate tools. + * Assigns a message to the model and runs the appropriate sideEffects. * * @private * @async - * @param {Promise} messagePromise - The message to be assigned. + * @param {ModelMessage} inputMessage - The message to be assigned. * @returns {Promise} - A Promise that resolves to the next messageId. */ - async #assign(messagePromise: Promise) { - const preparedMessage = await this.before(await messagePromise); + async #assign(inputMessage: ModelMessage) { + const preparedMessage = await this.#before(inputMessage); const message = await this.#model.assign(preparedMessage); - await Promise.all( - Object.entries(message).map(async ([prop, value]) => { - const tool = this.findTool(prop); - - if (tool) { - console.log(`Using tool: ${prop}`); - return tool.run({ [prop]: value }); - } - - return Promise.resolve(); - }) - ); - const modifiedMessage = await this.after(message); + const modifiedMessage = await this.#after(message); + Object.entries(modifiedMessage).forEach(([prop, value]) => { + const sideEffect = this.findSideEffect(prop); + if (sideEffect) { + console.log(`Using side effect on: ${prop}`); + sideEffect.run(value); + } + }); console.log("modifiedMessage"); console.log(modifiedMessage); const messageId = await this.#store.set(modifiedMessage); - return this.finally(messageId, modifiedMessage); + return this.#finally(messageId, modifiedMessage); } /** - * Finds a tool with the specified property. + * Finds a side effect with the specified property. * * @param {string} prop - The property to search for. - * @returns {Tool | undefined} - The found tool or undefined if not found. + * @returns {SideEffect | undefined} - The found side effect or undefined if not found. */ - findTool(prop: string): Tool | undefined { - return this.#tools.find(tool => tool.prop === prop); + findSideEffect(prop: string): SideEffect | undefined { + return this.#sideEffects.find(sideEffect => sideEffect.prop === prop); } /** - * Retrieves a message from the store using the messageId. + * Performs the current task using the provided messageId. * - * @private - * @async - * @param {string} messageId - The messageId to retrieve the message. - * @returns {Promise} - A Promise that resolves to the retrieved message. + * @param {string} messageId - The messageId to the task. + * @returns {string} - The id to the next message */ - async #retrieve(messageId: string) { - return this.#store.get(messageId); + async do(messageId: string) { + return this.#assign(await this.#store.get(messageId)); } - /** - * Sets the current task using the provided messageId. - * - * @param {string} messageId - The messageId to set the task. - */ - // eslint-disable-next-line accessor-pairs - set task(messageId: string) { - this.#task = this.#retrieve(messageId); + get sideEffects() { + return this.#sideEffects; } - /** - * Gets the result of the current task assignment. - * - * @returns {Promise} - A Promise that resolves to the next messageId. - */ - get result() { - return this.#assign(this.#task); + set sideEffects(sideEffects: SideEffect[]) { + this.#sideEffects = sideEffects; + } + + get before() { + return this.#before; + } + + set before(callback: AgentOptions["before"]) { + this.#before = callback; + } + + get after() { + return this.#after; + } + + set after(callback: AgentOptions["after"]) { + this.#after = callback; + } + + get finally() { + return this.#finally; + } + + set finally(callback: AgentOptions["finally"]) { + this.#finally = callback; + } + + get model() { + return this.#model; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d15ea5c..ab8ee9a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,4 @@ export * from "./types.js"; export * from "./utils.js"; +export * from "./memory-adapter.js"; export * from "./agent.js"; diff --git a/packages/core/src/memory-adapter.ts b/packages/core/src/memory-adapter.ts new file mode 100644 index 0000000..6037ad8 --- /dev/null +++ b/packages/core/src/memory-adapter.ts @@ -0,0 +1,45 @@ +import { LRUCache } from "lru-cache"; +import { nanoid } from "nanoid"; + +import type { ModelMessage, StoreAdapter } from "./types.js"; + +/** + * Represents a memory store adapter for storing and retrieving messages. + * + * @class MemoryAdapter + * @implements StoreAdapter + */ +export class MemoryAdapter implements StoreAdapter { + #store: LRUCache = new LRUCache({ max: 50 }); + /** + * Stores a message in the file system and returns the messageId. + * + * @async + * @template Message - A type that extends ModelMessage. + * @param {Message} message - The message to store. + * @returns {Promise} - A Promise that resolves to the messageId. + */ + async set(message: Message): Promise { + const messageId = nanoid(); + this.#store.set(messageId, message); + + return messageId; + } + + /** + * Retrieves a message by messageId from the file system. + * + * @async + * @param {string} messageId - The messageId of the message to retrieve. + * @returns {Promise} - A Promise that resolves to the message. + * @throws {Error} - If there is an error retrieving the message. + */ + async get(messageId: string): Promise { + const message = this.#store.get(messageId); + if (message) { + return message; + } + + throw new Error(`Error retrieving message with ID ${messageId}`); + } +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 075b048..a43462e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,3 +1,16 @@ +/** + * Represents a file with its associated content and path. + * + * @interface FileContentWithPath + * @property {string} path - The path to the file. + * @property {string} content - The content of the file. + */ +export interface FileContentWithPath { + path: string; + + content: string; +} + /** * Represents a model message with an optional array of files. * Each file has a path and content. @@ -5,8 +18,8 @@ * @interface ModelMessage * @property {Array<{ path: string; content: string }>} [files] - An optional array of files. */ -export interface ModelMessage { - files?: { path: string; content: string }[]; +export interface ModelMessage extends Record { + files?: FileContentWithPath[]; } /** @@ -22,17 +35,16 @@ export interface ModelAdapter { } /** - * Represents a tool with a property and a run method. + * Represents a side effect with a property and a run method. The side effect is run on the property * - * @interface Tool + * @interface SideEffect * @property {string} prop - A string property. * @property {(message: ModelMessage) => Promise} run - A function that takes a ModelMessage and returns * a Promise that resolves to void. */ -export interface Tool { +export interface SideEffect { prop: string; - - run(message: ModelMessage): Promise; + run(value: T): Promise; } /** @@ -49,7 +61,8 @@ export type ReasonableTemperature = 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 * @template OutMessage - The output message type. */ export interface AgentOptions< - InMessage extends ModelMessage = ModelMessage, + Store extends StoreAdapter = StoreAdapter, + Message extends ModelMessage = ModelMessage, OutMessage extends ModelMessage = ModelMessage > { /** @@ -58,7 +71,7 @@ export interface AgentOptions< * @param {InMessage} message - The input message. * @returns {Promise} - A Promise that resolves to the transformed message. */ - before?(message: InMessage): Promise; + before?(message: Message): Promise; /** * A function that transforms the output message after it has been processed by the model. @@ -66,7 +79,7 @@ export interface AgentOptions< * @param {OutMessage} message - The output message. * @returns {Promise} - A Promise that resolves to the transformed message. */ - after?(message: OutMessage): Promise; + after?(message: OriginalMessage): Promise; /** * A function that runs after the Agent has processed the output message. @@ -78,9 +91,11 @@ export interface AgentOptions< finally?(messageId: string, message: OutMessage): Promise; /** - * An array of tools that the Agent can use. + * An array of sideEffects that the Agent can use. */ - tools?: Tool[]; + sideEffects?: SideEffect[]; + + store?: Store; } /** diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 61ef77f..6437f17 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -2,7 +2,14 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { Agent } from "./agent.js"; -import type { ModelAdapter, ModelMessage, StoreAdapter } from "./types.js"; +import { memoryStore } from "./agent.js"; +import type { + FileContentWithPath, + ModelAdapter, + ModelMessage, + SideEffect, + StoreAdapter, +} from "./types.js"; /** * Extracts the code block from a given string, if any. @@ -75,6 +82,29 @@ export async function writeFile( } } +/** + * Creates a file writer for writing output files. + * + * @param {string} dir - The directory where the output files should be written. + * @param {BufferEncoding} [encoding="utf-8"] - the encoding that should vbe used when writing files + * @returns {SideEffect} - The file writer instance. + */ +export function createFileWriter( + dir: string, + encoding: BufferEncoding = "utf-8" +): SideEffect { + return { + prop: "files", + async run(files) { + await Promise.all( + files.map(async file => + writeFile(path.join(dir, file.path), file.content, encoding) + ) + ); + }, + }; +} + /** * Minifies a template literal by removing leading and trailing whitespace. * @@ -142,31 +172,21 @@ export function createInstruction(role: string, tasks: string, format: Record} - A Promise that resolves to the result of the AI agent task. - */ -export async function getResult(messageId: string, agent: Agent) { - agent.task = messageId; - return agent.result; -} - /** * Runs a sequence of agents in a chain, passing the output of each agent as input to the next agent. - * @param featureId The ID of the feature or story being worked on. + * @param message * @param chain An array of agents to be executed in sequence. + * @param store The store for the messages * @returns The final message ID produced by the last agent in the chain. */ -export async function sprint< - Model extends ModelAdapter = ModelAdapter, - Store extends StoreAdapter = StoreAdapter ->(featureId: string, chain: Agent[]) { +export async function sequence( + message: ModelMessage, + chain: Agent, Store>[], + store = memoryStore +) { + const featureId = await store.set(message); return chain.reduce( - async (messageId, agent) => getResult(await messageId, agent), + async (messageId, agent) => agent.do(await messageId), Promise.resolve(featureId) ); } diff --git a/packages/openai/package.json b/packages/openai/package.json index dab20d9..15b88bd 100644 --- a/packages/openai/package.json +++ b/packages/openai/package.json @@ -28,7 +28,10 @@ "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { - ".": "./index.js" + ".": { + "types": "./dist/index.d.ts", + "default": "./index.js" + } }, "files": [ "dist" @@ -38,14 +41,10 @@ "clean": "npx rimraf dist" }, "dependencies": { - "@hyv/core": "^0.0.3", - "type-fest": "^3.9.0" - }, - "devDependencies": { - "openai": "^3.2.1" - }, - "peerDependencies": { - "openai": "^3.2.1" + "@hyv/core": "0.0.3", + "type-fest": "3.9.0", + "dotenv": "16.0.3", + "openai": "3.2.1" }, "publishConfig": { "access": "public" diff --git a/packages/openai/src/config.ts b/packages/openai/src/config.ts new file mode 100644 index 0000000..80023be --- /dev/null +++ b/packages/openai/src/config.ts @@ -0,0 +1,12 @@ +import process from "node:process"; + +import { config } from "dotenv"; +import { Configuration, OpenAIApi } from "openai"; + +config(); + +export const defaultOpenAI = new OpenAIApi( + new Configuration({ + apiKey: process.env.OPENAI_API_KEY, + }) +); diff --git a/packages/openai/src/dall-e-model-adapter.ts b/packages/openai/src/dall-e-model-adapter.ts index b131d6a..92807a0 100644 --- a/packages/openai/src/dall-e-model-adapter.ts +++ b/packages/openai/src/dall-e-model-adapter.ts @@ -1,31 +1,33 @@ import type { ModelAdapter, ModelMessage } from "@hyv/core"; import type { OpenAIApi } from "openai"; +import { defaultOpenAI } from "./config.js"; import type { DallEOptions, ImageMessage } from "./types.js"; -export class DallEModelAdapter< - Options extends DallEOptions = DallEOptions, - Message extends ModelMessage = ModelMessage -> implements ModelAdapter +export class DallEModelAdapter + implements ModelAdapter { - #options: Options; - #openai: OpenAIApi; + #options: DallEOptions; + #openAI: OpenAIApi; /** * Creates an instance of the DallEModelAdapter class. * * @param {Options} options - The DALL-E model options. - * @param {OpenAIApi} openai - A configured openai API instance. + * @param {OpenAIApi} openAI - A configured openAI API instance. */ - constructor(options: Options, openai: OpenAIApi) { + constructor( + options: DallEOptions = { size: "256x256", n: 1 }, + openAI: OpenAIApi = defaultOpenAI + ) { this.#options = options; - this.#openai = openai; + this.#openAI = openAI; } async assign(task: Message & ImageMessage): Promise { try { const files = await Promise.all( task.images.map(async image => { - const response = await this.#openai.createImage({ + const response = await this.#openAI.createImage({ ...this.#options, prompt: image.prompt, // eslint-disable-next-line camelcase diff --git a/packages/openai/src/gpt-model-adapter.ts b/packages/openai/src/gpt-model-adapter.ts index d6b03a6..1582ef5 100644 --- a/packages/openai/src/gpt-model-adapter.ts +++ b/packages/openai/src/gpt-model-adapter.ts @@ -1,7 +1,8 @@ import type { ModelAdapter, ModelMessage } from "@hyv/core"; -import { extractCode } from "@hyv/core"; +import { createInstruction, extractCode } from "@hyv/core"; import type { ChatCompletionRequestMessage, OpenAIApi } from "openai"; +import { defaultOpenAI } from "./config.js"; import type { GPTOptions } from "./types.js"; /** @@ -13,22 +14,36 @@ import type { GPTOptions } from "./types.js"; * @property {Options} #options - The GPT model options. * @property {ChatCompletionRequestMessage[]} history - An array of chat completion request messages. */ -export class GPTModelAdapter implements ModelAdapter { +export class GPTModelAdapter + implements ModelAdapter +{ #options: Options; - #openai: OpenAIApi; + #openAI: OpenAIApi; readonly history: ChatCompletionRequestMessage[]; /** * Creates an instance of the GPTModelAdapter class. * * @param {Options} options - The GPT model options. - * @param {OpenAIApi} openai - A configured openai API instance. + * @param {OpenAIApi} openAI - A configured openAI API instance. */ - constructor(options: Options, openai: OpenAIApi) { - console.log("systemInstruction"); - console.log(options.systemInstruction); + constructor( + options: Options = { + temperature: 0.5, + model: "gpt-3.5-turbo", + historySize: 1, + maxTokens: 512, + systemInstruction: createInstruction("AI", "think, reason, reflect, answer", { + thought: "string", + reason: "string", + reflection: "string", + answer: "string", + }), + } as Options, + openAI: OpenAIApi = defaultOpenAI + ) { this.#options = options; - this.#openai = openai; + this.#openAI = openAI; this.history = []; } @@ -60,7 +75,7 @@ export class GPTModelAdapter implements ModelAdapter try { this.addMessageToHistory({ role: "user", content: JSON.stringify(task) }); - const completion = await this.#openai.createChatCompletion({ + const completion = await this.#openAI.createChatCompletion({ model: this.#options.model, // eslint-disable-next-line camelcase max_tokens: this.#options.maxTokens, @@ -81,4 +96,36 @@ export class GPTModelAdapter implements ModelAdapter throw new Error(`Error assigning task in GPTModelAdapter: ${error.message}`); } } + + get systemInstruction() { + return this.#options.systemInstruction; + } + + set systemInstruction(systemInstruction: string) { + this.#options.systemInstruction = systemInstruction; + } + + get maxTokens() { + return this.#options.maxTokens; + } + + set maxTokens(maxTokens: number) { + this.#options.maxTokens = maxTokens; + } + + get temperature() { + return this.#options.temperature; + } + + set temperature(temperature: number) { + this.#options.maxTokens = temperature; + } + + get historySize() { + return this.#options.temperature; + } + + set historySize(historySize: number) { + this.#options.maxTokens = historySize; + } } diff --git a/packages/openai/src/types.ts b/packages/openai/src/types.ts index 046930a..0daca19 100644 --- a/packages/openai/src/types.ts +++ b/packages/openai/src/types.ts @@ -13,11 +13,11 @@ import type { Except } from "type-fest"; * @property {string} systemInstruction - The system instruction. */ export interface GPTOptions { - model: string; - temperature: ReasonableTemperature; - maxTokens: number; - historySize: number; - systemInstruction: string; + model?: string; + temperature?: ReasonableTemperature; + maxTokens?: number; + historySize?: number; + systemInstruction?: string; } /** @@ -30,7 +30,7 @@ export interface GPTOptions { */ export interface GPT3Options extends GPTOptions { model: "gpt-3.5-turbo"; - historySize: 1 | 2; + historySize?: 1 | 2; } /** @@ -43,7 +43,7 @@ export interface GPT3Options extends GPTOptions { */ export interface GPT4Options extends GPTOptions { model: "gpt-4"; - historySize: 1 | 2 | 3 | 4; + historySize?: 1 | 2 | 3 | 4; } export interface ImageMessage { diff --git a/packages/store/package.json b/packages/store/package.json index 9f43ae0..0c54a74 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -28,7 +28,10 @@ "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { - ".": "./index.js" + ".": { + "types": "./dist/index.d.ts", + "default": "./index.js" + } }, "scripts": { "build": "rollup -c", @@ -38,7 +41,7 @@ "dist" ], "dependencies": { - "@hyv/core": "^0.0.3", + "@hyv/core": "0.0.3", "nanoid": "4.0.2" }, "publishConfig": { diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 9859074..41cbc1a 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,2 +1 @@ -export * from "./utils.js"; export * from "./fs-adapter.js"; diff --git a/packages/store/src/utils.ts b/packages/store/src/utils.ts deleted file mode 100644 index 87e2db4..0000000 --- a/packages/store/src/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from "node:path"; - -import type { ModelMessage, Tool } from "@hyv/core"; -import { writeFile } from "@hyv/core"; - -/** - * Creates a file writer tool for writing output files. - * - * @param {string} dir - The directory where the output files should be written. - * @param {BufferEncoding} [encoding="utf-8"] - the encoding that should vbe used when writing files - * @returns {Tool} - The file writer tool instance. - */ -export function createFileWriter(dir: string, encoding: BufferEncoding = "utf-8"): Tool { - return { - prop: "files", - async run(message: ModelMessage) { - await Promise.all( - message.files.map(async file => - writeFile(path.join(dir, "output", file.path), file.content, encoding) - ) - ); - }, - }; -}