Skip to content

Commit

Permalink
feat(bots): add function calling
Browse files Browse the repository at this point in the history
  • Loading branch information
furiousOyster committed Nov 2, 2023
1 parent beef90e commit db6c875
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 55 deletions.
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": [
"run-script",
"cli"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
},
{
"name": "Debug Jest Tests",
"type": "node",
Expand Down
112 changes: 112 additions & 0 deletions bots/git.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Git

Acts like a git command to demonstrate function calling

```
{ "role": "system", "content": "You are an intelligent computer terminal that is capable of natural language. You will gather from your user all the information you need to make one of the function calls you have been supplied with." }
```

## FUNCTIONS

```json
[
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Git Command",
"description": "A representation of a git command to be executed",
"type": "object",
"required": ["subcommand"],
"properties": {
"subcommand": {
"type": "string",
"description": "The git subcommand to be executed",
"enum": ["clone", "commit", "push", "pull", "checkout", "status"]
},
"options": {
"type": "object",
"description": "Options for the git command",
"properties": {
"clone": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "The URL of the repository to clone"
},
"directory": {
"type": "string",
"description": "The name of a new directory to clone into"
}
},
"required": ["repository"]
},
"commit": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The commit message"
}
},
"required": ["message"]
},
"push": {
"type": "object",
"properties": {
"remote": {
"type": "string",
"description": "The remote name"
},
"branch": {
"type": "string",
"description": "The branch name"
}
}
},
"pull": {
"type": "object",
"properties": {
"remote": {
"type": "string",
"description": "The remote name"
},
"branch": {
"type": "string",
"description": "The branch name"
}
}
},
"checkout": {
"type": "object",
"properties": {
"branch": {
"type": "string",
"description": "The branch or commit to checkout"
}
},
"required": ["branch"]
},
"status": {
"type": "object",
"description": "Git status options"
}
}
},
"globalOptions": {
"type": "object",
"description": "Global options for the git command",
"properties": {
"version": {
"type": "boolean",
"description": "Show git version"
},
"help": {
"type": "boolean",
"description": "Show help for git command"
}
}
}
}
}
]
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"license": "AGPL-3.0",
"scripts": {
"postinstall": "patch-package --patch-dir .patches",
"start": "yarn && NODE_NO_WARNINGS=1 node --loader=import-jsx src/cli.js",
"start": "yarn && yarn cli",
"cli": "NODE_NO_WARNINGS=1 node --loader=import-jsx src/cli.js",
"dev": "NODE_NO_WARNINGS=1 nodemon --no-stdin --loader=import-jsx src/cli.js",
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules --loader=import-jsx node_modules/.bin/jest",
"watch": "DEBUG_COLORS=1 NODE_NO_WARNINGS=1 node --experimental-vm-modules --loader=import-jsx node_modules/.bin/jest --watch",
Expand Down
55 changes: 44 additions & 11 deletions src/ai.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ export default class AI {
static create({ session } = {}) {
const ai = new AI()
ai.#disk = Disk.create(session)
ai.#push(...ai.#disk.load())
const loaded = ai.#disk.load()
expect(loaded).has.property('session')
ai.#push(...loaded.session)
debug('loaded session', ai.session)
return ai
}
Expand All @@ -46,28 +48,59 @@ export default class AI {
}
async *#stream() {
await this.#disk.flush(this.session)
const results = []
const contentParts = []
const functionParts = []
debug('session', this.session)
const messages = await this.#disk.expand(this.session)
const { messages, functions } = await this.#disk.expand(this.session)
debug('messages', messages)
debug('functions', functions)
if (this.#injectedNextResponse) {
yield* this.#injectedNextResponse
results.push(...this.#injectedNextResponse)
contentParts.push(...this.#injectedNextResponse)
this.#injectedNextResponse = undefined
} else {
const stream = await this.#openAi.chat.completions.create({
const params = {
model: 'gpt-4',
messages,
stream: true,
})
}
functions.length && (params.functions = functions)
const stream = await this.#openAi.chat.completions.create(params)
for await (const part of stream) {
const result = part.choices[0]?.delta?.content || ''
results.push(result)
yield result
const content = part.choices[0]?.delta?.content || ''
content && contentParts.push(content)
const fn = part.choices[0]?.delta?.function_call || ''
fn && functionParts.push(fn)
if (content) {
yield content
}
if (fn && fn.name) {
yield 'FUNCTION: ' + fn.name
}
}
}
const result = results.join('')
this.#push({ role: 'assistant', content: result })
if (functionParts.length) {
let name = ''

Check failure on line 83 in src/ai.js

View workflow job for this annotation

GitHub Actions / test

'name' is assigned a value but never used
let args = ''
functionParts.forEach((part) => {
if (part.name) {
name += part.name
} else if (part.arguments) {
args += part.arguments
} else {
throw Error('unknown function part' + JSON.stringify(part, null, 2))
}
})
const string = '\n' + args
yield string
this.#push({ role: 'assistant', content: string })
}
if (contentParts.length) {
this.#push({ role: 'assistant', content: contentParts.join('') })
}
if (functionParts.length && contentParts.length) {
throw new Error('both function and content parts received')
}
await this.#disk.flush(this.session)
}
#injectedNextResponse
Expand Down
114 changes: 71 additions & 43 deletions src/disk.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from 'fs'
import { expect } from 'chai'
import Debug from 'debug'
import BookLoader from './tools/book-loader.js'
import assert from 'assert-fast'
const debug = Debug('disk')
const SESSIONS_DIR = 'sessions'
const BOTS_DIR = 'bots'
Expand All @@ -29,50 +30,48 @@ export default class Disk {
}
async expand(session) {
expect(session).to.be.an('array')
const withBots = []
const messages = []
const functions = []
for (const item of session) {
if (item.role === 'bot') {
const bot = item.content
await this.loadBot(bot)
debug('prompts for', bot, this.#bots.get(bot))
withBots.push(...this.#bots.get(bot))
const botName = item.content
const bot = await this.loadBot(botName)
debug('bot for', botName, bot)
messages.push(...bot.messages)
functions.push(...bot.functions)
} else {
withBots.push(item)
messages.push(item)
}
}

return withBots
return { messages, functions }
}
load() {
if (!this.#session) {
return []
}
try {
fs.accessSync(this.#session)
} catch (err) {
return []
}
try {
const session = []
const data = fs.readFileSync(this.#session)
const lines = data.toString().split('\n')
for (const line of lines) {
if (!line) {
continue
if (this.#session) {
try {
fs.accessSync(this.#session)
const session = []
const data = fs.readFileSync(this.#session)
const lines = data.toString().split('\n')
for (const line of lines) {
if (!line) {
continue
}
try {
session.push(JSON.parse(line))
} catch (err) {
// TODO make the AI parse the error offer corrections
}
}
try {
session.push(JSON.parse(line))
} catch (err) {
// TODO make the AI parse the error offer corrections
// TODO check that expanding the session works correctly
return { session }
} catch (err) {
// TODO log the error somewhere for the user to receive comment on
if (err.code !== 'ENOENT') {
console.error(err)
}
}
// TODO check that expanding the session works correctly
return session
} catch (err) {
// TODO log the error somewhere for the user to receive comment on
console.error(err)
return []
}
return { session: [] }
}
async flush(session) {
expect(session).is.an('array')
Expand Down Expand Up @@ -105,25 +104,54 @@ export default class Disk {
const filename = `${BOTS_DIR}/${bot}.md`
const data = fs.readFileSync(filename)
const lines = data.toString().split('\n')
const botPrompts = []
const messages = []
let functionsString
for (const line of lines) {
if (!line) {
continue
}
try {
botPrompts.push(JSON.parse(line))
} catch (err) {
// TODO make the AI parse the error offer corrections

if (line.trim() === '## FUNCTIONS') {
expect(functionsString).to.be.undefined
functionsString = ' '
} else if (functionsString) {
if (!line.trim().startsWith('```')) {
functionsString += line
}
} else {
try {
messages.push(JSON.parse(line))
} catch (err) {
// TODO make the AI parse the error offer corrections
}
}
}
expect(messages).length.to.be.above(0)

const functions = []
try {
if (functionsString) {
const functionsArray = JSON.parse(functionsString)
for (const fn of functionsArray) {
assert(fn.title, `missing title for ${JSON.stringify(fn, null, 2)}`)
assert(fn.description, `missing description for ${fn.title}`)
const name = fn.title.replace(' ', '_')
const { description, ...parameters } = fn
debug('adding function', name)
functions.push({ name, description, parameters })
}
}
} catch (error) {
console.error('failed to parse functions: ' + functionsString)
}
expect(botPrompts).length.to.be.above(0)
debug('loaded bot', bot, botPrompts.length)

debug('loaded bot', bot, messages.length, functions.length)
if (bot === 'book-loader') {
debug('knowledge matcher loaded without using knowledge matcher')
this.#bots.set(bot, botPrompts)
this.#bots.set(bot, { messages, functions })
} else {
const withKnowledge = await this.expandKnowledge(botPrompts)
this.#bots.set(bot, withKnowledge)
const withKnowledge = await this.expandKnowledge(messages)
this.#bots.set(bot, { messages: withKnowledge, functions })
}
// need to reloop and insert all the other bots it loads
}
Expand Down

0 comments on commit db6c875

Please sign in to comment.