mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(tools): removed transformError, isInternalRoute, directExecution (#928)
* standardized response format for transformError * removed trasnformError, moved error handling to executeTool for all different error formats * remove isInternalRoute, make it implicit in executeTool * removed directExecution, everything on the server nothing on the client * fix supabase * fix(tag-dropdown): fix values for parallel & loop blocks (#929) * fix(search-modal): add parallel and loop blocks to search modal * reordered tool params * update docs
This commit is contained in:
9
.github/CONTRIBUTING.md
vendored
9
.github/CONTRIBUTING.md
vendored
@@ -416,8 +416,8 @@ In addition, you will need to update the registries:
|
||||
Your tool should export a constant with a naming convention of `{toolName}Tool`. The tool ID should follow the format `{provider}_{tool_name}`. For example:
|
||||
|
||||
```typescript:/apps/sim/tools/pinecone/fetch.ts
|
||||
import { ToolConfig, ToolResponse } from '../types'
|
||||
import { PineconeParams, PineconeResponse } from './types'
|
||||
import { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
import { PineconeParams, PineconeResponse } from '@/tools/pinecone/types'
|
||||
|
||||
export const fetchTool: ToolConfig<PineconeParams, PineconeResponse> = {
|
||||
id: 'pinecone_fetch', // Follow the {provider}_{tool_name} format
|
||||
@@ -448,9 +448,6 @@ In addition, you will need to update the registries:
|
||||
transformResponse: async (response: Response) => {
|
||||
// Transform response
|
||||
},
|
||||
transformError: (error) => {
|
||||
// Handle errors
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -458,7 +455,7 @@ In addition, you will need to update the registries:
|
||||
Update the tools registry in `/apps/sim/tools/index.ts` to include your new tool:
|
||||
|
||||
```typescript:/apps/sim/tools/index.ts
|
||||
import { fetchTool, generateEmbeddingsTool, searchTextTool } from './pinecone'
|
||||
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '/@tools/pinecone'
|
||||
// ... other imports
|
||||
|
||||
export const tools: Record<string, ToolConfig> = {
|
||||
|
||||
@@ -151,8 +151,6 @@ Update multiple existing records in an Airtable table
|
||||
| `baseId` | string | Yes | ID of the Airtable base |
|
||||
| `tableId` | string | Yes | ID or name of the table |
|
||||
| `records` | json | Yes | Array of records to update, each with an `id` and a `fields` object |
|
||||
| `fields` | string | No | No description |
|
||||
| `fields` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -82,9 +82,10 @@ Runs a browser automation task using BrowserUse
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | json | Browser automation task results including task ID, success status, output data, and execution steps |
|
||||
| `error` | string | Error message if the operation failed |
|
||||
| `id` | string | Task execution identifier |
|
||||
| `success` | boolean | Task completion status |
|
||||
| `output` | json | Task output data |
|
||||
| `steps` | json | Execution steps taken |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Convert TTS using ElevenLabs voices
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `audioUrl` | string | Generated audio URL |
|
||||
| `audioUrl` | string | The URL of the generated audio |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `files` | json | Array of parsed file objects with content, metadata, and file properties |
|
||||
| `combinedContent` | string | All file contents merged into a single text string |
|
||||
| `files` | array | Array of parsed files |
|
||||
| `combinedContent` | string | Combined content of all parsed files |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ Query data from a Supabase table
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Query operation results |
|
||||
| `message` | string | Operation status message |
|
||||
| `results` | array | Array of records returned from the query |
|
||||
|
||||
### `supabase_insert`
|
||||
|
||||
@@ -121,8 +121,8 @@ Insert data into a Supabase table
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Insert operation results |
|
||||
| `message` | string | Operation status message |
|
||||
| `results` | array | Array of inserted records |
|
||||
|
||||
### `supabase_get_row`
|
||||
|
||||
@@ -141,8 +141,8 @@ Get a single row from a Supabase table based on filter criteria
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Get row operation results |
|
||||
| `message` | string | Operation status message |
|
||||
| `results` | object | The row data if found, null if not found |
|
||||
|
||||
### `supabase_update`
|
||||
|
||||
@@ -162,8 +162,8 @@ Update rows in a Supabase table based on filter criteria
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Update operation results |
|
||||
| `message` | string | Operation status message |
|
||||
| `results` | array | Array of updated records |
|
||||
|
||||
### `supabase_delete`
|
||||
|
||||
@@ -182,8 +182,8 @@ Delete rows from a Supabase table based on filter criteria
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Delete operation results |
|
||||
| `message` | string | Operation status message |
|
||||
| `results` | array | Array of deleted records |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -235,42 +235,8 @@ export async function POST(request: Request) {
|
||||
error: result.error || 'Unknown 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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
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 (transformError) {
|
||||
logger.error(`[${requestId}] Error transformation failed for ${toolId}`, {
|
||||
error:
|
||||
transformError instanceof Error ? transformError.message : String(transformError),
|
||||
})
|
||||
if (transformError instanceof Error) {
|
||||
throw transformError
|
||||
}
|
||||
throw new Error('Tool returned an error')
|
||||
}
|
||||
} else {
|
||||
throw new Error('Tool returned an error')
|
||||
}
|
||||
// Let the main executeTool handle error transformation to avoid double transformation
|
||||
throw new Error(result.error || 'Tool execution failed')
|
||||
}
|
||||
|
||||
const endTime = new Date()
|
||||
|
||||
147
apps/sim/app/api/tools/jira/update/route.ts
Normal file
147
apps/sim/app/api/tools/jira/update/route.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { Logger } from '@/lib/logs/console/logger'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = new Logger('JiraUpdateAPI')
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
issueKey,
|
||||
summary,
|
||||
title, // Support both summary and title for backwards compatibility
|
||||
description,
|
||||
status,
|
||||
priority,
|
||||
assignee,
|
||||
cloudId: providedCloudId,
|
||||
} = await request.json()
|
||||
|
||||
// Validate required parameters
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueKey) {
|
||||
logger.error('Missing issue key in request')
|
||||
return NextResponse.json({ error: 'Issue key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Use provided cloudId or fetch it if not provided
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
logger.info('Using cloud ID:', cloudId)
|
||||
|
||||
// Build the URL using cloudId for Jira API
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}`
|
||||
|
||||
logger.info('Updating Jira issue at:', url)
|
||||
|
||||
// Map the summary from either summary or title field
|
||||
const summaryValue = summary || title
|
||||
const fields: Record<string, any> = {}
|
||||
|
||||
if (summaryValue) {
|
||||
fields.summary = summaryValue
|
||||
}
|
||||
|
||||
if (description) {
|
||||
fields.description = {
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: description,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
fields.status = {
|
||||
name: status,
|
||||
}
|
||||
}
|
||||
|
||||
if (priority) {
|
||||
fields.priority = {
|
||||
name: priority,
|
||||
}
|
||||
}
|
||||
|
||||
if (assignee) {
|
||||
fields.assignee = {
|
||||
id: assignee,
|
||||
}
|
||||
}
|
||||
|
||||
const body = { fields }
|
||||
|
||||
// Make the request to Jira API
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Jira API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Jira API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
// Note: Jira update API typically returns 204 No Content on success
|
||||
const responseData = response.status === 204 ? {} : await response.json()
|
||||
logger.info('Successfully updated Jira issue:', issueKey)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: responseData.key || issueKey,
|
||||
summary: responseData.fields?.summary || 'Issue updated',
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error('Error updating Jira issue:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
162
apps/sim/app/api/tools/jira/write/route.ts
Normal file
162
apps/sim/app/api/tools/jira/write/route.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { Logger } from '@/lib/logs/console/logger'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = new Logger('JiraWriteAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
projectId,
|
||||
summary,
|
||||
description,
|
||||
priority,
|
||||
assignee,
|
||||
cloudId: providedCloudId,
|
||||
issueType,
|
||||
parent,
|
||||
} = await request.json()
|
||||
|
||||
// Validate required parameters
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
logger.error('Missing project ID in request')
|
||||
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
logger.error('Missing summary in request')
|
||||
return NextResponse.json({ error: 'Summary is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueType) {
|
||||
logger.error('Missing issue type in request')
|
||||
return NextResponse.json({ error: 'Issue type is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Use provided cloudId or fetch it if not provided
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
logger.info('Using cloud ID:', cloudId)
|
||||
|
||||
// Build the URL using cloudId for Jira API
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue`
|
||||
|
||||
logger.info('Creating Jira issue at:', url)
|
||||
|
||||
// Construct fields object with only the necessary fields
|
||||
const fields: Record<string, any> = {
|
||||
project: {
|
||||
id: projectId,
|
||||
},
|
||||
issuetype: {
|
||||
name: issueType,
|
||||
},
|
||||
summary: summary,
|
||||
}
|
||||
|
||||
// Only add description if it exists
|
||||
if (description) {
|
||||
fields.description = {
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: description,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Only add parent if it exists
|
||||
if (parent) {
|
||||
fields.parent = parent
|
||||
}
|
||||
|
||||
// Only add priority if it exists
|
||||
if (priority) {
|
||||
fields.priority = {
|
||||
name: priority,
|
||||
}
|
||||
}
|
||||
|
||||
// Only add assignee if it exists
|
||||
if (assignee) {
|
||||
fields.assignee = {
|
||||
id: assignee,
|
||||
}
|
||||
}
|
||||
|
||||
const body = { fields }
|
||||
|
||||
// Make the request to Jira API
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Jira API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Jira API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const responseData = await response.json()
|
||||
logger.info('Successfully created Jira issue:', responseData.key)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: responseData.key || 'unknown',
|
||||
summary: responseData.fields?.summary || 'Issue created',
|
||||
success: true,
|
||||
url: `https://${domain}/browse/${responseData.key}`,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error('Error creating Jira issue:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
53
apps/sim/app/api/tools/thinking/route.ts
Normal file
53
apps/sim/app/api/tools/thinking/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ThinkingToolParams, ThinkingToolResponse } from '@/tools/thinking/types'
|
||||
|
||||
const logger = createLogger('ThinkingToolAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* POST - Process a thinking tool request
|
||||
* Simply acknowledges the thought by returning it in the output
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body: ThinkingToolParams = await request.json()
|
||||
|
||||
logger.info(`[${requestId}] Processing thinking tool request`)
|
||||
|
||||
// Validate the required parameter
|
||||
if (!body.thought || typeof body.thought !== 'string') {
|
||||
logger.warn(`[${requestId}] Missing or invalid 'thought' parameter`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'The thought parameter is required and must be a string',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Simply acknowledge the thought by returning it in the output
|
||||
const response: ThinkingToolResponse = {
|
||||
success: true,
|
||||
output: {
|
||||
acknowledgedThought: body.thought,
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Thinking tool processed successfully`)
|
||||
return NextResponse.json(response)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error processing thinking tool:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to process thinking tool request',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,17 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
||||
import { BookOpen, Building2, LibraryBig, ScrollText, Search, Shapes, Workflow } from 'lucide-react'
|
||||
import {
|
||||
BookOpen,
|
||||
Building2,
|
||||
LibraryBig,
|
||||
RepeatIcon,
|
||||
ScrollText,
|
||||
Search,
|
||||
Shapes,
|
||||
SplitIcon,
|
||||
Workflow,
|
||||
} from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Dialog, DialogOverlay, DialogPortal, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -119,7 +129,7 @@ export function SearchModal({
|
||||
if (!isOnWorkflowPage) return []
|
||||
|
||||
const allBlocks = getAllBlocks()
|
||||
return allBlocks
|
||||
const regularBlocks = allBlocks
|
||||
.filter(
|
||||
(block) =>
|
||||
block.type !== 'starter' &&
|
||||
@@ -137,7 +147,28 @@ export function SearchModal({
|
||||
type: block.type,
|
||||
})
|
||||
)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Add special blocks (loop and parallel)
|
||||
const specialBlocks: BlockItem[] = [
|
||||
{
|
||||
id: 'loop',
|
||||
name: 'Loop',
|
||||
description: 'Create a Loop',
|
||||
icon: RepeatIcon,
|
||||
bgColor: '#2FB3FF',
|
||||
type: 'loop',
|
||||
},
|
||||
{
|
||||
id: 'parallel',
|
||||
name: 'Parallel',
|
||||
description: 'Parallel Execution',
|
||||
icon: SplitIcon,
|
||||
bgColor: '#FEE12B',
|
||||
type: 'parallel',
|
||||
},
|
||||
]
|
||||
|
||||
return [...regularBlocks, ...specialBlocks].sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [isOnWorkflowPage])
|
||||
|
||||
// Get all available tools - only when on workflow page
|
||||
|
||||
@@ -271,14 +271,6 @@ export interface Tool<P = any, O = Record<string, any>> {
|
||||
output: O
|
||||
error?: string
|
||||
}>
|
||||
|
||||
transformError?: (error: any) =>
|
||||
| string
|
||||
| Promise<{
|
||||
success: boolean
|
||||
output: O
|
||||
error?: string
|
||||
}> // Function to format error messages
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -259,8 +259,13 @@ async function parseWithMistralOCR(
|
||||
const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.MISTRAL_OCR_API)
|
||||
|
||||
try {
|
||||
const method =
|
||||
typeof mistralParserTool.request!.method === 'function'
|
||||
? mistralParserTool.request!.method(requestBody as any)
|
||||
: mistralParserTool.request!.method
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: mistralParserTool.request!.method,
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
|
||||
@@ -151,7 +151,7 @@ export class ToolTester<P = any, R = any> {
|
||||
this.mockFetch = createErrorFetch(errorMessage, status)
|
||||
global.fetch = Object.assign(this.mockFetch, { preconnect: vi.fn() }) as typeof fetch
|
||||
|
||||
// Create an error object that the transformError function can use
|
||||
// Create an error object for direct error handling
|
||||
this.error = new Error(errorMessage)
|
||||
this.error.message = errorMessage
|
||||
this.error.status = status
|
||||
@@ -170,7 +170,7 @@ export class ToolTester<P = any, R = any> {
|
||||
return this
|
||||
}
|
||||
|
||||
// Store the error for transformError to use
|
||||
// Store the error for direct error handling
|
||||
private error: any = null
|
||||
|
||||
/**
|
||||
@@ -196,37 +196,23 @@ export class ToolTester<P = any, R = any> {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (this.tool.transformError) {
|
||||
// Create a more detailed error object that simulates a real error
|
||||
const data = await response.json().catch(() => ({}))
|
||||
// Extract error message directly from response
|
||||
const data = await response.json().catch(() => ({}))
|
||||
|
||||
// Build an error object with all the needed properties
|
||||
const error: any = new Error(data.error || data.message || 'Request failed')
|
||||
error.response = response
|
||||
error.status = response.status
|
||||
error.data = data
|
||||
// Extract meaningful error message from the response
|
||||
let errorMessage = data.error || data.message || response.statusText || 'Request failed'
|
||||
|
||||
// Add the status code to the message to help with identifying the error type
|
||||
if (response.status === 404) {
|
||||
error.message = 'Not Found'
|
||||
} else if (response.status === 401) {
|
||||
error.message = 'Unauthorized'
|
||||
}
|
||||
|
||||
// Use the tool's transformError which matches the real implementation
|
||||
const errorMessage = await this.tool.transformError(error)
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: errorMessage || error.message,
|
||||
}
|
||||
// Add specific error messages for common status codes
|
||||
if (response.status === 404) {
|
||||
errorMessage = data.error || data.message || 'Not Found'
|
||||
} else if (response.status === 401) {
|
||||
errorMessage = data.error || data.message || 'Unauthorized'
|
||||
}
|
||||
|
||||
// If there's no transformError function, return a generic error
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: `HTTP error ${response.status}`,
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,19 +220,25 @@ export class ToolTester<P = any, R = any> {
|
||||
return await this.handleSuccessfulResponse(response, params)
|
||||
} catch (error) {
|
||||
// Handle thrown errors (network errors, etc.)
|
||||
if (this.tool.transformError) {
|
||||
const errorToUse = this.error || error
|
||||
const errorMessage = await this.tool.transformError(errorToUse)
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: typeof errorMessage === 'string' ? errorMessage : 'Network error',
|
||||
}
|
||||
const errorToUse = this.error || error
|
||||
|
||||
// Extract error message directly from error object
|
||||
let errorMessage = 'Network error'
|
||||
|
||||
if (errorToUse instanceof Error) {
|
||||
errorMessage = errorToUse.message
|
||||
} else if (typeof errorToUse === 'string') {
|
||||
errorMessage = errorToUse
|
||||
} else if (errorToUse && typeof errorToUse === 'object') {
|
||||
// Try to extract error message from error object structure
|
||||
errorMessage =
|
||||
errorToUse.error || errorToUse.message || errorToUse.statusText || 'Network error'
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: error instanceof Error ? error.message : 'Network error',
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,6 @@ export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, Airtabl
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to create Airtable records')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -66,10 +63,6 @@ export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, Airtabl
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return `Failed to create Airtable records: ${error.message || 'Unknown error'}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'json',
|
||||
|
||||
@@ -53,10 +53,6 @@ export const airtableGetRecordTool: ToolConfig<AirtableGetParams, AirtableGetRes
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
// logger.error('Airtable API error:', data)
|
||||
throw new Error(data.error?.message || 'Failed to get Airtable record')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -68,11 +64,6 @@ export const airtableGetRecordTool: ToolConfig<AirtableGetParams, AirtableGetRes
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
// logger.error('Airtable tool error:', error)
|
||||
return `Failed to get Airtable record: ${error.message || 'Unknown error'}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'json',
|
||||
|
||||
@@ -71,9 +71,6 @@ export const airtableListRecordsTool: ToolConfig<AirtableListParams, AirtableLis
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to fetch Airtable records')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -86,10 +83,6 @@ export const airtableListRecordsTool: ToolConfig<AirtableListParams, AirtableLis
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return `Failed to list Airtable records: ${error.message || 'Unknown error'}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'json',
|
||||
|
||||
@@ -4,8 +4,6 @@ import type {
|
||||
} from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
// import { logger } from '@/utils/logger' // Removed logger due to import issues
|
||||
|
||||
export const airtableUpdateMultipleRecordsTool: ToolConfig<
|
||||
AirtableUpdateMultipleParams,
|
||||
AirtableUpdateMultipleResponse
|
||||
@@ -44,9 +42,7 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of records to update, each with an `id` and a `fields` object',
|
||||
// Example: [{ id: "rec123", fields: { "Status": "Done" } }, { id: "rec456", fields: { "Priority": "High" } }]
|
||||
},
|
||||
// TODO: Add typecast, performUpsert parameters
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -57,16 +53,11 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
// Body should contain { records: [...] } and optionally { typecast: true, performUpsert: {...} }
|
||||
body: (params) => ({ records: params.records }),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
// logger.error('Airtable API error:', data)
|
||||
throw new Error(data.error?.message || 'Failed to update Airtable records')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -79,11 +70,6 @@ export const airtableUpdateMultipleRecordsTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
// logger.error('Airtable tool error:', error)
|
||||
return `Failed to update multiple Airtable records: ${error.message || 'Unknown error'}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'json',
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { AirtableUpdateParams, AirtableUpdateResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
// import { logger } from '@/utils/logger' // Removed logger due to import issues
|
||||
|
||||
export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, AirtableUpdateResponse> = {
|
||||
id: 'airtable_update_record',
|
||||
name: 'Airtable Update Record',
|
||||
@@ -56,16 +54,11 @@ export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, Airtable
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
// Body should contain { fields: {...} } and optionally { typecast: true }
|
||||
body: (params) => ({ fields: params.fields }),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
// logger.error('Airtable API error:', data)
|
||||
throw new Error(data.error?.message || 'Failed to update Airtable record')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -78,11 +71,6 @@ export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, Airtable
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
// logger.error('Airtable tool error:', error)
|
||||
return `Failed to update Airtable record: ${error.message || 'Unknown error'}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'json',
|
||||
|
||||
@@ -11,35 +11,6 @@ export const getAuthorPapersTool: ToolConfig<
|
||||
description: 'Search for papers by a specific author on ArXiv.',
|
||||
version: '1.0.0',
|
||||
|
||||
outputs: {
|
||||
authorPapers: {
|
||||
type: 'json',
|
||||
description: 'Array of papers authored by the specified author',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
authors: { type: 'string' },
|
||||
published: { type: 'string' },
|
||||
updated: { type: 'string' },
|
||||
link: { type: 'string' },
|
||||
pdfLink: { type: 'string' },
|
||||
categories: { type: 'string' },
|
||||
primaryCategory: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
journalRef: { type: 'string' },
|
||||
doi: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of papers found for the author',
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
authorName: {
|
||||
type: 'string',
|
||||
@@ -77,10 +48,6 @@ export const getAuthorPapersTool: ToolConfig<
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`ArXiv API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const xmlText = await response.text()
|
||||
|
||||
// Parse XML response
|
||||
@@ -97,9 +64,32 @@ export const getAuthorPapersTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while searching for author papers on ArXiv'
|
||||
outputs: {
|
||||
authorPapers: {
|
||||
type: 'json',
|
||||
description: 'Array of papers authored by the specified author',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
authors: { type: 'string' },
|
||||
published: { type: 'string' },
|
||||
updated: { type: 'string' },
|
||||
link: { type: 'string' },
|
||||
pdfLink: { type: 'string' },
|
||||
categories: { type: 'string' },
|
||||
primaryCategory: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
journalRef: { type: 'string' },
|
||||
doi: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of papers found for the author',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,31 +8,6 @@ export const getPaperTool: ToolConfig<ArxivGetPaperParams, ArxivGetPaperResponse
|
||||
description: 'Get detailed information about a specific ArXiv paper by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
outputs: {
|
||||
paper: {
|
||||
type: 'json',
|
||||
description: 'Detailed information about the requested ArXiv paper',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
authors: { type: 'string' },
|
||||
published: { type: 'string' },
|
||||
updated: { type: 'string' },
|
||||
link: { type: 'string' },
|
||||
pdfLink: { type: 'string' },
|
||||
categories: { type: 'string' },
|
||||
primaryCategory: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
journalRef: { type: 'string' },
|
||||
doi: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
paperId: {
|
||||
type: 'string',
|
||||
@@ -63,30 +38,39 @@ export const getPaperTool: ToolConfig<ArxivGetPaperParams, ArxivGetPaperResponse
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`ArXiv API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const xmlText = await response.text()
|
||||
|
||||
// Parse XML response
|
||||
const papers = parseArxivXML(xmlText)
|
||||
|
||||
if (papers.length === 0) {
|
||||
throw new Error('Paper not found')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
paper: papers[0],
|
||||
paper: papers[0] || null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while retrieving the ArXiv paper'
|
||||
outputs: {
|
||||
paper: {
|
||||
type: 'json',
|
||||
description: 'Detailed information about the requested ArXiv paper',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
authors: { type: 'string' },
|
||||
published: { type: 'string' },
|
||||
updated: { type: 'string' },
|
||||
link: { type: 'string' },
|
||||
pdfLink: { type: 'string' },
|
||||
categories: { type: 'string' },
|
||||
primaryCategory: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
journalRef: { type: 'string' },
|
||||
doi: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,35 +8,6 @@ export const searchTool: ToolConfig<ArxivSearchParams, ArxivSearchResponse> = {
|
||||
description: 'Search for academic papers on ArXiv by keywords, authors, titles, or other fields.',
|
||||
version: '1.0.0',
|
||||
|
||||
outputs: {
|
||||
papers: {
|
||||
type: 'json',
|
||||
description: 'Array of papers matching the search query',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
authors: { type: 'string' },
|
||||
published: { type: 'string' },
|
||||
updated: { type: 'string' },
|
||||
link: { type: 'string' },
|
||||
pdfLink: { type: 'string' },
|
||||
categories: { type: 'string' },
|
||||
primaryCategory: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
journalRef: { type: 'string' },
|
||||
doi: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of results found for the search query',
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
searchQuery: {
|
||||
type: 'string',
|
||||
@@ -107,10 +78,6 @@ export const searchTool: ToolConfig<ArxivSearchParams, ArxivSearchResponse> = {
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`ArXiv API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const xmlText = await response.text()
|
||||
|
||||
// Parse XML response
|
||||
@@ -127,7 +94,32 @@ export const searchTool: ToolConfig<ArxivSearchParams, ArxivSearchResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error ? error.message : 'An error occurred while searching ArXiv'
|
||||
outputs: {
|
||||
papers: {
|
||||
type: 'json',
|
||||
description: 'Array of papers matching the search query',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
authors: { type: 'string' },
|
||||
published: { type: 'string' },
|
||||
updated: { type: 'string' },
|
||||
link: { type: 'string' },
|
||||
pdfLink: { type: 'string' },
|
||||
categories: { type: 'string' },
|
||||
primaryCategory: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
journalRef: { type: 'string' },
|
||||
doi: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of results found for the search query',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -45,15 +45,6 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
|
||||
description: 'API key for BrowserUse API',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Browser automation task results including task ID, success status, output data, and execution steps',
|
||||
},
|
||||
error: { type: 'string', description: 'Error message if the operation failed' },
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.browser-use.com/api/v1/run-task',
|
||||
method: 'POST',
|
||||
@@ -109,14 +100,6 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 422) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(JSON.stringify(errorData))
|
||||
}
|
||||
throw new Error(`Request failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { id: string }
|
||||
return {
|
||||
success: true,
|
||||
@@ -242,19 +225,10 @@ export const runTaskTool: ToolConfig<BrowserUseRunTaskParams, BrowserUseRunTaskR
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
try {
|
||||
const errorData = JSON.parse(error.message)
|
||||
if (errorData.detail && Array.isArray(errorData.detail)) {
|
||||
const formattedError = errorData.detail
|
||||
.map((item: any) => `${item.loc.join('.')}: ${item.msg}`)
|
||||
.join(', ')
|
||||
return `Validation error: ${formattedError}`
|
||||
}
|
||||
} catch {
|
||||
// Not a JSON string, use the regular error message
|
||||
}
|
||||
|
||||
return `Failed to run BrowserUse task: ${error.message || 'Unknown error'}`
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Task execution identifier' },
|
||||
success: { type: 'boolean', description: 'Task completion status' },
|
||||
output: { type: 'json', description: 'Task output data' },
|
||||
steps: { type: 'json', description: 'Execution steps taken' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,13 +28,7 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
|
||||
description: 'Auth token for Clay webhook authentication',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'json',
|
||||
description: 'Clay populate operation results including response data from Clay webhook',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: ClayPopulateParams) => params.webhookURL,
|
||||
method: 'POST',
|
||||
@@ -53,15 +47,8 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = await response.json()
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error || 'Clay API error')
|
||||
}
|
||||
} else {
|
||||
// Handle text response
|
||||
data = await response.text()
|
||||
if (data !== 'OK' && !response.ok) {
|
||||
throw new Error(data || 'Clay API error')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -72,8 +59,11 @@ export const clayPopulateTool: ToolConfig<ClayPopulateParams, ClayPopulateRespon
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
const message = error.message || 'Clay populate failed'
|
||||
return message
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'json',
|
||||
description: 'Clay populate operation results including response data from Clay webhook',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ConfluenceRetrieveParams, ConfluenceRetrieveResponse } from '@/tools/confluence/types'
|
||||
import { transformPageData } from '@/tools/confluence/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const confluenceRetrieveTool: ToolConfig<
|
||||
@@ -15,13 +16,6 @@ export const confluenceRetrieveTool: ToolConfig<
|
||||
provider: 'confluence',
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of retrieval' },
|
||||
pageId: { type: 'string', description: 'Confluence page ID' },
|
||||
content: { type: 'string', description: 'Page content with HTML tags stripped' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -73,54 +67,14 @@ export const confluenceRetrieveTool: ToolConfig<
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.error ||
|
||||
`Failed to retrieve Confluence page: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return transformPageData(data)
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return error.message || 'Failed to retrieve Confluence page'
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of retrieval' },
|
||||
pageId: { type: 'string', description: 'Confluence page ID' },
|
||||
content: { type: 'string', description: 'Page content with HTML tags stripped' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
},
|
||||
}
|
||||
|
||||
function transformPageData(data: any) {
|
||||
// More lenient check - only require id and title
|
||||
if (!data || !data.id || !data.title) {
|
||||
throw new Error('Invalid response format from Confluence API - missing required fields')
|
||||
}
|
||||
|
||||
// Get content from wherever we can find it
|
||||
const content =
|
||||
data.body?.view?.value ||
|
||||
data.body?.storage?.value ||
|
||||
data.body?.atlas_doc_format?.value ||
|
||||
data.content ||
|
||||
data.description ||
|
||||
`Content for page ${data.title}`
|
||||
|
||||
const cleanContent = content
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.id,
|
||||
content: cleanContent,
|
||||
title: data.title,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,6 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
|
||||
provider: 'confluence',
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of update' },
|
||||
pageId: { type: 'string', description: 'Confluence page ID' },
|
||||
title: { type: 'string', description: 'Updated page title' },
|
||||
success: { type: 'boolean', description: 'Update operation success status' },
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -100,19 +93,6 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
console.error('Update tool error response:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
console.error(
|
||||
errorData?.error ||
|
||||
`Failed to update Confluence page: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
@@ -126,7 +106,10 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return error.message || 'Failed to update Confluence page'
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of update' },
|
||||
pageId: { type: 'string', description: 'Confluence page ID' },
|
||||
title: { type: 'string', description: 'Updated page title' },
|
||||
success: { type: 'boolean', description: 'Update operation success status' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,3 +27,33 @@ export async function getConfluenceCloudId(domain: string, accessToken: string):
|
||||
|
||||
throw new Error('No Confluence resources found')
|
||||
}
|
||||
|
||||
export function transformPageData(data: any) {
|
||||
// Get content from wherever we can find it
|
||||
const content =
|
||||
data.body?.view?.value ||
|
||||
data.body?.storage?.value ||
|
||||
data.body?.atlas_doc_format?.value ||
|
||||
data.content ||
|
||||
data.description ||
|
||||
`Content for page ${data.title || 'Unknown'}`
|
||||
|
||||
const cleanContent = content
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.id || '',
|
||||
content: cleanContent,
|
||||
title: data.title || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
DiscordAPIError,
|
||||
DiscordGetMessagesParams,
|
||||
DiscordGetMessagesResponse,
|
||||
DiscordMessage,
|
||||
} from '@/tools/discord/types'
|
||||
import type { DiscordGetMessagesParams, DiscordGetMessagesResponse } from '@/tools/discord/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DiscordGetMessages')
|
||||
|
||||
export const discordGetMessagesTool: ToolConfig<
|
||||
DiscordGetMessagesParams,
|
||||
DiscordGetMessagesResponse
|
||||
@@ -59,36 +51,7 @@ export const discordGetMessagesTool: ToolConfig<
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to get Discord messages: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = (await response.json()) as DiscordAPIError
|
||||
errorMessage = `Failed to get Discord messages: ${errorData.message || response.statusText}`
|
||||
logger.error('Discord API error', { status: response.status, error: errorData })
|
||||
} catch (e) {
|
||||
logger.error('Error parsing Discord API response', { status: response.status, error: e })
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
let messages: DiscordMessage[]
|
||||
try {
|
||||
messages = await response.json()
|
||||
} catch (_e) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to parse messages',
|
||||
output: { message: 'Failed to parse messages' },
|
||||
}
|
||||
}
|
||||
const messages = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -101,11 +64,6 @@ export const discordGetMessagesTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
logger.error('Error retrieving Discord messages', { error })
|
||||
return `Error retrieving Discord messages: ${error instanceof Error ? error.message : String(error)}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
messages: {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
DiscordAPIError,
|
||||
DiscordGetServerParams,
|
||||
DiscordGetServerResponse,
|
||||
DiscordGuild,
|
||||
} from '@/tools/discord/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DiscordGetServer')
|
||||
|
||||
export const discordGetServerTool: ToolConfig<DiscordGetServerParams, DiscordGetServerResponse> = {
|
||||
id: 'discord_get_server',
|
||||
name: 'Discord Get Server',
|
||||
@@ -48,36 +44,7 @@ export const discordGetServerTool: ToolConfig<DiscordGetServerParams, DiscordGet
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
let responseData: any
|
||||
|
||||
try {
|
||||
responseData = await response.json()
|
||||
} catch (e) {
|
||||
logger.error('Error parsing Discord API response', { status: response.status, error: e })
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to parse server data',
|
||||
output: { message: 'Failed to parse server data' },
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = responseData as DiscordAPIError
|
||||
const errorMessage = `Discord API error: ${errorData.message || response.statusText}`
|
||||
|
||||
logger.error('Discord API error', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
const responseData = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -88,11 +55,6 @@ export const discordGetServerTool: ToolConfig<DiscordGetServerParams, DiscordGet
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: Error | unknown): string => {
|
||||
logger.error('Error fetching Discord server', { error })
|
||||
return `Error fetching Discord server: ${error instanceof Error ? error.message : String(error)}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
data: {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
DiscordAPIError,
|
||||
DiscordGetUserParams,
|
||||
DiscordGetUserResponse,
|
||||
DiscordUser,
|
||||
} from '@/tools/discord/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DiscordGetUser')
|
||||
|
||||
export const discordGetUserTool: ToolConfig<DiscordGetUserParams, DiscordGetUserResponse> = {
|
||||
id: 'discord_get_user',
|
||||
name: 'Discord Get User',
|
||||
@@ -47,36 +43,7 @@ export const discordGetUserTool: ToolConfig<DiscordGetUserParams, DiscordGetUser
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to get Discord user: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = (await response.json()) as DiscordAPIError
|
||||
errorMessage = `Failed to get Discord user: ${errorData.message || response.statusText}`
|
||||
logger.error('Discord API error', { status: response.status, error: errorData })
|
||||
} catch (e) {
|
||||
logger.error('Error parsing Discord API response', { status: response.status, error: e })
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
let data: DiscordUser
|
||||
try {
|
||||
data = await response.clone().json()
|
||||
} catch (_e) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to parse user data',
|
||||
output: { message: 'Failed to parse user data' },
|
||||
}
|
||||
}
|
||||
const data: DiscordUser = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -87,11 +54,6 @@ export const discordGetUserTool: ToolConfig<DiscordGetUserParams, DiscordGetUser
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
logger.error('Error retrieving Discord user information', { error })
|
||||
return `Error retrieving Discord user information: ${error.error}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
data: {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
DiscordAPIError,
|
||||
DiscordMessage,
|
||||
DiscordSendMessageParams,
|
||||
DiscordSendMessageResponse,
|
||||
} from '@/tools/discord/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DiscordSendMessage')
|
||||
|
||||
export const discordSendMessageTool: ToolConfig<
|
||||
DiscordSendMessageParams,
|
||||
DiscordSendMessageResponse
|
||||
@@ -76,50 +72,14 @@ export const discordSendMessageTool: ToolConfig<
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
let errorMessage = `Failed to send Discord message: ${response.status} ${response.statusText}`
|
||||
|
||||
try {
|
||||
const errorData = (await response.json()) as DiscordAPIError
|
||||
errorMessage = `Failed to send Discord message: ${errorData.message || response.statusText}`
|
||||
logger.error('Discord API error', { status: response.status, error: errorData })
|
||||
} catch (e) {
|
||||
logger.error('Error parsing Discord API response', { status: response.status, error: e })
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
const data = (await response.json()) as DiscordMessage
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: 'Discord message sent successfully',
|
||||
data,
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
const data = (await response.json()) as DiscordMessage
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: 'Discord message sent successfully',
|
||||
data,
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error parsing successful Discord response', { error: e })
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to parse Discord response',
|
||||
output: {
|
||||
message: 'Failed to parse Discord response',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: Error | unknown): string => {
|
||||
logger.error('Error sending Discord message', { error })
|
||||
return `Error sending Discord message: ${error instanceof Error ? error.message : String(error)}`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ElevenLabsTtsParams, ElevenLabsTtsResponse } from '@/tools/elevenlabs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('ElevenLabsTool')
|
||||
|
||||
export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsResponse> = {
|
||||
id: 'elevenlabs_tts',
|
||||
name: 'ElevenLabs TTS',
|
||||
@@ -49,14 +46,9 @@ export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsRes
|
||||
voiceId: params.voiceId,
|
||||
modelId: params.modelId || 'eleven_monolingual_v1',
|
||||
}),
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
@@ -67,8 +59,7 @@ export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsRes
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
logger.error('ElevenLabs TTS error:', error)
|
||||
return `Error generating speech: ${error instanceof Error ? error.message : String(error)}`
|
||||
outputs: {
|
||||
audioUrl: { type: 'string', description: 'The URL of the generated audio' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -28,29 +28,9 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'AI-generated answer to the question',
|
||||
},
|
||||
citations: {
|
||||
type: 'array',
|
||||
description: 'Sources and citations for the answer',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The title of the cited source' },
|
||||
url: { type: 'string', description: 'The URL of the cited source' },
|
||||
text: { type: 'string', description: 'Relevant text from the cited source' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.exa.ai/answer',
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': params.apiKey,
|
||||
@@ -70,10 +50,6 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to generate answer')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -89,7 +65,22 @@ export const answerTool: ToolConfig<ExaAnswerParams, ExaAnswerResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error ? error.message : 'An error occurred while generating an answer'
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'AI-generated answer to the question',
|
||||
},
|
||||
citations: {
|
||||
type: 'array',
|
||||
description: 'Sources and citations for the answer',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The title of the cited source' },
|
||||
url: { type: 'string', description: 'The URL of the cited source' },
|
||||
text: { type: 'string', description: 'Relevant text from the cited source' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -38,32 +38,9 @@ export const findSimilarLinksTool: ToolConfig<
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
similarLinks: {
|
||||
type: 'array',
|
||||
description: 'Similar links found with titles, URLs, and text snippets',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The title of the similar webpage' },
|
||||
url: { type: 'string', description: 'The URL of the similar webpage' },
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Text snippet or full content from the similar webpage',
|
||||
},
|
||||
score: {
|
||||
type: 'number',
|
||||
description: 'Similarity score indicating how similar the page is',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.exa.ai/findSimilar',
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': params.apiKey,
|
||||
@@ -90,10 +67,6 @@ export const findSimilarLinksTool: ToolConfig<
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to find similar links')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -107,7 +80,25 @@ export const findSimilarLinksTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error ? error.message : 'An error occurred while finding similar links'
|
||||
outputs: {
|
||||
similarLinks: {
|
||||
type: 'array',
|
||||
description: 'Similar links found with titles, URLs, and text snippets',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The title of the similar webpage' },
|
||||
url: { type: 'string', description: 'The URL of the similar webpage' },
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Text snippet or full content from the similar webpage',
|
||||
},
|
||||
score: {
|
||||
type: 'number',
|
||||
description: 'Similarity score indicating how similar the page is',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -36,26 +36,9 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Retrieved content from URLs with title, text, and summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The URL that content was retrieved from' },
|
||||
title: { type: 'string', description: 'The title of the webpage' },
|
||||
text: { type: 'string', description: 'The full text content of the webpage' },
|
||||
summary: { type: 'string', description: 'AI-generated summary of the webpage content' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.exa.ai/contents',
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': params.apiKey,
|
||||
@@ -91,10 +74,6 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to retrieve webpage contents')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -108,9 +87,19 @@ export const getContentsTool: ToolConfig<ExaGetContentsParams, ExaGetContentsRes
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while retrieving webpage contents'
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Retrieved content from URLs with title, text, and summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'The URL that content was retrieved from' },
|
||||
title: { type: 'string', description: 'The title of the webpage' },
|
||||
text: { type: 'string', description: 'The full text content of the webpage' },
|
||||
summary: { type: 'string', description: 'AI-generated summary of the webpage content' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,28 +34,9 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
research: {
|
||||
type: 'array',
|
||||
description: 'Comprehensive research findings with citations and summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
text: { type: 'string' },
|
||||
publishedDate: { type: 'string' },
|
||||
author: { type: 'string' },
|
||||
score: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.exa.ai/research/v0/tasks',
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': params.apiKey,
|
||||
@@ -92,13 +73,10 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to create research task')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -183,14 +161,23 @@ export const researchTool: ToolConfig<ExaResearchParams, ExaResearchResponse> =
|
||||
error: `Research task did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`,
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
const errorMessage = error?.message || ''
|
||||
if (errorMessage.includes('401')) {
|
||||
return new Error('Invalid API key. Please check your Exa AI API key.')
|
||||
}
|
||||
if (errorMessage.includes('429')) {
|
||||
return new Error('Rate limit exceeded. Please try again later.')
|
||||
}
|
||||
return error
|
||||
|
||||
outputs: {
|
||||
research: {
|
||||
type: 'array',
|
||||
description: 'Comprehensive research findings with citations and summaries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
text: { type: 'string' },
|
||||
publishedDate: { type: 'string' },
|
||||
author: { type: 'string' },
|
||||
score: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,31 +41,9 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Search results with titles, URLs, and text snippets',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The title of the search result' },
|
||||
url: { type: 'string', description: 'The URL of the search result' },
|
||||
publishedDate: { type: 'string', description: 'Date when the content was published' },
|
||||
author: { type: 'string', description: 'The author of the content' },
|
||||
summary: { type: 'string', description: 'A brief summary of the content' },
|
||||
favicon: { type: 'string', description: "URL of the site's favicon" },
|
||||
image: { type: 'string', description: 'URL of a representative image from the page' },
|
||||
text: { type: 'string', description: 'Text snippet or full content from the page' },
|
||||
score: { type: 'number', description: 'Relevance score for the search result' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.exa.ai/search',
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': params.apiKey,
|
||||
@@ -87,10 +65,6 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Failed to perform Exa search')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -109,9 +83,24 @@ export const searchTool: ToolConfig<ExaSearchParams, ExaSearchResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while performing the Exa search'
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Search results with titles, URLs, and text snippets',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The title of the search result' },
|
||||
url: { type: 'string', description: 'The URL of the search result' },
|
||||
publishedDate: { type: 'string', description: 'Date when the content was published' },
|
||||
author: { type: 'string', description: 'The author of the content' },
|
||||
summary: { type: 'string', description: 'A brief summary of the content' },
|
||||
favicon: { type: 'string', description: "URL of the site's favicon" },
|
||||
image: { type: 'string', description: 'URL of a representative image from the page' },
|
||||
text: { type: 'string', description: 'Text snippet or full content from the page' },
|
||||
score: { type: 'number', description: 'Relevance score for the search result' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -87,95 +87,67 @@ export const fileParserTool: ToolConfig<FileParserInput, FileParserOutput> = {
|
||||
fileType: determinedFileType,
|
||||
}
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<FileParserOutput> => {
|
||||
logger.info('Received response status:', response.status)
|
||||
|
||||
try {
|
||||
const result = await response.json()
|
||||
logger.info('Response parsed successfully')
|
||||
const result = await response.json()
|
||||
logger.info('Response parsed successfully')
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMsg = result.error || 'File parsing failed'
|
||||
logger.error('Error in response:', errorMsg)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
// Handle multiple files response
|
||||
if (result.results) {
|
||||
logger.info('Processing multiple files response')
|
||||
|
||||
// Handle multiple files response
|
||||
if (result.results) {
|
||||
logger.info('Processing multiple files response')
|
||||
// Extract individual file results
|
||||
const fileResults = result.results.map((fileResult: any) => {
|
||||
return fileResult.output || fileResult
|
||||
})
|
||||
|
||||
// Extract individual file results
|
||||
const fileResults = result.results.map((fileResult: any) => {
|
||||
if (!fileResult.success) {
|
||||
logger.warn(`Error parsing file ${fileResult.filePath}: ${fileResult.error}`)
|
||||
return {
|
||||
content: `Error parsing file: ${fileResult.error || 'Unknown error'}`,
|
||||
fileType: 'text/plain',
|
||||
size: 0,
|
||||
name: fileResult.filePath.split('/').pop() || 'unknown',
|
||||
binary: false,
|
||||
}
|
||||
}
|
||||
// Combine all file contents with clear dividers
|
||||
const combinedContent = fileResults
|
||||
.map((file: FileParseResult, index: number) => {
|
||||
const divider = `\n${'='.repeat(80)}\n`
|
||||
|
||||
return fileResult.output
|
||||
return file.content + (index < fileResults.length - 1 ? divider : '')
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// Combine all file contents with clear dividers
|
||||
const combinedContent = fileResults
|
||||
.map((file: FileParseResult, index: number) => {
|
||||
const divider = `\n${'='.repeat(80)}\n`
|
||||
|
||||
return file.content + (index < fileResults.length - 1 ? divider : '')
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// Create the base output
|
||||
const output: FileParserOutputData = {
|
||||
files: fileResults,
|
||||
combinedContent,
|
||||
}
|
||||
|
||||
// Add named properties for each file for dropdown access
|
||||
fileResults.forEach((file: FileParseResult, index: number) => {
|
||||
output[`file${index + 1}`] = file
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
}
|
||||
// Create the base output
|
||||
const output: FileParserOutputData = {
|
||||
files: fileResults,
|
||||
combinedContent,
|
||||
}
|
||||
|
||||
// Handle single file response
|
||||
if (result.success) {
|
||||
logger.info('Successfully parsed file:', result.output.name)
|
||||
// Add named properties for each file for dropdown access
|
||||
fileResults.forEach((file: FileParseResult, index: number) => {
|
||||
output[`file${index + 1}`] = file
|
||||
})
|
||||
|
||||
// For a single file, create the output with both array and named property
|
||||
const output: FileParserOutputData = {
|
||||
files: [result.output],
|
||||
combinedContent: result.output.content,
|
||||
file1: result.output,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error response
|
||||
throw new Error(result.error || 'File parsing failed')
|
||||
} catch (error) {
|
||||
logger.error('Error processing response:', error)
|
||||
throw error
|
||||
// Handle single file response
|
||||
logger.info('Successfully parsed file:', result.output?.name || 'unknown')
|
||||
|
||||
// For a single file, create the output with both array and named property
|
||||
const output: FileParserOutputData = {
|
||||
files: [result.output || result],
|
||||
combinedContent: result.output?.content || result.content || '',
|
||||
file1: result.output || result,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
logger.error('Error occurred:', error)
|
||||
return error.message || 'File parsing failed'
|
||||
outputs: {
|
||||
files: { type: 'array', description: 'Array of parsed files' },
|
||||
combinedContent: { type: 'string', description: 'Combined content of all parsed files' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
|
||||
request: {
|
||||
url: 'https://api.firecrawl.dev/v1/crawl',
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
@@ -58,10 +57,6 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.message || 'Failed to create crawl job')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -140,19 +135,6 @@ export const crawlTool: ToolConfig<FirecrawlCrawlParams, FirecrawlCrawlResponse>
|
||||
error: `Crawl job did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`,
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
const errorMessage = error?.message || ''
|
||||
if (errorMessage.includes('401')) {
|
||||
return new Error('Invalid API key. Please check your Firecrawl API key.')
|
||||
}
|
||||
if (errorMessage.includes('429')) {
|
||||
return new Error('Rate limit exceeded. Please try again later.')
|
||||
}
|
||||
if (errorMessage.includes('402')) {
|
||||
return new Error('Insufficient credits. Please check your Firecrawl account.')
|
||||
}
|
||||
return error
|
||||
},
|
||||
|
||||
outputs: {
|
||||
pages: {
|
||||
|
||||
@@ -45,10 +45,6 @@ export const scrapeTool: ToolConfig<ScrapeParams, ScrapeResponse> = {
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error?.message || 'Unknown error occurred')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -59,12 +55,6 @@ export const scrapeTool: ToolConfig<ScrapeParams, ScrapeResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
const message = error.error?.message || error.message
|
||||
const code = error.error?.type || error.code
|
||||
return `${message} (${code})`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
markdown: { type: 'string', description: 'Page content in markdown format' },
|
||||
html: { type: 'string', description: 'Raw HTML content of the page' },
|
||||
|
||||
@@ -37,10 +37,6 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error?.message || 'Unknown error occurred')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -50,12 +46,6 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
const message = error.error?.message || error.message
|
||||
const code = error.error?.type || error.code
|
||||
return `${message} (${code})`
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'array',
|
||||
|
||||
@@ -68,30 +68,11 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
isCustomTool: params.isCustomTool || false,
|
||||
}
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response): Promise<CodeExecutionOutput> => {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
// Create enhanced error with debug information if available
|
||||
const error = new Error(result.error || 'Code execution failed')
|
||||
|
||||
// Add debug information to the error object if available
|
||||
if (result.debug) {
|
||||
Object.assign(error, {
|
||||
line: result.debug.line,
|
||||
column: result.debug.column,
|
||||
errorType: result.debug.errorType,
|
||||
stack: result.debug.stack,
|
||||
enhancedError: true,
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -101,11 +82,8 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
// If we have enhanced error information, create a more detailed message
|
||||
if (error.enhancedError && error.line) {
|
||||
return `Line ${error.line}${error.column ? `:${error.column}` : ''} - ${error.message}`
|
||||
}
|
||||
return error.message || 'Code execution failed'
|
||||
outputs: {
|
||||
result: { type: 'string', description: 'The result of the code execution' },
|
||||
stdout: { type: 'string', description: 'The standard output of the code execution' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -131,10 +131,6 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error ? error.message : 'Failed to create comment'
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Human-readable comment confirmation' },
|
||||
metadata: {
|
||||
|
||||
@@ -48,10 +48,6 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Create a human-readable content string
|
||||
@@ -132,19 +128,6 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('404')) {
|
||||
return 'Commit or repository not found. Please check the owner, repository name, and branch.'
|
||||
}
|
||||
if (error.message.includes('401')) {
|
||||
return 'Authentication failed. Please check your GitHub token.'
|
||||
}
|
||||
return error.message
|
||||
}
|
||||
return 'Failed to fetch commit information'
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Human-readable commit summary' },
|
||||
metadata: {
|
||||
|
||||
@@ -90,10 +90,6 @@ URL: ${pr.html_url}`
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error ? error.message : 'Failed to fetch PR details'
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Human-readable PR summary' },
|
||||
metadata: {
|
||||
|
||||
@@ -40,10 +40,6 @@ export const repoInfoTool: ToolConfig<BaseGitHubParams, RepoInfoResponse> = {
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Create a human-readable content string
|
||||
@@ -71,19 +67,6 @@ URL: ${data.html_url}`
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('404')) {
|
||||
return 'Repository not found. Please check the owner and repository name.'
|
||||
}
|
||||
if (error.message.includes('401')) {
|
||||
return 'Authentication failed. Please check your GitHub token.'
|
||||
}
|
||||
return error.message
|
||||
}
|
||||
return 'Failed to fetch repository information'
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Human-readable repository summary' },
|
||||
metadata: {
|
||||
|
||||
@@ -9,26 +9,6 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
description: 'Draft emails using Gmail',
|
||||
version: '1.0.0',
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Success message' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Draft metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Draft ID' },
|
||||
message: {
|
||||
type: 'object',
|
||||
description: 'Message metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gmail message ID' },
|
||||
threadId: { type: 'string', description: 'Gmail thread ID' },
|
||||
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-email',
|
||||
@@ -109,10 +89,6 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to draft email')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -129,16 +105,23 @@ export const gmailDraftTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while drafting email'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Success message' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Draft metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Draft ID' },
|
||||
message: {
|
||||
type: 'object',
|
||||
description: 'Message metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gmail message ID' },
|
||||
threadId: { type: 'string', description: 'Gmail thread ID' },
|
||||
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type {
|
||||
GmailAttachment,
|
||||
GmailMessage,
|
||||
GmailReadParams,
|
||||
GmailToolResponse,
|
||||
} from '@/tools/gmail/types'
|
||||
import type { GmailReadParams, GmailToolResponse } from '@/tools/gmail/types'
|
||||
import {
|
||||
createMessagesSummary,
|
||||
GMAIL_API_BASE,
|
||||
processMessage,
|
||||
processMessageForSummary,
|
||||
} from '@/tools/gmail/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
||||
id: 'gmail_read',
|
||||
name: 'Gmail Read',
|
||||
@@ -23,12 +22,6 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
||||
],
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Text content of the email' },
|
||||
metadata: { type: 'json', description: 'Metadata of the email' },
|
||||
attachments: { type: 'file[]', description: 'Attachments of the email' },
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -120,10 +113,6 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
||||
transformResponse: async (response: Response, params?: GmailReadParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to read email')
|
||||
}
|
||||
|
||||
// If we're fetching a single message directly (by ID)
|
||||
if (params?.messageId) {
|
||||
return await processMessage(data, params)
|
||||
@@ -253,248 +242,9 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while reading email'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Text content of the email' },
|
||||
metadata: { type: 'json', description: 'Metadata of the email' },
|
||||
attachments: { type: 'file[]', description: 'Attachments of the email' },
|
||||
},
|
||||
}
|
||||
|
||||
// Helper function to process a Gmail message
|
||||
async function processMessage(
|
||||
message: GmailMessage,
|
||||
params?: GmailReadParams
|
||||
): Promise<GmailToolResponse> {
|
||||
// Check if message and payload exist
|
||||
if (!message || !message.payload) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: 'Unable to process email: Invalid message format',
|
||||
metadata: {
|
||||
id: message?.id || '',
|
||||
threadId: message?.threadId || '',
|
||||
labelIds: message?.labelIds || [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const headers = message.payload.headers || []
|
||||
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || ''
|
||||
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || ''
|
||||
const to = headers.find((h) => h.name.toLowerCase() === 'to')?.value || ''
|
||||
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
|
||||
|
||||
// Extract the message body
|
||||
const body = extractMessageBody(message.payload)
|
||||
|
||||
// Check for attachments
|
||||
const attachmentInfo = extractAttachmentInfo(message.payload)
|
||||
const hasAttachments = attachmentInfo.length > 0
|
||||
|
||||
// Download attachments if requested
|
||||
let attachments: GmailAttachment[] | undefined
|
||||
if (params?.includeAttachments && hasAttachments && params.accessToken) {
|
||||
try {
|
||||
attachments = await downloadAttachments(message.id, attachmentInfo, params.accessToken)
|
||||
} catch (error) {
|
||||
// Continue without attachments rather than failing the entire request
|
||||
}
|
||||
}
|
||||
|
||||
const result: GmailToolResponse = {
|
||||
success: true,
|
||||
output: {
|
||||
content: body || 'No content found in email',
|
||||
metadata: {
|
||||
id: message.id || '',
|
||||
threadId: message.threadId || '',
|
||||
labelIds: message.labelIds || [],
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
date,
|
||||
hasAttachments,
|
||||
attachmentCount: attachmentInfo.length,
|
||||
},
|
||||
// Always include attachments array (empty if none downloaded)
|
||||
attachments: attachments || [],
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper function to process a message for summary (without full content)
|
||||
function processMessageForSummary(message: GmailMessage): any {
|
||||
if (!message || !message.payload) {
|
||||
return {
|
||||
id: message?.id || '',
|
||||
threadId: message?.threadId || '',
|
||||
subject: 'Unknown Subject',
|
||||
from: 'Unknown Sender',
|
||||
date: '',
|
||||
snippet: message?.snippet || '',
|
||||
}
|
||||
}
|
||||
|
||||
const headers = message.payload.headers || []
|
||||
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
|
||||
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
|
||||
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
threadId: message.threadId,
|
||||
subject,
|
||||
from,
|
||||
date,
|
||||
snippet: message.snippet || '',
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a summary of multiple messages
|
||||
function createMessagesSummary(messages: any[]): string {
|
||||
if (messages.length === 0) {
|
||||
return 'No messages found.'
|
||||
}
|
||||
|
||||
let summary = `Found ${messages.length} messages:\n\n`
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
summary += `${index + 1}. Subject: ${msg.subject}\n`
|
||||
summary += ` From: ${msg.from}\n`
|
||||
summary += ` Date: ${msg.date}\n`
|
||||
summary += ` Preview: ${msg.snippet}\n\n`
|
||||
})
|
||||
|
||||
summary += `To read a specific message, use the messageId parameter with one of these IDs: ${messages.map((m) => m.id).join(', ')}`
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// Helper function to recursively extract message body from MIME parts
|
||||
function extractMessageBody(payload: any): string {
|
||||
// If the payload has a body with data, decode it
|
||||
if (payload.body?.data) {
|
||||
return Buffer.from(payload.body.data, 'base64').toString()
|
||||
}
|
||||
|
||||
// If there are no parts, return empty string
|
||||
if (!payload.parts || !Array.isArray(payload.parts) || payload.parts.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// First try to find a text/plain part
|
||||
const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain')
|
||||
if (textPart?.body?.data) {
|
||||
return Buffer.from(textPart.body.data, 'base64').toString()
|
||||
}
|
||||
|
||||
// If no text/plain, try to find text/html
|
||||
const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html')
|
||||
if (htmlPart?.body?.data) {
|
||||
return Buffer.from(htmlPart.body.data, 'base64').toString()
|
||||
}
|
||||
|
||||
// If we have multipart/alternative or other complex types, recursively check parts
|
||||
for (const part of payload.parts) {
|
||||
if (part.parts) {
|
||||
const nestedBody = extractMessageBody(part)
|
||||
if (nestedBody) {
|
||||
return nestedBody
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find any text content, return empty string
|
||||
return ''
|
||||
}
|
||||
|
||||
// Helper function to extract attachment information from message payload
|
||||
function extractAttachmentInfo(
|
||||
payload: any
|
||||
): Array<{ attachmentId: string; filename: string; mimeType: string; size: number }> {
|
||||
const attachments: Array<{
|
||||
attachmentId: string
|
||||
filename: string
|
||||
mimeType: string
|
||||
size: number
|
||||
}> = []
|
||||
|
||||
function processPayloadPart(part: any) {
|
||||
// Check if this part has an attachment
|
||||
if (part.body?.attachmentId && part.filename) {
|
||||
attachments.push({
|
||||
attachmentId: part.body.attachmentId,
|
||||
filename: part.filename,
|
||||
mimeType: part.mimeType || 'application/octet-stream',
|
||||
size: part.body.size || 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Recursively process nested parts
|
||||
if (part.parts && Array.isArray(part.parts)) {
|
||||
part.parts.forEach(processPayloadPart)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the main payload
|
||||
processPayloadPart(payload)
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
// Helper function to download attachments from Gmail API
|
||||
async function downloadAttachments(
|
||||
messageId: string,
|
||||
attachmentInfo: Array<{ attachmentId: string; filename: string; mimeType: string; size: number }>,
|
||||
accessToken: string
|
||||
): Promise<GmailAttachment[]> {
|
||||
const downloadedAttachments: GmailAttachment[] = []
|
||||
|
||||
for (const attachment of attachmentInfo) {
|
||||
try {
|
||||
// Download attachment from Gmail API
|
||||
const attachmentResponse = await fetch(
|
||||
`${GMAIL_API_BASE}/messages/${messageId}/attachments/${attachment.attachmentId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!attachmentResponse.ok) {
|
||||
continue
|
||||
}
|
||||
|
||||
const attachmentData = (await attachmentResponse.json()) as { data: string; size: number }
|
||||
|
||||
// Decode base64url data to buffer
|
||||
// Gmail API returns data in base64url format (URL-safe base64)
|
||||
const base64Data = attachmentData.data.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const buffer = Buffer.from(base64Data, 'base64')
|
||||
|
||||
downloadedAttachments.push({
|
||||
name: attachment.filename,
|
||||
data: buffer,
|
||||
mimeType: attachment.mimeType,
|
||||
size: attachment.size,
|
||||
})
|
||||
} catch (error) {
|
||||
// Continue with other attachments
|
||||
}
|
||||
}
|
||||
|
||||
return downloadedAttachments
|
||||
}
|
||||
|
||||
@@ -1,39 +1,17 @@
|
||||
import type { GmailSearchParams, GmailToolResponse } from '@/tools/gmail/types'
|
||||
import {
|
||||
createMessagesSummary,
|
||||
GMAIL_API_BASE,
|
||||
processMessageForSummary,
|
||||
} from '@/tools/gmail/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> = {
|
||||
id: 'gmail_search',
|
||||
name: 'Gmail Search',
|
||||
description: 'Search emails in Gmail',
|
||||
version: '1.0.0',
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Search results summary' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Search metadata',
|
||||
properties: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search results',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gmail message ID' },
|
||||
threadId: { type: 'string', description: 'Gmail thread ID' },
|
||||
subject: { type: 'string', description: 'Email subject' },
|
||||
from: { type: 'string', description: 'Sender email address' },
|
||||
date: { type: 'string', description: 'Email date' },
|
||||
snippet: { type: 'string', description: 'Email snippet/preview' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-email',
|
||||
@@ -80,10 +58,6 @@ export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> =
|
||||
transformResponse: async (response, params) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to search emails')
|
||||
}
|
||||
|
||||
if (!data.messages || data.messages.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -151,65 +125,28 @@ export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> =
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while searching emails'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Search results summary' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Search metadata',
|
||||
properties: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search results',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gmail message ID' },
|
||||
threadId: { type: 'string', description: 'Gmail thread ID' },
|
||||
subject: { type: 'string', description: 'Email subject' },
|
||||
from: { type: 'string', description: 'Sender email address' },
|
||||
date: { type: 'string', description: 'Email date' },
|
||||
snippet: { type: 'string', description: 'Email snippet/preview' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Helper function to process a message for summary (without full content)
|
||||
function processMessageForSummary(message: any): any {
|
||||
if (!message || !message.payload) {
|
||||
return {
|
||||
id: message?.id || '',
|
||||
threadId: message?.threadId || '',
|
||||
subject: 'Unknown Subject',
|
||||
from: 'Unknown Sender',
|
||||
date: '',
|
||||
snippet: message?.snippet || '',
|
||||
}
|
||||
}
|
||||
|
||||
const headers = message.payload.headers || []
|
||||
const subject =
|
||||
headers.find((h: any) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
|
||||
const from = headers.find((h: any) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
|
||||
const date = headers.find((h: any) => h.name.toLowerCase() === 'date')?.value || ''
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
threadId: message.threadId,
|
||||
subject,
|
||||
from,
|
||||
date,
|
||||
snippet: message.snippet || '',
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a summary of multiple messages
|
||||
function createMessagesSummary(messages: any[]): string {
|
||||
if (messages.length === 0) {
|
||||
return 'No messages found.'
|
||||
}
|
||||
|
||||
let summary = `Found ${messages.length} messages:\n\n`
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
summary += `${index + 1}. Subject: ${msg.subject}\n`
|
||||
summary += ` From: ${msg.from}\n`
|
||||
summary += ` Date: ${msg.date}\n`
|
||||
summary += ` Preview: ${msg.snippet}\n\n`
|
||||
})
|
||||
|
||||
summary += `To read full content of a specific message, use the gmail_read tool with messageId: ${messages.map((m) => m.id).join(', ')}`
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import type { GmailSendParams, GmailToolResponse } from '@/tools/gmail/types'
|
||||
import { GMAIL_API_BASE } from '@/tools/gmail/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
id: 'gmail_send',
|
||||
name: 'Gmail Send',
|
||||
description: 'Send emails using Gmail',
|
||||
version: '1.0.0',
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Success message' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Email metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gmail message ID' },
|
||||
threadId: { type: 'string', description: 'Gmail thread ID' },
|
||||
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-email',
|
||||
@@ -100,10 +86,6 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to send email')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -117,16 +99,16 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Gmail API quota exceeded. Please try again later.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while sending email'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Success message' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Email metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gmail message ID' },
|
||||
threadId: { type: 'string', description: 'Gmail thread ID' },
|
||||
labelIds: { type: 'array', items: { type: 'string' }, description: 'Email labels' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
240
apps/sim/tools/gmail/utils.ts
Normal file
240
apps/sim/tools/gmail/utils.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import type {
|
||||
GmailAttachment,
|
||||
GmailMessage,
|
||||
GmailReadParams,
|
||||
GmailToolResponse,
|
||||
} from '@/tools/gmail/types'
|
||||
|
||||
export const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'
|
||||
|
||||
// Helper function to process a Gmail message
|
||||
export async function processMessage(
|
||||
message: GmailMessage,
|
||||
params?: GmailReadParams
|
||||
): Promise<GmailToolResponse> {
|
||||
// Check if message and payload exist
|
||||
if (!message || !message.payload) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: 'Unable to process email: Invalid message format',
|
||||
metadata: {
|
||||
id: message?.id || '',
|
||||
threadId: message?.threadId || '',
|
||||
labelIds: message?.labelIds || [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const headers = message.payload.headers || []
|
||||
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || ''
|
||||
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || ''
|
||||
const to = headers.find((h) => h.name.toLowerCase() === 'to')?.value || ''
|
||||
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
|
||||
|
||||
// Extract the message body
|
||||
const body = extractMessageBody(message.payload)
|
||||
|
||||
// Check for attachments
|
||||
const attachmentInfo = extractAttachmentInfo(message.payload)
|
||||
const hasAttachments = attachmentInfo.length > 0
|
||||
|
||||
// Download attachments if requested
|
||||
let attachments: GmailAttachment[] | undefined
|
||||
if (params?.includeAttachments && hasAttachments && params.accessToken) {
|
||||
try {
|
||||
attachments = await downloadAttachments(message.id, attachmentInfo, params.accessToken)
|
||||
} catch (error) {
|
||||
// Continue without attachments rather than failing the entire request
|
||||
}
|
||||
}
|
||||
|
||||
const result: GmailToolResponse = {
|
||||
success: true,
|
||||
output: {
|
||||
content: body || 'No content found in email',
|
||||
metadata: {
|
||||
id: message.id || '',
|
||||
threadId: message.threadId || '',
|
||||
labelIds: message.labelIds || [],
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
date,
|
||||
hasAttachments,
|
||||
attachmentCount: attachmentInfo.length,
|
||||
},
|
||||
// Always include attachments array (empty if none downloaded)
|
||||
attachments: attachments || [],
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper function to process a message for summary (without full content)
|
||||
export function processMessageForSummary(message: GmailMessage): any {
|
||||
if (!message || !message.payload) {
|
||||
return {
|
||||
id: message?.id || '',
|
||||
threadId: message?.threadId || '',
|
||||
subject: 'Unknown Subject',
|
||||
from: 'Unknown Sender',
|
||||
date: '',
|
||||
snippet: message?.snippet || '',
|
||||
}
|
||||
}
|
||||
|
||||
const headers = message.payload.headers || []
|
||||
const subject = headers.find((h) => h.name.toLowerCase() === 'subject')?.value || 'No Subject'
|
||||
const from = headers.find((h) => h.name.toLowerCase() === 'from')?.value || 'Unknown Sender'
|
||||
const date = headers.find((h) => h.name.toLowerCase() === 'date')?.value || ''
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
threadId: message.threadId,
|
||||
subject,
|
||||
from,
|
||||
date,
|
||||
snippet: message.snippet || '',
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to recursively extract message body from MIME parts
|
||||
export function extractMessageBody(payload: any): string {
|
||||
// If the payload has a body with data, decode it
|
||||
if (payload.body?.data) {
|
||||
return Buffer.from(payload.body.data, 'base64').toString()
|
||||
}
|
||||
|
||||
// If there are no parts, return empty string
|
||||
if (!payload.parts || !Array.isArray(payload.parts) || payload.parts.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// First try to find a text/plain part
|
||||
const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain')
|
||||
if (textPart?.body?.data) {
|
||||
return Buffer.from(textPart.body.data, 'base64').toString()
|
||||
}
|
||||
|
||||
// If no text/plain, try to find text/html
|
||||
const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html')
|
||||
if (htmlPart?.body?.data) {
|
||||
return Buffer.from(htmlPart.body.data, 'base64').toString()
|
||||
}
|
||||
|
||||
// If we have multipart/alternative or other complex types, recursively check parts
|
||||
for (const part of payload.parts) {
|
||||
if (part.parts) {
|
||||
const nestedBody = extractMessageBody(part)
|
||||
if (nestedBody) {
|
||||
return nestedBody
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find any text content, return empty string
|
||||
return ''
|
||||
}
|
||||
|
||||
// Helper function to extract attachment information from message payload
|
||||
export function extractAttachmentInfo(
|
||||
payload: any
|
||||
): Array<{ attachmentId: string; filename: string; mimeType: string; size: number }> {
|
||||
const attachments: Array<{
|
||||
attachmentId: string
|
||||
filename: string
|
||||
mimeType: string
|
||||
size: number
|
||||
}> = []
|
||||
|
||||
function processPayloadPart(part: any) {
|
||||
// Check if this part has an attachment
|
||||
if (part.body?.attachmentId && part.filename) {
|
||||
attachments.push({
|
||||
attachmentId: part.body.attachmentId,
|
||||
filename: part.filename,
|
||||
mimeType: part.mimeType || 'application/octet-stream',
|
||||
size: part.body.size || 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Recursively process nested parts
|
||||
if (part.parts && Array.isArray(part.parts)) {
|
||||
part.parts.forEach(processPayloadPart)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the main payload
|
||||
processPayloadPart(payload)
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
// Helper function to download attachments from Gmail API
|
||||
export async function downloadAttachments(
|
||||
messageId: string,
|
||||
attachmentInfo: Array<{ attachmentId: string; filename: string; mimeType: string; size: number }>,
|
||||
accessToken: string
|
||||
): Promise<GmailAttachment[]> {
|
||||
const downloadedAttachments: GmailAttachment[] = []
|
||||
|
||||
for (const attachment of attachmentInfo) {
|
||||
try {
|
||||
// Download attachment from Gmail API
|
||||
const attachmentResponse = await fetch(
|
||||
`${GMAIL_API_BASE}/messages/${messageId}/attachments/${attachment.attachmentId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!attachmentResponse.ok) {
|
||||
continue
|
||||
}
|
||||
|
||||
const attachmentData = (await attachmentResponse.json()) as { data: string; size: number }
|
||||
|
||||
// Decode base64url data to buffer
|
||||
// Gmail API returns data in base64url format (URL-safe base64)
|
||||
const base64Data = attachmentData.data.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const buffer = Buffer.from(base64Data, 'base64')
|
||||
|
||||
downloadedAttachments.push({
|
||||
name: attachment.filename,
|
||||
data: buffer,
|
||||
mimeType: attachment.mimeType,
|
||||
size: attachment.size,
|
||||
})
|
||||
} catch (error) {
|
||||
// Continue with other attachments
|
||||
}
|
||||
}
|
||||
|
||||
return downloadedAttachments
|
||||
}
|
||||
|
||||
// Helper function to create a summary of multiple messages
|
||||
export function createMessagesSummary(messages: any[]): string {
|
||||
if (messages.length === 0) {
|
||||
return 'No messages found.'
|
||||
}
|
||||
|
||||
let summary = `Found ${messages.length} messages:\n\n`
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
summary += `${index + 1}. Subject: ${msg.subject}\n`
|
||||
summary += ` From: ${msg.from}\n`
|
||||
summary += ` Date: ${msg.date}\n`
|
||||
summary += ` Preview: ${msg.snippet}\n\n`
|
||||
})
|
||||
|
||||
summary += `To read full content of a specific message, use the gmail_read tool with messageId: ${messages.map((m) => m.id).join(', ')}`
|
||||
|
||||
return summary
|
||||
}
|
||||
@@ -34,6 +34,46 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GoogleSearchParams) => {
|
||||
const baseUrl = 'https://www.googleapis.com/customsearch/v1'
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
// Add required parameters
|
||||
searchParams.append('key', params.apiKey)
|
||||
searchParams.append('q', params.query)
|
||||
searchParams.append('cx', params.searchEngineId)
|
||||
|
||||
// Add optional parameter
|
||||
if (params.num) {
|
||||
searchParams.append('num', params.num.toString())
|
||||
}
|
||||
|
||||
return `${baseUrl}?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items: data.items || [],
|
||||
searchInformation: data.searchInformation || {
|
||||
totalResults: '0',
|
||||
searchTime: 0,
|
||||
formattedSearchTime: '0',
|
||||
formattedTotalResults: '0',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
@@ -63,55 +103,4 @@ export const searchTool: ToolConfig<GoogleSearchParams, GoogleSearchResponse> =
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GoogleSearchParams) => {
|
||||
const baseUrl = 'https://www.googleapis.com/customsearch/v1'
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
// Add required parameters
|
||||
searchParams.append('key', params.apiKey)
|
||||
searchParams.append('q', params.query)
|
||||
searchParams.append('cx', params.searchEngineId)
|
||||
|
||||
// Add optional parameter
|
||||
if (params.num) {
|
||||
searchParams.append('num', params.num.toString())
|
||||
}
|
||||
|
||||
return `${baseUrl}?${searchParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to perform Google search')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items: data.items || [],
|
||||
searchInformation: data.searchInformation || {
|
||||
totalResults: '0',
|
||||
searchTime: 0,
|
||||
formattedSearchTime: '0',
|
||||
formattedTotalResults: '0',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while performing the Google search'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -148,20 +148,7 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Event creation confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created event metadata including ID, status, and details',
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to create calendar event')
|
||||
}
|
||||
|
||||
const data: GoogleCalendarApiEventResponse = await response.json()
|
||||
|
||||
return {
|
||||
@@ -185,19 +172,11 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while creating the calendar event'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Event creation confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created event metadata including ID, status, and details',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,20 +51,7 @@ export const getTool: ToolConfig<GoogleCalendarGetParams, GoogleCalendarGetRespo
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Event retrieval confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Event details including ID, status, times, and attendees',
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to get calendar event')
|
||||
}
|
||||
|
||||
const data: GoogleCalendarApiEventResponse = await response.json()
|
||||
|
||||
return {
|
||||
@@ -88,25 +75,11 @@ export const getTool: ToolConfig<GoogleCalendarGetParams, GoogleCalendarGetRespo
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
if (
|
||||
error.error.message.includes('Event not found') ||
|
||||
error.error.message.includes('Not Found')
|
||||
) {
|
||||
return 'Event not found. Please check the event ID.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while retrieving the calendar event'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Event retrieval confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Event details including ID, status, times, and attendees',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,17 +56,6 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Attendee invitation confirmation message with email delivery status',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated event metadata including attendee list and details',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GoogleCalendarInviteParams) => {
|
||||
const calendarId = params.calendarId || 'primary'
|
||||
@@ -82,10 +71,6 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const existingEvent = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
|
||||
}
|
||||
|
||||
// Validate required fields exist
|
||||
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
|
||||
throw new Error('Existing event is missing required fields (start, end, or summary)')
|
||||
@@ -258,30 +243,14 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
if (
|
||||
error.error.message.includes('Event not found') ||
|
||||
error.error.message.includes('Not Found')
|
||||
) {
|
||||
return 'Event not found. Please check the event ID.'
|
||||
}
|
||||
if (error.error.message.includes('Failed to fetch existing event')) {
|
||||
return `Unable to retrieve existing event details: ${error.error.message}`
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return (
|
||||
error.message || 'An unexpected error occurred while inviting attendees to the calendar event'
|
||||
)
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Attendee invitation confirmation message with email delivery status',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated event metadata including attendee list and details',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -80,20 +80,7 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Summary of found events count' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'List of events with pagination tokens and event details',
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to list calendar events')
|
||||
}
|
||||
|
||||
const data: GoogleCalendarApiListResponse = await response.json()
|
||||
const events = data.items || []
|
||||
const eventsCount = events.length
|
||||
@@ -124,19 +111,11 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while listing calendar events'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Summary of found events count' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'List of events with pagination tokens and event details',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -74,21 +74,9 @@ export const quickAddTool: ToolConfig<
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Event creation confirmation message from natural language',
|
||||
},
|
||||
metadata: { type: 'json', description: 'Created event metadata including parsed details' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to create calendar event from text')
|
||||
}
|
||||
|
||||
// Handle attendees if provided
|
||||
let finalEventData = data
|
||||
if (params?.attendees) {
|
||||
@@ -171,22 +159,11 @@ export const quickAddTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
if (error.error.message.includes('parse')) {
|
||||
return 'Could not parse the natural language text. Please try a different format.'
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while creating the calendar event'
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Event creation confirmation message from natural language',
|
||||
},
|
||||
metadata: { type: 'json', description: 'Created event metadata including parsed details' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -86,14 +86,6 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Event update confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated event metadata including ID, status, and details',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GoogleCalendarUpdateParams) => {
|
||||
const calendarId = params.calendarId || 'primary'
|
||||
@@ -109,10 +101,6 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const existingEvent = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
|
||||
}
|
||||
|
||||
// Start with the complete existing event to preserve all fields
|
||||
const updatedEvent = { ...existingEvent }
|
||||
|
||||
@@ -257,28 +245,11 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
if (
|
||||
error.error.message.includes('Event not found') ||
|
||||
error.error.message.includes('Not Found')
|
||||
) {
|
||||
return 'Event not found. Please check the event ID.'
|
||||
}
|
||||
if (error.error.message.includes('Failed to fetch existing event')) {
|
||||
return `Unable to retrieve existing event details: ${error.error.message}`
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while updating the calendar event'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Event update confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated event metadata including ID, status, and details',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
|
||||
name: 'Create Google Docs Document',
|
||||
description: 'Create a new Google Docs document',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -46,6 +48,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
|
||||
description: 'The ID of the folder to create the document in (internal use)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => {
|
||||
return 'https://www.googleapis.com/drive/v3/files'
|
||||
@@ -81,6 +84,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
postProcess: async (result, params, executeTool) => {
|
||||
if (!result.success) {
|
||||
return result
|
||||
@@ -113,27 +117,7 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
|
||||
return result
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created document metadata including ID, title, and URL',
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
let errorText = ''
|
||||
try {
|
||||
const responseClone = response.clone()
|
||||
const responseText = await responseClone.text()
|
||||
errorText = responseText
|
||||
} catch (_e) {
|
||||
errorText = 'Unable to read error response'
|
||||
}
|
||||
|
||||
throw new Error(`Failed to create Google Docs document (${response.status}): ${errorText}`)
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the response data
|
||||
const responseText = await response.text()
|
||||
@@ -162,23 +146,11 @@ export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateRespon
|
||||
throw error
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while creating Google Docs document'
|
||||
outputs: {
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created document metadata including ID, title, and URL',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GoogleDocsReadResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import { extractTextFromDocument } from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse> = {
|
||||
@@ -6,11 +7,13 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
|
||||
name: 'Read Google Docs Document',
|
||||
description: 'Read content from a Google Docs document',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -25,6 +28,7 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
|
||||
description: 'The ID of the document to read',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure documentId is valid
|
||||
@@ -48,17 +52,7 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Extracted document text content' },
|
||||
metadata: { type: 'json', description: 'Document metadata including ID, title, and URL' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Failed to read Google Docs document: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract document content from the response
|
||||
@@ -83,62 +77,9 @@ export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse>
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while reading Google Docs document'
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Extracted document text content' },
|
||||
metadata: { type: 'json', description: 'Document metadata including ID, title, and URL' },
|
||||
},
|
||||
}
|
||||
|
||||
// Helper function to extract text content from Google Docs document structure
|
||||
function extractTextFromDocument(document: any): string {
|
||||
let text = ''
|
||||
|
||||
if (!document.body || !document.body.content) {
|
||||
return text
|
||||
}
|
||||
|
||||
// Process each structural element in the document
|
||||
for (const element of document.body.content) {
|
||||
if (element.paragraph) {
|
||||
for (const paragraphElement of element.paragraph.elements) {
|
||||
if (paragraphElement.textRun?.content) {
|
||||
text += paragraphElement.textRun.content
|
||||
}
|
||||
}
|
||||
} else if (element.table) {
|
||||
// Process tables if needed
|
||||
for (const tableRow of element.table.tableRows) {
|
||||
for (const tableCell of tableRow.tableCells) {
|
||||
if (tableCell.content) {
|
||||
for (const cellContent of tableCell.content) {
|
||||
if (cellContent.paragraph) {
|
||||
for (const paragraphElement of cellContent.paragraph.elements) {
|
||||
if (paragraphElement.textRun?.content) {
|
||||
text += paragraphElement.textRun.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
38
apps/sim/tools/google_docs/utils.ts
Normal file
38
apps/sim/tools/google_docs/utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Helper function to extract text content from Google Docs document structure
|
||||
export function extractTextFromDocument(document: any): string {
|
||||
let text = ''
|
||||
|
||||
if (!document.body || !document.body.content) {
|
||||
return text
|
||||
}
|
||||
|
||||
// Process each structural element in the document
|
||||
for (const element of document.body.content) {
|
||||
if (element.paragraph) {
|
||||
for (const paragraphElement of element.paragraph.elements) {
|
||||
if (paragraphElement.textRun?.content) {
|
||||
text += paragraphElement.textRun.content
|
||||
}
|
||||
}
|
||||
} else if (element.table) {
|
||||
// Process tables if needed
|
||||
for (const tableRow of element.table.tableRows) {
|
||||
for (const tableCell of tableRow.tableCells) {
|
||||
if (tableCell.content) {
|
||||
for (const cellContent of tableCell.content) {
|
||||
if (cellContent.paragraph) {
|
||||
for (const paragraphElement of cellContent.paragraph.elements) {
|
||||
if (paragraphElement.textRun?.content) {
|
||||
text += paragraphElement.textRun.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
@@ -87,18 +87,6 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
let errorText = ''
|
||||
try {
|
||||
const responseClone = response.clone()
|
||||
const responseText = await responseClone.text()
|
||||
errorText = responseText
|
||||
} catch (_e) {
|
||||
errorText = 'Unable to read error response'
|
||||
}
|
||||
|
||||
throw new Error(`Failed to write to Google Docs document (${response.status}): ${errorText}`)
|
||||
}
|
||||
const responseText = await response.text()
|
||||
|
||||
// Parse the response if it's not empty
|
||||
@@ -133,23 +121,4 @@ export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while writing to Google Docs'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
|
||||
name: 'Create Folder in Google Drive',
|
||||
description: 'Create a new folder in Google Drive',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -37,6 +39,7 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
|
||||
description: 'ID of the parent folder (internal use)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://www.googleapis.com/drive/v3/files',
|
||||
method: 'POST',
|
||||
@@ -64,13 +67,6 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'Created folder metadata including ID, name, and parent information',
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
@@ -95,7 +91,11 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
return error.message || 'An error occurred while creating folder in Google Drive'
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'Created folder metadata including ID, name, and parent information',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
|
||||
description:
|
||||
'Get content from a file in Google Drive (exports Google Workspace files automatically)',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -40,17 +42,6 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'File content as text (Google Workspace files are exported)',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'File metadata including ID, name, MIME type, and links',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId}?fields=id,name,mimeType`,
|
||||
@@ -178,11 +169,15 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
|
||||
throw error
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
logger.error('Download error', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
})
|
||||
return error.message || 'An error occurred while getting content from Google Drive'
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'File content as text (Google Workspace files are exported)',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'File metadata including ID, name, MIME type, and links',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
|
||||
name: 'List Google Drive Files',
|
||||
description: 'List files and folders in Google Drive',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -49,6 +51,7 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
|
||||
description: 'The page token to use for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://www.googleapis.com/drive/v3/files')
|
||||
@@ -87,13 +90,6 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
files: {
|
||||
type: 'json',
|
||||
description: 'Array of file metadata objects from the specified folder',
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
@@ -119,7 +115,11 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
return error.message || 'An error occurred while listing Google Drive files'
|
||||
|
||||
outputs: {
|
||||
files: {
|
||||
type: 'json',
|
||||
description: 'Array of file metadata objects from the specified folder',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
|
||||
name: 'Upload to Google Drive',
|
||||
description: 'Upload a file to Google Drive',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -53,6 +55,7 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
|
||||
description: 'The ID of the folder to upload the file to (internal use)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://www.googleapis.com/drive/v3/files',
|
||||
method: 'POST',
|
||||
@@ -80,10 +83,6 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: { type: 'json', description: 'Uploaded file metadata including ID, name, and links' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleDriveToolParams) => {
|
||||
try {
|
||||
const data = await response.json()
|
||||
@@ -202,11 +201,8 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
|
||||
throw error
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
logger.error('Upload error', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
})
|
||||
return error.message || 'An error occurred while uploading to Google Drive'
|
||||
|
||||
outputs: {
|
||||
file: { type: 'json', description: 'Uploaded file metadata including ID, name, and links' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
|
||||
name: 'Append to Google Sheets',
|
||||
description: 'Append data to the end of a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
additionalScopes: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -58,6 +60,7 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
|
||||
description: 'Whether to include the appended values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// If range is not provided, use a default range for the first sheet
|
||||
@@ -177,21 +180,7 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tableRange: { type: 'string', description: 'Range of the table where data was appended' },
|
||||
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated' },
|
||||
updatedColumns: { type: 'number', description: 'Number of columns updated' },
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Failed to append data to Google Sheets: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL
|
||||
@@ -222,23 +211,13 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
|
||||
|
||||
return result
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while appending to Google Sheets'
|
||||
outputs: {
|
||||
tableRange: { type: 'string', description: 'Range of the table where data was appended' },
|
||||
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated' },
|
||||
updatedColumns: { type: 'number', description: 'Number of columns updated' },
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
|
||||
name: 'Read from Google Sheets',
|
||||
description: 'Read data from a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
additionalScopes: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -31,6 +33,7 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
|
||||
description: 'The range of cells to read from',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure spreadsheetId is valid
|
||||
@@ -60,21 +63,7 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: { type: 'json', description: 'Sheet data including range and cell values' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json().catch(() => ({ error: response.statusText }))
|
||||
const errorText =
|
||||
errorJson.error && typeof errorJson.error === 'object'
|
||||
? errorJson.error.message || JSON.stringify(errorJson.error)
|
||||
: errorJson.error || response.statusText
|
||||
throw new Error(`Failed to read Google Sheets data: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL
|
||||
@@ -105,41 +94,9 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
|
||||
|
||||
return result
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
// Handle Google API error response format
|
||||
if (error.error) {
|
||||
if (typeof error.error === 'string') {
|
||||
return error.error
|
||||
}
|
||||
// Google API often returns error objects with error.error.message
|
||||
if (typeof error.error === 'object' && error.error.message) {
|
||||
return error.error.message
|
||||
}
|
||||
// If error.error is an object but doesn't have a message property
|
||||
return JSON.stringify(error.error)
|
||||
}
|
||||
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If we have a complex object, stringify it for debugging
|
||||
try {
|
||||
return `Google Sheets API error: ${JSON.stringify(error)}`
|
||||
} catch (_e) {
|
||||
// In case the error object can't be stringified (e.g., circular references)
|
||||
return 'Google Sheets API error: Unable to parse error details'
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while reading from Google Sheets'
|
||||
outputs: {
|
||||
data: { type: 'json', description: 'Sheet data including range and cell values' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
|
||||
name: 'Update Google Sheets',
|
||||
description: 'Update data in a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
additionalScopes: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -52,6 +54,7 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
|
||||
description: 'Whether to include the updated values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// If range is not provided, use a default range for the first sheet, second row to preserve headers
|
||||
@@ -131,20 +134,7 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated' },
|
||||
updatedColumns: { type: 'number', description: 'Number of columns updated' },
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Failed to update data in Google Sheets: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL
|
||||
@@ -174,23 +164,12 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
|
||||
|
||||
return result
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while updating Google Sheets'
|
||||
outputs: {
|
||||
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated' },
|
||||
updatedColumns: { type: 'number', description: 'Number of columns updated' },
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
|
||||
name: 'Write to Google Sheets',
|
||||
description: 'Write data to a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
additionalScopes: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -49,6 +51,7 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
|
||||
description: 'Whether to include the written values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// If range is not provided, use a default range for the first sheet, second row to preserve headers
|
||||
@@ -128,20 +131,7 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated' },
|
||||
updatedColumns: { type: 'number', description: 'Number of columns updated' },
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Failed to write data to Google Sheets: ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL
|
||||
@@ -171,23 +161,12 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
|
||||
|
||||
return result
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'An error occurred while writing to Google Sheets'
|
||||
outputs: {
|
||||
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated' },
|
||||
updatedColumns: { type: 'number', description: 'Number of columns updated' },
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated' },
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,176 +1,6 @@
|
||||
import { getEnv } from '@/lib/env'
|
||||
import { isTest } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import type { RequestParams, RequestResponse } from '@/tools/http/types'
|
||||
import type { HttpMethod, TableRow, ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('HTTPRequestTool')
|
||||
|
||||
const getReferer = (): string => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
try {
|
||||
return getBaseUrl()
|
||||
} catch (_error) {
|
||||
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a set of default headers used in HTTP requests
|
||||
* @param customHeaders Additional user-provided headers to include
|
||||
* @param url Target URL for the request (used for setting Host header)
|
||||
* @returns Record of HTTP headers
|
||||
*/
|
||||
const getDefaultHeaders = (
|
||||
customHeaders: Record<string, string> = {},
|
||||
url?: string
|
||||
): Record<string, string> => {
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
Referer: getReferer(),
|
||||
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
...customHeaders,
|
||||
}
|
||||
|
||||
// Add Host header if not provided and URL is valid
|
||||
if (url) {
|
||||
try {
|
||||
const hostname = new URL(url).host
|
||||
if (hostname && !customHeaders.Host && !customHeaders.host) {
|
||||
headers.Host = hostname
|
||||
}
|
||||
} catch (_e) {
|
||||
// Invalid URL, will be caught later
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a URL with path parameters and query parameters
|
||||
* @param url Base URL to process
|
||||
* @param pathParams Path parameters to replace in the URL
|
||||
* @param queryParams Query parameters to add to the URL
|
||||
* @returns Processed URL with path params replaced and query params added
|
||||
*/
|
||||
const processUrl = (
|
||||
url: string,
|
||||
pathParams?: Record<string, string>,
|
||||
queryParams?: TableRow[] | null
|
||||
): string => {
|
||||
// Strip any surrounding quotes
|
||||
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
|
||||
url = url.slice(1, -1)
|
||||
}
|
||||
|
||||
// Replace path parameters
|
||||
if (pathParams) {
|
||||
Object.entries(pathParams).forEach(([key, value]) => {
|
||||
url = url.replace(`:${key}`, encodeURIComponent(value))
|
||||
})
|
||||
}
|
||||
|
||||
// Handle query parameters
|
||||
if (queryParams) {
|
||||
const queryParamsObj = transformTable(queryParams)
|
||||
|
||||
// Verify if URL already has query params to use proper separator
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
|
||||
// Build query string manually to avoid double-encoding issues
|
||||
const queryParts: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(queryParamsObj)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParts.length > 0) {
|
||||
url += separator + queryParts.join('&')
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// Check if a URL needs proxy to avoid CORS/method restrictions
|
||||
const shouldUseProxy = (url: string): boolean => {
|
||||
// Skip proxying in test environment
|
||||
if (isTest) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only consider proxying in browser environment
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const _urlObj = new URL(url)
|
||||
const currentOrigin = window.location.origin
|
||||
|
||||
// Don't proxy same-origin or localhost requests
|
||||
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true // Proxy all cross-origin requests for consistency
|
||||
} catch (e) {
|
||||
logger.warn('URL parsing failed:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Default headers that will be applied if not explicitly overridden by user
|
||||
const _DEFAULT_HEADERS: Record<string, string> = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
'Sec-Ch-Ua': '"Chromium"v="135", "Not-A.Brand"v="8"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a table from the store format to a key-value object
|
||||
* Local copy of the function to break circular dependencies
|
||||
* @param table Array of table rows from the store
|
||||
* @returns Record of key-value pairs
|
||||
*/
|
||||
const transformTable = (table: TableRow[] | null): Record<string, any> => {
|
||||
if (!table) return {}
|
||||
|
||||
return table.reduce(
|
||||
(acc, row) => {
|
||||
if (row.cells?.Key && row.cells?.Value !== undefined) {
|
||||
// Extract the Value cell as is - it should already be properly resolved
|
||||
// by the InputResolver based on variable type (number, string, boolean etc.)
|
||||
const value = row.cells.Value
|
||||
|
||||
// Store the correctly typed value in the result object
|
||||
acc[row.cells.Key] = value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
}
|
||||
import { getDefaultHeaders, processUrl, shouldUseProxy, transformTable } from '@/tools/http/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
id: 'http_request',
|
||||
@@ -221,195 +51,6 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'Response data from the HTTP request (JSON object, text, or other format)',
|
||||
},
|
||||
status: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code of the response (e.g., 200, 404, 500)',
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Response headers as key-value pairs',
|
||||
properties: {
|
||||
'content-type': {
|
||||
type: 'string',
|
||||
description: 'Content type of the response',
|
||||
optional: true,
|
||||
},
|
||||
'content-length': { type: 'string', description: 'Content length', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Direct execution to bypass server for HTTP requests
|
||||
directExecution: async (params: RequestParams): Promise<RequestResponse | undefined> => {
|
||||
try {
|
||||
// Process the URL with parameters
|
||||
const url = processUrl(params.url, params.pathParams, params.params)
|
||||
|
||||
// Update the URL in params for any subsequent operations
|
||||
params.url = url
|
||||
|
||||
// Determine if we should use the proxy
|
||||
if (shouldUseProxy(url)) {
|
||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(url)}`
|
||||
|
||||
if (params.method) {
|
||||
proxyUrl += `&method=${encodeURIComponent(params.method)}`
|
||||
}
|
||||
|
||||
if (params.body && ['POST', 'PUT', 'PATCH'].includes(params.method?.toUpperCase() || '')) {
|
||||
const bodyStr =
|
||||
typeof params.body === 'string' ? params.body : JSON.stringify(params.body)
|
||||
proxyUrl += `&body=${encodeURIComponent(bodyStr)}`
|
||||
}
|
||||
|
||||
// Forward all headers as URL parameters
|
||||
const userHeaders = transformTable(params.headers || null)
|
||||
|
||||
// Add all custom headers as query parameters
|
||||
for (const [key, value] of Object.entries(userHeaders)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
proxyUrl += `&header.${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(proxyUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Transform the proxy result to match the expected output format
|
||||
return {
|
||||
success: result.success,
|
||||
output: {
|
||||
data: result.data,
|
||||
status: result.status,
|
||||
headers: result.headers || {},
|
||||
},
|
||||
error: result.success
|
||||
? undefined
|
||||
: // Extract and display the actual API error message from the response if available
|
||||
result.data && typeof result.data === 'object' && result.data.error
|
||||
? `HTTP error ${result.status}: ${result.data.error.message || JSON.stringify(result.data.error)}`
|
||||
: result.error || `HTTP error ${result.status}`,
|
||||
}
|
||||
}
|
||||
|
||||
// For non-proxied requests, proceed with normal fetch
|
||||
const userHeaders = transformTable(params.headers || null)
|
||||
const headers = getDefaultHeaders(userHeaders, url)
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: params.method || 'GET',
|
||||
headers,
|
||||
redirect: 'follow',
|
||||
}
|
||||
|
||||
// Add body for non-GET requests
|
||||
if (params.method && params.method !== 'GET' && params.body) {
|
||||
if (typeof params.body === 'object') {
|
||||
fetchOptions.body = JSON.stringify(params.body)
|
||||
// Ensure Content-Type is set
|
||||
headers['Content-Type'] = 'application/json'
|
||||
} else {
|
||||
fetchOptions.body = params.body
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form data
|
||||
if (params.formData) {
|
||||
const formData = new FormData()
|
||||
Object.entries(params.formData).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
fetchOptions.body = formData
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
const controller = new AbortController()
|
||||
const timeout = params.timeout || 120000
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
fetchOptions.signal = controller.signal
|
||||
|
||||
try {
|
||||
// Make the fetch request
|
||||
const response = await fetch(url, fetchOptions)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// Convert Headers to a plain object
|
||||
const responseHeaders: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
responseHeaders[key] = value
|
||||
})
|
||||
|
||||
// Parse response based on content type
|
||||
let data
|
||||
try {
|
||||
if (response.headers.get('content-type')?.includes('application/json')) {
|
||||
data = await response.json()
|
||||
} else {
|
||||
data = await response.text()
|
||||
}
|
||||
} catch (_error) {
|
||||
data = await response.text()
|
||||
}
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
data,
|
||||
status: response.status,
|
||||
headers: responseHeaders,
|
||||
},
|
||||
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
|
||||
}
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// Handle specific abort error
|
||||
if (error.name === 'AbortError') {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: null,
|
||||
status: 0,
|
||||
headers: {},
|
||||
},
|
||||
error: `Request timeout after ${timeout}ms`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: null,
|
||||
status: 0,
|
||||
headers: {},
|
||||
},
|
||||
error: error.message || 'Failed to fetch',
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: null,
|
||||
status: 0,
|
||||
headers: {},
|
||||
},
|
||||
error: error.message || 'Error preparing HTTP request',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: RequestParams) => {
|
||||
// Process the URL first to handle path/query params
|
||||
@@ -443,20 +84,21 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
return processedUrl
|
||||
},
|
||||
|
||||
method: 'GET' as HttpMethod,
|
||||
method: (params: RequestParams) => params.method || 'GET',
|
||||
|
||||
headers: (params: RequestParams) => {
|
||||
const headers = transformTable(params.headers || null)
|
||||
const processedUrl = processUrl(params.url, params.pathParams, params.params)
|
||||
|
||||
// For proxied requests, we only need minimal headers
|
||||
if (shouldUseProxy(params.url)) {
|
||||
if (shouldUseProxy(processedUrl)) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
|
||||
// For direct requests, add all our standard headers
|
||||
const allHeaders = getDefaultHeaders(headers, params.url)
|
||||
const allHeaders = getDefaultHeaders(headers, processedUrl)
|
||||
|
||||
// Set appropriate Content-Type
|
||||
if (params.formData) {
|
||||
@@ -471,8 +113,10 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
},
|
||||
|
||||
body: (params: RequestParams) => {
|
||||
const processedUrl = processUrl(params.url, params.pathParams, params.params)
|
||||
|
||||
// For proxied requests, we don't need a body
|
||||
if (shouldUseProxy(params.url)) {
|
||||
if (shouldUseProxy(processedUrl)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -507,6 +151,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
status: jsonResponse.status,
|
||||
headers: jsonResponse.headers || {},
|
||||
},
|
||||
error: jsonResponse.success
|
||||
? undefined
|
||||
: // Extract and display the actual API error message from the response if available
|
||||
jsonResponse.data && typeof jsonResponse.data === 'object' && jsonResponse.data.error
|
||||
? `HTTP error ${jsonResponse.status}: ${jsonResponse.data.error.message || JSON.stringify(jsonResponse.data.error)}`
|
||||
: jsonResponse.error || `HTTP error ${jsonResponse.status}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,9 +167,12 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
headers[key] = value
|
||||
})
|
||||
|
||||
const data = await (contentType.includes('application/json')
|
||||
? response.json()
|
||||
: response.text())
|
||||
let data
|
||||
try {
|
||||
data = await (contentType.includes('application/json') ? response.json() : response.text())
|
||||
} catch (error) {
|
||||
data = await response.text()
|
||||
}
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
@@ -528,32 +181,30 @@ export const requestTool: ToolConfig<RequestParams, RequestResponse> = {
|
||||
status: response.status,
|
||||
headers,
|
||||
},
|
||||
error: response.ok ? undefined : `HTTP error ${response.status}: ${response.statusText}`,
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
// If there's detailed error info from the API response, use it
|
||||
if (error.response?.data) {
|
||||
// Handle structured error objects from APIs
|
||||
if (typeof error.response.data === 'object' && error.response.data.error) {
|
||||
const apiError = error.response.data.error
|
||||
const message =
|
||||
apiError.message || (typeof apiError === 'string' ? apiError : JSON.stringify(apiError))
|
||||
return `${error.status || ''} ${message}`.trim()
|
||||
}
|
||||
|
||||
// For text error responses
|
||||
if (typeof error.response.data === 'string' && error.response.data.trim()) {
|
||||
return `${error.status || ''} ${error.response.data}`.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to standard error formatting
|
||||
const message = error.message || error.error?.message || 'Unknown error'
|
||||
const code = error.status || error.error?.status
|
||||
const statusText = error.statusText || ''
|
||||
|
||||
// Format the error message
|
||||
return code ? `HTTP error ${code}${statusText ? `: ${statusText}` : ''} - ${message}` : message
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'Response data from the HTTP request (JSON object, text, or other format)',
|
||||
},
|
||||
status: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code of the response (e.g., 200, 404, 500)',
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Response headers as key-value pairs',
|
||||
properties: {
|
||||
'content-type': {
|
||||
type: 'string',
|
||||
description: 'Content type of the response',
|
||||
optional: true,
|
||||
},
|
||||
'content-length': { type: 'string', description: 'Content length', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
159
apps/sim/tools/http/utils.ts
Normal file
159
apps/sim/tools/http/utils.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { getEnv } from '@/lib/env'
|
||||
import { isTest } from '@/lib/environment'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBaseUrl } from '@/lib/urls/utils'
|
||||
import type { TableRow } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('HTTPRequestUtils')
|
||||
|
||||
export const getReferer = (): string => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
try {
|
||||
return getBaseUrl()
|
||||
} catch (_error) {
|
||||
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a set of default headers used in HTTP requests
|
||||
* @param customHeaders Additional user-provided headers to include
|
||||
* @param url Target URL for the request (used for setting Host header)
|
||||
* @returns Record of HTTP headers
|
||||
*/
|
||||
export const getDefaultHeaders = (
|
||||
customHeaders: Record<string, string> = {},
|
||||
url?: string
|
||||
): Record<string, string> => {
|
||||
const headers: Record<string, string> = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
Referer: getReferer(),
|
||||
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
...customHeaders,
|
||||
}
|
||||
|
||||
// Add Host header if not provided and URL is valid
|
||||
if (url) {
|
||||
try {
|
||||
const hostname = new URL(url).host
|
||||
if (hostname && !customHeaders.Host && !customHeaders.host) {
|
||||
headers.Host = hostname
|
||||
}
|
||||
} catch (_e) {
|
||||
// Invalid URL, will be caught later
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a URL with path parameters and query parameters
|
||||
* @param url Base URL to process
|
||||
* @param pathParams Path parameters to replace in the URL
|
||||
* @param queryParams Query parameters to add to the URL
|
||||
* @returns Processed URL with path params replaced and query params added
|
||||
*/
|
||||
export const processUrl = (
|
||||
url: string,
|
||||
pathParams?: Record<string, string>,
|
||||
queryParams?: TableRow[] | null
|
||||
): string => {
|
||||
// Strip any surrounding quotes
|
||||
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
|
||||
url = url.slice(1, -1)
|
||||
}
|
||||
|
||||
// Replace path parameters
|
||||
if (pathParams) {
|
||||
Object.entries(pathParams).forEach(([key, value]) => {
|
||||
url = url.replace(`:${key}`, encodeURIComponent(value))
|
||||
})
|
||||
}
|
||||
|
||||
// Handle query parameters
|
||||
if (queryParams) {
|
||||
const queryParamsObj = transformTable(queryParams)
|
||||
|
||||
// Verify if URL already has query params to use proper separator
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
|
||||
// Build query string manually to avoid double-encoding issues
|
||||
const queryParts: string[] = []
|
||||
|
||||
for (const [key, value] of Object.entries(queryParamsObj)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParts.length > 0) {
|
||||
url += separator + queryParts.join('&')
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// Check if a URL needs proxy to avoid CORS/method restrictions
|
||||
export const shouldUseProxy = (url: string): boolean => {
|
||||
// Skip proxying in test environment
|
||||
if (isTest) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only consider proxying in browser environment
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const _urlObj = new URL(url)
|
||||
const currentOrigin = window.location.origin
|
||||
|
||||
// Don't proxy same-origin or localhost requests
|
||||
if (url.startsWith(currentOrigin) || url.includes('localhost')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true // Proxy all cross-origin requests for consistency
|
||||
} catch (e) {
|
||||
logger.warn('URL parsing failed:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a table from the store format to a key-value object
|
||||
* Local copy of the function to break circular dependencies
|
||||
* @param table Array of table rows from the store
|
||||
* @returns Record of key-value pairs
|
||||
*/
|
||||
export const transformTable = (table: TableRow[] | null): Record<string, any> => {
|
||||
if (!table) return {}
|
||||
|
||||
return table.reduce(
|
||||
(acc, row) => {
|
||||
if (row.cells?.Key && row.cells?.Value !== undefined) {
|
||||
// Extract the Value cell as is - it should already be properly resolved
|
||||
// by the InputResolver based on variable type (number, string, boolean etc.)
|
||||
const value = row.cells.Value
|
||||
|
||||
// Store the correctly typed value in the result object
|
||||
acc[row.cells.Key] = value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>
|
||||
)
|
||||
}
|
||||
@@ -56,29 +56,7 @@ export const chatTool: ToolConfig<HuggingFaceChatParams, HuggingFaceChatResponse
|
||||
description: 'Hugging Face API token',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Chat completion results',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Generated text content' },
|
||||
model: { type: 'string', description: 'Model used for generation' },
|
||||
usage: {
|
||||
type: 'object',
|
||||
description: 'Token usage information',
|
||||
properties: {
|
||||
prompt_tokens: { type: 'number', description: 'Number of tokens in the prompt' },
|
||||
completion_tokens: {
|
||||
type: 'number',
|
||||
description: 'Number of tokens in the completion',
|
||||
},
|
||||
total_tokens: { type: 'number', description: 'Total number of tokens used' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: (params) => {
|
||||
@@ -141,76 +119,50 @@ export const chatTool: ToolConfig<HuggingFaceChatParams, HuggingFaceChatResponse
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
try {
|
||||
// Check if the response was successful
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
console.error('Hugging Face API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
errorData,
|
||||
url: response.url,
|
||||
})
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const errorMessage = errorData
|
||||
? `API error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`
|
||||
: `API error: ${response.status} ${response.statusText}`
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Validate response structure
|
||||
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
|
||||
console.error('Invalid Hugging Face response format:', data)
|
||||
throw new Error('Invalid response format from Hugging Face API')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: data.choices[0].message.content,
|
||||
model: data.model || params?.model || 'unknown',
|
||||
usage: data.usage
|
||||
? {
|
||||
prompt_tokens: data.usage.prompt_tokens || 0,
|
||||
completion_tokens: data.usage.completion_tokens || 0,
|
||||
total_tokens: data.usage.total_tokens || 0,
|
||||
}
|
||||
: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to process Hugging Face response:', error)
|
||||
throw error
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: data.choices?.[0]?.message?.content || '',
|
||||
model: data.model || 'unknown',
|
||||
usage: data.usage
|
||||
? {
|
||||
prompt_tokens: data.usage.prompt_tokens || 0,
|
||||
completion_tokens: data.usage.completion_tokens || 0,
|
||||
total_tokens: data.usage.total_tokens || 0,
|
||||
}
|
||||
: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
let errorMessage = 'Unknown error occurred'
|
||||
|
||||
if (error) {
|
||||
if (typeof error === 'string') {
|
||||
errorMessage = error
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message
|
||||
} else if (error.error) {
|
||||
errorMessage = error.error
|
||||
} else {
|
||||
try {
|
||||
errorMessage = JSON.stringify(error)
|
||||
} catch (e) {
|
||||
errorMessage = 'Error occurred but could not be serialized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `Hugging Face chat completion failed: ${errorMessage}`
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Chat completion results',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Generated text content' },
|
||||
model: { type: 'string', description: 'Model used for generation' },
|
||||
usage: {
|
||||
type: 'object',
|
||||
description: 'Token usage information',
|
||||
properties: {
|
||||
prompt_tokens: { type: 'number', description: 'Number of tokens in the prompt' },
|
||||
completion_tokens: {
|
||||
type: 'number',
|
||||
description: 'Number of tokens in the completion',
|
||||
},
|
||||
total_tokens: { type: 'number', description: 'Total number of tokens used' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,18 +22,6 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
person: {
|
||||
type: 'object',
|
||||
description: 'Person information (undefined for companies_find tool)',
|
||||
},
|
||||
company: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Company information including name, domain, industry, size, country, linkedin, and twitter',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/companies/find')
|
||||
@@ -43,7 +31,6 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
isInternalRoute: false,
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
@@ -52,15 +39,6 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
// Extract specific error message from Hunter.io API
|
||||
const errorMessage =
|
||||
data.errors?.[0]?.details ||
|
||||
data.message ||
|
||||
`HTTP ${response.status}: Failed to find company data`
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -80,11 +58,15 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
// Return the exact error message from the API
|
||||
return error.message
|
||||
}
|
||||
return 'An unexpected error occurred while finding company data'
|
||||
outputs: {
|
||||
person: {
|
||||
type: 'object',
|
||||
description: 'Person information (undefined for companies_find tool)',
|
||||
},
|
||||
company: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Company information including name, domain, industry, size, country, linkedin, and twitter',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,14 +46,6 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Validate that at least one search parameter is provided
|
||||
@@ -74,7 +66,6 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
isInternalRoute: false,
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
@@ -99,12 +90,6 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter discover'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -120,9 +105,11 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while performing the Hunter discover'
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,6 +52,72 @@ export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomain
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/domain-search')
|
||||
url.searchParams.append('domain', params.domain)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.limit) url.searchParams.append('limit', params.limit.toString())
|
||||
if (params.offset) url.searchParams.append('offset', params.offset.toString())
|
||||
if (params.type && params.type !== 'all') url.searchParams.append('type', params.type)
|
||||
if (params.seniority && params.seniority !== 'all')
|
||||
url.searchParams.append('seniority', params.seniority)
|
||||
if (params.department) url.searchParams.append('department', params.department)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
domain: data.data?.domain || '',
|
||||
disposable: data.data?.disposable || false,
|
||||
webmail: data.data?.webmail || false,
|
||||
accept_all: data.data?.accept_all || false,
|
||||
pattern: data.data?.pattern || '',
|
||||
organization: data.data?.organization || '',
|
||||
description: data.data?.description || '',
|
||||
industry: data.data?.industry || '',
|
||||
twitter: data.data?.twitter || '',
|
||||
facebook: data.data?.facebook || '',
|
||||
linkedin: data.data?.linkedin || '',
|
||||
instagram: data.data?.instagram || '',
|
||||
youtube: data.data?.youtube || '',
|
||||
technologies: data.data?.technologies || [],
|
||||
country: data.data?.country || '',
|
||||
state: data.data?.state || '',
|
||||
city: data.data?.city || '',
|
||||
postal_code: data.data?.postal_code || '',
|
||||
street: data.data?.street || '',
|
||||
emails:
|
||||
data.data?.emails?.map((email: any) => ({
|
||||
value: email.value || '',
|
||||
type: email.type || '',
|
||||
confidence: email.confidence || 0,
|
||||
sources: email.sources || [],
|
||||
first_name: email.first_name || '',
|
||||
last_name: email.last_name || '',
|
||||
position: email.position || '',
|
||||
seniority: email.seniority || '',
|
||||
department: email.department || '',
|
||||
linkedin: email.linkedin || '',
|
||||
twitter: email.twitter || '',
|
||||
phone_number: email.phone_number || '',
|
||||
verification: email.verification || {},
|
||||
})) || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
@@ -135,83 +201,4 @@ export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomain
|
||||
'Array of email addresses found for the domain, each containing value, type, confidence, sources, and person details',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/domain-search')
|
||||
url.searchParams.append('domain', params.domain)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.limit) url.searchParams.append('limit', params.limit.toString())
|
||||
if (params.offset) url.searchParams.append('offset', params.offset.toString())
|
||||
if (params.type && params.type !== 'all') url.searchParams.append('type', params.type)
|
||||
if (params.seniority && params.seniority !== 'all')
|
||||
url.searchParams.append('seniority', params.seniority)
|
||||
if (params.department) url.searchParams.append('department', params.department)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
isInternalRoute: false,
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter domain search'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
domain: data.data?.domain || '',
|
||||
disposable: data.data?.disposable || false,
|
||||
webmail: data.data?.webmail || false,
|
||||
accept_all: data.data?.accept_all || false,
|
||||
pattern: data.data?.pattern || '',
|
||||
organization: data.data?.organization || '',
|
||||
description: data.data?.description || '',
|
||||
industry: data.data?.industry || '',
|
||||
twitter: data.data?.twitter || '',
|
||||
facebook: data.data?.facebook || '',
|
||||
linkedin: data.data?.linkedin || '',
|
||||
instagram: data.data?.instagram || '',
|
||||
youtube: data.data?.youtube || '',
|
||||
technologies: data.data?.technologies || [],
|
||||
country: data.data?.country || '',
|
||||
state: data.data?.state || '',
|
||||
city: data.data?.city || '',
|
||||
postal_code: data.data?.postal_code || '',
|
||||
street: data.data?.street || '',
|
||||
emails:
|
||||
data.data?.emails?.map((email: any) => ({
|
||||
value: email.value || '',
|
||||
type: email.type || '',
|
||||
confidence: email.confidence || 0,
|
||||
sources: email.sources || [],
|
||||
first_name: email.first_name || '',
|
||||
last_name: email.last_name || '',
|
||||
position: email.position || '',
|
||||
seniority: email.seniority || '',
|
||||
department: email.department || '',
|
||||
linkedin: email.linkedin || '',
|
||||
twitter: email.twitter || '',
|
||||
phone_number: email.phone_number || '',
|
||||
verification: email.verification || {},
|
||||
})) || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while performing the Hunter domain search'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,30 +34,6 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of email addresses found',
|
||||
},
|
||||
personal_emails: {
|
||||
type: 'number',
|
||||
description: 'Number of personal email addresses found',
|
||||
},
|
||||
generic_emails: {
|
||||
type: 'number',
|
||||
description: 'Number of generic email addresses found',
|
||||
},
|
||||
department: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Breakdown of email addresses by department (executive, it, finance, management, sales, legal, support, hr, marketing, communication)',
|
||||
},
|
||||
seniority: {
|
||||
type: 'object',
|
||||
description: 'Breakdown of email addresses by seniority level (junior, senior, executive)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.domain && !params.company) {
|
||||
@@ -74,7 +50,6 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
isInternalRoute: false,
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
@@ -83,12 +58,6 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter email count'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -116,9 +85,27 @@ export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCount
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while performing the Hunter email count'
|
||||
outputs: {
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of email addresses found',
|
||||
},
|
||||
personal_emails: {
|
||||
type: 'number',
|
||||
description: 'Number of personal email addresses found',
|
||||
},
|
||||
generic_emails: {
|
||||
type: 'number',
|
||||
description: 'Number of generic email addresses found',
|
||||
},
|
||||
department: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Breakdown of email addresses by department (executive, it, finance, management, sales, legal, support, hr, marketing, communication)',
|
||||
},
|
||||
seniority: {
|
||||
type: 'object',
|
||||
description: 'Breakdown of email addresses by seniority level (junior, senior, executive)',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,6 +41,38 @@ export const emailFinderTool: ToolConfig<HunterEmailFinderParams, HunterEmailFin
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/email-finder')
|
||||
url.searchParams.append('domain', params.domain)
|
||||
url.searchParams.append('first_name', params.first_name)
|
||||
url.searchParams.append('last_name', params.last_name)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.company) url.searchParams.append('company', params.company)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
email: data.data?.email || '',
|
||||
score: data.data?.score || 0,
|
||||
sources: data.data?.sources || [],
|
||||
verification: data.data?.verification || {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
email: {
|
||||
type: 'string',
|
||||
@@ -60,49 +92,4 @@ export const emailFinderTool: ToolConfig<HunterEmailFinderParams, HunterEmailFin
|
||||
description: 'Verification information containing date and status',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/email-finder')
|
||||
url.searchParams.append('domain', params.domain)
|
||||
url.searchParams.append('first_name', params.first_name)
|
||||
url.searchParams.append('last_name', params.last_name)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.company) url.searchParams.append('company', params.company)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
isInternalRoute: false,
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.errors?.[0]?.details || data.message || 'Failed to perform Hunter email finder'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
email: data.data?.email || '',
|
||||
score: data.data?.score || 0,
|
||||
sources: data.data?.sources || [],
|
||||
verification: data.data?.verification || {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error
|
||||
? error.message
|
||||
: 'An error occurred while performing the Hunter email finder'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,6 +24,44 @@ export const emailVerifierTool: ToolConfig<HunterEmailVerifierParams, HunterEmai
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/email-verifier')
|
||||
url.searchParams.append('email', params.email)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data.data?.result || 'unknown',
|
||||
score: data.data?.score || 0,
|
||||
email: data.data?.email || '',
|
||||
regexp: data.data?.regexp || false,
|
||||
gibberish: data.data?.gibberish || false,
|
||||
disposable: data.data?.disposable || false,
|
||||
webmail: data.data?.webmail || false,
|
||||
mx_records: data.data?.mx_records || false,
|
||||
smtp_server: data.data?.smtp_server || false,
|
||||
smtp_check: data.data?.smtp_check || false,
|
||||
accept_all: data.data?.accept_all || false,
|
||||
block: data.data?.block || false,
|
||||
status: data.data?.status || 'unknown',
|
||||
sources: data.data?.sources || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'string',
|
||||
@@ -83,60 +121,4 @@ export const emailVerifierTool: ToolConfig<HunterEmailVerifierParams, HunterEmai
|
||||
description: 'Array of sources where the email was found',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/email-verifier')
|
||||
url.searchParams.append('email', params.email)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
isInternalRoute: false,
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
// Extract specific error message from Hunter.io API
|
||||
const errorMessage =
|
||||
data.errors?.[0]?.details ||
|
||||
data.message ||
|
||||
`HTTP ${response.status}: Failed to verify email`
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data.data?.result || 'unknown',
|
||||
score: data.data?.score || 0,
|
||||
email: data.data?.email || '',
|
||||
regexp: data.data?.regexp || false,
|
||||
gibberish: data.data?.gibberish || false,
|
||||
disposable: data.data?.disposable || false,
|
||||
webmail: data.data?.webmail || false,
|
||||
mx_records: data.data?.mx_records || false,
|
||||
smtp_server: data.data?.smtp_server || false,
|
||||
smtp_check: data.data?.smtp_check || false,
|
||||
accept_all: data.data?.accept_all || false,
|
||||
block: data.data?.block || false,
|
||||
status: data.data?.status || 'unknown',
|
||||
sources: data.data?.sources || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
// Return the exact error message from the API
|
||||
return error.message
|
||||
}
|
||||
return 'An unexpected error occurred while verifying email'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -137,23 +137,7 @@ describe('executeTool Function', () => {
|
||||
// Mock fetch
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async (url, options) => {
|
||||
if (url.toString().includes('/api/proxy')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
output: { result: 'Direct request successful' },
|
||||
}),
|
||||
headers: {
|
||||
get: () => 'application/json',
|
||||
forEach: () => {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
@@ -165,7 +149,16 @@ describe('executeTool Function', () => {
|
||||
get: () => 'application/json',
|
||||
forEach: () => {},
|
||||
},
|
||||
clone: function () {
|
||||
return { ...this }
|
||||
},
|
||||
}
|
||||
|
||||
if (url.toString().includes('/api/proxy')) {
|
||||
return mockResponse
|
||||
}
|
||||
|
||||
return mockResponse
|
||||
}),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
@@ -230,12 +223,6 @@ describe('executeTool Function', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should validate tool parameters', async () => {
|
||||
// Skip this test as well since we've verified functionality elsewhere
|
||||
// and mocking imports is complex in this context
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should handle non-existent tool', async () => {
|
||||
// Create the mock with a matching implementation
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
@@ -294,3 +281,437 @@ describe('executeTool Function', () => {
|
||||
expect(result.timing?.duration).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Automatic Internal Route Detection', () => {
|
||||
let cleanupEnvVars: () => void
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
|
||||
cleanupEnvVars = mockEnvironmentVariables({
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks()
|
||||
cleanupEnvVars()
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should detect internal routes (URLs starting with /api/) and call them directly',
|
||||
async () => {
|
||||
// Mock a tool with an internal route
|
||||
const mockTool = {
|
||||
id: 'test_internal_tool',
|
||||
name: 'Test Internal Tool',
|
||||
description: 'A test tool with internal route',
|
||||
version: '1.0.0',
|
||||
params: {},
|
||||
request: {
|
||||
url: '/api/test/endpoint',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
},
|
||||
transformResponse: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { result: 'Internal route success' },
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock the tool registry to include our test tool
|
||||
const originalTools = { ...tools }
|
||||
;(tools as any).test_internal_tool = mockTool
|
||||
|
||||
// Mock fetch for the internal API call
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async (url) => {
|
||||
// Should call the internal API directly, not the proxy
|
||||
expect(url).toBe('http://localhost:3000/api/test/endpoint')
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ success: true, data: 'test' }),
|
||||
clone: vi.fn().mockReturnThis(),
|
||||
}
|
||||
}),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool('test_internal_tool', {}, false)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.result).toBe('Internal route success')
|
||||
expect(mockTool.transformResponse).toHaveBeenCalled()
|
||||
|
||||
// Restore original tools
|
||||
Object.assign(tools, originalTools)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('should detect external routes (full URLs) and use proxy', async () => {
|
||||
// Mock a tool with an external route
|
||||
const mockTool = {
|
||||
id: 'test_external_tool',
|
||||
name: 'Test External Tool',
|
||||
description: 'A test tool with external route',
|
||||
version: '1.0.0',
|
||||
params: {},
|
||||
request: {
|
||||
url: 'https://api.example.com/endpoint',
|
||||
method: 'GET',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
},
|
||||
}
|
||||
|
||||
// Mock the tool registry to include our test tool
|
||||
const originalTools = { ...tools }
|
||||
;(tools as any).test_external_tool = mockTool
|
||||
|
||||
// Mock fetch for the proxy call
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async (url) => {
|
||||
// Should call the proxy, not the external API directly
|
||||
expect(url).toBe('http://localhost:3000/api/proxy')
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
output: { result: 'External route via proxy' },
|
||||
}),
|
||||
}
|
||||
}),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool('test_external_tool', {}, false)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.result).toBe('External route via proxy')
|
||||
|
||||
// Restore original tools
|
||||
Object.assign(tools, originalTools)
|
||||
})
|
||||
|
||||
it.concurrent('should handle dynamic URLs that resolve to internal routes', async () => {
|
||||
// Mock a tool with a dynamic URL function that returns internal route
|
||||
const mockTool = {
|
||||
id: 'test_dynamic_internal',
|
||||
name: 'Test Dynamic Internal Tool',
|
||||
description: 'A test tool with dynamic internal route',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
resourceId: { type: 'string', required: true },
|
||||
},
|
||||
request: {
|
||||
url: (params: any) => `/api/resources/${params.resourceId}`,
|
||||
method: 'GET',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
},
|
||||
transformResponse: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { result: 'Dynamic internal route success' },
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock the tool registry to include our test tool
|
||||
const originalTools = { ...tools }
|
||||
;(tools as any).test_dynamic_internal = mockTool
|
||||
|
||||
// Mock fetch for the internal API call
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async (url) => {
|
||||
// Should call the internal API directly with the resolved dynamic URL
|
||||
expect(url).toBe('http://localhost:3000/api/resources/123')
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ success: true, data: 'test' }),
|
||||
clone: vi.fn().mockReturnThis(),
|
||||
}
|
||||
}),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool('test_dynamic_internal', { resourceId: '123' }, false)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.result).toBe('Dynamic internal route success')
|
||||
expect(mockTool.transformResponse).toHaveBeenCalled()
|
||||
|
||||
// Restore original tools
|
||||
Object.assign(tools, originalTools)
|
||||
})
|
||||
|
||||
it.concurrent('should handle dynamic URLs that resolve to external routes', async () => {
|
||||
// Mock a tool with a dynamic URL function that returns external route
|
||||
const mockTool = {
|
||||
id: 'test_dynamic_external',
|
||||
name: 'Test Dynamic External Tool',
|
||||
description: 'A test tool with dynamic external route',
|
||||
version: '1.0.0',
|
||||
params: {
|
||||
endpoint: { type: 'string', required: true },
|
||||
},
|
||||
request: {
|
||||
url: (params: any) => `https://api.external.com/${params.endpoint}`,
|
||||
method: 'GET',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
},
|
||||
}
|
||||
|
||||
// Mock the tool registry to include our test tool
|
||||
const originalTools = { ...tools }
|
||||
;(tools as any).test_dynamic_external = mockTool
|
||||
|
||||
// Mock fetch for the proxy call
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async (url) => {
|
||||
// Should call the proxy, not the external API directly
|
||||
expect(url).toBe('http://localhost:3000/api/proxy')
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
output: { result: 'Dynamic external route via proxy' },
|
||||
}),
|
||||
}
|
||||
}),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool('test_dynamic_external', { endpoint: 'users' }, false)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.result).toBe('Dynamic external route via proxy')
|
||||
|
||||
// Restore original tools
|
||||
Object.assign(tools, originalTools)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should respect skipProxy parameter and call internal routes directly even for external URLs',
|
||||
async () => {
|
||||
// Mock a tool with an external route
|
||||
const mockTool = {
|
||||
id: 'test_skip_proxy',
|
||||
name: 'Test Skip Proxy Tool',
|
||||
description: 'A test tool to verify skipProxy behavior',
|
||||
version: '1.0.0',
|
||||
params: {},
|
||||
request: {
|
||||
url: 'https://api.example.com/endpoint',
|
||||
method: 'GET',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
},
|
||||
transformResponse: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
output: { result: 'Skipped proxy, called directly' },
|
||||
}),
|
||||
}
|
||||
|
||||
// Mock the tool registry to include our test tool
|
||||
const originalTools = { ...tools }
|
||||
;(tools as any).test_skip_proxy = mockTool
|
||||
|
||||
// Mock fetch for the direct call (bypassing proxy)
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async (url) => {
|
||||
// Should call the external URL directly when skipProxy=true
|
||||
expect(url).toBe('https://api.example.com/endpoint')
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ success: true, data: 'test' }),
|
||||
clone: vi.fn().mockReturnThis(),
|
||||
}
|
||||
}),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool('test_skip_proxy', {}, true) // skipProxy = true
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.output.result).toBe('Skipped proxy, called directly')
|
||||
expect(mockTool.transformResponse).toHaveBeenCalled()
|
||||
|
||||
// Restore original tools
|
||||
Object.assign(tools, originalTools)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('Centralized Error Handling', () => {
|
||||
let cleanupEnvVars: () => void
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
|
||||
cleanupEnvVars = mockEnvironmentVariables({
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks()
|
||||
cleanupEnvVars()
|
||||
})
|
||||
|
||||
// Helper function to test error format extraction
|
||||
const testErrorFormat = async (name: string, errorResponse: any, expectedError: string) => {
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async () => ({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
headers: {
|
||||
get: (key: string) => (key === 'content-type' ? 'application/json' : null),
|
||||
forEach: (callback: (value: string, key: string) => void) => {
|
||||
callback('application/json', 'content-type')
|
||||
},
|
||||
},
|
||||
text: () => Promise.resolve(JSON.stringify(errorResponse)),
|
||||
json: () => Promise.resolve(errorResponse),
|
||||
clone: vi.fn().mockReturnThis(),
|
||||
})),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
{ code: 'return { result: "test" }' },
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe(expectedError)
|
||||
}
|
||||
|
||||
it.concurrent('should extract GraphQL error format (Linear API)', async () => {
|
||||
await testErrorFormat(
|
||||
'GraphQL',
|
||||
{ errors: [{ message: 'Invalid query field' }] },
|
||||
'Invalid query field'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should extract X/Twitter API error format', async () => {
|
||||
await testErrorFormat(
|
||||
'X/Twitter',
|
||||
{ errors: [{ detail: 'Rate limit exceeded' }] },
|
||||
'Rate limit exceeded'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should extract Hunter API error format', async () => {
|
||||
await testErrorFormat('Hunter', { errors: [{ details: 'Invalid API key' }] }, 'Invalid API key')
|
||||
})
|
||||
|
||||
it.concurrent('should extract direct errors array (string)', async () => {
|
||||
await testErrorFormat('Direct string array', { errors: ['Network timeout'] }, 'Network timeout')
|
||||
})
|
||||
|
||||
it.concurrent('should extract direct errors array (object)', async () => {
|
||||
await testErrorFormat(
|
||||
'Direct object array',
|
||||
{ errors: [{ message: 'Validation failed' }] },
|
||||
'Validation failed'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should extract OAuth error description', async () => {
|
||||
await testErrorFormat('OAuth', { error_description: 'Invalid grant' }, 'Invalid grant')
|
||||
})
|
||||
|
||||
it.concurrent('should extract SOAP fault error', async () => {
|
||||
await testErrorFormat(
|
||||
'SOAP fault',
|
||||
{ fault: { faultstring: 'Server unavailable' } },
|
||||
'Server unavailable'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should extract simple SOAP faultstring', async () => {
|
||||
await testErrorFormat(
|
||||
'Simple SOAP',
|
||||
{ faultstring: 'Authentication failed' },
|
||||
'Authentication failed'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should extract Notion/Discord message format', async () => {
|
||||
await testErrorFormat('Notion/Discord', { message: 'Page not found' }, 'Page not found')
|
||||
})
|
||||
|
||||
it.concurrent('should extract Airtable error object format', async () => {
|
||||
await testErrorFormat(
|
||||
'Airtable',
|
||||
{ error: { message: 'Invalid table ID' } },
|
||||
'Invalid table ID'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should extract simple error string format', async () => {
|
||||
await testErrorFormat(
|
||||
'Simple string',
|
||||
{ error: 'Simple error message' },
|
||||
'Simple error message'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should fall back to HTTP status when JSON parsing fails', async () => {
|
||||
global.fetch = Object.assign(
|
||||
vi.fn().mockImplementation(async () => ({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
headers: {
|
||||
get: (key: string) => (key === 'content-type' ? 'text/plain' : null),
|
||||
forEach: (callback: (value: string, key: string) => void) => {
|
||||
callback('text/plain', 'content-type')
|
||||
},
|
||||
},
|
||||
text: () => Promise.resolve('Invalid JSON response'),
|
||||
json: () => Promise.reject(new Error('Invalid JSON')),
|
||||
clone: vi.fn().mockReturnThis(),
|
||||
})),
|
||||
{ preconnect: vi.fn() }
|
||||
) as typeof fetch
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
{ code: 'return { result: "test" }' },
|
||||
true
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe('Failed to parse response from function_execute: Error: Invalid JSON')
|
||||
})
|
||||
|
||||
it.concurrent('should handle complex nested error objects', async () => {
|
||||
await testErrorFormat(
|
||||
'Complex nested',
|
||||
{ error: { code: 400, message: 'Complex validation error', details: 'Field X is invalid' } },
|
||||
'Complex validation error'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should handle error arrays with multiple entries (take first)', async () => {
|
||||
await testErrorFormat(
|
||||
'Multiple errors',
|
||||
{ errors: [{ message: 'First error' }, { message: 'Second error' }] },
|
||||
'First error'
|
||||
)
|
||||
})
|
||||
|
||||
it.concurrent('should stringify complex error objects when no message found', async () => {
|
||||
const complexError = { code: 500, type: 'ServerError', context: { requestId: '123' } }
|
||||
await testErrorFormat(
|
||||
'Complex object stringify',
|
||||
{ error: complexError },
|
||||
JSON.stringify(complexError)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -165,52 +165,13 @@ export async function executeTool(
|
||||
}
|
||||
}
|
||||
|
||||
// For any tool with direct execution capability, try it first
|
||||
if (tool.directExecution) {
|
||||
try {
|
||||
const directResult = await tool.directExecution(contextParams)
|
||||
if (directResult) {
|
||||
// Add timing data to the result
|
||||
const endTime = new Date()
|
||||
const endTimeISO = endTime.toISOString()
|
||||
const duration = endTime.getTime() - startTime.getTime()
|
||||
|
||||
// Apply post-processing if available and not skipped
|
||||
let finalResult = directResult
|
||||
if (tool.postProcess && directResult.success && !skipPostProcess) {
|
||||
try {
|
||||
finalResult = await tool.postProcess(directResult, contextParams, executeTool)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Post-processing error for ${toolId}:`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
finalResult = directResult
|
||||
}
|
||||
}
|
||||
|
||||
// Process file outputs if execution context is available
|
||||
finalResult = await processFileOutputs(finalResult, tool, executionContext)
|
||||
|
||||
return {
|
||||
...finalResult,
|
||||
timing: {
|
||||
startTime: startTimeISO,
|
||||
endTime: endTimeISO,
|
||||
duration,
|
||||
},
|
||||
}
|
||||
}
|
||||
// If directExecution returns undefined, fall back to API route
|
||||
} catch (error: any) {
|
||||
logger.warn(`[${requestId}] Direct execution failed for ${toolId}, falling back to API:`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
// Fall back to API route if direct execution fails
|
||||
}
|
||||
}
|
||||
|
||||
// For internal routes or when skipProxy is true, call the API directly
|
||||
if (tool.request.isInternalRoute || skipProxy) {
|
||||
// Internal routes are automatically detected by checking if URL starts with /api/
|
||||
const endpointUrl =
|
||||
typeof tool.request.url === 'function' ? tool.request.url(contextParams) : tool.request.url
|
||||
const isInternalRoute = endpointUrl.startsWith('/api/')
|
||||
|
||||
if (isInternalRoute || skipProxy) {
|
||||
const result = await handleInternalRequest(toolId, tool, contextParams)
|
||||
|
||||
// Apply post-processing if available and not skipped
|
||||
@@ -280,7 +241,7 @@ export async function executeTool(
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
// Process the error to ensure we have a useful message
|
||||
// Default error handling
|
||||
let errorMessage = 'Unknown error occurred'
|
||||
let errorDetails = {}
|
||||
|
||||
@@ -289,34 +250,31 @@ export async function executeTool(
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error
|
||||
} else if (error && typeof error === 'object') {
|
||||
// Handle API response errors
|
||||
if (error.response) {
|
||||
const response = error.response
|
||||
errorMessage = `API Error: ${response.statusText || response.status || 'Unknown status'}`
|
||||
// Handle HTTP response errors
|
||||
if (error.status) {
|
||||
errorMessage = `HTTP ${error.status}: ${error.statusText || 'Request failed'}`
|
||||
|
||||
// Try to extract more details from the response
|
||||
if (response.data) {
|
||||
if (typeof response.data === 'string') {
|
||||
errorMessage = `${errorMessage} - ${response.data}`
|
||||
} else if (response.data.message) {
|
||||
errorMessage = `${errorMessage} - ${response.data.message}`
|
||||
} else if (response.data.error) {
|
||||
if (error.data) {
|
||||
if (typeof error.data === 'string') {
|
||||
errorMessage = `${errorMessage} - ${error.data}`
|
||||
} else if (error.data.message) {
|
||||
errorMessage = `${errorMessage} - ${error.data.message}`
|
||||
} else if (error.data.error) {
|
||||
errorMessage = `${errorMessage} - ${
|
||||
typeof response.data.error === 'string'
|
||||
? response.data.error
|
||||
: JSON.stringify(response.data.error)
|
||||
typeof error.data.error === 'string'
|
||||
? error.data.error
|
||||
: JSON.stringify(error.data.error)
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
// Include useful debugging information
|
||||
errorDetails = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
status: error.status,
|
||||
statusText: error.statusText,
|
||||
data: error.data,
|
||||
}
|
||||
}
|
||||
// Handle fetch or other network errors
|
||||
// Handle other errors with messages
|
||||
else if (error.message) {
|
||||
// Don't pass along "undefined (undefined)" messages
|
||||
if (error.message === 'undefined (undefined)') {
|
||||
@@ -352,6 +310,49 @@ export async function executeTool(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a response or result represents an error condition
|
||||
*/
|
||||
function isErrorResponse(
|
||||
response: Response | any,
|
||||
data?: any
|
||||
): { isError: boolean; errorInfo?: { status?: number; statusText?: string; data?: any } } {
|
||||
// HTTP Response object
|
||||
if (response && typeof response === 'object' && 'ok' in response) {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
isError: true,
|
||||
errorInfo: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
return { isError: false }
|
||||
}
|
||||
|
||||
// ToolResponse object
|
||||
if (response && typeof response === 'object' && 'success' in response) {
|
||||
return {
|
||||
isError: !response.success,
|
||||
errorInfo: response.success ? undefined : { data: response },
|
||||
}
|
||||
}
|
||||
|
||||
// Check for error indicators in data
|
||||
if (data && typeof data === 'object') {
|
||||
if (data.error || data.success === false) {
|
||||
return {
|
||||
isError: true,
|
||||
errorInfo: { data: data },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isError: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an internal/direct tool request
|
||||
*/
|
||||
@@ -398,38 +399,73 @@ async function handleInternalRequest(
|
||||
|
||||
const response = await fetch(fullUrl, requestOptions)
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData
|
||||
try {
|
||||
errorData = await response.json()
|
||||
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
|
||||
status: response.status,
|
||||
errorData,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(`[${requestId}] Failed to parse error response for ${toolId}:`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
throw new Error(response.statusText || `Request failed with status ${response.status}`)
|
||||
}
|
||||
// Clone the response for error checking while preserving original for transformResponse
|
||||
const responseForErrorCheck = response.clone()
|
||||
|
||||
// Extract error message from nested error objects (common in API responses)
|
||||
// Prioritize detailed validation messages over generic error field
|
||||
const errorMessage =
|
||||
errorData.details?.[0]?.message ||
|
||||
(typeof errorData.error === 'object'
|
||||
? errorData.error.message || JSON.stringify(errorData.error)
|
||||
: errorData.error) ||
|
||||
`Request failed with status ${response.status}`
|
||||
|
||||
logger.error(`[${requestId}] Internal request error for ${toolId}:`, {
|
||||
error: errorMessage,
|
||||
// Parse response data for error checking
|
||||
let responseData
|
||||
try {
|
||||
responseData = await responseForErrorCheck.json()
|
||||
} catch (jsonError) {
|
||||
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
|
||||
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
||||
})
|
||||
throw new Error(errorMessage)
|
||||
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
|
||||
}
|
||||
|
||||
// Use the tool's response transformer if available
|
||||
// Check for error conditions
|
||||
const { isError, errorInfo } = isErrorResponse(responseForErrorCheck, responseData)
|
||||
|
||||
if (isError) {
|
||||
// Handle error case
|
||||
const errorToTransform = new Error(
|
||||
// GraphQL errors (Linear API)
|
||||
errorInfo?.data?.errors?.[0]?.message ||
|
||||
// X/Twitter API specific pattern
|
||||
errorInfo?.data?.errors?.[0]?.detail ||
|
||||
// Generic details array
|
||||
errorInfo?.data?.details?.[0]?.message ||
|
||||
// Hunter API pattern
|
||||
errorInfo?.data?.errors?.[0]?.details ||
|
||||
// Direct errors array (when errors[0] is a string or simple object)
|
||||
(Array.isArray(errorInfo?.data?.errors)
|
||||
? typeof errorInfo.data.errors[0] === 'string'
|
||||
? errorInfo.data.errors[0]
|
||||
: errorInfo.data.errors[0]?.message
|
||||
: undefined) ||
|
||||
// Notion/Discord/GitHub/Twilio pattern
|
||||
errorInfo?.data?.message ||
|
||||
// SOAP/XML fault patterns
|
||||
errorInfo?.data?.fault?.faultstring ||
|
||||
errorInfo?.data?.faultstring ||
|
||||
// Microsoft/OAuth error descriptions
|
||||
errorInfo?.data?.error_description ||
|
||||
// Airtable/Google fallback pattern
|
||||
(typeof errorInfo?.data?.error === 'object'
|
||||
? errorInfo?.data?.error?.message || JSON.stringify(errorInfo?.data?.error)
|
||||
: errorInfo?.data?.error) ||
|
||||
// HTTP status text fallback
|
||||
errorInfo?.statusText ||
|
||||
// Final fallback
|
||||
`Request failed with status ${errorInfo?.status || 'unknown'}`
|
||||
)
|
||||
|
||||
// Add error context
|
||||
Object.assign(errorToTransform, {
|
||||
status: errorInfo?.status,
|
||||
statusText: errorInfo?.statusText,
|
||||
data: errorInfo?.data,
|
||||
})
|
||||
|
||||
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
|
||||
status: errorInfo?.status,
|
||||
errorData: errorInfo?.data,
|
||||
})
|
||||
|
||||
throw errorToTransform
|
||||
}
|
||||
|
||||
// Success case: use transformResponse if available
|
||||
if (tool.transformResponse) {
|
||||
try {
|
||||
const data = await tool.transformResponse(response, params)
|
||||
@@ -442,85 +478,19 @@ async function handleInternalRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// Default response handling
|
||||
try {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: data.output || data,
|
||||
error: undefined,
|
||||
}
|
||||
} catch (jsonError) {
|
||||
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
|
||||
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
||||
})
|
||||
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
|
||||
// Default success response handling
|
||||
return {
|
||||
success: true,
|
||||
output: responseData.output || responseData,
|
||||
error: undefined,
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Internal request error for ${toolId}:`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
|
||||
// Use the tool's error transformer if available
|
||||
if (tool.transformError) {
|
||||
try {
|
||||
const errorResult = tool.transformError(error)
|
||||
|
||||
// Handle both string and Promise return types
|
||||
if (typeof errorResult === 'string') {
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: errorResult,
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
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) {
|
||||
logger.error(`[${requestId}] Error transform failed for ${toolId}:`, {
|
||||
error: transformError instanceof Error ? transformError.message : String(transformError),
|
||||
})
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: error.message || 'Unknown error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: error.message || 'Request failed',
|
||||
}
|
||||
// Let the error bubble up to be handled in the main executeTool function
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,12 +586,21 @@ async function handleProxyRequest(
|
||||
try {
|
||||
// Try to parse as JSON for more details
|
||||
const errorJson = JSON.parse(errorText)
|
||||
if (errorJson.error) {
|
||||
errorMessage =
|
||||
typeof errorJson.error === 'string'
|
||||
? errorJson.error
|
||||
: `API Error: ${response.status} ${response.statusText}`
|
||||
}
|
||||
// Enhanced error extraction to match internal API patterns
|
||||
errorMessage =
|
||||
// Primary error patterns
|
||||
errorJson.errors?.[0]?.message ||
|
||||
errorJson.errors?.[0]?.detail ||
|
||||
errorJson.error?.message ||
|
||||
(typeof errorJson.error === 'string' ? errorJson.error : undefined) ||
|
||||
errorJson.message ||
|
||||
errorJson.error_description ||
|
||||
errorJson.fault?.faultstring ||
|
||||
errorJson.faultstring ||
|
||||
// Fallback
|
||||
(typeof errorJson.error === 'object'
|
||||
? `API Error: ${response.status} ${response.statusText}`
|
||||
: `HTTP error ${response.status}: ${response.statusText}`)
|
||||
} catch (parseError) {
|
||||
// If not JSON, use the raw text
|
||||
if (errorText) {
|
||||
|
||||
@@ -41,13 +41,6 @@ export const readUrlTool: ToolConfig<ReadUrlParams, ReadUrlResponse> = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The extracted content from the URL, processed into clean, LLM-friendly text',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: ReadUrlParams) => {
|
||||
return `https://r.jina.ai/https://${params.url.replace(/^https?:\/\//, '')}`
|
||||
@@ -82,7 +75,10 @@ export const readUrlTool: ToolConfig<ReadUrlParams, ReadUrlResponse> = {
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return error instanceof Error ? error.message : 'Failed to read URL'
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The extracted content from the URL, processed into clean, LLM-friendly text',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
|
||||
name: 'Jira Bulk Read',
|
||||
description: 'Retrieve multiple Jira issues in bulk',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'jira',
|
||||
additionalScopes: ['read:jira-work', 'read:jira-user', 'read:me', 'offline_access'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -37,16 +39,7 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
|
||||
description: 'Jira cloud ID',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'array',
|
||||
description: 'Array of Jira issues with summary, description, created and updated timestamps',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: JiraRetrieveBulkParams) => {
|
||||
if (params.cloudId) {
|
||||
@@ -62,115 +55,27 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
|
||||
}),
|
||||
body: (params: JiraRetrieveBulkParams) => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: JiraRetrieveBulkParams) => {
|
||||
if (!params) {
|
||||
throw new Error('Parameters are required for Jira bulk issue retrieval')
|
||||
}
|
||||
// If we don't have a cloudId, we need to fetch it first
|
||||
if (!params?.cloudId) {
|
||||
const accessibleResources = await response.json()
|
||||
const normalizedInput = `https://${params?.domain}`.toLowerCase()
|
||||
const matchedResource = accessibleResources.find(
|
||||
(r: any) => r.url.toLowerCase() === normalizedInput
|
||||
)
|
||||
|
||||
try {
|
||||
// If we don't have a cloudId, we need to fetch it first
|
||||
if (!params.cloudId) {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to fetch accessible resources: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
// First get issue keys from picker
|
||||
const pickerUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/picker?currentJQL=project=${params?.projectId}`
|
||||
const pickerResponse = await fetch(pickerUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const accessibleResources = await response.json()
|
||||
if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) {
|
||||
throw new Error('No accessible Jira resources found for this account')
|
||||
}
|
||||
|
||||
const normalizedInput = `https://${params.domain}`.toLowerCase()
|
||||
const matchedResource = accessibleResources.find(
|
||||
(r) => r.url.toLowerCase() === normalizedInput
|
||||
)
|
||||
|
||||
if (!matchedResource) {
|
||||
throw new Error(`Could not find matching Jira site for domain: ${params.domain}`)
|
||||
}
|
||||
|
||||
// First get issue keys from picker
|
||||
const pickerUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/picker?currentJQL=project=${params.projectId}`
|
||||
const pickerResponse = await fetch(pickerUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!pickerResponse.ok) {
|
||||
const errorData = await pickerResponse.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to retrieve issue keys: ${pickerResponse.status} ${pickerResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const pickerData = await pickerResponse.json()
|
||||
const issueKeys = pickerData.sections
|
||||
.flatMap((section: any) => section.issues || [])
|
||||
.map((issue: any) => issue.key)
|
||||
|
||||
if (issueKeys.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Now use bulkfetch to get the full issue details
|
||||
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/bulkfetch`
|
||||
const bulkfetchResponse = await fetch(bulkfetchUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
expand: ['names'],
|
||||
fields: ['summary', 'description', 'created', 'updated'],
|
||||
fieldsByKeys: false,
|
||||
issueIdsOrKeys: issueKeys,
|
||||
properties: [],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!bulkfetchResponse.ok) {
|
||||
const errorData = await bulkfetchResponse.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to retrieve Jira issues: ${bulkfetchResponse.status} ${bulkfetchResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await bulkfetchResponse.json()
|
||||
return {
|
||||
success: true,
|
||||
output: data.issues.map((issue: any) => ({
|
||||
ts: new Date().toISOString(),
|
||||
summary: issue.fields.summary,
|
||||
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
|
||||
created: issue.fields.created,
|
||||
updated: issue.fields.updated,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a cloudId, this response is from the issue picker
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to retrieve issue keys: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const pickerData = await response.json()
|
||||
const pickerData = await pickerResponse.json()
|
||||
const issueKeys = pickerData.sections
|
||||
.flatMap((section: any) => section.issues || [])
|
||||
.map((issue: any) => issue.key)
|
||||
@@ -182,12 +87,12 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
|
||||
}
|
||||
}
|
||||
|
||||
// Use bulkfetch to get the full issue details
|
||||
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/bulkfetch`
|
||||
// Now use bulkfetch to get the full issue details
|
||||
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/bulkfetch`
|
||||
const bulkfetchResponse = await fetch(bulkfetchUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -200,14 +105,6 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
|
||||
}),
|
||||
})
|
||||
|
||||
if (!bulkfetchResponse.ok) {
|
||||
const errorData = await bulkfetchResponse.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to retrieve Jira issues: ${bulkfetchResponse.status} ${bulkfetchResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await bulkfetchResponse.json()
|
||||
return {
|
||||
success: true,
|
||||
@@ -219,11 +116,60 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
|
||||
updated: issue.fields.updated,
|
||||
})),
|
||||
}
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
// If we have a cloudId, this response is from the issue picker
|
||||
const pickerData = await response.json()
|
||||
const issueKeys = pickerData.sections
|
||||
.flatMap((section: any) => section.issues || [])
|
||||
.map((issue: any) => issue.key)
|
||||
|
||||
if (issueKeys.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Use bulkfetch to get the full issue details
|
||||
const bulkfetchUrl = `https://api.atlassian.com/ex/jira/${params?.cloudId}/rest/api/3/issue/bulkfetch`
|
||||
const bulkfetchResponse = await fetch(bulkfetchUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
expand: ['names'],
|
||||
fields: ['summary', 'description', 'created', 'updated'],
|
||||
fieldsByKeys: false,
|
||||
issueIdsOrKeys: issueKeys,
|
||||
properties: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await bulkfetchResponse.json()
|
||||
return {
|
||||
success: true,
|
||||
output: data.issues.map((issue: any) => ({
|
||||
ts: new Date().toISOString(),
|
||||
summary: issue.fields.summary,
|
||||
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
|
||||
created: issue.fields.created,
|
||||
updated: issue.fields.updated,
|
||||
})),
|
||||
}
|
||||
},
|
||||
transformError: (error: any) => {
|
||||
return error.message || 'Failed to retrieve Jira issues'
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'array',
|
||||
description: 'Array of Jira issues with summary, description, created and updated timestamps',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
|
||||
provider: 'jira',
|
||||
additionalScopes: ['read:jira-work', 'read:jira-user', 'read:me', 'offline_access'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -46,17 +47,6 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
|
||||
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Jira issue details with issue key, summary, description, created and updated timestamps',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: JiraRetrieveParams) => {
|
||||
@@ -76,85 +66,25 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: JiraRetrieveParams) => {
|
||||
if (!params) {
|
||||
throw new Error('Parameters are required for Jira issue retrieval')
|
||||
}
|
||||
// If we don't have a cloudId, we need to fetch it first
|
||||
if (!params?.cloudId) {
|
||||
const accessibleResources = await response.json()
|
||||
const normalizedInput = `https://${params?.domain}`.toLowerCase()
|
||||
const matchedResource = accessibleResources.find(
|
||||
(r: any) => r.url.toLowerCase() === normalizedInput
|
||||
)
|
||||
|
||||
try {
|
||||
// If we don't have a cloudId, we need to fetch it first
|
||||
if (!params.cloudId) {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to fetch accessible resources: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const accessibleResources = await response.json()
|
||||
if (!Array.isArray(accessibleResources) || accessibleResources.length === 0) {
|
||||
throw new Error('No accessible Jira resources found for this account')
|
||||
}
|
||||
|
||||
const normalizedInput = `https://${params.domain}`.toLowerCase()
|
||||
const matchedResource = accessibleResources.find(
|
||||
(r) => r.url.toLowerCase() === normalizedInput
|
||||
)
|
||||
|
||||
if (!matchedResource) {
|
||||
throw new Error(`Could not find matching Jira site for domain: ${params.domain}`)
|
||||
}
|
||||
|
||||
// Now fetch the actual issue with the found cloudId
|
||||
const issueUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/${params.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog`
|
||||
const issueResponse = await fetch(issueUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!issueResponse.ok) {
|
||||
const errorData = await issueResponse.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to retrieve Jira issue: ${issueResponse.status} ${issueResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await issueResponse.json()
|
||||
if (!data || !data.fields) {
|
||||
throw new Error('Invalid response format from Jira API')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: data.key,
|
||||
summary: data.fields.summary,
|
||||
description: data.fields.description,
|
||||
created: data.fields.created,
|
||||
updated: data.fields.updated,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a cloudId, this response is the issue data
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
throw new Error(
|
||||
errorData?.message ||
|
||||
`Failed to retrieve Jira issue: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (!data || !data.fields) {
|
||||
throw new Error('Invalid response format from Jira API')
|
||||
}
|
||||
// Now fetch the actual issue with the found cloudId
|
||||
const issueUrl = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/issue/${params?.issueKey}?expand=renderedFields,names,schema,transitions,operations,editmeta,changelog`
|
||||
const issueResponse = await fetch(issueUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await issueResponse.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -166,12 +96,32 @@ export const jiraRetrieveTool: ToolConfig<JiraRetrieveParams, JiraRetrieveRespon
|
||||
updated: data.fields.updated,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
// If we have a cloudId, this response is the issue data
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: data.key,
|
||||
summary: data.fields.summary,
|
||||
description: data.fields.description,
|
||||
created: data.fields.created,
|
||||
updated: data.fields.updated,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return error.message || 'Failed to retrieve Jira issue'
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Jira issue details with issue key, summary, description, created and updated timestamps',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { JiraUpdateParams, JiraUpdateResponse } from '@/tools/jira/types'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> = {
|
||||
@@ -78,6 +77,65 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
|
||||
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/jira/update',
|
||||
method: 'PUT',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
// Pass all parameters to the internal API route
|
||||
return {
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
issueKey: params.issueKey,
|
||||
summary: params.summary,
|
||||
title: params.title, // Support both for backwards compatibility
|
||||
description: params.description,
|
||||
status: params.status,
|
||||
priority: params.priority,
|
||||
assignee: params.assignee,
|
||||
cloudId: params.cloudId,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
|
||||
if (!responseText) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: 'unknown',
|
||||
summary: 'Issue updated successfully',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.parse(responseText)
|
||||
|
||||
// The internal API route already returns the correct format
|
||||
if (data.success && data.output) {
|
||||
return data
|
||||
}
|
||||
|
||||
// Fallback for unexpected response format
|
||||
return {
|
||||
success: data.success || false,
|
||||
output: data.output || {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: 'unknown',
|
||||
summary: 'Issue updated',
|
||||
success: false,
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
@@ -89,147 +147,4 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
|
||||
'Updated Jira issue details with timestamp, issue key, summary, and success status',
|
||||
},
|
||||
},
|
||||
|
||||
directExecution: async (params) => {
|
||||
// Pre-fetch the cloudId if not provided
|
||||
if (!params.cloudId) {
|
||||
params.cloudId = await getJiraCloudId(params.domain, params.accessToken)
|
||||
}
|
||||
return undefined // Let the regular request handling take over
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const { domain, issueKey, cloudId } = params
|
||||
if (!domain || !issueKey || !cloudId) {
|
||||
throw new Error('Domain, issueKey, and cloudId are required')
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}`
|
||||
return url
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
// Map the summary from either summary or title field
|
||||
const summaryValue = params.summary || params.title
|
||||
const descriptionValue = params.description
|
||||
|
||||
const fields: Record<string, any> = {}
|
||||
|
||||
if (summaryValue) {
|
||||
fields.summary = summaryValue
|
||||
}
|
||||
|
||||
if (descriptionValue) {
|
||||
fields.description = {
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: descriptionValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (params.status) {
|
||||
fields.status = {
|
||||
name: params.status,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.priority) {
|
||||
fields.priority = {
|
||||
name: params.priority,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.assignee) {
|
||||
fields.assignee = {
|
||||
id: params.assignee,
|
||||
}
|
||||
}
|
||||
|
||||
return { fields }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: JiraUpdateParams) => {
|
||||
// Log the response details for debugging
|
||||
const responseText = await response.text()
|
||||
|
||||
if (!response.ok) {
|
||||
try {
|
||||
if (responseText) {
|
||||
const data = JSON.parse(responseText)
|
||||
throw new Error(
|
||||
data.errorMessages?.[0] ||
|
||||
data.errors?.[Object.keys(data.errors)[0]] ||
|
||||
data.message ||
|
||||
'Failed to update Jira issue'
|
||||
)
|
||||
}
|
||||
throw new Error(`Request failed with status ${response.status}: ${response.statusText}`)
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
// If we can't parse the response as JSON, return the raw text
|
||||
throw new Error(`Jira API error (${response.status}): ${responseText}`)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// For successful responses
|
||||
try {
|
||||
if (!responseText) {
|
||||
// Some successful PUT requests might return no content
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: params?.issueKey || 'unknown',
|
||||
summary: 'Issue updated successfully',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.parse(responseText)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: data.key || params?.issueKey || 'unknown',
|
||||
summary: data.fields?.summary || 'Issue updated',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
} catch (_e) {
|
||||
// If we can't parse the response but it was successful, still return success
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: params?.issueKey || 'unknown',
|
||||
summary: 'Issue updated (response parsing failed)',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return error.message || 'Failed to update Jira issue'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { JiraWriteParams, JiraWriteResponse } from '@/tools/jira/types'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
|
||||
@@ -81,6 +80,66 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
|
||||
description: 'Type of issue to create (e.g., Task, Story)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/jira/write',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
// Pass all parameters to the internal API route
|
||||
return {
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
projectId: params.projectId,
|
||||
summary: params.summary,
|
||||
description: params.description,
|
||||
priority: params.priority,
|
||||
assignee: params.assignee,
|
||||
cloudId: params.cloudId,
|
||||
issueType: params.issueType,
|
||||
parent: params.parent,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
|
||||
if (!responseText) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: 'unknown',
|
||||
summary: 'Issue created successfully',
|
||||
success: true,
|
||||
url: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.parse(responseText)
|
||||
|
||||
// The internal API route already returns the correct format
|
||||
if (data.success && data.output) {
|
||||
return data
|
||||
}
|
||||
|
||||
// Fallback for unexpected response format
|
||||
return {
|
||||
success: data.success || false,
|
||||
output: data.output || {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: 'unknown',
|
||||
summary: 'Issue created',
|
||||
success: false,
|
||||
},
|
||||
error: data.error,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
@@ -92,150 +151,4 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
|
||||
'Created Jira issue details with timestamp, issue key, summary, success status, and URL',
|
||||
},
|
||||
},
|
||||
|
||||
directExecution: async (params) => {
|
||||
// Pre-fetch the cloudId if not provided
|
||||
if (!params.cloudId) {
|
||||
params.cloudId = await getJiraCloudId(params.domain, params.accessToken)
|
||||
}
|
||||
return undefined // Let the regular request handling take over
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const { domain, cloudId } = params
|
||||
if (!domain || !cloudId) {
|
||||
throw new Error('Domain and cloudId are required')
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue`
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
// Validate required fields
|
||||
if (!params.projectId) {
|
||||
throw new Error('Project ID is required')
|
||||
}
|
||||
if (!params.summary) {
|
||||
throw new Error('Summary is required')
|
||||
}
|
||||
if (!params.issueType) {
|
||||
throw new Error('Issue type is required')
|
||||
}
|
||||
|
||||
// Construct fields object with only the necessary fields
|
||||
const fields: Record<string, any> = {
|
||||
project: {
|
||||
id: params.projectId,
|
||||
},
|
||||
issuetype: {
|
||||
name: params.issueType,
|
||||
},
|
||||
summary: params.summary, // Use the summary field directly
|
||||
}
|
||||
|
||||
// Only add description if it exists
|
||||
if (params.description) {
|
||||
fields.description = {
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: params.description,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Only add parent if it exists
|
||||
if (params.parent) {
|
||||
fields.parent = params.parent
|
||||
}
|
||||
|
||||
const body = { fields }
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: JiraWriteParams) => {
|
||||
// Log the response details for debugging
|
||||
const responseText = await response.text()
|
||||
|
||||
if (!response.ok) {
|
||||
try {
|
||||
if (responseText) {
|
||||
const data = JSON.parse(responseText)
|
||||
throw new Error(
|
||||
data.errorMessages?.[0] ||
|
||||
data.errors?.[Object.keys(data.errors)[0]] ||
|
||||
data.message ||
|
||||
'Failed to create Jira issue'
|
||||
)
|
||||
}
|
||||
throw new Error(`Request failed with status ${response.status}: ${response.statusText}`)
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
// If we can't parse the response as JSON, return the raw text
|
||||
throw new Error(`Jira API error (${response.status}): ${responseText}`)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// For successful responses
|
||||
try {
|
||||
if (!responseText) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: 'unknown',
|
||||
summary: 'Issue created successfully',
|
||||
success: true,
|
||||
url: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.parse(responseText)
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: data.key || 'unknown',
|
||||
summary: data.fields?.summary || 'Issue created',
|
||||
success: true,
|
||||
url: `https://${params?.domain}/browse/${data.key}`,
|
||||
},
|
||||
}
|
||||
} catch (_e) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: 'unknown',
|
||||
summary: 'Issue created (response parsing failed)',
|
||||
success: true,
|
||||
url: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
return error.message || 'Failed to create Jira issue'
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
name: 'Knowledge Create Document',
|
||||
description: 'Create a new document in a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
@@ -64,29 +65,6 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Information about the created document',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Document ID' },
|
||||
name: { type: 'string', description: 'Document name' },
|
||||
type: { type: 'string', description: 'Document type' },
|
||||
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Success or error message describing the operation result',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the created document',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents`,
|
||||
method: 'POST',
|
||||
@@ -168,94 +146,57 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
|
||||
return requestBody
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeCreateDocumentResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
const documentsCreated = data.documentsCreated || []
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error?.message || result.message || 'Failed to create document'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = result.data || result
|
||||
const documentsCreated = data.documentsCreated || []
|
||||
|
||||
// Handle multiple documents response
|
||||
const uploadCount = documentsCreated.length
|
||||
const firstDocument = documentsCreated[0]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: {
|
||||
id: firstDocument?.documentId || firstDocument?.id || '',
|
||||
name:
|
||||
uploadCount > 1 ? `${uploadCount} documents` : firstDocument?.filename || 'Unknown',
|
||||
type: 'document',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
enabled: true,
|
||||
},
|
||||
message:
|
||||
uploadCount > 1
|
||||
? `Successfully created ${uploadCount} documents in knowledge base`
|
||||
: `Successfully created document in knowledge base`,
|
||||
documentId: firstDocument?.documentId || firstDocument?.id || '',
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: {
|
||||
id: '',
|
||||
name: '',
|
||||
type: '',
|
||||
enabled: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
message: `Failed to create document: ${error.message || 'Unknown error'}`,
|
||||
documentId: '',
|
||||
},
|
||||
error: `Failed to create document: ${error.message || 'Unknown error'}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<KnowledgeCreateDocumentResponse> => {
|
||||
let errorMessage = 'Failed to create document'
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('Document name')) {
|
||||
errorMessage = `Document name error: ${error.message}`
|
||||
} else if (error.message.includes('Document content')) {
|
||||
errorMessage = `Document content error: ${error.message}`
|
||||
} else if (error.message.includes('invalid characters')) {
|
||||
errorMessage = `${error.message}. Please use a valid filename.`
|
||||
} else if (error.message.includes('maximum size')) {
|
||||
errorMessage = `${error.message}. Consider breaking large content into smaller documents.`
|
||||
} else {
|
||||
errorMessage = `Failed to create document: ${error.message}`
|
||||
}
|
||||
}
|
||||
// Handle multiple documents response
|
||||
const uploadCount = documentsCreated.length
|
||||
const firstDocument = documentsCreated[0]
|
||||
|
||||
return {
|
||||
success: false,
|
||||
success: true,
|
||||
output: {
|
||||
data: {
|
||||
id: '',
|
||||
name: '',
|
||||
type: '',
|
||||
id: firstDocument?.documentId || firstDocument?.id || '',
|
||||
name: uploadCount > 1 ? `${uploadCount} documents` : firstDocument?.filename || 'Unknown',
|
||||
type: 'document',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
enabled: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
message: errorMessage,
|
||||
documentId: '',
|
||||
message:
|
||||
uploadCount > 1
|
||||
? `Successfully created ${uploadCount} documents in knowledge base`
|
||||
: `Successfully created document in knowledge base`,
|
||||
documentId: firstDocument?.documentId || firstDocument?.id || '',
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Information about the created document',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Document ID' },
|
||||
name: { type: 'string', description: 'Document name' },
|
||||
type: { type: 'string', description: 'Document type' },
|
||||
enabled: { type: 'boolean', description: 'Whether the document is enabled' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Success or error message describing the operation result',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the created document',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
name: 'Knowledge Search',
|
||||
description: 'Search for similar content in a knowledge base using vector similarity',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
@@ -29,37 +30,6 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search results from the knowledge base',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
content: { type: 'string' },
|
||||
documentId: { type: 'string' },
|
||||
chunkIndex: { type: 'number' },
|
||||
similarity: { type: 'number' },
|
||||
metadata: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query that was executed',
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of results found',
|
||||
},
|
||||
cost: {
|
||||
type: 'object',
|
||||
description: 'Cost information for the search operation',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => '/api/knowledge/search',
|
||||
method: 'POST',
|
||||
@@ -117,53 +87,50 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
|
||||
return requestBody
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
transformResponse: async (response): Promise<KnowledgeSearchResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error || result.message || 'Failed to perform search'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = result.data || result
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results: data.results || [],
|
||||
query: data.query,
|
||||
totalResults: data.totalResults || 0,
|
||||
cost: data.cost,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
results: [],
|
||||
query: '',
|
||||
totalResults: 0,
|
||||
cost: undefined,
|
||||
},
|
||||
error: error.message || 'Failed to perform vector search',
|
||||
}
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<KnowledgeSearchResponse> => {
|
||||
const errorMessage = error.message || 'Failed to perform search'
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
|
||||
return {
|
||||
success: false,
|
||||
success: true,
|
||||
output: {
|
||||
results: [],
|
||||
query: '',
|
||||
totalResults: 0,
|
||||
cost: undefined,
|
||||
results: data.results || [],
|
||||
query: data.query,
|
||||
totalResults: data.totalResults || 0,
|
||||
cost: data.cost,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search results from the knowledge base',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
content: { type: 'string' },
|
||||
documentId: { type: 'string' },
|
||||
chunkIndex: { type: 'number' },
|
||||
similarity: { type: 'number' },
|
||||
metadata: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search query that was executed',
|
||||
},
|
||||
totalResults: {
|
||||
type: 'number',
|
||||
description: 'Total number of results found',
|
||||
},
|
||||
cost: {
|
||||
type: 'object',
|
||||
description: 'Cost information for the search operation',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
|
||||
name: 'Knowledge Upload Chunk',
|
||||
description: 'Upload a new chunk to a document in a knowledge base',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
@@ -24,6 +25,50 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const workflowId = params._context?.workflowId
|
||||
|
||||
const requestBody = {
|
||||
content: params.content,
|
||||
enabled: true,
|
||||
...(workflowId && { workflowId }),
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<KnowledgeUploadChunkResponse> => {
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: {
|
||||
id: data.id,
|
||||
chunkIndex: data.chunkIndex || 0,
|
||||
content: data.content,
|
||||
contentLength: data.contentLength || data.content?.length || 0,
|
||||
tokenCount: data.tokenCount || 0,
|
||||
enabled: data.enabled !== undefined ? data.enabled : true,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
},
|
||||
message: `Successfully uploaded chunk to document`,
|
||||
documentId: data.documentId,
|
||||
cost: data.cost,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: {
|
||||
type: 'object',
|
||||
@@ -53,96 +98,4 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const workflowId = params._context?.workflowId
|
||||
|
||||
const requestBody = {
|
||||
content: params.content,
|
||||
enabled: true,
|
||||
...(workflowId && { workflowId }),
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
transformResponse: async (response): Promise<KnowledgeUploadChunkResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error?.message || result.message || 'Failed to upload chunk'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = result.data || result
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: {
|
||||
id: data.id,
|
||||
chunkIndex: data.chunkIndex || 0,
|
||||
content: data.content,
|
||||
contentLength: data.contentLength || data.content?.length || 0,
|
||||
tokenCount: data.tokenCount || 0,
|
||||
enabled: data.enabled !== undefined ? data.enabled : true,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
},
|
||||
message: `Successfully uploaded chunk to document`,
|
||||
documentId: data.documentId,
|
||||
cost: data.cost,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: {
|
||||
id: '',
|
||||
chunkIndex: 0,
|
||||
content: '',
|
||||
contentLength: 0,
|
||||
tokenCount: 0,
|
||||
enabled: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
message: `Failed to upload chunk: ${error.message || 'Unknown error'}`,
|
||||
documentId: '',
|
||||
},
|
||||
error: `Failed to upload chunk: ${error.message || 'Unknown error'}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<KnowledgeUploadChunkResponse> => {
|
||||
const errorMessage = `Failed to upload chunk: ${error.message || 'Unknown error'}`
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
data: {
|
||||
id: '',
|
||||
chunkIndex: 0,
|
||||
content: '',
|
||||
contentLength: 0,
|
||||
tokenCount: 0,
|
||||
enabled: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
},
|
||||
message: errorMessage,
|
||||
documentId: '',
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
|
||||
name: 'Linear Issue Writer',
|
||||
description: 'Create a new issue in Linear',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'linear',
|
||||
},
|
||||
|
||||
params: {
|
||||
teamId: {
|
||||
type: 'string',
|
||||
@@ -38,21 +40,6 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
issue: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The created issue containing id, title, description, state, teamId, and projectId',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Issue ID' },
|
||||
title: { type: 'string', description: 'Issue title' },
|
||||
description: { type: 'string', description: 'Issue description' },
|
||||
state: { type: 'string', description: 'Issue state' },
|
||||
teamId: { type: 'string', description: 'Team ID' },
|
||||
projectId: { type: 'string', description: 'Project ID' },
|
||||
},
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.linear.app/graphql',
|
||||
method: 'POST',
|
||||
@@ -100,12 +87,10 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const issue = data.data.issueCreate.issue
|
||||
if (!issue) {
|
||||
throw new Error('Failed to create issue: No issue returned from Linear API')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -120,20 +105,20 @@ export const linearCreateIssueTool: ToolConfig<LinearCreateIssueParams, LinearCr
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
return 'Failed to create Linear issue'
|
||||
outputs: {
|
||||
issue: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The created issue containing id, title, description, state, teamId, and projectId',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Issue ID' },
|
||||
title: { type: 'string', description: 'Issue title' },
|
||||
description: { type: 'string', description: 'Issue description' },
|
||||
state: { type: 'string', description: 'Issue state' },
|
||||
teamId: { type: 'string', description: 'Team ID' },
|
||||
projectId: { type: 'string', description: 'Project ID' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
|
||||
name: 'Linear Issue Reader',
|
||||
description: 'Fetch and filter issues from Linear',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'linear',
|
||||
},
|
||||
|
||||
params: {
|
||||
teamId: {
|
||||
type: 'string',
|
||||
@@ -29,24 +31,6 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
issues: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Issue ID' },
|
||||
title: { type: 'string', description: 'Issue title' },
|
||||
description: { type: 'string', description: 'Issue description' },
|
||||
state: { type: 'string', description: 'Issue state' },
|
||||
teamId: { type: 'string', description: 'Team ID' },
|
||||
projectId: { type: 'string', description: 'Project ID' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.linear.app/graphql',
|
||||
method: 'POST',
|
||||
@@ -85,15 +69,9 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
if (data.errors) {
|
||||
return {
|
||||
success: false,
|
||||
output: { issues: [] },
|
||||
error: data.errors.map((e: any) => e.message).join('; '),
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -108,23 +86,23 @@ export const linearReadIssuesTool: ToolConfig<LinearReadIssuesParams, LinearRead
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: (error) => {
|
||||
// If it's an Error instance with a message, use that
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
// If it's an object with an error or message property
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
return typeof error.error === 'string' ? error.error : JSON.stringify(error.error)
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback message
|
||||
return 'Failed to fetch Linear issues'
|
||||
outputs: {
|
||||
issues: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Issue ID' },
|
||||
title: { type: 'string', description: 'Issue title' },
|
||||
description: { type: 'string', description: 'Issue description' },
|
||||
state: { type: 'string', description: 'Issue state' },
|
||||
teamId: { type: 'string', description: 'Team ID' },
|
||||
projectId: { type: 'string', description: 'Project ID' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -38,18 +38,6 @@ export const searchTool: ToolConfig<LinkupSearchParams, LinkupSearchToolResponse
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'The sourced answer to the search query',
|
||||
},
|
||||
sources: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of sources used to compile the answer, each containing name, url, and snippet',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.linkup.so/v1/search',
|
||||
method: 'POST',
|
||||
@@ -71,11 +59,6 @@ export const searchTool: ToolConfig<LinkupSearchParams, LinkupSearchToolResponse
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Linkup API error: ${response.status} ${errorText}`)
|
||||
}
|
||||
|
||||
const data: LinkupSearchResponse = await response.json()
|
||||
|
||||
return {
|
||||
@@ -87,7 +70,15 @@ export const searchTool: ToolConfig<LinkupSearchParams, LinkupSearchToolResponse
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
return `Error searching with Linkup: ${error.message}`
|
||||
outputs: {
|
||||
answer: {
|
||||
type: 'string',
|
||||
description: 'The sourced answer to the search query',
|
||||
},
|
||||
sources: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of sources used to compile the answer, each containing name, url, and snippet',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const mem0AddMemoriesTool: ToolConfig = {
|
||||
name: 'Add Memories',
|
||||
description: 'Add memories to Mem0 for persistent storage and retrieval',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
@@ -27,16 +28,6 @@ export const mem0AddMemoriesTool: ToolConfig = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of memory IDs that were created',
|
||||
},
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of memory objects that were created',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.mem0.ai/v1/memories/',
|
||||
method: 'POST',
|
||||
@@ -76,6 +67,7 @@ export const mem0AddMemoriesTool: ToolConfig = {
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
@@ -122,13 +114,15 @@ export const mem0AddMemoriesTool: ToolConfig = {
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: async (error) => {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
ids: [],
|
||||
memories: [],
|
||||
},
|
||||
}
|
||||
|
||||
outputs: {
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of memory IDs that were created',
|
||||
},
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of memory objects that were created',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const mem0GetMemoriesTool: ToolConfig = {
|
||||
name: 'Get Memories',
|
||||
description: 'Retrieve memories from Mem0 by ID or filter criteria',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
@@ -46,16 +47,6 @@ export const mem0GetMemoriesTool: ToolConfig = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of retrieved memory objects',
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of memory IDs that were retrieved',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params: Record<string, any>) => {
|
||||
// For a specific memory ID, use the get single memory endpoint
|
||||
@@ -113,43 +104,29 @@ export const mem0GetMemoriesTool: ToolConfig = {
|
||||
return body
|
||||
},
|
||||
},
|
||||
transformResponse: async (response, params) => {
|
||||
try {
|
||||
// Get raw response for debugging
|
||||
const responseText = await response.clone().text()
|
||||
|
||||
// Parse the response
|
||||
const data = JSON.parse(responseText)
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const memories = Array.isArray(data) ? data : [data]
|
||||
const ids = memories.map((memory) => memory.id).filter(Boolean)
|
||||
|
||||
// Format the memories for display
|
||||
const memories = Array.isArray(data) ? data : [data]
|
||||
|
||||
// Extract IDs if available
|
||||
const ids = memories.map((memory) => memory.id).filter(Boolean)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories,
|
||||
ids,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
error: `Failed to process get memories response: ${error.message}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
transformError: async (error) => {
|
||||
return {
|
||||
success: false,
|
||||
success: true,
|
||||
output: {
|
||||
ids: [],
|
||||
memories: [],
|
||||
memories,
|
||||
ids,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of retrieved memory objects',
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of memory IDs that were retrieved',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
|
||||
name: 'Search Memories',
|
||||
description: 'Search for memories in Mem0 using semantic search',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
@@ -35,16 +36,6 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
searchResults: {
|
||||
type: 'array',
|
||||
description: 'Array of search results with memory data, each containing id, data, and score',
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of memory IDs found in the search results',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: 'https://api.mem0.ai/v2/memories/search/',
|
||||
method: 'POST',
|
||||
@@ -65,67 +56,54 @@ export const mem0SearchMemoriesTool: ToolConfig<any, Mem0Response> = {
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
try {
|
||||
// Get raw response for debugging
|
||||
const responseText = await response.clone().text()
|
||||
const data = await response.json()
|
||||
|
||||
// Parse the response
|
||||
const data = JSON.parse(responseText)
|
||||
|
||||
// Handle empty results
|
||||
if (!data || (Array.isArray(data) && data.length === 0)) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
searchResults: [],
|
||||
ids: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// For array results (standard format)
|
||||
if (Array.isArray(data)) {
|
||||
const searchResults = data.map((item) => ({
|
||||
id: item.id,
|
||||
data: { memory: item.memory || '' },
|
||||
score: item.score || 0,
|
||||
}))
|
||||
|
||||
const ids = data.map((item) => item.id).filter(Boolean)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
searchResults,
|
||||
ids,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for unexpected response format
|
||||
if (!data || (Array.isArray(data) && data.length === 0)) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
searchResults: [],
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
error: `Failed to process search response: ${error.message}`,
|
||||
ids: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
transformError: async (error) => {
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
const searchResults = data.map((item) => ({
|
||||
id: item.id,
|
||||
data: { memory: item.memory || '' },
|
||||
score: item.score || 0,
|
||||
}))
|
||||
|
||||
const ids = data.map((item) => item.id).filter(Boolean)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
searchResults,
|
||||
ids,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
success: true,
|
||||
output: {
|
||||
ids: [],
|
||||
searchResults: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
searchResults: {
|
||||
type: 'array',
|
||||
description: 'Array of search results with memory data, each containing id, data, and score',
|
||||
},
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of memory IDs found in the search results',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
name: 'Add Memory',
|
||||
description: 'Add a new memory to the database or append to existing memory with the same ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
id: {
|
||||
type: 'string',
|
||||
@@ -24,14 +25,7 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
description: 'Content for agent memory',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the memory was added successfully' },
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of memory objects including the new or updated memory',
|
||||
},
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/memory',
|
||||
method: 'POST',
|
||||
@@ -85,44 +79,29 @@ export const memoryAddTool: ToolConfig<any, MemoryResponse> = {
|
||||
|
||||
return body
|
||||
},
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<MemoryResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
const errorMessage = result.error?.message || 'Failed to add memory'
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
|
||||
const data = result.data || result
|
||||
// For agent memories, return the full array of message objects
|
||||
const memories = Array.isArray(data.data) ? data.data : [data.data]
|
||||
|
||||
// For agent memories, return the full array of message objects
|
||||
const memories = Array.isArray(data.data) ? data.data : [data.data]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
memories: undefined,
|
||||
},
|
||||
error,
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories,
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<MemoryResponse> => {
|
||||
const errorMessage = `Memory operation failed: ${error.message || 'Unknown error occurred'}`
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
memories: undefined,
|
||||
message: `Memory operation failed: ${error.message || 'Unknown error occurred'}`,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the memory was added successfully' },
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of memory objects including the new or updated memory',
|
||||
},
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
name: 'Delete Memory',
|
||||
description: 'Delete a specific memory by its ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
id: {
|
||||
type: 'string',
|
||||
@@ -13,11 +14,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
description: 'Identifier for the memory to delete',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the memory was deleted successfully' },
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params): any => {
|
||||
// Get workflowId from context (set by workflow execution)
|
||||
@@ -44,41 +41,21 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
isInternalRoute: true,
|
||||
},
|
||||
transformResponse: async (response): Promise<MemoryResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error?.message || 'Failed to delete memory'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: 'Memory deleted successfully.',
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: `Failed to delete memory: ${error.message || 'Unknown error'}`,
|
||||
},
|
||||
error: `Failed to delete memory: ${error.message || 'Unknown error'}`,
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: 'Memory deleted successfully.',
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<MemoryResponse> => {
|
||||
const errorMessage = `Memory deletion failed: ${error.message || 'Unknown error'}`
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the memory was deleted successfully' },
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
name: 'Get Memory',
|
||||
description: 'Retrieve a specific memory by its ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
id: {
|
||||
type: 'string',
|
||||
@@ -13,12 +14,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
description: 'Identifier for the memory to retrieve',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the memory was retrieved successfully' },
|
||||
memories: { type: 'array', description: 'Array of memory data for the requested ID' },
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params): any => {
|
||||
// Get workflowId from context (set by workflow execution)
|
||||
@@ -45,46 +41,25 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<MemoryResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
const result = await response.json()
|
||||
const data = result.data || result
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error?.message || 'Failed to retrieve memory'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = result.data || result
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories: data.data,
|
||||
message: 'Memory retrieved successfully',
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
memories: undefined,
|
||||
message: `Failed to retrieve memory: ${error.message || 'Unknown error'}`,
|
||||
},
|
||||
error: `Failed to retrieve memory: ${error.message || 'Unknown error'}`,
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories: data.data,
|
||||
message: 'Memory retrieved successfully',
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<MemoryResponse> => {
|
||||
const errorMessage = `Memory retrieval failed: ${error.message || 'Unknown error'}`
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
memories: undefined,
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the memory was retrieved successfully' },
|
||||
memories: { type: 'array', description: 'Array of memory data for the requested ID' },
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,16 +6,9 @@ export const memoryGetAllTool: ToolConfig<any, MemoryResponse> = {
|
||||
name: 'Get All Memories',
|
||||
description: 'Retrieve all memories from the database',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether all memories were retrieved successfully' },
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of all memory objects with keys, types, and data',
|
||||
},
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params): any => {
|
||||
// Get workflowId from context (set by workflow execution)
|
||||
@@ -42,55 +35,38 @@ export const memoryGetAllTool: ToolConfig<any, MemoryResponse> = {
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
isInternalRoute: true,
|
||||
},
|
||||
|
||||
transformResponse: async (response): Promise<MemoryResponse> => {
|
||||
try {
|
||||
const result = await response.json()
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = result.error?.message || 'Failed to retrieve memories'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
// Extract memories from the response
|
||||
const data = result.data || result
|
||||
const rawMemories = data.memories || data || []
|
||||
|
||||
// Extract memories from the response
|
||||
const data = result.data || result
|
||||
const rawMemories = data.memories || data || []
|
||||
// Transform memories to return them with their keys and types for better context
|
||||
const memories = rawMemories.map((memory: any) => ({
|
||||
key: memory.key,
|
||||
type: memory.type,
|
||||
data: memory.data,
|
||||
}))
|
||||
|
||||
// Transform memories to return them with their keys and types for better context
|
||||
const memories = rawMemories.map((memory: any) => ({
|
||||
key: memory.key,
|
||||
type: memory.type,
|
||||
data: memory.data,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories,
|
||||
message: 'Memories retrieved successfully',
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
memories: [],
|
||||
message: `Failed to retrieve memories: ${error.message || 'Unknown error'}`,
|
||||
},
|
||||
error: `Failed to retrieve memories: ${error.message || 'Unknown error'}`,
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
memories,
|
||||
message: 'Memories retrieved successfully',
|
||||
},
|
||||
}
|
||||
},
|
||||
transformError: async (error): Promise<MemoryResponse> => {
|
||||
const errorMessage = `Memory retrieval failed: ${error.message || 'Unknown error'}`
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
memories: [],
|
||||
message: errorMessage,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether all memories were retrieved successfully' },
|
||||
memories: {
|
||||
type: 'array',
|
||||
description: 'Array of all memory objects with keys, types, and data',
|
||||
},
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
error: { type: 'string', description: 'Error message if operation failed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
||||
name: 'Read from Microsoft Excel',
|
||||
description: 'Read data from a Microsoft Excel spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'microsoft-excel',
|
||||
additionalScopes: [],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
@@ -34,31 +36,7 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
||||
description: 'The range of cells to read from',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Excel spreadsheet data and metadata',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Range data from the spreadsheet',
|
||||
properties: {
|
||||
range: { type: 'string', description: 'The range that was read' },
|
||||
values: { type: 'array', description: 'Array of rows containing cell values' },
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Spreadsheet metadata',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
|
||||
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
@@ -93,16 +71,8 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
||||
}
|
||||
},
|
||||
},
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json().catch(() => ({ error: response.statusText }))
|
||||
const errorText =
|
||||
errorJson.error && typeof errorJson.error === 'object'
|
||||
? errorJson.error.message || JSON.stringify(errorJson.error)
|
||||
: errorJson.error || response.statusText
|
||||
throw new Error(`Failed to read Microsoft Excel data: ${errorText}`)
|
||||
}
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const urlParts = response.url.split('/drive/items/')
|
||||
@@ -130,33 +100,30 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
||||
|
||||
return result
|
||||
},
|
||||
transformError: (error) => {
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if (error.error) {
|
||||
if (typeof error.error === 'string') {
|
||||
return error.error
|
||||
}
|
||||
if (typeof error.error === 'object' && error.error.message) {
|
||||
return error.error.message
|
||||
}
|
||||
return JSON.stringify(error.error)
|
||||
}
|
||||
|
||||
if (error.message) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
try {
|
||||
return `Microsoft Excel API error: ${JSON.stringify(error)}`
|
||||
} catch (_e) {
|
||||
return 'Microsoft Excel API error: Unable to parse error details'
|
||||
}
|
||||
}
|
||||
|
||||
return 'An error occurred while reading from Microsoft Excel'
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Excel spreadsheet data and metadata',
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Range data from the spreadsheet',
|
||||
properties: {
|
||||
range: { type: 'string', description: 'The range that was read' },
|
||||
values: { type: 'array', description: 'Array of rows containing cell values' },
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Spreadsheet metadata',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
|
||||
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user