This commit is contained in:
Wlad Paiva
2023-10-10 21:10:51 -03:00
commit 543f35e6ae
9 changed files with 803 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
+36
View File
@@ -0,0 +1,36 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"noDebug": true,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/",
"stopOnEntry": false
}
]
}
+15
View File
@@ -0,0 +1,15 @@
# meu-autogen
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.0.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
Executable
BIN
View File
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
{
"name": "autogen",
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"ai": "^2.2.14",
"openai": "^4.11.1"
}
}
+70
View File
@@ -0,0 +1,70 @@
import { Message } from "./types";
/**
* (In preview) An abstract class for AI agent.
*
* An agent can communicate with other agents and perform actions.
* Different agents can differ in what actions they perform in the `receive` method.
*/
export abstract class Agent {
private _name: string;
/**
* @param name name of the agent.
*/
constructor(name: string) {
this._name = name;
}
/**
* Get the name of the agent.
* @returns The name of the agent.
*/
get name(): string {
return this._name;
}
/**
* (Abstract method) Send a message to another agent.
* @abstract
* @param message The message to send.
* @param recipient The recipient agent.
* @param requestReply Whether a reply is requested.
*/
abstract send(
message: string | Message,
recipient: Agent,
requestReply?: boolean
): Promise<void>;
/**
* (Abstract async method) Receive a message from another agent.
* @abstract
* @param message The message received.
* @param sender The sender agent.
* @param requestReply Whether a reply is requested.
*/
abstract receive(
message: string | Message,
sender: Agent,
requestReply?: boolean
): Promise<void>;
/**
* (Abstract async method) Generate a reply based on the received messages.
* @abstract
* @param messages A list of messages received.
* @param sender The sender agent.
* @returns The generated reply. If None, no reply is generated.
*/
abstract generateReply(
messages?: Message[],
sender?: Agent
): Promise<string | null>;
/**
* (Abstract method) Reset the agent.
* @abstract
*/
abstract reset(): void;
}
+404
View File
@@ -0,0 +1,404 @@
import OpenAI from "openai";
import { OpenAIStream, StreamingTextResponse } from "ai";
import { Agent } from "./agent";
import type { Callable, LlmConfig, Message, ReplyFunc, Role } from "./types";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
// const response = await openai.chat.completions.create({
// model: "gpt-3.5-turbo",
// stream: true,
// messages: [
// {
// content: "Hello, how are you?",
// role: "user",
// },
// ],
// // functions: [
// // {
// // name: "get_weather",
// // description: "Gets the weather in a given city.",
// // parameters: {
// // type: "object",
// // properties: {
// // city: {
// // type: "string",
// // description: "The city to get the weather for.",
// // },
// // },
// // }
// // }
// // ]
// });
// const stream = OpenAIStream(response);
// // for await (const chunk of stream) {
// // process.stdout.write(chunk);
// // }
// const result = new StreamingTextResponse(stream);
// const first = await result.text();
// console.log("🔥 ~ ", first);
// const response2 = await openai.chat.completions.create({
// model: "gpt-3.5-turbo",
// stream: true,
// messages: [
// {
// content: first,
// role: "user",
// },
// ],
// });
// const stream2 = OpenAIStream(response2);
// const result2 = new StreamingTextResponse(stream2);
// const second = await result2.text();
// console.log("🔴 ~ ", second);
class ConversableAgent extends Agent {
private _messages: Map<Agent, Message[]>;
private replyFuncList: (ReplyFunc & { init_config: unknown })[] = [];
private onMessageReceived?: (message: Message, sender: Agent) => void;
private defaultAutoReply: string;
constructor(
name: string,
config: {
/**
* Default auto reply.
* @default ""
*/
defaultAutoReply?: string;
} = {}
) {
super(name);
this._messages = new Map<Agent, Message[]>();
this.onMessageReceived = (message, sender) => {
console.log(`${sender.name}: ${message.content}`);
};
const { defaultAutoReply = "" } = config;
this.defaultAutoReply = defaultAutoReply;
this.registerReply({
trigger: this,
replyFunc: this.generateOaiReply,
});
// TODO: implement those
// this.registerReply({
// trigger: this,
// replyFunc: this.generate_code_execution_reply,
// });
// this.registerReply({
// trigger: this,
// replyFunc: this.generate_function_call_reply,
// });
// this.registerReply({
// trigger: this,
// replyFunc: this.check_termination_and_human_reply,
// });
}
/**
* Get chat messages.
* @returns The chat messages.
*/
get chatMessages() {
return this._messages;
}
/**
* Convert a message to a dictionary. The message can be a string or a dictionary.
* The string will be put in the "content" field of the new dictionary.
* @param message The message to convert.
*/
static messageToDict(message: string | Message) {
return typeof message === "string" ? { content: message } : message;
}
/**
* Append a message to the ChatCompletion conversation.
* - If the message received is a string, it will be put in the "content" field of the new dictionary.
* - If the message received is a dictionary but does not have any of the two fields "content" or "function_call",
* this message is not a valid ChatCompletion message.
* - If only "function_call" is provided, "content" will be set to None if not provided, and the role of the message will be forced "assistant".
*
* @param message The message to append.
* @param role The role of the message.
* @param agent The agent that sent the message.
* @returns whether the message is appended to the ChatCompletion conversation.
*/
public appendOaiMessage(message: string | Message, role: Role, agent: Agent) {
const converted = ConversableAgent.messageToDict(message);
// create oai message to be appended to the oai conversation that can be passed to oai directly.
if (
!converted.content &&
(!("function_call" in converted) || !converted.function_call)
) {
return false;
}
const newMessage: Message = {
...converted,
role:
"role" in converted && converted.role === "function"
? "function"
: "function_call" in converted && converted.function_call
? "assistant"
: role,
};
if (!this._messages.has(agent)) {
this._messages.set(agent, []);
}
this._messages.get(agent)!.push(newMessage);
return true;
}
async send(
message: string | Message,
recipient: Agent,
requestReply?: boolean | undefined
) {
// When the agent composes and sends the message, the role of the message is "assistant"
// unless it's "function".
const valid = this.appendOaiMessage(message, "assistant", recipient);
if (!valid) {
throw new Error(
"Message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided."
);
}
await recipient.receive(message, this, requestReply);
}
/**
* When the agent receives a message, the role of the message is "user".
* (If 'role' exists and is 'function', it will remain unchanged.)
* @param message
* @param sender
*/
private processReceivedMessage(message: string | Message, sender: Agent) {
const converted = ConversableAgent.messageToDict(message);
const valid = this.appendOaiMessage(message, "user", sender);
if (!valid) {
throw new Error(
"Received message can't be converted into a valid ChatCompletion message. Either content or function_call must be provided."
);
}
this.onMessageReceived?.({ role: "user", ...converted }, sender);
}
async receive(
message: string | Message,
sender: Agent,
requestReply?: boolean
) {
this.processReceivedMessage(message, sender);
if (!requestReply) {
return;
}
const reply = await this.generateReply(
this.chatMessages.get(sender),
sender
);
if (reply) {
await this.send(reply, sender);
}
}
/**
* Register a reply function.
*
* The reply function will be called when the trigger matches the sender.
* The function registered later will be checked earlier by default.
* To change the order, set the position to a positive integer.
*/
public registerReply({
trigger,
replyFunc,
position = 0,
config,
resetConfig,
}: {
/**
* The trigger to activate the reply function.
* - If a class is provided, the reply function will be called when the sender is an instance of the class.
* - If a string is provided, the reply function will be called when the sender's name matches the string.
* - If an agent instance is provided, the reply function will be called when the sender is the agent instance.
* - If a callable is provided, the reply function will be called when the callable returns True.
* - If a list is provided, the reply function will be called when any of the triggers in the list is activated.
* - If None is provided, the reply function will be called only when the sender is None.
* Note: Be sure to register `None` as a trigger if you would like to trigger an auto-reply function with non-empty messages and `sender=None`.
*/
trigger: string | Agent | Callable | unknown[];
/**
* The function takes a recipient agent, a list of messages, a sender agent and a config as input and returns a reply message.
*/
replyFunc: Callable;
/**
* The position of the reply function in the reply function list.
* The function registered later will be checked earlier by default.
* To change the order, set the position to a positive integer.
*/
position?: number;
/**
* The config to be passed to the reply function.
* When an agent is reset, the config will be reset to the original value.
*/
config?: LlmConfig;
/**
* the function to reset the config.
* The function returns None.
*/
resetConfig?: Callable;
}) {
if (!(trigger instanceof (Agent || String || Function || Array))) {
throw new Error(
"trigger must be a class, a string, an agent, a callable or a list."
);
}
this.replyFuncList.splice(position, 0, {
trigger,
replyFunc,
config,
init_config: config,
resetConfig,
});
}
/**
* Reply based on the conversation history and the sender.
*
* Either messages or sender must be provided.
* Register a reply_func with `None` as one trigger for it to be activated when `messages` is non-empty and `sender` is `None`.
* Use registered auto reply functions to generate replies.
* By default, the following functions are checked in order:
* 1. check_termination_and_human_reply
* 2. generate_function_call_reply
* 3. generate_code_execution_reply
* 4. generate_oai_reply
* Every function returns a tuple (final, reply).
* When a function returns final=False, the next function will be checked.
* So by default, termination and human reply will be checked first.
* If not terminating and human reply is skipped, execute function or code and return the result.
* AI replies are generated only when no code execution is performed.
* @param messages a list of messages in the conversation history.
* @param sender sender of an Agent instance.
* @param exclude a list of functions to exclude.
* @returns reply. None if no reply is generated.
*/
async generateReply(
messages?: Message[],
sender?: Agent
): Promise<string | null> {
if (!messages && !sender) {
throw new Error("Either messages or sender must be provided.");
}
if (!messages) {
messages = this.chatMessages.get(sender!) || [];
}
for (const replyFuncTuple of this.replyFuncList) {
const replyFunc = replyFuncTuple.replyFunc;
if (replyFuncTuple.trigger instanceof Agent) {
const { success, reply } = await replyFunc(
messages,
sender,
replyFuncTuple.config
);
if (success) {
return reply;
}
}
}
return this.defaultAutoReply;
}
/**
* Generate a reply using `autogen.oai`
* @param messages
* @param sender
* @param config
*/
public async generateOaiReply(
messages?: Message[],
sender?: Agent,
config?: LlmConfig
): Promise<
| {
success: false;
reply: null;
}
| {
success: true;
reply: string;
}
> {
// TODO: config
// const llmConfig = config || this.llmConfig;
if (!sender) {
throw new Error("Sender must be provided.");
}
if (!messages) {
messages = this._messages.get(sender);
}
// TODO: implement this
// response = oai.ChatCompletion.create(
// context=messages[-1].pop("context", None), messages=self._oai_system_message + messages, **llm_config
// )
// return True, oai.ChatCompletion.extract_text_or_function_call(response)[0]
const response2 = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
stream: true,
messages: messages!,
});
const stream2 = OpenAIStream(response2);
const result2 = new StreamingTextResponse(stream2);
const second = await result2.text();
return {
success: true,
reply: second,
} as const;
}
reset(): void {
throw new Error("Method not implemented.");
}
}
const first = new ConversableAgent("🔥");
const second = new ConversableAgent("🔴");
await first.send(
{ content: "Hello, how are you?", role: "user" },
second,
true
);
// TODO: 1. Configuration
// TODO: 2. generate_function_call_reply
// TODO: 3. check_termination_and_human_reply
// TODO: 4. Cache
// TODO: 5. UserProxy
// TODO: 6. Assistant
// TODO: 7. Group
// TODO: 8. GroupManager
console.log("🔥 ~ first.chatMessages", first.chatMessages);
console.log("🔥 ~ second.chatMessages", second.chatMessages);
+65
View File
@@ -0,0 +1,65 @@
import type OpenAI from "openai";
import type { Agent } from "./agent";
/**
* OpenAI Chat API message.
*/
export type Message = OpenAI.Chat.Completions.ChatCompletionMessageParam;
export type Role = OpenAI.Chat.Completions.ChatCompletionMessageParam["role"];
export type Callable = (
messages?: Message[],
sender?: Agent,
config?: LlmConfig
) => Promise<
| {
success: false;
reply: null;
}
| {
success: true;
reply: string;
}
>;
export type LlmConfig = {
// TODO: Add types for this
};
export type ReplyFunc = {
/**
* The trigger to activate the reply function.
* - If a class is provided, the reply function will be called when the sender is an instance of the class.
* - If a string is provided, the reply function will be called when the sender's name matches the string.
* - If an agent instance is provided, the reply function will be called when the sender is the agent instance.
* - If a callable is provided, the reply function will be called when the callable returns True.
* - If a list is provided, the reply function will be called when any of the triggers in the list is activated.
* - If None is provided, the reply function will be called only when the sender is None.
* Note: Be sure to register `None` as a trigger if you would like to trigger an auto-reply function with non-empty messages and `sender=None`.
*/
trigger: string | Agent | Callable | unknown[];
/**
* The function takes a recipient agent, a list of messages, a sender agent and a config as input and returns a reply message.
*/
replyFunc: Callable;
/**
* The position of the reply function in the reply function list.
* The function registered later will be checked earlier by default.
* To change the order, set the position to a positive integer.
*/
position?: number;
/**
* The config to be passed to the reply function.
* When an agent is reset, the config will be reset to the original value.
*/
config?: LlmConfig;
/**
* the function to reset the config.
* The function returns None.
*/
resetConfig?: Callable;
};
export type MessageXXX = Message & {
// TODO:
id: string;
createdAt?: Date;
};
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": [
"bun-types" // add Bun global
]
}
}