fix(agiloft): correct response parsing, add EWGetChoiceLineId tool (#4477)

* fix(agiloft): correct response parsing, add EWGetChoiceLineId tool

* fix(agiloft): address PR review feedback
This commit is contained in:
Waleed
2026-05-06 11:55:11 -07:00
committed by GitHub
parent 48331451b0
commit 6d4ffff327
21 changed files with 287 additions and 67 deletions
+24 -2
View File
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="agiloft"
color="#FFFFFF"
color="#001028"
/>
{/* MANUAL-CONTENT-START:intro */}
@@ -137,6 +137,28 @@ Delete a record from an Agiloft table.
| `id` | string | ID of the deleted record |
| `deleted` | boolean | Whether the record was successfully deleted |
### `agiloft_get_choice_line_id`
Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `instanceUrl` | string | Yes | Agiloft instance URL \(e.g., https://mycompany.agiloft.com\) |
| `knowledgeBase` | string | Yes | Knowledge base name |
| `login` | string | Yes | Agiloft username |
| `password` | string | Yes | Agiloft password |
| `table` | string | Yes | Table name \(e.g., "case", "contracts"\) |
| `fieldName` | string | Yes | Choice field name \(e.g., "priority", "status"\) |
| `value` | string | Yes | Choice display value to resolve \(e.g., "High", "Active"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `choiceLineId` | number | Internal numeric line ID of the choice value |
### `agiloft_lock_record`
Lock, unlock, or check the lock status of an Agiloft record.
@@ -254,7 +276,7 @@ List saved searches defined for an Agiloft table.
| `searches` | array | List of saved searches for the table |
| ↳ `name` | string | Saved search name |
| ↳ `label` | string | Saved search display label |
| ↳ `id` | string | Saved search database identifier |
| ↳ `id` | number | Saved search database identifier |
| ↳ `description` | string | Saved search description |
### `agiloft_search_records`
+4 -4
View File
@@ -87,7 +87,7 @@ List persons (users) in PostHog. Returns user profiles with their properties and
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `region` | string | No | PostHog region: us \(default\) or eu |
| `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) |
| `limit` | number | No | Number of persons to return \(default: 100, max: 100\) |
@@ -115,7 +115,7 @@ Get detailed information about a specific person in PostHog by their ID or UUID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `region` | string | No | PostHog region: us \(default\) or eu |
| `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) |
| `personId` | string | Yes | Person ID or UUID to retrieve \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) |
@@ -139,7 +139,7 @@ Delete a person from PostHog. This will remove all associated events and data. U
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `region` | string | No | PostHog region: us \(default\) or eu |
| `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) |
| `personId` | string | Yes | Person ID or UUID to delete \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) |
@@ -158,7 +158,7 @@ Execute a HogQL query in PostHog. HogQL is PostHog
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `personalApiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) |
| `region` | string | No | PostHog region: us \(default\) or eu |
| `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) |
| `query` | string | Yes | HogQL query to execute. Example: \{"kind": "HogQLQuery", "query": "SELECT event, count\(\) FROM events WHERE timestamp &gt; now\(\) - INTERVAL 1 DAY GROUP BY event"\} |
@@ -321,7 +321,7 @@
"name": "Agiloft",
"description": "Manage records in Agiloft CLM",
"longDescription": "Integrate with Agiloft contract lifecycle management to create, read, update, delete, and search records. Supports file attachments, SQL-based selection, saved searches, and record locking across any table in your knowledge base.",
"bgColor": "#FFFFFF",
"bgColor": "#001028",
"iconName": "AgiloftIcon",
"docsUrl": "https://docs.sim.ai/tools/agiloft",
"operations": [
@@ -372,9 +372,13 @@
{
"name": "Lock Record",
"description": "Lock, unlock, or check the lock status of an Agiloft record."
},
{
"name": "Get Choice Line ID",
"description": "Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields."
}
],
"operationCount": 12,
"operationCount": 13,
"triggers": [],
"triggerCount": 0,
"authType": "none",
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftAttachContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
@@ -90,7 +91,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const agiloftResponse = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': userFile.type || 'application/octet-stream',
'Content-Type': 'application/octet-stream',
Authorization: `Bearer ${token}`,
},
body: new Uint8Array(fileBuffer),
@@ -136,9 +137,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
} catch (error) {
logger.error(`[${requestId}] Error attaching file to Agiloft:`, error)
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 }
)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftRetrieveContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
@@ -127,9 +128,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
} catch (error) {
logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error)
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 }
)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
+33 -6
View File
@@ -13,7 +13,7 @@ export const AgiloftBlock: BlockConfig = {
category: 'tools',
integrationType: IntegrationType.Productivity,
tags: ['automation'],
bgColor: '#FFFFFF',
bgColor: '#001028',
icon: AgiloftIcon,
authMode: AuthMode.ApiKey,
@@ -35,6 +35,7 @@ export const AgiloftBlock: BlockConfig = {
{ label: 'Remove Attachment', id: 'remove_attachment' },
{ label: 'Attachment Info', id: 'attachment_info' },
{ label: 'Lock Record', id: 'lock_record' },
{ label: 'Get Choice Line ID', id: 'get_choice_line_id' },
],
value: () => 'search_records',
},
@@ -44,7 +45,6 @@ export const AgiloftBlock: BlockConfig = {
type: 'short-input',
placeholder: 'https://mycompany.agiloft.com',
required: true,
password: false,
},
{
id: 'knowledgeBase',
@@ -151,16 +151,36 @@ export const AgiloftBlock: BlockConfig = {
id: 'fieldName',
title: 'Field Name',
type: 'short-input',
placeholder: 'e.g., attached_docs',
placeholder: 'e.g., attached_docs, priority',
condition: {
field: 'operation',
value: ['attach_file', 'retrieve_attachment', 'remove_attachment', 'attachment_info'],
value: [
'attach_file',
'retrieve_attachment',
'remove_attachment',
'attachment_info',
'get_choice_line_id',
],
},
required: {
field: 'operation',
value: ['attach_file', 'retrieve_attachment', 'remove_attachment', 'attachment_info'],
value: [
'attach_file',
'retrieve_attachment',
'remove_attachment',
'attachment_info',
'get_choice_line_id',
],
},
},
{
id: 'value',
title: 'Choice Value',
type: 'short-input',
placeholder: 'e.g., High, Active',
condition: { field: 'operation', value: 'get_choice_line_id' },
required: { field: 'operation', value: 'get_choice_line_id' },
},
{
id: 'uploadFile',
title: 'File',
@@ -254,6 +274,7 @@ export const AgiloftBlock: BlockConfig = {
'agiloft_attachment_info',
'agiloft_create_record',
'agiloft_delete_record',
'agiloft_get_choice_line_id',
'agiloft_lock_record',
'agiloft_read_record',
'agiloft_remove_attachment',
@@ -288,7 +309,8 @@ export const AgiloftBlock: BlockConfig = {
data: { type: 'string', description: 'Record data as JSON' },
query: { type: 'string', description: 'Search query' },
where: { type: 'string', description: 'SQL WHERE clause for select' },
fieldName: { type: 'string', description: 'Attachment field name' },
fieldName: { type: 'string', description: 'Attachment field name or choice field name' },
value: { type: 'string', description: 'Choice value to resolve to its line ID' },
attachFile: { type: 'file', description: 'File to attach' },
fileName: { type: 'string', description: 'Name for the attached file' },
position: { type: 'string', description: 'Attachment position index' },
@@ -403,5 +425,10 @@ export const AgiloftBlock: BlockConfig = {
description: 'Minutes until the lock expires',
condition: { field: 'operation', value: 'lock_record' },
},
choiceLineId: {
type: 'number',
description: 'Internal numeric ID of the resolved choice value',
condition: { field: 'operation', value: 'get_choice_line_id' },
},
},
}
+2 -2
View File
@@ -50,8 +50,8 @@ export const agiloftAttachBodySchema = z.object({
table: z.string().min(1, 'Table is required'),
recordId: z.string().min(1, 'Record ID is required'),
fieldName: z.string().min(1, 'Field name is required'),
file: FileInputSchema.optional().nullable(),
fileName: z.string().optional().nullable(),
file: FileInputSchema.optional(),
fileName: z.string().optional(),
})
export const agiloftRetrieveContract = defineRouteContract({
+7 -3
View File
@@ -91,9 +91,13 @@ export const agiloftAttachmentInfoTool: ToolConfig<
for (let i = 0; i < result.length; i++) {
const item = result[i] as Record<string, unknown>
attachments.push({
position: (item.position as number) ?? i,
name: (item.name as string) ?? (item.filename as string) ?? '',
size: (item.size as number) ?? 0,
position: (item.filePosition as number) ?? (item.position as number) ?? i,
name:
(item.fileName as string) ??
(item.name as string) ??
(item.filename as string) ??
'',
size: (item.size as number) ?? (item.fileSize as number) ?? 0,
})
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ export const agiloftCreateRecordTool: ToolConfig<AgiloftCreateRecordParams, Agil
(base) => ({
url: buildCreateRecordUrl(base, params),
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body,
}),
async (response) => {
+1
View File
@@ -60,6 +60,7 @@ export const agiloftDeleteRecordTool: ToolConfig<AgiloftDeleteRecordParams, Agil
(base) => ({
url: buildDeleteRecordUrl(base, params),
method: 'DELETE',
headers: { Accept: 'application/json' },
}),
async (response) => {
if (!response.ok) {
@@ -0,0 +1,130 @@
import type {
AgiloftGetChoiceLineIdParams,
AgiloftGetChoiceLineIdResponse,
} from '@/tools/agiloft/types'
import { buildGetChoiceLineIdUrl, executeAgiloftRequest } from '@/tools/agiloft/utils'
import type { ToolConfig } from '@/tools/types'
export const agiloftGetChoiceLineIdTool: ToolConfig<
AgiloftGetChoiceLineIdParams,
AgiloftGetChoiceLineIdResponse
> = {
id: 'agiloft_get_choice_line_id',
name: 'Agiloft Get Choice Line ID',
description:
'Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft instance URL (e.g., https://mycompany.agiloft.com)',
},
knowledgeBase: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Knowledge base name',
},
login: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Agiloft password',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., "case", "contracts")',
},
fieldName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Choice field name (e.g., "priority", "status")',
},
value: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Choice display value to resolve (e.g., "High", "Active")',
},
},
request: {
url: 'https://placeholder.agiloft.com',
method: 'GET',
headers: () => ({}),
},
directExecution: async (params) => {
return executeAgiloftRequest<AgiloftGetChoiceLineIdResponse>(
params,
(base) => ({
url: buildGetChoiceLineIdUrl(base, params),
method: 'GET',
headers: { Accept: 'application/json' },
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { choiceLineId: null },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = data.result ?? data
let choiceLineId: number | null = null
if (typeof result === 'number') {
choiceLineId = result
} else if (typeof result === 'string') {
const parsed = Number(result)
choiceLineId = Number.isFinite(parsed) ? parsed : null
} else if (typeof result === 'object' && result !== null) {
const obj = result as Record<string, unknown>
const idVal = obj.id ?? obj.choiceLineId ?? obj.lineId
if (typeof idVal === 'number') {
choiceLineId = idVal
} else if (typeof idVal === 'string') {
const parsed = Number(idVal)
choiceLineId = Number.isFinite(parsed) ? parsed : null
}
}
if (choiceLineId === null) {
return {
success: false,
output: { choiceLineId: null },
error: `No choice line ID found for value "${params.value}" in field "${params.fieldName}"`,
}
}
return {
success: data.success !== false,
output: { choiceLineId },
}
}
)
},
outputs: {
choiceLineId: {
type: 'number',
description: 'Internal numeric line ID of the choice value',
optional: true,
},
},
}
+1
View File
@@ -2,6 +2,7 @@ export { agiloftAttachFileTool } from '@/tools/agiloft/attach_file'
export { agiloftAttachmentInfoTool } from '@/tools/agiloft/attachment_info'
export { agiloftCreateRecordTool } from '@/tools/agiloft/create_record'
export { agiloftDeleteRecordTool } from '@/tools/agiloft/delete_record'
export { agiloftGetChoiceLineIdTool } from '@/tools/agiloft/get_choice_line_id'
export { agiloftLockRecordTool } from '@/tools/agiloft/lock_record'
export { agiloftReadRecordTool } from '@/tools/agiloft/read_record'
export { agiloftRemoveAttachmentTool } from '@/tools/agiloft/remove_attachment'
+1
View File
@@ -65,6 +65,7 @@ export const agiloftReadRecordTool: ToolConfig<AgiloftReadRecordParams, AgiloftR
(base) => ({
url: buildReadRecordUrl(base, params),
method: 'GET',
headers: { Accept: 'application/json' },
}),
async (response) => {
if (!response.ok) {
+2 -2
View File
@@ -67,7 +67,7 @@ export const agiloftRemoveAttachmentTool: ToolConfig<
request: {
url: 'https://placeholder.agiloft.com',
method: 'GET',
method: 'DELETE',
headers: () => ({}),
},
@@ -76,7 +76,7 @@ export const agiloftRemoveAttachmentTool: ToolConfig<
params,
(base) => ({
url: buildRemoveAttachmentUrl(base, params),
method: 'GET',
method: 'DELETE',
}),
async (response) => {
const text = await response.text()
+6 -2
View File
@@ -107,8 +107,12 @@ export const agiloftSavedSearchTool: ToolConfig<
properties: {
name: { type: 'string', description: 'Saved search name' },
label: { type: 'string', description: 'Saved search display label' },
id: { type: 'string', description: 'Saved search database identifier' },
description: { type: 'string', description: 'Saved search description' },
id: { type: 'number', description: 'Saved search database identifier' },
description: {
type: 'string',
description: 'Saved search description',
optional: true,
},
},
},
},
+29 -30
View File
@@ -94,45 +94,44 @@ export const agiloftSearchRecordsTool: ToolConfig<
const data = (await response.json()) as Record<string, unknown>
const records: Record<string, unknown>[] = []
const result = (data.result ?? data) as Record<string, unknown>
if (data.result && Array.isArray(data.result)) {
for (const item of data.result as Record<string, unknown>[]) {
if (Array.isArray(result)) {
for (const item of result as Record<string, unknown>[]) {
records.push(item)
}
} else if (Array.isArray(data)) {
for (const item of data as Record<string, unknown>[]) {
records.push(item)
}
} else if (data.results && Array.isArray(data.results)) {
for (const item of data.results as Record<string, unknown>[]) {
records.push(item)
}
} else if (data.records && Array.isArray(data.records)) {
for (const item of data.records as Record<string, unknown>[]) {
records.push(item)
}
} else if (typeof data.EWREST_length === 'number') {
const count = data.EWREST_length as number
for (let i = 0; i < count; i++) {
const record: Record<string, unknown> = {}
for (const key of Object.keys(data)) {
const match = key.match(/^EWREST_(.+)_(\d+)$/)
if (match && Number(match[2]) === i) {
record[match[1]] = data[key]
} else {
const lengthRaw = result.EWREST_length ?? data.EWREST_length
const count = typeof lengthRaw === 'string' ? Number(lengthRaw) : (lengthRaw as number)
if (typeof count === 'number' && Number.isFinite(count)) {
const source = (result.EWREST_length != null ? result : data) as Record<string, unknown>
for (let i = 0; i < count; i++) {
const record: Record<string, unknown> = {}
for (const key of Object.keys(source)) {
const match = key.match(/^EWREST_(.+)_(\d+)$/)
if (match && Number(match[2]) === i) {
record[match[1]] = source[key]
}
}
if (Object.keys(record).length > 0) {
records.push(record)
}
}
if (Object.keys(record).length > 0) {
records.push(record)
}
}
}
const totalCount =
(data.totalCount as number) ??
(data.total as number) ??
(data.count as number) ??
(data.EWREST_length as number) ??
const totalCountRaw =
result.totalCount ??
result.total ??
result.count ??
result.EWREST_length ??
data.totalCount ??
data.total ??
data.count ??
data.EWREST_length ??
records.length
const totalCount =
typeof totalCountRaw === 'string' ? Number(totalCountRaw) : (totalCountRaw as number)
const page = params.page ? Number(params.page) : 0
const limit = params.limit ? Number(params.limit) : 25
+11 -3
View File
@@ -95,14 +95,22 @@ export const agiloftSelectRecordsTool: ToolConfig<
}
}
const totalCount =
data.EWREST_id_length ?? data.totalCount ?? data.total ?? data.count ?? recordIds.length
const totalCountRaw =
result.EWREST_id_length ??
result.totalCount ??
result.total ??
result.count ??
data.EWREST_id_length ??
data.totalCount ??
data.total ??
data.count ??
recordIds.length
return {
success: data.success !== false,
output: {
recordIds,
totalCount: Number(totalCount),
totalCount: Number(totalCountRaw),
},
}
}
+11
View File
@@ -160,3 +160,14 @@ export interface AgiloftRemoveAttachmentResponse extends ToolResponse {
remainingAttachments: number
}
}
export interface AgiloftGetChoiceLineIdParams extends AgiloftBaseParams {
fieldName: string
value: string
}
export interface AgiloftGetChoiceLineIdResponse extends ToolResponse {
output: {
choiceLineId: number | null
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ export const agiloftUpdateRecordTool: ToolConfig<AgiloftUpdateRecordParams, Agil
(base) => ({
url: buildUpdateRecordUrl(base, params),
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body,
}),
async (response) => {
+10
View File
@@ -4,6 +4,7 @@ import type {
AgiloftAttachmentInfoParams,
AgiloftBaseParams,
AgiloftDeleteRecordParams,
AgiloftGetChoiceLineIdParams,
AgiloftLockRecordParams,
AgiloftReadRecordParams,
AgiloftRemoveAttachmentParams,
@@ -243,6 +244,15 @@ export function buildAttachFileUrl(
return `${base}/ewws/EWAttach?$KB=${kb}&$table=${table}&$lang=en&id=${recordId}&field=${fieldName}&fileName=${encodedFileName}`
}
export function buildGetChoiceLineIdUrl(
base: string,
params: AgiloftGetChoiceLineIdParams
): string {
const field = encodeURIComponent(params.fieldName.trim())
const value = encodeURIComponent(params.value.trim())
return `${base}/ewws/EWGetChoiceLineId/.json?${buildEwBaseQuery(params)}&field=${field}&value=${value}`
}
export function getLockHttpMethod(lockAction: string): HttpMethod {
switch (lockAction) {
case 'lock':
+2
View File
@@ -60,6 +60,7 @@ import {
agiloftAttachmentInfoTool,
agiloftCreateRecordTool,
agiloftDeleteRecordTool,
agiloftGetChoiceLineIdTool,
agiloftLockRecordTool,
agiloftReadRecordTool,
agiloftRemoveAttachmentTool,
@@ -3003,6 +3004,7 @@ export const tools: Record<string, ToolConfig> = {
agiloft_attachment_info: agiloftAttachmentInfoTool,
agiloft_create_record: agiloftCreateRecordTool,
agiloft_delete_record: agiloftDeleteRecordTool,
agiloft_get_choice_line_id: agiloftGetChoiceLineIdTool,
agiloft_lock_record: agiloftLockRecordTool,
agiloft_read_record: agiloftReadRecordTool,
agiloft_remove_attachment: agiloftRemoveAttachmentTool,