mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-11 16:08:04 -05:00
Compare commits
5 Commits
feat/custo
...
fix/chat-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f67caf0798 | ||
|
|
0b853a7d95 | ||
|
|
4f31560a0e | ||
|
|
8b5027f2a6 | ||
|
|
c6c658a6e1 |
@@ -80,6 +80,10 @@ export function VoiceInterface({
|
||||
const currentStateRef = useRef<'idle' | 'listening' | 'agent_speaking'>('idle')
|
||||
const isCallEndedRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
isCallEndedRef.current = false
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
currentStateRef.current = state
|
||||
}, [state])
|
||||
@@ -119,6 +123,8 @@ export function VoiceInterface({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
if (isPlayingAudio && state !== 'agent_speaking') {
|
||||
clearResponseTimeout()
|
||||
setState('agent_speaking')
|
||||
@@ -139,6 +145,9 @@ export function VoiceInterface({
|
||||
}
|
||||
}
|
||||
} else if (!isPlayingAudio && state === 'agent_speaking') {
|
||||
// Don't unmute/restart if call has ended
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
setState('idle')
|
||||
setCurrentTranscript('')
|
||||
|
||||
@@ -226,6 +235,8 @@ export function VoiceInterface({
|
||||
recognition.onstart = () => {}
|
||||
|
||||
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
const currentState = currentStateRef.current
|
||||
|
||||
if (isMutedRef.current || currentState !== 'listening') {
|
||||
@@ -303,6 +314,8 @@ export function VoiceInterface({
|
||||
}, [isSupported, onVoiceTranscript, setResponseTimeout])
|
||||
|
||||
const startListening = useCallback(() => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
if (!isInitialized || isMuted || state !== 'idle') {
|
||||
return
|
||||
}
|
||||
@@ -320,6 +333,9 @@ export function VoiceInterface({
|
||||
}, [isInitialized, isMuted, state])
|
||||
|
||||
const stopListening = useCallback(() => {
|
||||
// Don't process if call has ended
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
setState('idle')
|
||||
setCurrentTranscript('')
|
||||
|
||||
@@ -333,12 +349,15 @@ export function VoiceInterface({
|
||||
}, [])
|
||||
|
||||
const handleInterrupt = useCallback(() => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
if (state === 'agent_speaking') {
|
||||
onInterrupt?.()
|
||||
setState('listening')
|
||||
setCurrentTranscript('')
|
||||
|
||||
setIsMuted(false)
|
||||
isMutedRef.current = false
|
||||
if (mediaStreamRef.current) {
|
||||
mediaStreamRef.current.getAudioTracks().forEach((track) => {
|
||||
track.enabled = true
|
||||
@@ -356,11 +375,22 @@ export function VoiceInterface({
|
||||
}, [state, onInterrupt])
|
||||
|
||||
const handleCallEnd = useCallback(() => {
|
||||
// Mark call as ended FIRST to prevent any effects from restarting recognition
|
||||
isCallEndedRef.current = true
|
||||
|
||||
// Set muted to true to prevent auto-start effect from triggering
|
||||
setIsMuted(true)
|
||||
isMutedRef.current = true
|
||||
|
||||
setState('idle')
|
||||
setCurrentTranscript('')
|
||||
setIsMuted(false)
|
||||
|
||||
// Immediately disable audio tracks to stop listening
|
||||
if (mediaStreamRef.current) {
|
||||
mediaStreamRef.current.getAudioTracks().forEach((track) => {
|
||||
track.enabled = false
|
||||
})
|
||||
}
|
||||
|
||||
if (recognitionRef.current) {
|
||||
try {
|
||||
@@ -377,6 +407,8 @@ export function VoiceInterface({
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
if (event.code === 'Space') {
|
||||
event.preventDefault()
|
||||
handleInterrupt()
|
||||
@@ -388,6 +420,8 @@ export function VoiceInterface({
|
||||
}, [handleInterrupt])
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
if (state === 'agent_speaking') {
|
||||
handleInterrupt()
|
||||
return
|
||||
@@ -395,6 +429,7 @@ export function VoiceInterface({
|
||||
|
||||
const newMutedState = !isMuted
|
||||
setIsMuted(newMutedState)
|
||||
isMutedRef.current = newMutedState
|
||||
|
||||
if (mediaStreamRef.current) {
|
||||
mediaStreamRef.current.getAudioTracks().forEach((track) => {
|
||||
@@ -417,6 +452,8 @@ export function VoiceInterface({
|
||||
}, [isSupported, setupSpeechRecognition, setupAudio])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCallEndedRef.current) return
|
||||
|
||||
if (isInitialized && !isMuted && state === 'idle') {
|
||||
startListening()
|
||||
}
|
||||
|
||||
@@ -41,16 +41,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
],
|
||||
placeholder: 'Select Airtable account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'baseId',
|
||||
@@ -134,8 +124,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, records, fields, ...rest } = params
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, records, fields, ...rest } = params
|
||||
let parsedRecords: any | undefined
|
||||
let parsedFields: any | undefined
|
||||
|
||||
@@ -153,7 +142,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
|
||||
// Construct parameters based on operation
|
||||
const baseParams = {
|
||||
...authParam,
|
||||
credential,
|
||||
...rest,
|
||||
}
|
||||
|
||||
|
||||
@@ -32,20 +32,11 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
id: 'credential',
|
||||
title: 'Asana Account',
|
||||
type: 'oauth-input',
|
||||
|
||||
required: true,
|
||||
serviceId: 'asana',
|
||||
requiredScopes: ['default'],
|
||||
placeholder: 'Select Asana account',
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'workspace',
|
||||
@@ -211,7 +202,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, operation } = params
|
||||
const { credential, operation } = params
|
||||
|
||||
const projectsArray = params.projects
|
||||
? params.projects
|
||||
@@ -220,12 +211,14 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
.filter((p: string) => p.length > 0)
|
||||
: undefined
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const baseParams = {
|
||||
accessToken: credential?.accessToken,
|
||||
}
|
||||
|
||||
switch (operation) {
|
||||
case 'get_task':
|
||||
return {
|
||||
...authParam,
|
||||
...baseParams,
|
||||
taskGid: params.taskGid,
|
||||
workspace: params.getTasks_workspace,
|
||||
project: params.getTasks_project,
|
||||
@@ -233,7 +226,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
}
|
||||
case 'create_task':
|
||||
return {
|
||||
...authParam,
|
||||
...baseParams,
|
||||
workspace: params.workspace,
|
||||
name: params.name,
|
||||
notes: params.notes,
|
||||
@@ -242,7 +235,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
}
|
||||
case 'update_task':
|
||||
return {
|
||||
...authParam,
|
||||
...baseParams,
|
||||
taskGid: params.taskGid,
|
||||
name: params.name,
|
||||
notes: params.notes,
|
||||
@@ -252,12 +245,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
}
|
||||
case 'get_projects':
|
||||
return {
|
||||
...authParam,
|
||||
...baseParams,
|
||||
workspace: params.workspace,
|
||||
}
|
||||
case 'search_tasks':
|
||||
return {
|
||||
...authParam,
|
||||
...baseParams,
|
||||
workspace: params.workspace,
|
||||
text: params.searchText,
|
||||
assignee: params.assignee,
|
||||
@@ -266,12 +259,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
}
|
||||
case 'add_comment':
|
||||
return {
|
||||
...authParam,
|
||||
...baseParams,
|
||||
taskGid: params.taskGid,
|
||||
text: params.commentText,
|
||||
}
|
||||
default:
|
||||
return authParam
|
||||
return baseParams
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,16 +76,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
],
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'pageId',
|
||||
@@ -265,7 +255,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
pageId,
|
||||
manualPageId,
|
||||
operation,
|
||||
@@ -275,7 +264,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const effectivePageId = (pageId || manualPageId || '').trim()
|
||||
|
||||
const requiresPageId = [
|
||||
@@ -301,7 +289,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
|
||||
if (operation === 'upload_attachment') {
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
pageId: effectivePageId,
|
||||
operation,
|
||||
file: attachmentFile,
|
||||
@@ -312,7 +300,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
}
|
||||
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
pageId: effectivePageId || undefined,
|
||||
operation,
|
||||
...rest,
|
||||
|
||||
@@ -49,16 +49,6 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
|
||||
],
|
||||
placeholder: 'Select Dropbox account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Upload operation inputs
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
],
|
||||
value: () => 'send_gmail',
|
||||
},
|
||||
// Gmail Credentials (basic mode)
|
||||
// Gmail Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Gmail Account',
|
||||
@@ -51,17 +51,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
],
|
||||
placeholder: 'Select Gmail account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Send Email Fields
|
||||
{
|
||||
@@ -388,7 +377,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
folder,
|
||||
manualFolder,
|
||||
destinationLabel,
|
||||
@@ -449,7 +437,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,7 +28,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
],
|
||||
value: () => 'create',
|
||||
},
|
||||
// Google Calendar Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Calendar Account',
|
||||
@@ -37,17 +36,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
serviceId: 'google-calendar',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
placeholder: 'Select Google Calendar account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Calendar selector (basic mode)
|
||||
{
|
||||
@@ -61,7 +49,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual calendar ID input (advanced mode) - no dependsOn needed for text input
|
||||
// Manual calendar ID input (advanced mode)
|
||||
{
|
||||
id: 'manualCalendarId',
|
||||
title: 'Calendar ID',
|
||||
@@ -225,7 +213,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
operation,
|
||||
attendees,
|
||||
replaceExisting,
|
||||
@@ -266,7 +253,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
}
|
||||
|
||||
return {
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
...processedParams,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Google Docs Credentials (basic mode)
|
||||
// Google Docs Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
@@ -39,17 +39,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Document selector (basic mode)
|
||||
{
|
||||
@@ -72,6 +61,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'documentId',
|
||||
placeholder: 'Enter document ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read', 'write'] },
|
||||
},
|
||||
@@ -105,6 +95,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
},
|
||||
@@ -142,15 +133,8 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
documentId,
|
||||
manualDocumentId,
|
||||
folderSelector,
|
||||
folderId,
|
||||
...rest
|
||||
} = params
|
||||
const { credential, documentId, manualDocumentId, folderSelector, folderId, ...rest } =
|
||||
params
|
||||
|
||||
const effectiveDocumentId = (documentId || manualDocumentId || '').trim()
|
||||
const effectiveFolderId = (folderSelector || folderId || '').trim()
|
||||
@@ -159,7 +143,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
...rest,
|
||||
documentId: effectiveDocumentId || undefined,
|
||||
folderId: effectiveFolderId || undefined,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
],
|
||||
value: () => 'create_folder',
|
||||
},
|
||||
// Google Drive Credentials (basic mode)
|
||||
// Google Drive Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Drive Account',
|
||||
@@ -40,17 +40,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google Drive account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Create/Upload File Fields
|
||||
{
|
||||
@@ -335,7 +324,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
folderSelector,
|
||||
manualFolderId,
|
||||
fileSelector,
|
||||
@@ -351,7 +339,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
const effectiveFileId = (fileSelector || manualFileId || '').trim()
|
||||
|
||||
return {
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
folderId: effectiveFolderId || undefined,
|
||||
fileId: effectiveFileId || undefined,
|
||||
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
|
||||
|
||||
@@ -13,7 +13,6 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleFormsIcon,
|
||||
subBlocks: [
|
||||
// Google Forms Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
@@ -26,17 +25,6 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'formId',
|
||||
@@ -44,6 +32,7 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'Enter the Google Form ID',
|
||||
dependsOn: ['credential'],
|
||||
},
|
||||
{
|
||||
id: 'responseId',
|
||||
@@ -64,7 +53,7 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
config: {
|
||||
tool: () => 'google_forms_get_responses',
|
||||
params: (params) => {
|
||||
const { credential, accessToken, formId, responseId, pageSize, ...rest } = params
|
||||
const { credential, formId, responseId, pageSize, ...rest } = params
|
||||
|
||||
const effectiveFormId = String(formId || '').trim()
|
||||
if (!effectiveFormId) {
|
||||
@@ -76,7 +65,7 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
formId: effectiveFormId,
|
||||
responseId: responseId ? String(responseId).trim() : undefined,
|
||||
pageSize: pageSize ? Number(pageSize) : undefined,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,7 +33,6 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
],
|
||||
value: () => 'list_groups',
|
||||
},
|
||||
// Google Groups Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Groups Account',
|
||||
@@ -45,17 +44,6 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member',
|
||||
],
|
||||
placeholder: 'Select Google Workspace account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -233,13 +221,12 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, operation, ...rest } = params
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, operation, ...rest } = params
|
||||
|
||||
switch (operation) {
|
||||
case 'list_groups':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
customer: rest.customer,
|
||||
domain: rest.domain,
|
||||
query: rest.query,
|
||||
@@ -248,19 +235,19 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
case 'get_group':
|
||||
case 'delete_group':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
}
|
||||
case 'create_group':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
email: rest.email,
|
||||
name: rest.name,
|
||||
description: rest.description,
|
||||
}
|
||||
case 'update_group':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
name: rest.newName,
|
||||
email: rest.newEmail,
|
||||
@@ -268,7 +255,7 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
}
|
||||
case 'list_members':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
maxResults: rest.maxResults ? Number(rest.maxResults) : undefined,
|
||||
roles: rest.roles,
|
||||
@@ -276,32 +263,32 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
case 'get_member':
|
||||
case 'remove_member':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
memberKey: rest.memberKey,
|
||||
}
|
||||
case 'add_member':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
email: rest.memberEmail,
|
||||
role: rest.role,
|
||||
}
|
||||
case 'update_member':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
memberKey: rest.memberKey,
|
||||
role: rest.role,
|
||||
}
|
||||
case 'has_member':
|
||||
return {
|
||||
...authParam,
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
memberKey: rest.memberKey,
|
||||
}
|
||||
default:
|
||||
return { ...(credential ? { credential } : { accessToken }), ...rest }
|
||||
return { credential, ...rest }
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Google Sheets Credentials (basic mode)
|
||||
// Google Sheets Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
@@ -40,17 +40,6 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Spreadsheet Selector
|
||||
{
|
||||
@@ -75,6 +64,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'ID of the spreadsheet (from URL)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Range
|
||||
@@ -178,8 +168,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, values, spreadsheetId, manualSpreadsheetId, ...rest } =
|
||||
params
|
||||
const { credential, values, spreadsheetId, manualSpreadsheetId, ...rest } = params
|
||||
|
||||
const parsedValues = values ? JSON.parse(values as string) : undefined
|
||||
|
||||
@@ -193,7 +182,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
values: parsedValues,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Google Slides Credentials (basic mode)
|
||||
// Google Slides Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
@@ -43,17 +43,6 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Presentation selector (basic mode) - for operations that need an existing presentation
|
||||
{
|
||||
@@ -79,6 +68,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'presentationId',
|
||||
placeholder: 'Enter presentation ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -133,6 +123,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
},
|
||||
@@ -325,7 +316,6 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
presentationId,
|
||||
manualPresentationId,
|
||||
folderSelector,
|
||||
@@ -344,7 +334,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
const result: Record<string, any> = {
|
||||
...rest,
|
||||
presentationId: effectivePresentationId || undefined,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
|
||||
// Handle operation-specific params
|
||||
|
||||
@@ -30,7 +30,6 @@ export const GoogleVaultBlock: BlockConfig = {
|
||||
value: () => 'list_matters_export',
|
||||
},
|
||||
|
||||
// Google Vault Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Vault Account',
|
||||
@@ -42,17 +41,6 @@ export const GoogleVaultBlock: BlockConfig = {
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
placeholder: 'Select Google Vault account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Create Hold inputs
|
||||
{
|
||||
@@ -230,10 +218,10 @@ export const GoogleVaultBlock: BlockConfig = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, ...rest } = params
|
||||
const { credential, ...rest } = params
|
||||
return {
|
||||
...rest,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,16 +67,6 @@ export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
|
||||
],
|
||||
placeholder: 'Select HubSpot account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'contactId',
|
||||
@@ -834,7 +824,6 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
operation,
|
||||
propertiesToSet,
|
||||
properties,
|
||||
@@ -845,9 +834,8 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const cleanParams: Record<string, any> = {
|
||||
...authParam,
|
||||
credential,
|
||||
}
|
||||
|
||||
const createUpdateOps = [
|
||||
|
||||
@@ -92,16 +92,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'delete:issue-link:jira',
|
||||
],
|
||||
placeholder: 'Select Jira account',
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Project selector (basic mode)
|
||||
{
|
||||
@@ -453,23 +443,14 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
projectId,
|
||||
manualProjectId,
|
||||
issueKey,
|
||||
manualIssueKey,
|
||||
...rest
|
||||
} = params
|
||||
const { credential, projectId, manualProjectId, issueKey, manualIssueKey, ...rest } = params
|
||||
|
||||
// Use the selected IDs or the manually entered ones
|
||||
const effectiveProjectId = (projectId || manualProjectId || '').trim()
|
||||
const effectiveIssueKey = (issueKey || manualIssueKey || '').trim()
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const baseParams = {
|
||||
...authParam,
|
||||
credential,
|
||||
domain: params.domain,
|
||||
}
|
||||
|
||||
|
||||
@@ -133,16 +133,6 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
requiredScopes: ['read', 'write'],
|
||||
placeholder: 'Select Linear account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Team selector (for most operations)
|
||||
{
|
||||
@@ -1271,14 +1261,9 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
const effectiveTeamId = (params.teamId || params.manualTeamId || '').trim()
|
||||
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
|
||||
|
||||
// Auth param handling
|
||||
const authParam = params.credential
|
||||
? { credential: params.credential }
|
||||
: { accessToken: params.accessToken }
|
||||
|
||||
// Base params that most operations need
|
||||
const baseParams: Record<string, any> = {
|
||||
...authParam,
|
||||
credential: params.credential,
|
||||
}
|
||||
|
||||
// Operation-specific param mapping
|
||||
|
||||
@@ -27,7 +27,7 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
|
||||
value: () => 'share_post',
|
||||
},
|
||||
|
||||
// LinkedIn OAuth Authentication (basic mode)
|
||||
// LinkedIn OAuth Authentication
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'LinkedIn Account',
|
||||
@@ -35,17 +35,6 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
|
||||
serviceId: 'linkedin',
|
||||
requiredScopes: ['profile', 'openid', 'email', 'w_member_social'],
|
||||
placeholder: 'Select LinkedIn account',
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -91,18 +80,18 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
|
||||
},
|
||||
params: (inputs) => {
|
||||
const operation = inputs.operation || 'share_post'
|
||||
const { credential, accessToken, ...rest } = inputs
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, ...rest } = inputs
|
||||
|
||||
if (operation === 'get_profile') {
|
||||
return authParam
|
||||
return {
|
||||
accessToken: credential,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: rest.text,
|
||||
visibility: rest.visibility || 'PUBLIC',
|
||||
...authParam,
|
||||
accessToken: credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,7 +27,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Microsoft Excel Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
@@ -43,17 +42,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'spreadsheetId',
|
||||
@@ -73,6 +61,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'Enter spreadsheet ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
@@ -171,7 +160,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
values,
|
||||
spreadsheetId,
|
||||
manualSpreadsheetId,
|
||||
@@ -205,7 +193,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
values: parsedValues,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
|
||||
if (params.operation === 'table_add') {
|
||||
|
||||
@@ -57,7 +57,6 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
{ label: 'Update Task Details', id: 'update_task_details' },
|
||||
],
|
||||
},
|
||||
// Microsoft Planner Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
@@ -73,17 +72,6 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Plan ID - for various operations
|
||||
@@ -354,7 +342,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
|
||||
const baseParams: MicrosoftPlannerBlockParams = {
|
||||
...rest,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
|
||||
// Handle different task ID fields
|
||||
|
||||
@@ -39,7 +39,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
],
|
||||
value: () => 'read_chat',
|
||||
},
|
||||
// Microsoft Teams Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
@@ -69,17 +68,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'teamId',
|
||||
@@ -327,7 +315,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
operation,
|
||||
teamId,
|
||||
manualTeamId,
|
||||
@@ -349,7 +336,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
|
||||
const baseParams: Record<string, any> = {
|
||||
...rest,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
|
||||
if ((operation === 'read_chat' || operation === 'read_channel') && includeAttachments) {
|
||||
|
||||
@@ -38,16 +38,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
requiredScopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
|
||||
placeholder: 'Select Notion account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Read/Write operation - Page ID
|
||||
{
|
||||
@@ -232,8 +222,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, operation, properties, filter, sorts, ...rest } = params
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, operation, properties, filter, sorts, ...rest } = params
|
||||
|
||||
// Parse properties from JSON string for create operations
|
||||
let parsedProperties
|
||||
@@ -276,7 +265,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...authParam,
|
||||
credential,
|
||||
...(parsedProperties ? { properties: parsedProperties } : {}),
|
||||
...(parsedFilter ? { filter: JSON.stringify(parsedFilter) } : {}),
|
||||
...(parsedSorts ? { sorts: JSON.stringify(parsedSorts) } : {}),
|
||||
|
||||
@@ -33,7 +33,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
{ label: 'Delete File', id: 'delete' },
|
||||
],
|
||||
},
|
||||
// OneDrive Credentials (basic mode)
|
||||
// One Drive Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
@@ -48,17 +48,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Create File Fields
|
||||
{
|
||||
@@ -175,6 +164,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_file', 'upload'] },
|
||||
},
|
||||
@@ -212,6 +202,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter parent folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_folder' },
|
||||
},
|
||||
@@ -243,6 +234,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'folderId',
|
||||
placeholder: 'Enter folder ID (leave empty for root folder)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
},
|
||||
@@ -360,16 +352,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
folderId,
|
||||
fileId,
|
||||
mimeType,
|
||||
values,
|
||||
downloadFileName,
|
||||
...rest
|
||||
} = params
|
||||
const { credential, folderId, fileId, mimeType, values, downloadFileName, ...rest } = params
|
||||
|
||||
let normalizedValues: ReturnType<typeof normalizeExcelValuesForToolParams>
|
||||
if (values !== undefined) {
|
||||
@@ -377,7 +360,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
}
|
||||
|
||||
return {
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
...rest,
|
||||
values: normalizedValues,
|
||||
folderId: folderId || undefined,
|
||||
|
||||
@@ -34,7 +34,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
],
|
||||
value: () => 'send_outlook',
|
||||
},
|
||||
// Microsoft Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
@@ -52,17 +51,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'to',
|
||||
@@ -338,7 +326,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
folder,
|
||||
manualFolder,
|
||||
destinationFolder,
|
||||
@@ -391,7 +378,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,16 +57,6 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
|
||||
],
|
||||
placeholder: 'Select Pipedrive account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
@@ -670,11 +660,10 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, operation, ...rest } = params
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, operation, ...rest } = params
|
||||
|
||||
const cleanParams: Record<string, any> = {
|
||||
...authParam,
|
||||
credential,
|
||||
}
|
||||
|
||||
Object.entries(rest).forEach(([key, value]) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
value: () => 'get_posts',
|
||||
},
|
||||
|
||||
// Reddit OAuth Authentication (basic mode)
|
||||
// Reddit OAuth Authentication
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Reddit Account',
|
||||
@@ -63,17 +63,6 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
],
|
||||
placeholder: 'Select Reddit account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Common fields - appear for all actions
|
||||
@@ -566,9 +555,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
},
|
||||
params: (inputs) => {
|
||||
const operation = inputs.operation || 'get_posts'
|
||||
const { credential, accessToken, ...rest } = inputs
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, ...rest } = inputs
|
||||
|
||||
if (operation === 'get_comments') {
|
||||
return {
|
||||
@@ -576,7 +563,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
subreddit: rest.subreddit,
|
||||
sort: rest.commentSort,
|
||||
limit: rest.commentLimit ? Number.parseInt(rest.commentLimit) : undefined,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +572,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
subreddit: rest.subreddit,
|
||||
time: rest.controversialTime,
|
||||
limit: rest.controversialLimit ? Number.parseInt(rest.controversialLimit) : undefined,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +583,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
sort: rest.searchSort,
|
||||
time: rest.searchTime,
|
||||
limit: rest.searchLimit ? Number.parseInt(rest.searchLimit) : undefined,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -608,7 +595,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
url: rest.postType === 'link' ? rest.url : undefined,
|
||||
nsfw: rest.nsfw === 'true',
|
||||
spoiler: rest.spoiler === 'true',
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,7 +603,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
return {
|
||||
id: rest.voteId,
|
||||
dir: Number.parseInt(rest.voteDirection),
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,14 +611,14 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
return {
|
||||
id: rest.saveId,
|
||||
category: rest.saveCategory,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'unsave') {
|
||||
return {
|
||||
id: rest.saveId,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,7 +626,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
return {
|
||||
parent_id: rest.replyParentId,
|
||||
text: rest.replyText,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,14 +634,14 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
return {
|
||||
thing_id: rest.editThingId,
|
||||
text: rest.editText,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
return {
|
||||
id: rest.deleteId,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +649,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
return {
|
||||
subreddit: rest.subscribeSubreddit,
|
||||
action: rest.subscribeAction,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +658,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
sort: rest.sort,
|
||||
limit: rest.limit ? Number.parseInt(rest.limit) : undefined,
|
||||
time: rest.sort === 'top' ? rest.time : undefined,
|
||||
...authParam,
|
||||
credential: credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,16 +66,6 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
requiredScopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
placeholder: 'Select Salesforce account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// Common fields for GET operations
|
||||
{
|
||||
@@ -600,9 +590,8 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, operation, ...rest } = params
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const cleanParams: Record<string, any> = { ...authParam }
|
||||
const { credential, operation, ...rest } = params
|
||||
const cleanParams: Record<string, any> = { credential }
|
||||
Object.entries(rest).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
cleanParams[key] = value
|
||||
|
||||
@@ -33,7 +33,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
{ label: 'Upload File', id: 'upload_file' },
|
||||
],
|
||||
},
|
||||
// SharePoint Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
@@ -49,17 +48,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -167,6 +155,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'siteId',
|
||||
placeholder: 'Enter site ID (leave empty for root site)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create_page' },
|
||||
},
|
||||
@@ -266,7 +255,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, siteSelector, manualSiteId, mimeType, ...rest } = params
|
||||
const { credential, siteSelector, manualSiteId, mimeType, ...rest } = params
|
||||
|
||||
const effectiveSiteId = (siteSelector || manualSiteId || '').trim()
|
||||
|
||||
@@ -326,7 +315,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
// Handle file upload files parameter
|
||||
const fileParam = uploadFiles || files
|
||||
const baseParams = {
|
||||
...(credential ? { credential } : { accessToken }),
|
||||
credential,
|
||||
siteId: effectiveSiteId || undefined,
|
||||
pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
|
||||
@@ -71,16 +71,6 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
|
||||
],
|
||||
placeholder: 'Select Shopify account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'shopDomain',
|
||||
@@ -536,11 +526,8 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
|
||||
return params.operation || 'shopify_list_products'
|
||||
},
|
||||
params: (params) => {
|
||||
const authParam = params.credential
|
||||
? { credential: params.credential }
|
||||
: { accessToken: params.accessToken }
|
||||
const baseParams: Record<string, unknown> = {
|
||||
...authParam,
|
||||
credential: params.credential,
|
||||
shopDomain: params.shopDomain?.trim(),
|
||||
}
|
||||
|
||||
|
||||
@@ -159,16 +159,6 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'oauth-input',
|
||||
serviceId: 'spotify',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// === SEARCH ===
|
||||
|
||||
@@ -45,16 +45,6 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
|
||||
requiredScopes: ['read', 'write'],
|
||||
placeholder: 'Select Trello account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -340,10 +330,9 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, operation, limit, closed, dueComplete, ...rest } = params
|
||||
const { operation, limit, closed, dueComplete, ...rest } = params
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const result: Record<string, any> = { ...rest, ...authParam }
|
||||
const result: Record<string, any> = { ...rest }
|
||||
|
||||
if (limit && operation === 'trello_get_actions') {
|
||||
result.limit = Number.parseInt(limit, 10)
|
||||
|
||||
@@ -37,16 +37,6 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
requiredScopes: ['login', 'data'],
|
||||
placeholder: 'Select Wealthbox account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'noteId',
|
||||
@@ -177,25 +167,16 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
accessToken,
|
||||
operation,
|
||||
contactId,
|
||||
manualContactId,
|
||||
taskId,
|
||||
manualTaskId,
|
||||
...rest
|
||||
} = params
|
||||
const { credential, operation, contactId, manualContactId, taskId, manualTaskId, ...rest } =
|
||||
params
|
||||
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
// Handle both selector and manual inputs
|
||||
const effectiveContactId = (contactId || manualContactId || '').trim()
|
||||
const effectiveTaskId = (taskId || manualTaskId || '').trim()
|
||||
|
||||
const baseParams = {
|
||||
...rest,
|
||||
...authParam,
|
||||
credential,
|
||||
}
|
||||
|
||||
if (operation === 'read_note' || operation === 'write_note') {
|
||||
|
||||
@@ -38,16 +38,6 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
|
||||
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
|
||||
placeholder: 'Select Webflow account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'collectionId',
|
||||
@@ -118,8 +108,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, fieldData, ...rest } = params
|
||||
const authParam = credential ? { credential } : { accessToken }
|
||||
const { credential, fieldData, ...rest } = params
|
||||
let parsedFieldData: any | undefined
|
||||
|
||||
try {
|
||||
@@ -131,7 +120,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
|
||||
}
|
||||
|
||||
const baseParams = {
|
||||
...authParam,
|
||||
credential,
|
||||
...rest,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
|
||||
value: () => 'wordpress_create_post',
|
||||
},
|
||||
|
||||
// Credential selector for OAuth (basic mode)
|
||||
// Credential selector for OAuth
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'WordPress Account',
|
||||
@@ -68,16 +68,6 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
|
||||
requiredScopes: ['global'],
|
||||
placeholder: 'Select WordPress account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Site ID for WordPress.com (required for OAuth)
|
||||
@@ -675,11 +665,8 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
|
||||
tool: (params) => params.operation || 'wordpress_create_post',
|
||||
params: (params) => {
|
||||
// OAuth authentication for WordPress.com
|
||||
const authParam = params.credential
|
||||
? { credential: params.credential }
|
||||
: { accessToken: params.accessToken }
|
||||
const baseParams: Record<string, any> = {
|
||||
...authParam,
|
||||
credential: params.credential,
|
||||
siteId: params.siteId,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ export const XBlock: BlockConfig<XResponse> = {
|
||||
],
|
||||
value: () => 'x_write',
|
||||
},
|
||||
// X Credentials (basic mode)
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'X Account',
|
||||
@@ -35,17 +34,6 @@ export const XBlock: BlockConfig<XResponse> = {
|
||||
serviceId: 'x',
|
||||
requiredScopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
|
||||
placeholder: 'Select X account',
|
||||
mode: 'basic',
|
||||
},
|
||||
// Direct access token (advanced mode)
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
@@ -155,9 +143,11 @@ export const XBlock: BlockConfig<XResponse> = {
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, accessToken, ...rest } = params
|
||||
const { credential, ...rest } = params
|
||||
|
||||
const parsedParams: Record<string, any> = credential ? { credential } : { accessToken }
|
||||
const parsedParams: Record<string, any> = {
|
||||
credential: credential,
|
||||
}
|
||||
|
||||
Object.keys(rest).forEach((key) => {
|
||||
const value = rest[key]
|
||||
|
||||
@@ -53,16 +53,6 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
|
||||
],
|
||||
placeholder: 'Select Zoom account',
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Access Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter OAuth access token',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
// User ID for create/list operations
|
||||
{
|
||||
@@ -376,11 +366,8 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
|
||||
return params.operation || 'zoom_create_meeting'
|
||||
},
|
||||
params: (params) => {
|
||||
const authParam = params.credential
|
||||
? { credential: params.credential }
|
||||
: { accessToken: params.accessToken }
|
||||
const baseParams: Record<string, any> = {
|
||||
...authParam,
|
||||
credential: params.credential,
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
|
||||
@@ -987,18 +987,21 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
try {
|
||||
const executionData = JSON.parse(executionDataHeader)
|
||||
|
||||
// If execution data contains full content, persist to memory
|
||||
if (ctx && inputs && executionData.output?.content) {
|
||||
const assistantMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: executionData.output.content,
|
||||
}
|
||||
// Fire and forget - don't await
|
||||
memoryService
|
||||
.persistMemoryMessage(ctx, inputs, assistantMessage, block.id)
|
||||
.catch((error) =>
|
||||
logger.error('Failed to persist streaming response to memory:', error)
|
||||
// If execution data contains content or tool calls, persist to memory
|
||||
if (
|
||||
ctx &&
|
||||
inputs &&
|
||||
(executionData.output?.content || executionData.output?.toolCalls?.list?.length)
|
||||
) {
|
||||
const toolCalls = executionData.output?.toolCalls?.list
|
||||
const messages = this.buildMessagesForMemory(executionData.output.content, toolCalls)
|
||||
|
||||
// Fire and forget - don't await, persist all messages
|
||||
Promise.all(
|
||||
messages.map((message) =>
|
||||
memoryService.persistMemoryMessage(ctx, inputs, message, block.id)
|
||||
)
|
||||
).catch((error) => logger.error('Failed to persist streaming response to memory:', error))
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1117,25 +1120,28 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract content from regular response
|
||||
// Extract content and tool calls from regular response
|
||||
const blockOutput = result as any
|
||||
const content = blockOutput?.content
|
||||
const toolCalls = blockOutput?.toolCalls?.list
|
||||
|
||||
if (!content || typeof content !== 'string') {
|
||||
// Build messages to persist
|
||||
const messages = this.buildMessagesForMemory(content, toolCalls)
|
||||
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const assistantMessage: Message = {
|
||||
role: 'assistant',
|
||||
content,
|
||||
// Persist all messages
|
||||
for (const message of messages) {
|
||||
await memoryService.persistMemoryMessage(ctx, inputs, message, blockId)
|
||||
}
|
||||
|
||||
await memoryService.persistMemoryMessage(ctx, inputs, assistantMessage, blockId)
|
||||
|
||||
logger.debug('Persisted assistant response to memory', {
|
||||
workflowId: ctx.workflowId,
|
||||
memoryType: inputs.memoryType,
|
||||
conversationId: inputs.conversationId,
|
||||
messageCount: messages.length,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist response to memory:', error)
|
||||
@@ -1143,6 +1149,69 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds messages for memory storage including tool calls and results
|
||||
* Returns proper OpenAI-compatible message format:
|
||||
* - Assistant message with tool_calls array (if tools were used)
|
||||
* - Tool role messages with results (one per tool call)
|
||||
* - Final assistant message with content (if present)
|
||||
*/
|
||||
private buildMessagesForMemory(
|
||||
content: string | undefined,
|
||||
toolCalls: any[] | undefined
|
||||
): Message[] {
|
||||
const messages: Message[] = []
|
||||
|
||||
if (toolCalls?.length) {
|
||||
// Generate stable IDs for each tool call (only if not provided by provider)
|
||||
// Use index to ensure uniqueness even for same tool name in same millisecond
|
||||
const toolCallsWithIds = toolCalls.map((tc: any, index: number) => ({
|
||||
...tc,
|
||||
_stableId:
|
||||
tc.id ||
|
||||
`call_${tc.name}_${Date.now()}_${index}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
}))
|
||||
|
||||
// Add assistant message with tool_calls
|
||||
const formattedToolCalls = toolCallsWithIds.map((tc: any) => ({
|
||||
id: tc._stableId,
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: tc.name,
|
||||
arguments: tc.rawArguments || JSON.stringify(tc.arguments || {}),
|
||||
},
|
||||
}))
|
||||
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: formattedToolCalls,
|
||||
})
|
||||
|
||||
// Add tool result messages using the same stable IDs
|
||||
for (const tc of toolCallsWithIds) {
|
||||
const resultContent =
|
||||
typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result || {})
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: resultContent,
|
||||
tool_call_id: tc._stableId,
|
||||
name: tc.name, // Store tool name for providers that need it (e.g., Google/Gemini)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add final assistant response if present
|
||||
if (content && typeof content === 'string') {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
private processProviderResponse(
|
||||
response: any,
|
||||
block: SerializedBlock,
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('Memory', () => {
|
||||
})
|
||||
|
||||
describe('applySlidingWindow (message-based)', () => {
|
||||
it('should keep last N conversation messages', () => {
|
||||
it('should keep last N turns (turn = user message + assistant response)', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'System prompt' },
|
||||
{ role: 'user', content: 'Message 1' },
|
||||
@@ -43,9 +43,10 @@ describe('Memory', () => {
|
||||
{ role: 'assistant', content: 'Response 3' },
|
||||
]
|
||||
|
||||
const result = (memoryService as any).applySlidingWindow(messages, '4')
|
||||
// Limit to 2 turns: should keep turns 2 and 3
|
||||
const result = (memoryService as any).applySlidingWindow(messages, '2')
|
||||
|
||||
expect(result.length).toBe(5)
|
||||
expect(result.length).toBe(5) // system + 2 turns (4 messages)
|
||||
expect(result[0].role).toBe('system')
|
||||
expect(result[0].content).toBe('System prompt')
|
||||
expect(result[1].content).toBe('Message 2')
|
||||
@@ -113,19 +114,18 @@ describe('Memory', () => {
|
||||
it('should preserve first system message and exclude it from token count', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: 'A' }, // System message - always preserved
|
||||
{ role: 'user', content: 'B' }, // ~1 token
|
||||
{ role: 'assistant', content: 'C' }, // ~1 token
|
||||
{ role: 'user', content: 'D' }, // ~1 token
|
||||
{ role: 'user', content: 'B' }, // ~1 token (turn 1)
|
||||
{ role: 'assistant', content: 'C' }, // ~1 token (turn 1)
|
||||
{ role: 'user', content: 'D' }, // ~1 token (turn 2)
|
||||
]
|
||||
|
||||
// Limit to 2 tokens - should fit system message + last 2 conversation messages (D, C)
|
||||
// Limit to 2 tokens - fits turn 2 (D=1 token), but turn 1 (B+C=2 tokens) would exceed
|
||||
const result = (memoryService as any).applySlidingWindowByTokens(messages, '2', 'gpt-4o')
|
||||
|
||||
// Should have: system message + 2 conversation messages = 3 total
|
||||
expect(result.length).toBe(3)
|
||||
// Should have: system message + turn 2 (1 message) = 2 total
|
||||
expect(result.length).toBe(2)
|
||||
expect(result[0].role).toBe('system') // First system message preserved
|
||||
expect(result[1].content).toBe('C') // Second most recent conversation message
|
||||
expect(result[2].content).toBe('D') // Most recent conversation message
|
||||
expect(result[1].content).toBe('D') // Most recent turn
|
||||
})
|
||||
|
||||
it('should process messages from newest to oldest', () => {
|
||||
@@ -249,29 +249,29 @@ describe('Memory', () => {
|
||||
})
|
||||
|
||||
describe('Token-based vs Message-based comparison', () => {
|
||||
it('should produce different results for same message count limit', () => {
|
||||
it('should produce different results based on turn limits vs token limits', () => {
|
||||
const messages: Message[] = [
|
||||
{ role: 'user', content: 'A' }, // Short message (~1 token)
|
||||
{ role: 'user', content: 'A' }, // Short message (~1 token) - turn 1
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'This is a much longer response that takes many more tokens',
|
||||
}, // Long message (~15 tokens)
|
||||
{ role: 'user', content: 'B' }, // Short message (~1 token)
|
||||
}, // Long message (~15 tokens) - turn 1
|
||||
{ role: 'user', content: 'B' }, // Short message (~1 token) - turn 2
|
||||
]
|
||||
|
||||
// Message-based: last 2 messages
|
||||
const messageResult = (memoryService as any).applySlidingWindow(messages, '2')
|
||||
expect(messageResult.length).toBe(2)
|
||||
// Turn-based with limit 1: keeps last turn only
|
||||
const messageResult = (memoryService as any).applySlidingWindow(messages, '1')
|
||||
expect(messageResult.length).toBe(1) // Only turn 2 (message B)
|
||||
|
||||
// Token-based: with limit of 10 tokens, might fit all 3 messages or just last 2
|
||||
// Token-based: with limit of 10 tokens, fits turn 2 (1 token) but not turn 1 (~16 tokens)
|
||||
const tokenResult = (memoryService as any).applySlidingWindowByTokens(
|
||||
messages,
|
||||
'10',
|
||||
'gpt-4o'
|
||||
)
|
||||
|
||||
// The long message should affect what fits
|
||||
expect(tokenResult.length).toBeGreaterThanOrEqual(1)
|
||||
// Both should only fit the last turn due to the long assistant message
|
||||
expect(tokenResult.length).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -202,13 +202,51 @@ export class Memory {
|
||||
const systemMessages = messages.filter((msg) => msg.role === 'system')
|
||||
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
|
||||
|
||||
const recentMessages = conversationMessages.slice(-limit)
|
||||
// Group messages into conversation turns
|
||||
// A turn = user message + any tool calls/results + assistant response
|
||||
const turns = this.groupMessagesIntoTurns(conversationMessages)
|
||||
|
||||
// Take the last N turns
|
||||
const recentTurns = turns.slice(-limit)
|
||||
|
||||
// Flatten back to messages
|
||||
const recentMessages = recentTurns.flat()
|
||||
|
||||
const firstSystemMessage = systemMessages.length > 0 ? [systemMessages[0]] : []
|
||||
|
||||
return [...firstSystemMessage, ...recentMessages]
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups messages into conversation turns.
|
||||
* A turn starts with a user message and includes all subsequent messages
|
||||
* until the next user message (tool calls, tool results, assistant response).
|
||||
*/
|
||||
private groupMessagesIntoTurns(messages: Message[]): Message[][] {
|
||||
const turns: Message[][] = []
|
||||
let currentTurn: Message[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'user') {
|
||||
// Start a new turn
|
||||
if (currentTurn.length > 0) {
|
||||
turns.push(currentTurn)
|
||||
}
|
||||
currentTurn = [msg]
|
||||
} else {
|
||||
// Add to current turn (assistant, tool, etc.)
|
||||
currentTurn.push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last turn
|
||||
if (currentTurn.length > 0) {
|
||||
turns.push(currentTurn)
|
||||
}
|
||||
|
||||
return turns
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply token-based sliding window to limit conversation by token count
|
||||
*
|
||||
@@ -216,6 +254,11 @@ export class Memory {
|
||||
* - For consistency with message-based sliding window, the first system message is preserved
|
||||
* - System messages are excluded from the token count
|
||||
* - This ensures system prompts are always available while limiting conversation history
|
||||
*
|
||||
* Turn handling:
|
||||
* - Messages are grouped into turns (user + tool calls/results + assistant response)
|
||||
* - Complete turns are added to stay within token limit
|
||||
* - This prevents breaking tool call/result pairs
|
||||
*/
|
||||
private applySlidingWindowByTokens(
|
||||
messages: Message[],
|
||||
@@ -233,25 +276,31 @@ export class Memory {
|
||||
const systemMessages = messages.filter((msg) => msg.role === 'system')
|
||||
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
|
||||
|
||||
// Group into turns to keep tool call/result pairs together
|
||||
const turns = this.groupMessagesIntoTurns(conversationMessages)
|
||||
|
||||
const result: Message[] = []
|
||||
let currentTokenCount = 0
|
||||
|
||||
// Add conversation messages from most recent backwards
|
||||
for (let i = conversationMessages.length - 1; i >= 0; i--) {
|
||||
const message = conversationMessages[i]
|
||||
const messageTokens = getAccurateTokenCount(message.content, model)
|
||||
// Add turns from most recent backwards
|
||||
for (let i = turns.length - 1; i >= 0; i--) {
|
||||
const turn = turns[i]
|
||||
const turnTokens = turn.reduce(
|
||||
(sum, msg) => sum + getAccurateTokenCount(msg.content || '', model),
|
||||
0
|
||||
)
|
||||
|
||||
if (currentTokenCount + messageTokens <= tokenLimit) {
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
if (currentTokenCount + turnTokens <= tokenLimit) {
|
||||
result.unshift(...turn)
|
||||
currentTokenCount += turnTokens
|
||||
} else if (result.length === 0) {
|
||||
logger.warn('Single message exceeds token limit, including anyway', {
|
||||
messageTokens,
|
||||
logger.warn('Single turn exceeds token limit, including anyway', {
|
||||
turnTokens,
|
||||
tokenLimit,
|
||||
messageRole: message.role,
|
||||
turnMessages: turn.length,
|
||||
})
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
result.unshift(...turn)
|
||||
currentTokenCount += turnTokens
|
||||
break
|
||||
} else {
|
||||
// Token limit reached, stop processing
|
||||
@@ -259,17 +308,20 @@ export class Memory {
|
||||
}
|
||||
}
|
||||
|
||||
// No need to remove orphaned messages - turns are already complete
|
||||
const cleanedResult = result
|
||||
|
||||
logger.debug('Applied token-based sliding window', {
|
||||
totalMessages: messages.length,
|
||||
conversationMessages: conversationMessages.length,
|
||||
includedMessages: result.length,
|
||||
includedMessages: cleanedResult.length,
|
||||
totalTokens: currentTokenCount,
|
||||
tokenLimit,
|
||||
})
|
||||
|
||||
// Preserve first system message and prepend to results (consistent with message-based window)
|
||||
const firstSystemMessage = systemMessages.length > 0 ? [systemMessages[0]] : []
|
||||
return [...firstSystemMessage, ...result]
|
||||
return [...firstSystemMessage, ...cleanedResult]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,7 +376,7 @@ export class Memory {
|
||||
// Count tokens used by system messages first
|
||||
let systemTokenCount = 0
|
||||
for (const msg of systemMessages) {
|
||||
systemTokenCount += getAccurateTokenCount(msg.content, model)
|
||||
systemTokenCount += getAccurateTokenCount(msg.content || '', model)
|
||||
}
|
||||
|
||||
// Calculate remaining tokens available for conversation messages
|
||||
@@ -339,30 +391,36 @@ export class Memory {
|
||||
return systemMessages
|
||||
}
|
||||
|
||||
// Group into turns to keep tool call/result pairs together
|
||||
const turns = this.groupMessagesIntoTurns(conversationMessages)
|
||||
|
||||
const result: Message[] = []
|
||||
let currentTokenCount = 0
|
||||
|
||||
for (let i = conversationMessages.length - 1; i >= 0; i--) {
|
||||
const message = conversationMessages[i]
|
||||
const messageTokens = getAccurateTokenCount(message.content, model)
|
||||
for (let i = turns.length - 1; i >= 0; i--) {
|
||||
const turn = turns[i]
|
||||
const turnTokens = turn.reduce(
|
||||
(sum, msg) => sum + getAccurateTokenCount(msg.content || '', model),
|
||||
0
|
||||
)
|
||||
|
||||
if (currentTokenCount + messageTokens <= remainingTokens) {
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
if (currentTokenCount + turnTokens <= remainingTokens) {
|
||||
result.unshift(...turn)
|
||||
currentTokenCount += turnTokens
|
||||
} else if (result.length === 0) {
|
||||
logger.warn('Single message exceeds remaining context window, including anyway', {
|
||||
messageTokens,
|
||||
logger.warn('Single turn exceeds remaining context window, including anyway', {
|
||||
turnTokens,
|
||||
remainingTokens,
|
||||
systemTokenCount,
|
||||
messageRole: message.role,
|
||||
turnMessages: turn.length,
|
||||
})
|
||||
result.unshift(message)
|
||||
currentTokenCount += messageTokens
|
||||
result.unshift(...turn)
|
||||
currentTokenCount += turnTokens
|
||||
break
|
||||
} else {
|
||||
logger.info('Auto-trimmed conversation history to fit context window', {
|
||||
originalMessages: conversationMessages.length,
|
||||
trimmedMessages: result.length,
|
||||
originalTurns: turns.length,
|
||||
trimmedTurns: turns.length - i - 1,
|
||||
conversationTokens: currentTokenCount,
|
||||
systemTokens: systemTokenCount,
|
||||
totalTokens: currentTokenCount + systemTokenCount,
|
||||
@@ -372,6 +430,7 @@ export class Memory {
|
||||
}
|
||||
}
|
||||
|
||||
// No need to remove orphaned messages - turns are already complete
|
||||
return [...systemMessages, ...result]
|
||||
}
|
||||
|
||||
@@ -638,7 +697,7 @@ export class Memory {
|
||||
/**
|
||||
* Validate inputs to prevent malicious data or performance issues
|
||||
*/
|
||||
private validateInputs(conversationId?: string, content?: string): void {
|
||||
private validateInputs(conversationId?: string, content?: string | null): void {
|
||||
if (conversationId) {
|
||||
if (conversationId.length > 255) {
|
||||
throw new Error('Conversation ID too long (max 255 characters)')
|
||||
|
||||
@@ -37,10 +37,22 @@ export interface ToolInput {
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant'
|
||||
content: string
|
||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||
content: string | null
|
||||
function_call?: any
|
||||
tool_calls?: any[]
|
||||
tool_calls?: ToolCallMessage[]
|
||||
tool_call_id?: string
|
||||
/** Tool name for tool role messages (used by providers like Google/Gemini) */
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface ToolCallMessage {
|
||||
id: string
|
||||
type: 'function'
|
||||
function: {
|
||||
name: string
|
||||
arguments: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface StreamingConfig {
|
||||
|
||||
@@ -4,7 +4,12 @@ import type { StreamingExecution } from '@/executor/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getProviderDefaultModel, getProviderModels } from '../models'
|
||||
import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
|
||||
import { prepareToolExecution, prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '../utils'
|
||||
|
||||
const logger = createLogger('AnthropicProvider')
|
||||
|
||||
@@ -68,8 +73,12 @@ export const anthropicProvider: ProviderConfig = {
|
||||
|
||||
// Add remaining messages
|
||||
if (request.messages) {
|
||||
request.messages.forEach((msg) => {
|
||||
// Sanitize messages to ensure proper tool call/result pairing
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
|
||||
sanitizedMessages.forEach((msg) => {
|
||||
if (msg.role === 'function') {
|
||||
// Legacy function role format
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
@@ -80,7 +89,41 @@ export const anthropicProvider: ProviderConfig = {
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if (msg.role === 'tool') {
|
||||
// Modern tool role format (OpenAI-compatible)
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: (msg as any).tool_call_id,
|
||||
content: msg.content || '',
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
// Modern tool_calls format (OpenAI-compatible)
|
||||
const toolUseContent = msg.tool_calls.map((tc: any) => ({
|
||||
type: 'tool_use',
|
||||
id: tc.id,
|
||||
name: tc.function?.name || tc.name,
|
||||
input:
|
||||
typeof tc.function?.arguments === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(tc.function.arguments)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
})()
|
||||
: tc.function?.arguments || tc.arguments || {},
|
||||
}))
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: toolUseContent,
|
||||
})
|
||||
} else if (msg.function_call) {
|
||||
// Legacy function_call format
|
||||
const toolUseId = `${msg.function_call.name}-${Date.now()}`
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
@@ -490,9 +533,14 @@ ${fieldDescriptions}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original tool use ID from the API response
|
||||
const toolUseId = toolUse.id || generateToolUseId(toolName)
|
||||
|
||||
toolCalls.push({
|
||||
id: toolUseId,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: JSON.stringify(toolArgs),
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
@@ -501,7 +549,6 @@ ${fieldDescriptions}
|
||||
})
|
||||
|
||||
// Add the tool call and result to messages (both success and failure)
|
||||
const toolUseId = generateToolUseId(toolName)
|
||||
|
||||
currentMessages.push({
|
||||
role: 'assistant',
|
||||
@@ -840,9 +887,14 @@ ${fieldDescriptions}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original tool use ID from the API response
|
||||
const toolUseId = toolUse.id || generateToolUseId(toolName)
|
||||
|
||||
toolCalls.push({
|
||||
id: toolUseId,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: JSON.stringify(toolArgs),
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
@@ -851,7 +903,6 @@ ${fieldDescriptions}
|
||||
})
|
||||
|
||||
// Add the tool call and result to messages (both success and failure)
|
||||
const toolUseId = generateToolUseId(toolName)
|
||||
|
||||
currentMessages.push({
|
||||
role: 'assistant',
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -120,9 +121,10 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add remaining messages
|
||||
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Transform tools to Azure OpenAI format if provided
|
||||
@@ -417,8 +419,10 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -86,9 +87,10 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add remaining messages
|
||||
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Transform tools to Cerebras format if provided
|
||||
@@ -323,8 +325,10 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -84,9 +85,10 @@ export const deepseekProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add remaining messages
|
||||
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Transform tools to OpenAI format if provided
|
||||
@@ -323,8 +325,10 @@ export const deepseekProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -552,9 +553,14 @@ export const googleProvider: ProviderConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a unique ID for this tool call (Google doesn't provide one)
|
||||
const toolCallId = `call_${toolName}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: JSON.stringify(toolArgs),
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
@@ -1087,9 +1093,10 @@ function convertToGeminiFormat(request: ProviderRequest): {
|
||||
contents.push({ role: 'user', parts: [{ text: request.context }] })
|
||||
}
|
||||
|
||||
// Process messages
|
||||
// Process messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages && request.messages.length > 0) {
|
||||
for (const message of request.messages) {
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
for (const message of sanitizedMessages) {
|
||||
if (message.role === 'system') {
|
||||
// Add to system instruction
|
||||
if (!systemInstruction) {
|
||||
@@ -1119,10 +1126,28 @@ function convertToGeminiFormat(request: ProviderRequest): {
|
||||
contents.push({ role: 'model', parts: functionCalls })
|
||||
}
|
||||
} else if (message.role === 'tool') {
|
||||
// Convert tool response (Gemini only accepts user/model roles)
|
||||
// Convert tool response to Gemini's functionResponse format
|
||||
// Gemini uses 'user' role for function responses
|
||||
const functionName = (message as any).name || 'function'
|
||||
|
||||
let responseData: any
|
||||
try {
|
||||
responseData =
|
||||
typeof message.content === 'string' ? JSON.parse(message.content) : message.content
|
||||
} catch {
|
||||
responseData = { result: message.content }
|
||||
}
|
||||
|
||||
contents.push({
|
||||
role: 'user',
|
||||
parts: [{ text: `Function result: ${message.content}` }],
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: functionName,
|
||||
response: responseData,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -75,9 +76,10 @@ export const groqProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add remaining messages
|
||||
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Transform tools to function format if provided
|
||||
@@ -296,8 +298,10 @@ export const groqProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -100,8 +101,10 @@ export const mistralProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Sanitize messages to ensure proper tool call/result pairing
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
const tools = request.tools?.length
|
||||
@@ -355,8 +358,10 @@ export const mistralProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import { prepareToolExecution } from '@/providers/utils'
|
||||
import { prepareToolExecution, sanitizeMessagesForProvider } from '@/providers/utils'
|
||||
import { useProvidersStore } from '@/stores/providers/store'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
@@ -126,9 +126,10 @@ export const ollamaProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add remaining messages
|
||||
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Transform tools to OpenAI format if provided
|
||||
@@ -407,8 +408,10 @@ export const ollamaProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -103,9 +104,10 @@ export const openaiProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add remaining messages
|
||||
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Transform tools to OpenAI format if provided
|
||||
@@ -398,8 +400,10 @@ export const openaiProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -93,8 +94,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
allMessages.push({ role: 'user', content: request.context })
|
||||
}
|
||||
|
||||
// Sanitize messages to ensure proper tool call/result pairing
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
const tools = request.tools?.length
|
||||
@@ -303,8 +306,10 @@ export const openRouterProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -1049,3 +1049,96 @@ export function prepareToolExecution(
|
||||
|
||||
return { toolParams, executionParams }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes messages array to ensure proper tool call/result pairing
|
||||
* This prevents provider errors like "tool_result without corresponding tool_use"
|
||||
*
|
||||
* Rules enforced:
|
||||
* 1. Every tool message must have a matching tool_calls message before it
|
||||
* 2. Every tool_calls in an assistant message should have corresponding tool results
|
||||
* 3. Messages maintain their original order
|
||||
*/
|
||||
export function sanitizeMessagesForProvider(
|
||||
messages: Array<{
|
||||
role: string
|
||||
content?: string | null
|
||||
tool_calls?: Array<{ id: string; [key: string]: any }>
|
||||
tool_call_id?: string
|
||||
[key: string]: any
|
||||
}>
|
||||
): typeof messages {
|
||||
if (!messages || messages.length === 0) {
|
||||
return messages
|
||||
}
|
||||
|
||||
// Build a map of tool_call IDs to their positions
|
||||
const toolCallIdToIndex = new Map<string, number>()
|
||||
const toolResultIds = new Set<string>()
|
||||
|
||||
// First pass: collect all tool_call IDs and tool result IDs
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.id) {
|
||||
toolCallIdToIndex.set(tc.id, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === 'tool' && msg.tool_call_id) {
|
||||
toolResultIds.add(msg.tool_call_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: filter messages
|
||||
const result: typeof messages = []
|
||||
|
||||
for (const msg of messages) {
|
||||
// For tool messages: only include if there's a matching tool_calls before it
|
||||
if (msg.role === 'tool') {
|
||||
const toolCallId = msg.tool_call_id
|
||||
if (toolCallId && toolCallIdToIndex.has(toolCallId)) {
|
||||
result.push(msg)
|
||||
} else {
|
||||
logger.debug('Removing orphaned tool message', { toolCallId })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// For assistant messages with tool_calls: only include tool_calls that have results
|
||||
if (msg.role === 'assistant' && msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
const validToolCalls = msg.tool_calls.filter((tc) => tc.id && toolResultIds.has(tc.id))
|
||||
|
||||
if (validToolCalls.length === 0) {
|
||||
// No valid tool calls - if there's content, keep as regular message
|
||||
if (msg.content) {
|
||||
const { tool_calls, ...msgWithoutToolCalls } = msg
|
||||
result.push(msgWithoutToolCalls)
|
||||
} else {
|
||||
logger.debug('Removing assistant message with orphaned tool_calls', {
|
||||
toolCallIds: msg.tool_calls.map((tc) => tc.id),
|
||||
})
|
||||
}
|
||||
} else if (validToolCalls.length === msg.tool_calls.length) {
|
||||
// All tool calls are valid
|
||||
result.push(msg)
|
||||
} else {
|
||||
// Some tool calls are orphaned - keep only valid ones
|
||||
result.push({ ...msg, tool_calls: validToolCalls })
|
||||
logger.debug('Filtered orphaned tool_calls from message', {
|
||||
original: msg.tool_calls.length,
|
||||
kept: validToolCalls.length,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// All other messages pass through
|
||||
result.push(msg)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { useProvidersStore } from '@/stores/providers/store'
|
||||
@@ -140,8 +141,10 @@ export const vllmProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Sanitize messages to ensure proper tool call/result pairing
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
const tools = request.tools?.length
|
||||
@@ -400,8 +403,10 @@ export const vllmProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
sanitizeMessagesForProvider,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -83,8 +84,10 @@ export const xAIProvider: ProviderConfig = {
|
||||
})
|
||||
}
|
||||
|
||||
// Sanitize messages to ensure proper tool call/result pairing
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
|
||||
allMessages.push(...sanitizedMessages)
|
||||
}
|
||||
|
||||
// Set up tools
|
||||
@@ -364,8 +367,10 @@ export const xAIProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
id: toolCall.id,
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
rawArguments: toolCall.function.arguments,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
|
||||
@@ -32,11 +32,7 @@ function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: bool
|
||||
const fieldMode = subBlockConfig.mode
|
||||
|
||||
if (fieldMode === 'advanced' && !isAdvancedMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (fieldMode === 'basic' && isAdvancedMode) {
|
||||
return false
|
||||
return false // Skip advanced-only fields when in basic mode
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user