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
18 changes: 9 additions & 9 deletions examples/meta-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import process from 'node:process';
import { openai } from '@ai-sdk/openai';
import { StackOneToolSet, Tools } from '@stackone/ai';
import { type JsonObject, StackOneToolSet, Tools } from '@stackone/ai';
import { generateText, stepCountIs } from 'ai';

const apiKey = process.env.STACKONE_API_KEY;
Expand Down Expand Up @@ -160,7 +160,7 @@ const directMetaToolUsage = async (): Promise<void> => {

try {
// Prepare parameters based on the tool's schema
let params: Record<string, unknown> = {};
let params = {} satisfies JsonObject;
if (firstTool.name === 'bamboohr_list_employees') {
params = { limit: 5 };
} else if (firstTool.name === 'bamboohr_create_employee') {
Expand All @@ -173,7 +173,7 @@ const directMetaToolUsage = async (): Promise<void> => {

const result = await executeTool.execute({
toolName: firstTool.name,
params: params,
params,
});

console.log('Execution result:', JSON.stringify(result, null, 2));
Expand Down Expand Up @@ -209,7 +209,7 @@ const dynamicToolRouter = async (): Promise<void> => {
const metaTools = await combinedTools.metaTools();

// Create a router function that finds and executes tools based on intent
const routeAndExecute = async (intent: string, params: Record<string, unknown> = {}) => {
const routeAndExecute = async (intent: string, params: JsonObject = {}) => {
const filterTool = metaTools.getTool('meta_search_tools');
const executeTool = metaTools.getTool('meta_execute_tool');
if (!filterTool || !executeTool) throw new Error('Meta tools not found');
Expand All @@ -221,18 +221,18 @@ const dynamicToolRouter = async (): Promise<void> => {
minScore: 0.5,
});

const tools = searchResult.tools as Array<{ name: string; score: number }>;
if (tools.length === 0) {
const tools = searchResult.tools;
if (!Array.isArray(tools) || tools.length === 0) {
return { error: 'No relevant tools found for the given intent' };
}

const selectedTool = tools[0];
const selectedTool = tools[0] as { name: string; score: number };
console.log(`Routing to: ${selectedTool.name} (score: ${selectedTool.score.toFixed(2)})`);

// Execute the selected tool
return await executeTool.execute({
toolName: selectedTool.name,
params: params,
params,
});
};

Expand All @@ -244,7 +244,7 @@ const dynamicToolRouter = async (): Promise<void> => {
params: { name: 'Jane Smith', email: 'jane@example.com' },
},
{ intent: 'Find recruitment candidates', params: { status: 'active' } },
];
] as const satisfies { intent: string; params: JsonObject }[];

for (const { intent, params } of intents) {
console.log(`\nIntent: "${intent}"`);
Expand Down
19 changes: 6 additions & 13 deletions examples/tanstack-ai-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,19 @@ describe('tanstack-ai-integration example e2e', () => {

// Get a specific tool
const employeeTool = tools.getTool('bamboohr_get_employee');
expect(employeeTool).toBeDefined();
assert(employeeTool, 'Expected bamboohr_get_employee tool to exist');

// Create TanStack AI compatible tool wrapper
// Use toJsonSchema() to get the parameter schema in JSON Schema format
const getEmployeeTool = {
name: employeeTool!.name,
description: employeeTool!.description,
inputSchema: employeeTool!.toJsonSchema(),
execute: async (args: Record<string, unknown>) => {
return employeeTool!.execute(args);
},
name: employeeTool.name,
description: employeeTool.description,
inputSchema: employeeTool.toJsonSchema(),
execute: employeeTool.execute.bind(employeeTool),
};

expect(getEmployeeTool.name).toBe('bamboohr_get_employee');
expect(getEmployeeTool.description).toContain('employee');
expect(getEmployeeTool.inputSchema).toBeDefined();
expect(getEmployeeTool.inputSchema.type).toBe('object');
expect(typeof getEmployeeTool.execute).toBe('function');
});

it('should execute tool directly', async () => {
Expand All @@ -68,9 +63,7 @@ describe('tanstack-ai-integration example e2e', () => {
name: employeeTool.name,
description: employeeTool.description,
inputSchema: employeeTool.toJsonSchema(),
execute: async (args: Record<string, unknown>) => {
return employeeTool.execute(args);
},
execute: employeeTool.execute.bind(employeeTool),
};

// Execute the tool directly to verify it works
Expand Down
20 changes: 10 additions & 10 deletions src/feedback.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { DEFAULT_BASE_URL } from './consts';
import { BaseTool } from './tool';
import type { ExecuteConfig, ExecuteOptions, JsonDict, ToolParameters } from './types';
import type { ExecuteConfig, ExecuteOptions, JsonObject, JsonValue, ToolParameters } from './types';
import { StackOneError } from './utils/errors';

interface FeedbackToolOptions {
Expand Down Expand Up @@ -107,9 +107,9 @@ export function createFeedbackTool(

tool.execute = async function (
this: BaseTool,
inputParams?: JsonDict | string,
inputParams?: JsonObject | string,
executeOptions?: ExecuteOptions,
): Promise<JsonDict> {
): Promise<JsonObject> {
try {
const rawParams =
typeof inputParams === 'string' ? JSON.parse(inputParams) : inputParams || {};
Expand Down Expand Up @@ -137,12 +137,12 @@ export function createFeedbackTool(
return {
multiple_requests: dryRunResults,
total_accounts: parsedParams.account_id.length,
} satisfies JsonDict;
} satisfies JsonObject;
}

// Send feedback to each account individually
const results = [];
const errors = [];
const results: Array<{ account_id: string; status: number; response: JsonValue }> = [];
const errors: Array<{ account_id: string; status?: number; error: string }> = [];

for (const accountId of parsedParams.account_id) {
try {
Expand All @@ -159,9 +159,9 @@ export function createFeedbackTool(
});

const text = await response.text();
let parsed: unknown;
let parsed: JsonValue;
try {
parsed = text ? JSON.parse(text) : {};
parsed = text ? (JSON.parse(text) satisfies JsonValue) : {};
} catch {
parsed = { raw: text };
}
Expand Down Expand Up @@ -191,7 +191,7 @@ export function createFeedbackTool(
}

// Return summary of all submissions in Python SDK format
const response: JsonDict = {
const response = {
message: `Feedback sent to ${parsedParams.account_id.length} account(s)`,
total_accounts: parsedParams.account_id.length,
successful: results.length,
Expand All @@ -208,7 +208,7 @@ export function createFeedbackTool(
error: e.error,
})),
],
};
} satisfies JsonObject;

// If all submissions failed, throw an error
if (errors.length > 0 && results.length === 0) {
Expand Down
8 changes: 0 additions & 8 deletions src/headers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { describe, expect, it } from 'vitest';
import { normaliseHeaders } from './headers';

describe('normaliseHeaders', () => {
Expand Down Expand Up @@ -43,12 +42,6 @@ describe('normaliseHeaders', () => {
});
});

it('skips undefined values', () => {
expect(normaliseHeaders({ foo: 'bar', baz: undefined })).toEqual({
foo: 'bar',
});
});

it('skips null values', () => {
expect(normaliseHeaders({ foo: 'bar', baz: null })).toEqual({
foo: 'bar',
Expand All @@ -64,7 +57,6 @@ describe('normaliseHeaders', () => {
object: { nested: 'value' },
array: [1, 2, 3],
nullValue: null,
undefinedValue: undefined,
}),
).toEqual({
string: 'text',
Expand Down
8 changes: 4 additions & 4 deletions src/headers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod/mini';
import type { JsonDict } from './types';
import type { JsonObject } from './types';

/**
* Known StackOne API header keys that are forwarded as HTTP headers
Expand All @@ -18,13 +18,13 @@ export const stackOneHeadersSchema = z.record(z.string(), z.string()).brand<'Sta
export type StackOneHeaders = z.infer<typeof stackOneHeadersSchema>;

/**
* Normalises header values from JsonDict to StackOneHeaders (branded type)
* Normalises header values from JsonObject to StackOneHeaders (branded type)
* Converts numbers and booleans to strings, and serialises objects to JSON
*
* @param headers - Headers object with unknown value types
* @param headers - Headers object with JSON value types
* @returns Normalised headers with string values only (branded type)
*/
export function normaliseHeaders(headers: JsonDict | undefined): StackOneHeaders {
export function normaliseHeaders(headers: JsonObject | undefined): StackOneHeaders {
if (!headers) return stackOneHeadersSchema.parse({});
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export type {
AISDKToolResult,
ExecuteConfig,
ExecuteOptions,
JsonDict,
JsonObject,
JsonValue,
ParameterLocation,
ToolDefinition,
} from './types';
Loading
Loading