pull/643/head^2
Travis Fischer 2024-06-01 19:34:25 -05:00
rodzic f4e7861064
commit 839f46097c
14 zmienionych plików z 750 dodań i 541 usunięć

Wyświetl plik

@ -4,5 +4,3 @@
# All of these environment vars must be defined either in your environment or in # All of these environment vars must be defined either in your environment or in
# a local .env file in order to run this project. # a local .env file in order to run this project.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
OPENAI_API_KEY=

Wyświetl plik

@ -0,0 +1,96 @@
#!/usr/bin/env node
import 'dotenv/config'
import {
ChatModel,
createAIFunction,
createAIRunner,
Msg
} from '@dexaai/dexter'
import { gracefulExit } from 'exit-hook'
import restoreCursor from 'restore-cursor'
import { z } from 'zod'
import { WeatherClient } from '../src/index.js'
/** Get the capital city for a given state. */
const getCapitalCity = createAIFunction(
{
name: 'get_capital_city',
description: 'Use this to get the the capital city for a given state',
argsSchema: z.object({
state: z
.string()
.length(2)
.describe(
'The state to get the capital city for, using the two letter abbreviation e.g. CA'
)
})
},
async ({ state }) => {
await new Promise((resolve) => setTimeout(resolve, 500))
let capitalCity = ''
switch (state) {
case 'CA':
capitalCity = 'Sacramento'
break
case 'NY':
capitalCity = 'Albany'
break
default:
capitalCity = 'Unknown'
}
return { capitalCity }
}
)
const weather = new WeatherClient()
const fns = [...weather.functions]
console.log('fns', fns)
const getCurrentWeather = weather.functions.get('get_current_weather')!
console.log('get_current_weather', getCurrentWeather)
/** A runner that uses the weather and capital city functions. */
const weatherCapitalRunner = createAIRunner({
chatModel: new ChatModel({ params: { model: 'gpt-4-1106-preview' } }),
functions: [
createAIFunction(
{
...getCurrentWeather.spec,
argsSchema: getCurrentWeather.inputSchema
},
getCurrentWeather.impl
),
getCapitalCity
],
systemMessage: `You use functions to answer questions about the weather and capital cities.`
})
async function main() {
restoreCursor()
// Run with a string input
const rString = await weatherCapitalRunner(
`Whats the capital of California and NY and the weather for both?`
)
console.log('rString', rString)
// Run with a message input
const rMessage = await weatherCapitalRunner({
messages: [
Msg.user(
`Whats the capital of California and NY and the weather for both?`
)
]
})
console.log('rMessage', rMessage)
}
try {
await main()
} catch (err) {
console.error('unexpected error', err)
gracefulExit(1)
}

Wyświetl plik

