diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 664faed7..f9259233 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -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
diff --git a/README.md b/README.md
index fcba8323..c2b46182 100644
--- a/README.md
+++ b/README.md
@@ -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({
@@ -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,
});
```
diff --git a/requirements-docs.txt b/requirements-docs.txt
index 66dd0e7d..6e5b62eb 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,3 +1,4 @@
+mkdocs
mkdocs-terminal
pygments
pymdown-extensions
\ No newline at end of file
diff --git a/src/models.ts b/src/models.ts
index dfee0324..34a90420 100644
--- a/src/models.ts
+++ b/src/models.ts
@@ -1,10 +1,17 @@
///
-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;
export type Headers = Record;
+// JSON Schema related types
+export type JsonSchemaProperties = Record;
+export type JsonSchemaType = JSONSchema7['type'];
+
/**
* Base exception for StackOne errors
*/
@@ -58,7 +65,7 @@ export interface ExecuteConfig {
*/
export interface ToolParameters {
type: string;
- properties: JsonDict;
+ properties: JsonSchemaProperties;
}
/**
@@ -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
@@ -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) {
@@ -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}`);
@@ -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 = {};
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) {
@@ -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' };
@@ -292,9 +304,10 @@ export class StackOneTool {
// Handle object types
if (cleanedProp.type === 'object' && 'properties' in prop) {
+ const propProperties = prop.properties as Record;
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' } }];
@@ -305,10 +318,10 @@ export class StackOneTool {
Object.entries(propValue).filter(([sk]) =>
['type', 'description', 'enum', 'items'].includes(sk)
)
- ),
+ ) as JSONSchema7,
];
})
- );
+ ) as Record;
}
properties[name] = cleanedProp;
@@ -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 => {
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}`);
@@ -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({
@@ -394,7 +407,7 @@ 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());
}
@@ -402,8 +415,8 @@ export class Tools {
* Convert all tools to AI SDK tools
* @returns Object with tool names as keys and AI SDK tools as values
*/
- toAISDKTools() {
- const result: Record = {};
+ toAISDKTools(): Record, JsonDict>> {
+ const result: Record, JsonDict>> = {};
for (const stackOneTool of this.tools) {
result[stackOneTool.name] = stackOneTool.toAISDKTool();
@@ -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 };
},
};
}
diff --git a/src/tests/fetch-specs.spec.ts b/src/tests/fetch-specs.spec.ts
index 1d2c421f..67b97ff8 100644
--- a/src/tests/fetch-specs.spec.ts
+++ b/src/tests/fetch-specs.spec.ts
@@ -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
diff --git a/src/tests/models.spec.ts b/src/tests/models.spec.ts
index a58f444a..e675d3a6 100644
--- a/src/tests/models.spec.ts
+++ b/src/tests/models.spec.ts
@@ -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 () => {
@@ -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', () => {
@@ -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;
+ const properties = aiSdkTool.parameters.jsonSchema.properties as Record<
+ string,
+ { type: string }
+ >;
expect(properties).toBeDefined();
expect(properties.id).toBeDefined();
expect(properties.id.type).toBe('string');
@@ -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;
+ const properties = schema.properties as Record;
expect(properties.stringParam.type).toBe('string');
expect(properties.numberParam.type).toBe('number');
expect(properties.booleanParam.type).toBe('boolean');
@@ -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', () => {
diff --git a/src/tests/openapi-loader.spec.ts b/src/tests/openapi-loader.spec.ts
index 868b600c..0031e6e9 100644
--- a/src/tests/openapi-loader.spec.ts
+++ b/src/tests/openapi-loader.spec.ts
@@ -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',
@@ -85,7 +85,7 @@ describe('Loader', () => {
return Buffer.from('{}');
}
- ) as typeof fs.readFileSync;
+ ) as unknown as typeof fs.readFileSync;
const specs = loadSpecs();
diff --git a/src/tests/schema-validation.spec.ts b/src/tests/schema-validation.spec.ts
index de800a88..f4e9c09d 100644
--- a/src/tests/schema-validation.spec.ts
+++ b/src/tests/schema-validation.spec.ts
@@ -1,9 +1,10 @@
import { describe, expect, it } from 'bun:test';
import { jsonSchema } from 'ai';
+import type { JSONSchema7 } from 'json-schema';
import { StackOneTool } from '../models';
// Helper function to validate array items in a schema
-const validateArrayItems = (obj: any, path = ''): string[] => {
+const validateArrayItems = (obj: Record, path = ''): string[] => {
const errors: string[] = [];
if (typeof obj !== 'object' || obj === null) {
@@ -18,16 +19,16 @@ const validateArrayItems = (obj: any, path = ''): string[] => {
}
// Recursively check properties
- if (obj.properties) {
+ if (obj.properties && typeof obj.properties === 'object') {
for (const [key, value] of Object.entries(obj.properties)) {
const nestedPath = path ? `${path}.${key}` : key;
- errors.push(...validateArrayItems(value, nestedPath));
+ errors.push(...validateArrayItems(value as Record, nestedPath));
}
}
// Check items of arrays
if (obj.items && typeof obj.items === 'object') {
- errors.push(...validateArrayItems(obj.items, `${path}.items`));
+ errors.push(...validateArrayItems(obj.items as Record, `${path}.items`));
}
return errors;
@@ -135,54 +136,90 @@ describe('Schema Validation', () => {
const tool = createArrayTestTool();
const openAIFormat = tool.toOpenAI();
- const errors = validateArrayItems(openAIFormat.function.parameters);
+ const parameters = openAIFormat.function.parameters;
+ if (!parameters) {
+ throw new Error('Parameters should be defined');
+ }
+
+ const errors = validateArrayItems(parameters as Record);
expect(errors.length).toBe(0);
});
it('should handle simple arrays without items', () => {
const tool = createArrayTestTool();
const openAIFormat = tool.toOpenAI();
+ const parameters = openAIFormat.function.parameters;
+
+ if (!parameters || !parameters.properties) {
+ throw new Error('Parameters or properties should be defined');
+ }
- // Check that simpleArray has items
- const simpleArray = openAIFormat.function.parameters.properties.simpleArray;
+ // TypeScript doesn't know the structure of properties, so we need to cast
+ const properties = parameters.properties as Record;
+ const simpleArray = properties.simpleArray;
expect(simpleArray.items).toBeDefined();
- expect(simpleArray.items.type).toBe('string');
+ expect((simpleArray.items as JSONSchema7).type).toBe('string');
});
it('should preserve existing array items', () => {
const tool = createArrayTestTool();
const openAIFormat = tool.toOpenAI();
+ const parameters = openAIFormat.function.parameters;
+
+ if (!parameters || !parameters.properties) {
+ throw new Error('Parameters or properties should be defined');
+ }
- // Check that arrayWithItems preserved its items
- const arrayWithItems = openAIFormat.function.parameters.properties.arrayWithItems;
+ // TypeScript doesn't know the structure of properties, so we need to cast
+ const properties = parameters.properties as Record;
+ const arrayWithItems = properties.arrayWithItems;
expect(arrayWithItems.items).toBeDefined();
- expect(arrayWithItems.items.type).toBe('string');
+ expect((arrayWithItems.items as JSONSchema7).type).toBe('string');
});
it('should handle nested arrays in objects', () => {
const tool = createArrayTestTool();
const openAIFormat = tool.toOpenAI();
+ const parameters = openAIFormat.function.parameters;
+
+ if (!parameters || !parameters.properties) {
+ throw new Error('Parameters or properties should be defined');
+ }
+
+ // TypeScript doesn't know the structure of properties, so we need to cast
+ const properties = parameters.properties as Record;
+ const nestedObject = properties.nestedObject;
+ if (!nestedObject.properties) {
+ throw new Error('Nested object properties should be defined');
+ }
- // Check that nestedArray has items
- const nestedArray =
- openAIFormat.function.parameters.properties.nestedObject.properties.nestedArray;
+ const nestedArray = nestedObject.properties.nestedArray as JSONSchema7;
expect(nestedArray.items).toBeDefined();
});
it('should handle deeply nested arrays', () => {
const tool = createArrayTestTool();
const openAIFormat = tool.toOpenAI();
+ const parameters = openAIFormat.function.parameters;
- // The structure is simplified in the OpenAI format
- // Just verify that level1 exists and is an object
- const deeplyNestedProperties =
- openAIFormat.function.parameters.properties.deeplyNested.properties;
- expect(deeplyNestedProperties.level1).toBeDefined();
- expect(deeplyNestedProperties.level1.type).toBe('object');
+ if (!parameters || !parameters.properties) {
+ throw new Error('Parameters or properties should be defined');
+ }
+
+ // TypeScript doesn't know the structure of properties, so we need to cast
+ const properties = parameters.properties as Record;
+ const deeplyNested = properties.deeplyNested;
+ if (!deeplyNested.properties) {
+ throw new Error('Deeply nested properties should be defined');
+ }
+
+ const level1 = deeplyNested.properties.level1 as JSONSchema7;
+ expect(level1).toBeDefined();
+ expect(level1.type).toBe('object');
// Since we can't directly test the deeply nested array (it's simplified in the output),
// we'll verify our validation function doesn't find any errors
- const errors = validateArrayItems(openAIFormat.function.parameters);
+ const errors = validateArrayItems(parameters as Record);
expect(errors.length).toBe(0);
});
});
@@ -199,14 +236,24 @@ describe('Schema Validation', () => {
it('should handle the problematic nested array case', () => {
const tool = createNestedArrayTestTool();
const openAIFormat = tool.toOpenAI();
+ const parameters = openAIFormat.function.parameters;
+
+ if (!parameters || !parameters.properties) {
+ throw new Error('Parameters or properties should be defined');
+ }
+
+ // TypeScript doesn't know the structure of properties, so we need to cast
+ const properties = parameters.properties as Record;
+ const filter = properties.filter;
+ if (!filter.properties) {
+ throw new Error('Filter properties should be defined');
+ }
- // Check if the nested array has items
- const typeIds = openAIFormat.function.parameters.properties.filter.properties.type_ids;
+ const typeIds = filter.properties.type_ids as JSONSchema7;
expect(typeIds.items).toBeDefined();
// Verify that the schema can be used with jsonSchema
- const schema = openAIFormat.function.parameters;
- const aiSchema = jsonSchema(schema);
+ const aiSchema = jsonSchema(parameters);
expect(aiSchema).toBeDefined();
});
});
diff --git a/src/tests/toolset.spec.ts b/src/tests/toolset.spec.ts
index 78918c8e..717dac09 100644
--- a/src/tests/toolset.spec.ts
+++ b/src/tests/toolset.spec.ts
@@ -1,6 +1,6 @@
import { describe, expect, it, spyOn } from 'bun:test';
import { env } from 'bun';
-import { ParameterLocation, StackOneTool } from '../models';
+import { ParameterLocation, StackOneTool, type Tools } from '../models';
import { OpenAPIParser } from '../openapi/parser';
import { StackOneToolSet } from '../toolset';
@@ -69,7 +69,9 @@ describe('StackOneToolSet', () => {
[Symbol.iterator]: function* () {
yield tool;
},
- } as Tools;
+ toOpenAI: () => [tool.toOpenAI()],
+ toAISDKTools: () => ({ [tool.name]: tool.toAISDKTool() }),
+ } as unknown as Tools;
}
// Return empty tools collection for non-matching filter
@@ -77,7 +79,9 @@ describe('StackOneToolSet', () => {
length: 0,
getTool: () => undefined,
[Symbol.iterator]: function* () {},
- } as Tools;
+ toOpenAI: () => [],
+ toAISDKTools: () => ({}),
+ } as unknown as Tools;
};
try {
@@ -108,7 +112,9 @@ describe('StackOneToolSet', () => {
length: 0,
getTool: () => undefined,
[Symbol.iterator]: function* () {},
- } as Tools;
+ toOpenAI: () => [],
+ toAISDKTools: () => ({}),
+ } as unknown as Tools;
};
try {
@@ -142,7 +148,7 @@ describe('StackOneToolSet', () => {
execute: {
headers: {},
method: 'GET',
- url: `${(this as { baseUrl: string }).baseUrl}/test/{id}`,
+ url: `${(this as unknown as { baseUrl: string }).baseUrl}/test/{id}`,
name: 'test_tool',
parameterLocations: { id: ParameterLocation.PATH },
},
@@ -201,7 +207,7 @@ describe('StackOneToolSet', () => {
execute: {
headers: {},
method: 'GET',
- url: `${(this as { baseUrl: string }).baseUrl}/hris/employees/{id}`,
+ url: `${(this as unknown as { baseUrl: string }).baseUrl}/hris/employees/{id}`,
name: 'hris_get_employee',
parameterLocations: { id: ParameterLocation.PATH },
},