feat: added postprocessing to tools for tools that need multiple api calls in succession

This commit is contained in:
Waleed Latif
2025-03-10 04:37:45 -07:00
parent b4930404c9
commit 6688c6093c
8 changed files with 256 additions and 47 deletions
+34 -5
View File
@@ -16,13 +16,42 @@ export async function POST(request: Request) {
throw new Error(`Tool not found: ${toolId}`)
}
// Use executeTool with skipProxy=true to prevent recursive proxy calls
const result = await executeTool(toolId, params, true)
// Use executeTool with skipProxy=true to prevent recursive proxy calls, and skipPostProcess=true to prevent duplicate post-processing
const result = await executeTool(toolId, params, true, true)
if (!result.success) {
throw new Error(
tool.transformError ? tool.transformError(result) : 'Tool returned an error'
)
if (tool.transformError) {
try {
const errorResult = tool.transformError(result)
// Handle both string and Promise return types
if (typeof errorResult === 'string') {
throw new Error(errorResult)
} else {
// It's a Promise, await it
const transformedError = await errorResult
// If it's a string or has an error property, use it
if (typeof transformedError === 'string') {
throw new Error(transformedError)
} else if (
transformedError &&
typeof transformedError === 'object' &&
'error' in transformedError
) {
throw new Error(transformedError.error || 'Tool returned an error')
}
// Fallback
throw new Error('Tool returned an error')
}
} catch (e) {
if (e instanceof Error) {
throw e
}
throw new Error('Tool returned an error')
}
} else {
throw new Error('Tool returned an error')
}
}
return NextResponse.json(result)
+7 -1
View File
@@ -164,7 +164,13 @@ export interface Tool<P = any, O = Record<string, any>> {
error?: string
}>
transformError?: (error: any) => string // Function to format error messages
transformError?: (error: any) =>
| string
| Promise<{
success: boolean
output: O
error?: string
}> // Function to format error messages
}
/**
+58 -10
View File
@@ -50,6 +50,44 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
return requestBody
},
},
postProcess: async (result, params, executeTool) => {
// Only add content if it was provided and not already added during creation
// The Google Docs API doesn't directly support content in the create request,
// so we need to add it separately via the write tool
if (result.success && params.content) {
console.log('Google Docs create - Post-processing: Adding content to document')
const documentId = result.output.metadata.documentId
if (documentId) {
try {
const writeParams = {
accessToken: params.accessToken,
documentId: documentId,
content: params.content,
}
console.log('Content to add:', params.content)
// Use the write tool to add content
const writeResult = await executeTool('google_docs_write', writeParams)
if (!writeResult.success) {
console.warn(
'Failed to add content to document, but document was created:',
writeResult.error
)
} else {
console.log('Google Docs create - Content added successfully')
}
} catch (error) {
console.warn('Error adding content to document:', error)
// Don't fail the overall operation if adding content fails
}
}
}
// Return the original result regardless of post-processing outcome
return result
},
transformResponse: async (response: Response) => {
if (!response.ok) {
let errorText = ''
@@ -67,7 +105,9 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
try {
// Get the response data
const responseText = await response.text()
const data = JSON.parse(responseText)
const documentId = data.documentId
const title = data.title
@@ -86,19 +126,27 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
},
}
} catch (error) {
console.error('Google Docs create - Error processing response:', error)
throw error
}
},
transformError: (error) => {
if (typeof error === 'object' && error !== null) {
if (error.message) {
return error.message
}
return (
JSON.stringify(error, null, 2) || 'An error occurred while creating Google Docs document'
)
}
transformError: async (error) => {
console.error('Google Docs create - Transform error:', error)
return error.toString() || 'An error occurred while creating Google Docs document'
const errorMessage =
typeof error === 'object' && error !== null
? error.message || JSON.stringify(error, null, 2)
: error.toString() || 'An error occurred while creating Google Docs document'
return {
success: false,
output: {
metadata: {
documentId: '',
title: '',
},
},
error: errorMessage,
}
},
}
+16 -4
View File
@@ -70,11 +70,23 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
},
}
},
transformError: (error) => {
if (typeof error === 'object' && error !== null) {
return JSON.stringify(error) || 'An error occurred while reading from Google Docs'
transformError: async (error) => {
const errorMessage =
typeof error === 'object' && error !== null
? error.message || JSON.stringify(error, null, 2)
: error.toString() || 'An error occurred while reading Google Docs document'
return {
success: false,
output: {
content: '',
metadata: {
documentId: '',
title: '',
},
},
error: errorMessage,
}
return error.message || 'An error occurred while reading from Google Docs'
},
}
+19 -12
View File
@@ -48,14 +48,13 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
}
// Following the exact format from the Google Docs API examples
// Note: We're not including tabId since it's optional for the main document body
// Always insert at the end of the document to avoid duplication
// See: https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertTextRequest
const requestBody = {
requests: [
{
insertText: {
location: {
index: 1,
},
endOfSegmentLocation: {},
text: params.content,
},
},
@@ -117,14 +116,22 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
throw error
}
},
transformError: (error) => {
if (typeof error === 'object' && error !== null) {
if (error.message) {
return error.message
}
return JSON.stringify(error, null, 2) || 'An error occurred while writing to Google Docs'
}
transformError: async (error) => {
const errorMessage =
typeof error === 'object' && error !== null
? error.message || JSON.stringify(error, null, 2)
: error.toString() || 'An error occurred while writing to Google Docs'
return error.toString() || 'An error occurred while writing to Google Docs'
return {
success: false,
output: {
updatedContent: false,
metadata: {
documentId: '',
title: '',
},
},
error: errorMessage,
}
},
}
+79 -7
View File
@@ -279,7 +279,7 @@ function getCustomTool(customToolId: string): ToolConfig | undefined {
error: undefined,
}
},
transformError: (error: any) =>
transformError: async (error: any) =>
`Custom tool execution error: ${error.message || 'Unknown error'}`,
}
}
@@ -288,7 +288,8 @@ function getCustomTool(customToolId: string): ToolConfig | undefined {
export async function executeTool(
toolId: string,
params: Record<string, any>,
skipProxy = false
skipProxy = false,
skipPostProcess = false
): Promise<ToolResponse> {
try {
const tool = getTool(toolId)
@@ -313,11 +314,36 @@ export async function executeTool(
// For internal routes or when skipProxy is true, call the API directly
if (tool.request.isInternalRoute || skipProxy) {
const result = await handleInternalRequest(toolId, tool, params)
// Apply post-processing if available and not skipped
if (tool.postProcess && result.success && !skipPostProcess) {
try {
return await tool.postProcess(result, params, executeTool)
} catch (error) {
console.error(`Error in post-processing for tool ${toolId}:`, error)
// Return original result if post-processing fails
return result
}
}
return result
}
// For external APIs, use the proxy
return await handleProxyRequest(toolId, params)
const result = await handleProxyRequest(toolId, params)
// Apply post-processing if available and not skipped
if (tool.postProcess && result.success && !skipPostProcess) {
try {
return await tool.postProcess(result, params, executeTool)
} catch (error) {
console.error(`Error in post-processing for tool ${toolId}:`, error)
// Return original result if post-processing fails
return result
}
}
return result
} catch (error: any) {
console.error(`Error executing tool ${toolId}:`, error)
@@ -396,12 +422,58 @@ async function handleInternalRequest(
error: undefined,
}
} catch (error: any) {
console.error(`Error executing internal tool ${toolId}:`, error)
// Use the tool's error transformer if available
if (tool.transformError) {
return {
success: false,
output: {},
error: tool.transformError(error),
try {
const errorResult = tool.transformError(error)
// Handle both string and Promise return types
if (typeof errorResult === 'string') {
return {
success: false,
output: {},
error: errorResult,
}
} else {
// It's a Promise, await it
const transformedError = await errorResult
// If it's a string or has an error property, use it
if (typeof transformedError === 'string') {
return {
success: false,
output: {},
error: transformedError,
}
} else if (transformedError && typeof transformedError === 'object') {
// If it's already a ToolResponse, return it directly
if ('success' in transformedError) {
return transformedError
}
// If it has an error property, use it
if ('error' in transformedError) {
return {
success: false,
output: {},
error: transformedError.error,
}
}
}
// Fallback
return {
success: false,
output: {},
error: 'Unknown error',
}
}
} catch (transformError) {
console.error(`Error transforming error for tool ${toolId}:`, transformError)
return {
success: false,
output: {},
error: error.message || 'Unknown error',
}
}
}
+10 -3
View File
@@ -14,7 +14,7 @@ export interface OAuthConfig {
additionalScopes?: string[] // Additional scopes required for the tool
}
export interface ToolConfig<P = any, R extends ToolResponse = ToolResponse> {
export interface ToolConfig<P = any, R = any> {
// Basic tool identification
id: string
name: string
@@ -48,9 +48,16 @@ export interface ToolConfig<P = any, R extends ToolResponse = ToolResponse> {
// Direct execution in browser (optional) - bypasses HTTP request
directExecution?: (params: P) => Promise<R | undefined>
// Post-processing (optional) - allows additional processing after the initial request
postProcess?: (
result: R extends ToolResponse ? R : ToolResponse,
params: P,
executeTool: (toolId: string, params: Record<string, any>) => Promise<ToolResponse>
) => Promise<R extends ToolResponse ? R : ToolResponse>
// Response handling
transformResponse: (response: Response) => Promise<R>
transformError: (error: any) => string
transformResponse?: (response: Response) => Promise<R>
transformError?: (error: any) => string | Promise<R>
}
export interface TableRow {
+33 -5
View File
@@ -77,12 +77,40 @@ export async function executeRequest(
}
// Use the tool's error transformer or a default message
const error = tool.transformError
? tool.transformError(errorContent)
: errorContent.message || `${toolId} API error: ${externalResponse.statusText}`
if (tool.transformError) {
try {
const errorResult = tool.transformError(errorContent)
console.error(`${toolId} error:`, error)
throw new Error(error)
// Handle both string and Promise return types
if (typeof errorResult === 'string') {
throw new Error(errorResult)
} else {
// It's a Promise, await it
const transformedError = await errorResult
// If it's a string or has an error property, use it
if (typeof transformedError === 'string') {
throw new Error(transformedError)
} else if (
transformedError &&
typeof transformedError === 'object' &&
'error' in transformedError
) {
throw new Error(transformedError.error || 'Tool returned an error')
}
// Fallback
throw new Error('Tool returned an error')
}
} catch (e) {
if (e instanceof Error) {
throw e
}
throw new Error(`${toolId} API error: ${externalResponse.statusText}`)
}
} else {
const error = errorContent.message || `${toolId} API error: ${externalResponse.statusText}`
console.error(`${toolId} error:`, error)
throw new Error(error)
}
}
const transformResponse =