@ -7,7 +7,7 @@ import restoreCursor from 'restore-cursor'
// import { SearxngClient } from '../src/services/searxng-client.js' // import { SearxngClient } from '../src/services/searxng-client.js'
// import { ClearbitClient } from '../src/index.js' // import { ClearbitClient } from '../src/index.js'
// import { ProxycurlClient } from '../src/services/proxycurl-client.js' // import { ProxycurlClient } from '../src/services/proxycurl-client.js'
// import { WikipediaClient } from '../src/services/wikipedia-client.js' import { WikipediaClient } from '../src/index.js'
/** /**
* Scratch pad for testing. * Scratch pad for testing.
@ -27,11 +27,11 @@ async function main() {
// }) // })
// console.log(JSON.stringify(res, null, 2)) // console.log(JSON.stringify(res, null, 2))
// const wikipedia = new WikipediaClient() const wikipedia = new WikipediaClient()
// const res = await wikipedia.getPageSummary({ const res = await wikipedia.getPageSummary({
// title: 'Naruto_(TV_series)' title: 'Naruto_(TV_series)'
// }) })
// console.log(JSON.stringify(res, null, 2)) console.log(JSON.stringify(res, null, 2))
// const searxng = new SearxngClient() // const searxng = new SearxngClient()
// const res = await searxng.search({ // const res = await searxng.search({

Wyświetl plik

@ -30,7 +30,7 @@
], ],
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"build": "tsup", "build": "tsc",
"dev": "tsup --watch", "dev": "tsup --watch",
"clean": "del dist", "clean": "del dist",
"prebuild": "run-s clean", "prebuild": "run-s clean",
@ -45,7 +45,7 @@
"test:unit": "vitest run" "test:unit": "vitest run"
}, },
"dependencies": { "dependencies": {
"@nangohq/node": "^0.39.30", "@nangohq/node": "^0.39.32",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"delay": "^6.0.0", "delay": "^6.0.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@ -58,32 +58,38 @@
"restore-cursor": "^5.0.0", "restore-cursor": "^5.0.0",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"twitter-api-sdk": "^1.2.1", "twitter-api-sdk": "^1.2.1",
"type-fest": "^4.16.0", "type-fest": "^4.18.3",
"zod": "^3.23.3", "zod": "^3.23.3",
"zod-to-json-schema": "^3.23.0" "zod-to-json-schema": "^3.23.0"
}, },
"devDependencies": { "devDependencies": {
"@dexaai/dexter": "^2.0.3",
"@fisch0920/eslint-config": "^1.3.1", "@fisch0920/eslint-config": "^1.3.1",
"@total-typescript/ts-reset": "^0.5.1", "@total-typescript/ts-reset": "^0.5.1",
"@types/node": "^20.12.7", "@types/node": "^20.12.7",
"del-cli": "^5.1.0", "del-cli": "^5.1.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"husky": "^9.0.11", "husky": "^9.0.11",
"lint-staged": "^15.2.4", "lint-staged": "^15.2.5",
"np": "^10.0.5", "np": "^10.0.5",
"npm-run-all2": "^6.2.0", "npm-run-all2": "^6.2.0",
"only-allow": "^1.2.1", "only-allow": "^1.2.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-node": "^10.9.2",
"tsup": "^8.0.2", "tsup": "^8.0.2",
"tsx": "^4.10.5", "tsx": "^4.11.0",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite": "^5.2.10", "vitest": "2.0.0-beta.3"
"vitest": "^1.5.0"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx}": [ "*.{ts,tsx}": [
"eslint --fix", "eslint --fix",
"prettier --ignore-unknown --write" "prettier --ignore-unknown --write"
] ]
},
"pnpm": {
"overrides": {
"esbuild": "~0.21.4"
}
} }
} }

Plik diff jest za duży Load Diff

Wyświetl plik

@ -5,7 +5,9 @@ export class AIFunctionSet implements Iterable<types.AIFunction> {
protected readonly _map: Map<string, types.AIFunction> protected readonly _map: Map<string, types.AIFunction>
constructor(functions?: readonly types.AIFunction[]) { constructor(functions?: readonly types.AIFunction[]) {
this._map = new Map(functions ? functions.map((fn) => [fn.name, fn]) : null) this._map = new Map(
functions ? functions.map((fn) => [fn.spec.name, fn]) : null
)
} }
get size(): number { get size(): number {

Wyświetl plik

@ -26,6 +26,14 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Return>(
/** Implementation of the function to call with the parsed arguments. */ /** Implementation of the function to call with the parsed arguments. */
implementation: (params: z.infer<InputSchema>) => types.MaybePromise<Return> implementation: (params: z.infer<InputSchema>) => types.MaybePromise<Return>
): types.AIFunction<InputSchema, Return> { ): types.AIFunction<InputSchema, Return> {
assert(spec.name, 'Missing required AIFunction "spec.name"')
assert(spec.inputSchema, 'Missing required AIFunction "spec.inputSchema"')
assert(implementation, 'Missing required AIFunction "implementation"')
assert(
typeof implementation === 'function',
'Required AIFunction "implementation" must be a function'
)
/** Parse the arguments string, optionally reading from a message. */ /** Parse the arguments string, optionally reading from a message. */
const parseInput = (input: string | types.Msg) => { const parseInput = (input: string | types.Msg) => {
if (typeof input === 'string') { if (typeof input === 'string') {
@ -55,6 +63,7 @@ export function createAIFunction<InputSchema extends z.ZodObject<any>, Return>(
description: spec.description?.trim() ?? '', description: spec.description?.trim() ?? '',
parameters: zodToJsonSchema(spec.inputSchema) parameters: zodToJsonSchema(spec.inputSchema)
} }
aiFunction.impl = implementation
return aiFunction return aiFunction
} }

Wyświetl plik

@ -6,7 +6,7 @@ export class AIToolSet implements Iterable<types.AITool> {
constructor(tools?: readonly types.AITool[]) { constructor(tools?: readonly types.AITool[]) {
this._map = new Map( this._map = new Map(
tools ? tools.map((tool) => [tool.function.name, tool]) : [] tools ? tools.map((tool) => [tool.spec.function.name, tool]) : []
) )
} }
@ -15,7 +15,7 @@ export class AIToolSet implements Iterable<types.AITool> {
} }
add(tool: types.AITool): this { add(tool: types.AITool): this {
this._map.set(tool.function.name, tool) this._map.set(tool.spec.function.name, tool)
return this return this
} }
@ -44,7 +44,7 @@ export class AIToolSet implements Iterable<types.AITool> {
const keysToIncludeSet = new Set(keys) const keysToIncludeSet = new Set(keys)
return new AIToolSet( return new AIToolSet(
Array.from(this).filter((tool) => Array.from(this).filter((tool) =>
keysToIncludeSet.has(tool.function.name) keysToIncludeSet.has(tool.spec.function.name)
) )
) )
} }
@ -53,7 +53,7 @@ export class AIToolSet implements Iterable<types.AITool> {
const keysToExcludeSet = new Set(keys) const keysToExcludeSet = new Set(keys)
return new AIToolSet( return new AIToolSet(
Array.from(this).filter( Array.from(this).filter(
(tool) => !keysToExcludeSet.has(tool.function.name) (tool) => !keysToExcludeSet.has(tool.spec.function.name)
) )
) )
} }

