Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ jobs:
with:
python-version: "3.11"
enable-cache: true
cache-dependency-glob: "**/requirements-docs.txt"

- name: Install all dependencies
run: uv pip install -r requirements-docs.txt
- name: Install Python dependencies
run: uv pip install --system -r requirements-docs.txt

- name: Install bun
uses: oven-sh/setup-bun@v2

- name: Install dependencies
- name: Install Node dependencies
run: bun install

- name: Build documentation
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const toolset = new StackOneToolSet();
const tools = toolset.getTools('hris_*', 'your-account-id');

// Convert to OpenAI functions
const openAIFunctions = tools.toOpenAIFunctions();
const openAITools = tools.toOpenAI();

// Use with OpenAI
const openai = new OpenAI({
Expand All @@ -102,7 +102,7 @@ const response = await openai.chat.completions.create({
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'List all employees' },
],
tools: openAIFunctions,
tools: openAITools,
});
```

Expand Down
1 change: 1 addition & 0 deletions requirements-docs.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mkdocs
mkdocs-terminal
pygments
pymdown-extensions
69 changes: 41 additions & 28 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/// <reference types="bun-types" />
import { jsonSchema, tool } from 'ai';
import { type Schema, type Tool, type ToolExecutionOptions, jsonSchema, tool } from 'ai';
// Import OpenAPI and JSON Schema types
import type { JSONSchema7, JSONSchema7Definition } from 'json-schema';
import type { ChatCompletionTool } from 'openai/resources/chat/completions';

// Type aliases for common types
export type JsonDict = Record<string, unknown>;
export type Headers = Record<string, string>;

// JSON Schema related types
export type JsonSchemaProperties = Record<string, JSONSchema7Definition>;
export type JsonSchemaType = JSONSchema7['type'];

/**
* Base exception for StackOne errors
*/
Expand Down Expand Up @@ -58,7 +65,7 @@ export interface ExecuteConfig {
*/
export interface ToolParameters {
type: string;
properties: JsonDict;
properties: JsonSchemaProperties;
}

/**
Expand Down Expand Up @@ -213,7 +220,12 @@ export class StackOneTool {
} else if (bodyType === 'multipart') {
const formData = new FormData();
for (const [key, value] of Object.entries(bodyParams)) {
formData.append(key, value);
// Convert value to string or Blob as required by FormData.append
if (value instanceof Blob) {
formData.append(key, value);
} else {
formData.append(key, String(value));
}
}
fetchOptions.body = formData;
// Content-Type is automatically set by the browser for FormData
Expand All @@ -225,7 +237,7 @@ export class StackOneTool {

// Handle errors
if (!response.ok) {
let responseBody = null;
let responseBody: string | JsonDict = '';
try {
responseBody = await response.json();
} catch (_e) {
Expand All @@ -241,7 +253,7 @@ export class StackOneTool {

// Parse response
const result = await response.json();
return typeof result === 'object' ? result : { result };
return typeof result === 'object' && result !== null ? result : { result };
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Invalid JSON in arguments: ${error.message}`);
Expand All @@ -254,15 +266,16 @@ export class StackOneTool {
* Convert this tool to OpenAI's tool format
* @returns Tool definition in OpenAI tool format
*/
toOpenAI(): JsonDict {
toOpenAI(): ChatCompletionTool {
// Clean properties and handle special types
const properties: JsonDict = {};
const properties: Record<string, JSONSchema7> = {};
const required: string[] = [];

for (const [name, prop] of Object.entries(this.parameters.properties)) {
if (typeof prop === 'object') {
for (const [name, propValue] of Object.entries(this.parameters.properties)) {
if (typeof propValue === 'object' && propValue !== null) {
const prop = propValue as JSONSchema7;
// Only keep standard JSON Schema properties
const cleanedProp: JsonDict = {};
const cleanedProp: JSONSchema7 = {};

// Copy basic properties
if ('type' in prop) {
Expand All @@ -278,12 +291,11 @@ export class StackOneTool {
// Handle array types
if (cleanedProp.type === 'array') {
// Ensure all arrays have an items property
if ('items' in prop && typeof prop.items === 'object') {
if ('items' in prop && typeof prop.items === 'object' && prop.items !== null) {
const itemsObj = prop.items as JSONSchema7;
cleanedProp.items = Object.fromEntries(
Object.entries(prop.items).filter(([k]) =>
['type', 'description', 'enum'].includes(k)
)
);
Object.entries(itemsObj).filter(([k]) => ['type', 'description', 'enum'].includes(k))
) as JSONSchema7;
} else {
// Default to string items if not specified
cleanedProp.items = { type: 'string' };
Expand All @@ -292,9 +304,10 @@ export class StackOneTool {

// Handle object types
if (cleanedProp.type === 'object' && 'properties' in prop) {
const propProperties = prop.properties as Record<string, JSONSchema7>;
cleanedProp.properties = Object.fromEntries(
Object.entries(prop.properties).map(([k, v]) => {
const propValue = v as JsonDict;
Object.entries(propProperties).map(([k, v]) => {
const propValue = v as JSONSchema7;
// Recursively ensure arrays in nested objects have items
if (propValue.type === 'array' && !('items' in propValue)) {
return [k, { ...propValue, items: { type: 'string' } }];
Expand All @@ -305,10 +318,10 @@ export class StackOneTool {
Object.entries(propValue).filter(([sk]) =>
['type', 'description', 'enum', 'items'].includes(sk)
)
),
) as JSONSchema7,
];
})
);
) as Record<string, JSONSchema7>;
}

properties[name] = cleanedProp;
Expand Down Expand Up @@ -336,11 +349,11 @@ export class StackOneTool {
toAISDKTool() {
// Create a wrapper function that will handle the execution
const executeWrapper = async (
args: JsonDict,
_options: { toolCallId: string; messages: JsonDict[]; abortSignal?: AbortSignal }
) => {
args: unknown,
_options: ToolExecutionOptions
): Promise<JsonDict> => {
try {
return await this.execute(args);
return await this.execute(args as JsonDict);
} catch (error) {
if (error instanceof StackOneError) {
throw new Error(`StackOne Error: ${error.message}`);
Expand All @@ -353,7 +366,7 @@ export class StackOneTool {
const openAIFormat = this.toOpenAI();

// Use the OpenAI function parameters as our JSON schema
const schema = jsonSchema(openAIFormat.function.parameters);
const schema = jsonSchema(openAIFormat.function.parameters as JSONSchema7);

// Return the AI SDK tool
return tool({
Expand Down Expand Up @@ -394,16 +407,16 @@ export class Tools {
* Convert all tools to OpenAI format
* @returns Array of tools in the format expected by OpenAI's API
*/
toOpenAI(): JsonDict[] {
toOpenAI(): ChatCompletionTool[] {
return this.tools.map((tool) => tool.toOpenAI());
}

/**
* Convert all tools to AI SDK tools
* @returns Object with tool names as keys and AI SDK tools as values
*/
toAISDKTools() {
const result: Record<string, any> = {};
toAISDKTools(): Record<string, Tool<Schema<unknown>, JsonDict>> {
const result: Record<string, Tool<Schema<unknown>, JsonDict>> = {};

for (const stackOneTool of this.tools) {
result[stackOneTool.name] = stackOneTool.toAISDKTool();
Expand All @@ -424,7 +437,7 @@ export class Tools {
if (index < tools.length) {
return { value: tools[index++], done: false };
}
return { value: undefined as any, done: true };
return { value: undefined, done: true };
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/tests/fetch-specs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('fetch-specs script', () => {

// Test fetchSpec function
const hrisSpec = await fetchSpec('hris');
expect(hrisSpec.info.title).toBe('HRIS API');
expect((hrisSpec.info as { title: string }).title).toBe('HRIS API');
expect(mockFetch).toHaveBeenCalledTimes(1);

// Reset mock call count
Expand Down
27 changes: 19 additions & 8 deletions src/tests/models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ describe('StackOneTool', () => {

expect(tool.name).toBe('test_tool');
expect(tool.description).toBe('Test tool');
expect(tool.parameters.type).toBe('object');
expect(tool.parameters.properties.id.type).toBe('string');
expect((tool.parameters as { type: string }).type).toBe('object');
expect(
(tool.parameters as unknown as { properties: { id: { type: string } } }).properties.id.type
).toBe('string');
});

it('should execute with parameters', async () => {
Expand Down Expand Up @@ -111,8 +113,11 @@ describe('StackOneTool', () => {
expect(openAIFormat.type).toBe('function');
expect(openAIFormat.function.name).toBe('test_tool');
expect(openAIFormat.function.description).toBe('Test tool');
expect(openAIFormat.function.parameters.type).toBe('object');
expect(openAIFormat.function.parameters.properties.id.type).toBe('string');
expect(openAIFormat.function.parameters?.type).toBe('object');
expect(
(openAIFormat.function.parameters as { properties: { id: { type: string } } }).properties.id
.type
).toBe('string');
});

it('should convert to AI SDK tool format', () => {
Expand All @@ -132,7 +137,10 @@ describe('StackOneTool', () => {
expect(aiSdkTool.parameters.jsonSchema.type).toBe('object');

// Use type assertions to handle possibly undefined properties
const properties = aiSdkTool.parameters.jsonSchema.properties as Record<string, any>;
const properties = aiSdkTool.parameters.jsonSchema.properties as Record<
string,
{ type: string }
>;
expect(properties).toBeDefined();
expect(properties.id).toBeDefined();
expect(properties.id.type).toBe('string');
Expand Down Expand Up @@ -180,7 +188,7 @@ describe('StackOneTool', () => {
expect(schema.type).toBe('object');

// Use type assertions to handle possibly undefined properties
const properties = schema.properties as Record<string, any>;
const properties = schema.properties as Record<string, { type: string }>;
expect(properties.stringParam.type).toBe('string');
expect(properties.numberParam.type).toBe('number');
expect(properties.booleanParam.type).toBe('boolean');
Expand Down Expand Up @@ -249,8 +257,11 @@ describe('Tools', () => {
expect(openAITools[0].type).toBe('function');
expect(openAITools[0].function.name).toBe('test_tool');
expect(openAITools[0].function.description).toBe('Test tool');
expect(openAITools[0].function.parameters.type).toBe('object');
expect(openAITools[0].function.parameters.properties.id.type).toBe('string');
expect(openAITools[0].function.parameters?.type).toBe('object');
expect(
(openAITools[0].function.parameters as { properties: { id: { type: string } } }).properties.id
.type
).toBe('string');
});

it('should convert all tools to AI SDK tools', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/tests/openapi-loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Loader', () => {

// Mock fs.readdirSync to return test files
const mockReadDirSync = spyOn(fs, 'readdirSync');
(mockReadDirSync.mockImplementation as (callback: () => string[]) => void)(() => [
(mockReadDirSync.mockImplementation as unknown as (callback: () => string[]) => void)(() => [
'hris.json',
'ats.json',
'not-json.txt',
Expand All @@ -85,7 +85,7 @@ describe('Loader', () => {

return Buffer.from('{}');
}
) as typeof fs.readFileSync;
) as unknown as typeof fs.readFileSync;

const specs = loadSpecs();

Expand Down
Loading