Skip to content

Commit

Permalink
Merge pull request #9 from atinylittleshell/features/primitive_arrays
Browse files Browse the repository at this point in the history
support object fields that are array of primitives
  • Loading branch information
atinylittleshell committed Aug 20, 2023
2 parents 4913bb9 + d67864d commit c7df755
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-meals-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'function-gpt': minor
---

support object fields that are array of primitives
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class BrowseSession extends ChatGPTSession {
// Define the type of the input parameter for functions above.
class BrowseParams {
// Decorate each field with @gptObjectField to provide necessary metadata.
@gptObjectField('string', 'url of the web page to browse', true)
public url: string = '';
@gptObjectField('string', 'url of the web page to browse')
public url!: string;
}

const session = new BrowseSession();
Expand Down
24 changes: 16 additions & 8 deletions src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export function gptFunction(description: string, inputType: new () => unknown) {
}

export function gptObjectField(
type: 'string' | 'number' | 'boolean' | (new () => unknown) | [new () => unknown],
type: 'string' | 'number' | 'boolean' | ['string' | 'number' | 'boolean'] | (new () => unknown) | [new () => unknown],
description: string,
required = true,
optional = false,
) {
return function (target: object, propertyKey: string) {
const ctor = target.constructor as new () => unknown;
Expand All @@ -51,38 +51,46 @@ export function gptObjectField(
name: propertyKey,
description,
type: { type: 'string' },
required,
required: !optional,
});
} else if (type === 'number') {
metadata.fields.push({
name: propertyKey,
description,
type: { type: 'number' },
required,
required: !optional,
});
} else if (type === 'boolean') {
metadata.fields.push({
name: propertyKey,
description,
type: { type: 'boolean' },
required,
required: !optional,
});
} else if (Array.isArray(type)) {
const elementType = type[0];
metadata.fields.push({
name: propertyKey,
description,
type: {
type: 'array',
elementType: GPT_TYPE_METADATA.get(type[0]) as GPTTypeMetadata,
elementType:
elementType === 'string'
? { type: 'string' }
: elementType === 'number'
? { type: 'number' }
: elementType === 'boolean'
? { type: 'boolean' }
: (GPT_TYPE_METADATA.get(elementType) as GPTTypeMetadata),
},
required,
required: !optional,
});
} else if (typeof type === 'function') {
metadata.fields.push({
name: propertyKey,
description,
type: GPT_TYPE_METADATA.get(type) as GPTTypeMetadata,
required,
required: !optional,
});
}

Expand Down
81 changes: 81 additions & 0 deletions tests/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, test } from 'vitest';

import { ChatGPTSession, gptFunction, gptObjectField } from '../index.js';

process.env.OPENAI_API_KEY = 'test';

test('basic function schema is generated correctly', async () => {
class TestFuncInput {
@gptObjectField('string', 'this is a test string', true)
public testString!: string;

@gptObjectField('number', 'this is a test number', false)
public testNumber!: number;
}

class TestSession extends ChatGPTSession {
@gptFunction('this is a test function', TestFuncInput)
testFunc(params: TestFuncInput) {
return params;
}
}

const testSession = new TestSession();
const schema = testSession.getFunctionSchema();
expect(schema).toEqual([
{
name: 'testFunc',
description: 'this is a test function',
parameters: {
type: 'object',
properties: {
testString: {
type: 'string',
description: 'this is a test string',
},
testNumber: {
type: 'number',
description: 'this is a test number',
},
},
required: ['testNumber'],
},
},
]);
});

test('input parameter can be an array of strings', () => {
class TestParam {
@gptObjectField(['string'], 'test words')
words!: string[];
}

class TestSession extends ChatGPTSession {
@gptFunction('this is a test function', TestParam)
testFunc(params: TestParam) {
return params;
}
}

const testSession = new TestSession();
const schema = testSession.getFunctionSchema();
expect(schema).toEqual([
{
name: 'testFunc',
description: 'this is a test function',
parameters: {
type: 'object',
properties: {
words: {
type: 'array',
items: {
type: 'string',
},
description: 'test words',
},
},
required: ['words'],
},
},
]);
});
46 changes: 0 additions & 46 deletions tests/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,52 +57,6 @@ afterEach(() => {
vi.clearAllMocks();
});

test('function schema is generated correctly', async () => {
class TestFuncInput {
@gptObjectField('string', 'this is a test string', true)
public testString: string = '';

@gptObjectField('number', 'this is a test number', false)
public testNumber: number = 0;
}

class TestSession extends ChatGPTSession {
constructor() {
super({
apiKey: 'test',
});
}

@gptFunction('this is a test function', TestFuncInput)
testFunc(params: TestFuncInput) {
return params;
}
}

const testSession = new TestSession();
const schema = testSession.getFunctionSchema();
expect(schema).toEqual([
{
name: 'testFunc',
description: 'this is a test function',
parameters: {
type: 'object',
properties: {
testString: {
type: 'string',
description: 'this is a test string',
},
testNumber: {
type: 'number',
description: 'this is a test number',
},
},
required: ['testString'],
},
},
]);
});

const fetch = vi.fn().mockImplementation(() => Promise.resolve());

test('function calling should work', async () => {
Expand Down

0 comments on commit c7df755

Please sign in to comment.