Added ToolResponse interface to standardize block response format, updated executor to use response transformation specified in the tool it is using, confirmed agent & api tool working individually

This commit is contained in:
Waleed Latif
2025-01-19 23:36:26 -08:00
parent 2f3fa0c059
commit f23350e6b4
11 changed files with 112 additions and 157 deletions

View File

@@ -14,7 +14,7 @@ export const ApiBlock: BlockConfig = {
access: ['http.request']
},
workflow: {
outputType: 'json',
outputType: 'any',
inputs: {
url: 'string',
method: 'string',

View File

@@ -3,7 +3,7 @@ import type { JSX } from 'react'
export type BlockIcon = (props: SVGProps<SVGSVGElement>) => JSX.Element
export type BlockCategory = 'basic' | 'advanced'
export type OutputType = 'string' | 'number' | 'json' | 'boolean'
export type OutputType = 'string' | 'number' | 'json' | 'boolean' | 'any'
export type ParamType = 'string' | 'number' | 'boolean' | 'json'
export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code'

View File

@@ -52,12 +52,11 @@ export class Executor {
});
if (!response.ok) {
const error = await response.json();
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(tool.transformError(error));
}
const data = await response.json();
const result = tool.transformResponse(data);
const result = await tool.transformResponse(response);
// Validate the output matches the interface
this.validateToolOutput(block, result);

View File

@@ -1,4 +1,4 @@
import { ToolConfig } from '../types';
import { ToolConfig, ToolResponse } from '../types';
interface ChatParams {
apiKey: string;
@@ -10,8 +10,7 @@ interface ChatParams {
stream?: boolean;
}
interface ChatResponse {
output: string;
interface ChatResponse extends ToolResponse {
tokens?: number;
model: string;
}
@@ -68,10 +67,11 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
}
},
transformResponse: (data) => {
transformResponse: async (response: Response) => {
const data = await response.json();
return {
output: data.content[0].text,
tokens: data.usage?.input_tokens + data.usage?.output_tokens,
output: data.completion,
tokens: data.usage?.total_tokens,
model: data.model
};
},

View File

@@ -1,4 +1,4 @@
import { ToolConfig } from '../types';
import { ToolConfig, ToolResponse } from '../types';
interface ChatParams {
apiKey: string;
@@ -10,10 +10,10 @@ interface ChatParams {
topK?: number;
}
interface ChatResponse {
output: string;
interface ChatResponse extends ToolResponse {
tokens?: number;
model: string;
safetyRatings?: any[];
}
export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
@@ -71,10 +71,13 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
}
},
transformResponse: (data) => {
transformResponse: async (response: Response) => {
const data = await response.json();
return {
output: data.candidates[0].content.parts[0].text,
model: 'gemini-pro'
tokens: data.usage?.totalTokens,
model: data.model,
safetyRatings: data.candidates[0].safetyRatings
};
},

View File

@@ -1,4 +1,4 @@
import { ToolConfig, HttpMethod } from '../types';
import { ToolConfig, HttpMethod, ToolResponse } from '../types';
interface RequestParams {
url: string;
@@ -12,8 +12,7 @@ interface RequestParams {
validateStatus?: (status: number) => boolean;
}
interface RequestResponse {
data: any;
interface RequestResponse extends ToolResponse {
status: number;
headers: Record<string, string>;
}
@@ -120,23 +119,22 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
}
},
transformResponse: (response) => {
// Try to parse response based on content-type
const contentType = response.headers?.['content-type'] || '';
let data = response.data;
transformResponse: async (response: Response) => {
// Convert Headers to a plain object
const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
if (contentType.includes('application/json')) {
try {
data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
} catch (e) {
// Keep original data if parsing fails
}
}
// Parse response based on content type
const data = await (response.headers.get('content-type')?.includes('application/json')
? response.json()
: response.text());
return {
data,
output: data,
status: response.status,
headers: response.headers
headers
};
},
@@ -148,4 +146,4 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
: '';
return `${message} (${code})${details}`;
}
};
};

View File

@@ -1,33 +1,29 @@
import { ToolConfig } from '../types';
import { ToolConfig, ToolResponse } from '../types';
interface ContactParams {
interface ContactsParams {
apiKey: string;
email: string;
action: 'create' | 'update' | 'search' | 'delete';
id?: string;
email?: string;
firstName?: string;
lastName?: string;
phone?: string;
company?: string;
id?: string;
properties?: Record<string, string>;
properties?: Record<string, any>;
limit?: number;
after?: string;
data: Record<string, any>;
}
interface ContactResponse {
id: string;
properties: {
email: string;
firstname?: string;
lastname?: string;
phone?: string;
company?: string;
[key: string]: any;
interface ContactsResponse extends ToolResponse {
totalResults?: number;
pagination?: {
hasMore: boolean;
offset: number;
};
createdAt: string;
updatedAt: string;
}
export const contactsTool: ToolConfig<ContactParams, ContactResponse | ContactResponse[]> = {
export const contactsTool: ToolConfig<ContactsParams, ContactsResponse> = {
id: 'hubspot.contacts',
name: 'HubSpot Contacts',
description: 'Manage HubSpot contacts - create, search, and update contact records',
@@ -116,22 +112,12 @@ export const contactsTool: ToolConfig<ContactParams, ContactResponse | ContactRe
}
},
transformResponse: (data) => {
if (Array.isArray(data.results)) {
// Search response
return data.results.map((contact: { id: string; properties: Record<string, any>; createdAt: string; updatedAt: string }) => ({
id: contact.id,
properties: contact.properties,
createdAt: contact.createdAt,
updatedAt: contact.updatedAt
}));
}
// Single contact response
transformResponse: async (response: Response) => {
const data = await response.json();
return {
id: data.id,
properties: data.properties,
createdAt: data.createdAt,
updatedAt: data.updatedAt
output: data.results || data,
totalResults: data.total,
pagination: data.paging
};
},

View File

@@ -1,4 +1,4 @@
import { ToolConfig } from '../types';
import { ToolConfig, ToolResponse } from '../types';
interface ChatParams {
apiKey: string;
@@ -12,8 +12,7 @@ interface ChatParams {
stream?: boolean;
}
interface ChatResponse {
output: string;
interface ChatResponse extends ToolResponse {
tokens?: number;
model: string;
}
@@ -55,7 +54,6 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
'Authorization': `Bearer ${params.apiKey}`
}),
body: (params) => {
console.log('OpenAI Chat Tool - Request Params:', JSON.stringify(params, null, 2));
const body = {
model: params.model || 'gpt-4o',
messages: [
@@ -68,12 +66,12 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
presence_penalty: params.presencePenalty,
stream: params.stream
};
console.log('OpenAI Chat Tool - Request Body:', JSON.stringify(body, null, 2));
return body;
}
},
transformResponse: (data) => {
transformResponse: async (response: Response) => {
const data = await response.json();
if (data.choices?.[0]?.delta?.content) {
return {
output: data.choices[0].delta.content,

View File

@@ -1,55 +1,52 @@
import { ToolConfig } from '../types';
import { ToolConfig, ToolResponse } from '../types';
interface OpportunityParams {
instanceUrl: string;
accessToken: string;
name: string;
accountId?: string;
stage?: string;
amount?: number;
closeDate?: string;
probability?: number;
description?: string;
apiKey: string;
action: 'create' | 'update' | 'search' | 'delete';
id?: string;
properties?: Record<string, any>;
query?: string;
limit?: number;
}
interface OpportunityResponse {
id: string;
name: string;
name?: string;
accountId?: string;
stage?: string;
amount?: number;
closeDate?: string;
probability?: number;
description?: string;
createdDate: string;
lastModifiedDate: string;
[key: string]: any;
properties?: Record<string, any>;
limit?: number;
offset?: number;
data: Record<string, any>;
}
export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityResponse | OpportunityResponse[]> = {
interface OpportunityResponse extends ToolResponse {
totalResults?: number;
pagination?: {
hasMore: boolean;
offset: number;
};
}
export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityResponse> = {
id: 'salesforce.opportunities',
name: 'Salesforce Opportunities',
description: 'Manage Salesforce opportunities - create, query, and update opportunity records',
version: '1.0.0',
params: {
instanceUrl: {
apiKey: {
type: 'string',
required: true,
description: 'Salesforce instance URL'
description: 'Salesforce API key'
},
accessToken: {
action: {
type: 'string',
required: true,
description: 'Salesforce access token'
description: 'Action to perform (create, update, search, delete)'
},
id: {
type: 'string',
description: 'Opportunity ID (required for updates)'
},
name: {
type: 'string',
required: true,
description: 'Opportunity name'
},
accountId: {
@@ -72,35 +69,28 @@ export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityRespons
type: 'number',
description: 'Probability of closing (%)'
},
description: {
type: 'string',
description: 'Opportunity description'
},
id: {
type: 'string',
description: 'Opportunity ID (required for updates)'
},
properties: {
type: 'object',
description: 'Additional opportunity fields'
},
query: {
type: 'string',
description: 'SOQL query for searching opportunities'
},
limit: {
type: 'number',
default: 100,
description: 'Maximum number of records to return'
},
offset: {
type: 'number',
description: 'Offset for pagination'
},
data: {
type: 'object',
description: 'Data for the action'
}
},
request: {
url: (params) => {
const baseUrl = `${params.instanceUrl}/services/data/v58.0/sobjects/Opportunity`;
if (params.query) {
return `${params.instanceUrl}/services/data/v58.0/query?q=${encodeURIComponent(params.query)}`;
}
const baseUrl = `${params.apiKey}@salesforce.com/services/data/v58.0/sobjects/Opportunity`;
if (params.id) {
return `${baseUrl}/${params.id}`;
}
@@ -109,13 +99,9 @@ export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityRespons
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${params.accessToken}`
'Authorization': `Bearer ${params.apiKey}`
}),
body: (params) => {
if (params.query) {
return {}; // Empty body for queries
}
const fields = {
Name: params.name,
...(params.accountId && { AccountId: params.accountId }),
@@ -123,7 +109,6 @@ export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityRespons
...(params.amount && { Amount: params.amount }),
...(params.closeDate && { CloseDate: params.closeDate }),
...(params.probability && { Probability: params.probability }),
...(params.description && { Description: params.description }),
...params.properties
};
@@ -131,36 +116,15 @@ export const opportunitiesTool: ToolConfig<OpportunityParams, OpportunityRespons
}
},
transformResponse: (data) => {
if (data.records) {
// Query response
return data.records.map((record: any) => ({
id: record.Id,
name: record.Name,
accountId: record.AccountId,
stage: record.StageName,
amount: record.Amount,
closeDate: record.CloseDate,
probability: record.Probability,
description: record.Description,
createdDate: record.CreatedDate,
lastModifiedDate: record.LastModifiedDate,
...record
}));
}
// Single record response
transformResponse: async (response: Response) => {
const data = await response.json();
return {
id: data.id || data.Id,
name: data.name || data.Name,
accountId: data.accountId || data.AccountId,
stage: data.stage || data.StageName,
amount: data.amount || data.Amount,
closeDate: data.closeDate || data.CloseDate,
probability: data.probability || data.Probability,
description: data.description || data.Description,
createdDate: data.CreatedDate,
lastModifiedDate: data.LastModifiedDate,
...data
output: data.records || data,
totalResults: data.totalSize,
pagination: {
hasMore: !data.done,
offset: data.nextRecordsUrl ? parseInt(data.nextRecordsUrl.split('-')[1]) : 0
}
};
},

View File

@@ -1,6 +1,11 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
export interface ToolConfig<P = any, R = any> {
export interface ToolResponse {
output: any; // All tools must provide an output field
[key: string]: any; // Tools can include additional metadata
}
export interface ToolConfig<P = any, R extends ToolResponse = ToolResponse> {
// Basic tool identification
id: string;
name: string;
@@ -24,6 +29,6 @@ export interface ToolConfig<P = any, R = any> {
};
// Response handling
transformResponse: (data: any) => R;
transformResponse: (response: Response) => Promise<R>;
transformError: (error: any) => string;
}

View File

@@ -1,4 +1,4 @@
import { ToolConfig } from '../types';
import { ToolConfig, ToolResponse } from '../types';
interface ChatParams {
apiKey: string;
@@ -11,10 +11,10 @@ interface ChatParams {
presencePenalty?: number;
}
interface ChatResponse {
output: string;
interface ChatResponse extends ToolResponse {
tokens?: number;
model: string;
reasoning?: string;
}
export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
@@ -69,11 +69,13 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
}
},
transformResponse: (data) => {
transformResponse: async (response: Response) => {
const data = await response.json();
return {
output: data.choices[0].message.content,
tokens: data.usage?.total_tokens,
model: data.model
model: data.model,
reasoning: data.choices[0]?.reasoning
};
},