Wyświetl plik

@ -8,8 +8,6 @@ import { AIFunctionSet } from './ai-function-set.js'
import { AIToolSet } from './ai-tool-set.js' import { AIToolSet } from './ai-tool-set.js'
import { assert } from './utils.js' import { assert } from './utils.js'
export const invocableMetadataKey = Symbol('invocable')
export abstract class AIToolsProvider { export abstract class AIToolsProvider {
private _tools?: AIToolSet private _tools?: AIToolSet
private _functions?: AIFunctionSet private _functions?: AIFunctionSet
@ -25,7 +23,9 @@ export abstract class AIToolsProvider {
get functions(): AIFunctionSet { get functions(): AIFunctionSet {
if (!this._functions) { if (!this._functions) {
const metadata = this.constructor[Symbol.metadata] const metadata = this.constructor[Symbol.metadata]
assert(metadata)
const invocables = (metadata?.invocables as Invocable[]) ?? [] const invocables = (metadata?.invocables as Invocable[]) ?? []
console.log({ metadata, invocables })
const aiFunctions = invocables.map((invocable) => { const aiFunctions = invocables.map((invocable) => {
const impl = (this as any)[invocable.methodName]?.bind(this) const impl = (this as any)[invocable.methodName]?.bind(this)
@ -81,13 +81,17 @@ export function aiFunction<
if (!context.metadata.invocables) { if (!context.metadata.invocables) {
context.metadata.invocables = [] context.metadata.invocables = []
} }
;(context.metadata.invocables as Invocable[]).push({ ;(context.metadata.invocables as Invocable[]).push({
name: name ?? methodName, name: name ?? methodName,
description, description,
inputSchema, inputSchema,
methodName methodName
}) })
console.log({
name,
methodName,
context
})
// context.addInitializer(function () { // context.addInitializer(function () {
// ;(this as any)[methodName] = (this as any)[methodName].bind(this) // ;(this as any)[methodName] = (this as any)[methodName].bind(this)

Wyświetl plik

@ -0,0 +1,14 @@
import { expect, test } from 'vitest'
import { WeatherClient } from './weather-client.js'
test('WeatherClient.functions', () => {
const weather = new WeatherClient({
apiKey: 'sk-test'
})
const fns = [...weather.functions]
console.log(fns)
expect(weather.functions.get('getCurrentWeather')).toBeTruthy()
})

Wyświetl plik

@ -19,3 +19,11 @@ if (typeof Symbol === 'function' && Symbol.metadata) {
value: _metadata value: _metadata
}) })
} }
// export {};
// declare global {
// interface SymbolConstructor {
// readonly metadata: unique symbol
// }
// }
// (Symbol as any).metadata ??= Symbol.for("Symbol.metadata")

Wyświetl plik

@ -40,6 +40,8 @@ export interface AIFunction<
/** The function spec for the OpenAI API `functions` property. */ /** The function spec for the OpenAI API `functions` property. */
spec: AIFunctionSpec spec: AIFunctionSpec
impl: (params: z.infer<InputSchema>) => MaybePromise<Return>
} }
/** /**

Wyświetl plik

@ -12,6 +12,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"jsx": "preserve", "jsx": "preserve",
// NOTE: these are deprecated
// "experimentalDecorators": true, // "experimentalDecorators": true,
// "emitDecoratorMetadata": true, // "emitDecoratorMetadata": true,

7
vite.config.ts 100644
Wyświetl plik

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
esbuild: {
target: 'es2022'
}
})