added functions

This commit is contained in:
Wlad Paiva
2023-10-19 14:27:44 -03:00
parent e0391f4040
commit 7224499248
8 changed files with 176 additions and 54 deletions
+30
View File
@@ -0,0 +1,30 @@
---
'aibitat': patch
---
### 🎉 Function calling is here!!!
You can now call functions from the conversation. This is a huge step forward in
making Aibitat more powerful and flexible.
To have your agents calling functions, use the `function` method to register a
function:
```ts
const aibitat = new AIbitat().function({
name: 'unique-function-name',
description: 'List of releases of AIbitat and the notes for each release.',
parameters: {
type: 'object',
properties: {},
},
handler: async () => {
const response = await fetch(
'https://github.com/wladiston/aibitat/releases',
)
const html = await response.text()
const text = cheerio.load(html).text()
return text
},
})
```
+17 -35
View File
@@ -37,7 +37,7 @@ by setting them on the specific node config.
- [x] **Group chats.** Agents chat with multiple other agents at the same time
as if they were in a slack channel. The next agent to reply is the most
likely to reply based on the conversation.
- [ ] **Function execution.** Agents can execute functions and return the result
- [x] **Function execution.** Agents can execute functions and return the result
to the conversation.
- [ ] **Cache**. Store conversation history in a cache to improve performance
and reduce the number of API calls.
@@ -61,14 +61,21 @@ by setting them on the specific node config.
You can install the package:
```bash
npm install aibitat
# to install bun go to https://bun.sh and follow the instructions
bun install aibitat
```
add you `OPEN_AI_API_KEY` to your environment variables and then use it like
this:
Create an `.env` file and add your `OPEN_AI_API_KEY`:
```bash
OPEN_AI_API_KEY=...
```
Then create a file called `index.ts` and add the following:
```ts
import {AIbitat} from 'aibitat'
import {terminal} from 'aibitat/plugins'
const aibitat = new AIbitat({
nodes: {
@@ -89,44 +96,19 @@ const aibitat = new AIbitat({
role: 'You reply "TERMINATE" if theres`s a confirmation',
},
},
})
aibitat.onMessage(({from, to, content}) => console.log(`${from}: ${content}`))
// 🧑: How much is 2 + 2?
// 🐭: The sum of 2 + 2 is 4.
// 🦁: That is correct.
// 🐶: TERMINATE
}).use(terminal())
await aibitat.start({
from: '🧑',
to: '🤖',
content: 'How much is 2 + 2?',
})
```
console.log('saving chats... ', aibitat.chats)
// saving chats... [
// {
// from: "🧑",
// to: "🤖",
// content: "How much is 2 + 2?",
// state: "success"
// }, {
// from: "🐭",
// to: "🤖",
// state: "success",
// content: "The sum of 2 + 2 is 4."
// }, {
// from: "🦁",
// to: "🤖",
// state: "success",
// content: "That is correct."
// }, {
// from: "🐶",
// to: "🤖",
// state: "success",
// content: "TERMINATE"
// }
// ]
Then run:
```bash
bun run index.ts
```
Nodes are the agents that will be used in the conversation and how they connect
+37
View File
@@ -0,0 +1,37 @@
import * as cheerio from 'cheerio'
import {AIbitat} from '../src'
import {terminal} from '../src/plugins'
const aibitat = new AIbitat({
nodes: {
'🧑': '🤖',
},
config: {
'🧑': {type: 'assistant'},
'🤖': {type: 'agent', functions: ['aibitat-releases']},
},
})
.use(terminal())
.function({
name: 'aibitat-releases',
description: 'List of releases of AIbitat and the notes for each release.',
parameters: {
type: 'object',
properties: {},
},
handler: async () => {
const response = await fetch(
'https://github.com/wladiston/aibitat/releases',
)
const html = await response.text()
const text = cheerio.load(html).text()
return text
},
})
await aibitat.start({
from: '🧑',
to: '🤖',
content: `Talk about the latest news about AIbitat`,
})
+18 -2
View File
@@ -291,13 +291,27 @@ describe('as a group', () => {
})
})
test('should call a function', async () => {
test.only('should call a function', async () => {
// FIX: I can't mock the API yet
// ai.create.mockImplementation(() =>
// Promise.resolve({
// function_call: {
// name: 'internet',
// arguments: '{"query": "I\'m feeling lucky"}',
// },
// }),
// )
const internet = mock((props: {query: string}) =>
Promise.resolve("I'm feeling lucky"),
)
const aibitat = new AIbitat({
...defaultaibitat,
config: {
...defaultaibitat.config,
'🤖': {type: 'agent', functions: ['internet']},
},
})
aibitat.function({
@@ -387,7 +401,9 @@ describe('when errors happen', () => {
throw error
}
return Promise.resolve('TERMINATE')
return Promise.resolve({
content: 'TERMINATE',
})
})
const aibitat = new AIbitat(defaultaibitat)
+4 -10
View File
@@ -207,7 +207,7 @@ export type FunctionDefinition = {
* hallucinate parameters not defined by your function schema. Validate the
* arguments in your code before calling your function.
*/
handler: (props: unknown, aibitat: AIbitat) => Promise<string> | string
handler: (...params: any[]) => Promise<string> | string
}
/**
@@ -716,15 +716,9 @@ ${this.getHistory({to})
// get the functions that the node can call
const functions =
fromConfig.functions?.map(name => {
const definition = this.functions.get(name)
if (!definition) {
throw new Error(`Function "${name}" is not defined`)
}
const {handler, ...rest} = definition
return rest
}) || []
(fromConfig.functions
?.map(name => this.functions.get(name))
.filter(a => !!a) as FunctionDefinition[]) || []
// get the chat completion
const content = await provider.create(messages, functions)
+6 -2
View File
@@ -1,4 +1,5 @@
import {Function, Message} from '../types.ts'
import {FunctionDefinition} from '../aibitat.ts'
import {Message} from '../types.ts'
/**
* A service that provides an AI client to create a completion.
@@ -24,5 +25,8 @@ export abstract class AIProvider<T> {
* @throws It should thrown known treated errors from `src/error.ts`.
* @param messages A list of messages to send.
*/
abstract create(messages: Message[], functions?: Function[]): Promise<string>
abstract create(
messages: Message[],
functions?: FunctionDefinition[],
): Promise<string>
}
+63 -4
View File
@@ -15,6 +15,7 @@ import OpenAI, {
UnprocessableEntityError as OpenAIUnprocessableEntityError,
} from 'openai'
import {FunctionDefinition} from '../aibitat.ts'
import {
APIError,
AuthorizationError,
@@ -96,7 +97,10 @@ export class OpenAIProvider extends AIProvider<OpenAI> {
* @param messages A list of messages to send to the OpenAI API.
* @returns The completion.
*/
async create(messages: Message[], functions?: Function[]) {
async create(
messages: Message[],
functions?: FunctionDefinition[],
): Promise<string> {
log(`calling 'openai.chat.completions.create' with model '${this.model}'`)
try {
@@ -109,10 +113,35 @@ export class OpenAIProvider extends AIProvider<OpenAI> {
log('cost: ', this.getCost(response.usage))
// FIX: parei aqui.. agora preciso retornar a function call e chamar a função no reply
// caso function_call
return response.choices[0].message.content!
if (functions && response.choices[0].message.function_call) {
// send the info on the function call and function response to GPT
// and return the response
const functionResponse = await this.callFunction(
functions,
response.choices[0].message.function_call,
)
return await this.create(
[
...messages,
response.choices[0].message, // extend conversation with assistant's reply
// extend conversation with function response
{
role: 'function',
name: response.choices[0].message.function_call.name,
content: functionResponse,
},
],
functions,
)
}
if (response.choices[0].message.content) {
return response.choices[0].message.content
}
throw new Error('No content found or function_call in the response')
// const stream = OpenAIStream(response)
// const result = new StreamingTextResponse(stream)
// return await result.text()
@@ -189,4 +218,34 @@ export class OpenAIProvider extends AIProvider<OpenAI> {
currency: 'USD',
}).format(total)
}
/**
* Call the function from the completion.
*
* @param functions The list of functions to call.
* @param completion The completion to get the function from.
* @returns The completion.
*/
async callFunction(
functions: FunctionDefinition[],
call: OpenAI.Chat.ChatCompletionMessage.FunctionCall,
) {
const funcToCall = functions.find(f => f.name === call.name)
if (!funcToCall) {
throw new Error(`Function '${call.name}' not found`)
}
let json: any
try {
json = JSON.parse(call.arguments)
} catch (error) {
throw new Error(
`Model created an invalid JSON: '${call.arguments}' for function '${call.name}'`,
)
}
log('calling function: ', funcToCall.name, 'with arguments: ', json)
return await funcToCall.handler(json)
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ export type Message = {
* The role of the messages author. One of `system`, `user`, `assistant`, or
* `function`.
*/
role: 'system' | 'user' | 'assistant' | 'function'
role: Role
/**
* The name and arguments of a function that should be called, as generated by the