This commit is contained in:
Lakee Sivaraya
2026-01-15 19:06:15 -08:00
parent cfffd050a2
commit 1a13762617
5 changed files with 310 additions and 4 deletions

View File

@@ -711,6 +711,9 @@ export class AgentBlockHandler implements BlockHandler {
getAllBlocks,
getToolAsync: (toolId: string) => getToolAsync(toolId, ctx.workflowId),
getTool,
workspaceId: ctx.workspaceId,
workflowId: ctx.workflowId,
executeTool,
})
if (transformedTool) {

View File

@@ -12,6 +12,7 @@
export * from './constants'
export * from './filters'
export * from './llm-enrichment'
export * from './query-builder'
export * from './service'
export * from './types'

View File

@@ -0,0 +1,184 @@
/**
* LLM tool enrichment utilities for table operations.
*
* Provides functions to enrich tool descriptions and parameter schemas
* with table-specific information so LLMs can construct proper queries.
*
* @module lib/table/llm-enrichment
*/
/**
* Table schema information used for LLM enrichment.
*/
export interface TableSchemaInfo {
name: string
columns: Array<{ name: string; type: string }>
}
/**
* Operations that use filters and need filter-specific enrichment.
*/
export const FILTER_OPERATIONS = new Set([
'table_query_rows',
'table_update_rows_by_filter',
'table_delete_rows_by_filter',
])
/**
* Operations that need column info for data construction.
*/
export const DATA_OPERATIONS = new Set([
'table_insert_row',
'table_batch_insert_rows',
'table_upsert_row',
'table_update_row',
])
/**
* Enriches a table tool description with schema information based on the operation type.
*
* @param originalDescription - The original tool description
* @param tableSchema - The table schema with name and columns
* @param toolId - The tool identifier to determine operation type
* @returns Enriched description with table-specific instructions
*/
export function enrichTableToolDescription(
originalDescription: string,
tableSchema: TableSchemaInfo,
toolId: string
): string {
if (!tableSchema.columns || tableSchema.columns.length === 0) {
return originalDescription
}
const columnList = tableSchema.columns.map((col) => ` - ${col.name} (${col.type})`).join('\n')
// Filter-based operations: emphasize filter usage
if (FILTER_OPERATIONS.has(toolId)) {
const stringCols = tableSchema.columns.filter((c) => c.type === 'string')
const numberCols = tableSchema.columns.filter((c) => c.type === 'number')
let filterExample = ''
if (stringCols.length > 0 && numberCols.length > 0) {
filterExample = `
Example filter: {"${stringCols[0].name}": {"$eq": "value"}, "${numberCols[0].name}": {"$lt": 50}}`
} else if (stringCols.length > 0) {
filterExample = `
Example filter: {"${stringCols[0].name}": {"$eq": "value"}}`
}
return `${originalDescription}
INSTRUCTIONS:
1. ALWAYS include a filter based on the user's question - queries without filters will fail
2. Construct the filter yourself from the user's question - do NOT ask for confirmation
3. Use exact match ($eq) by default unless the user specifies otherwise
4. Use limit=1000 to fetch matching rows, then count or process them as needed
Table "${tableSchema.name}" columns:
${columnList}
${filterExample}`
}
// Data operations: show columns for data construction
if (DATA_OPERATIONS.has(toolId)) {
const exampleCols = tableSchema.columns.slice(0, 3)
const dataExample = exampleCols.reduce(
(obj, col) => {
obj[col.name] = col.type === 'number' ? 123 : col.type === 'boolean' ? true : 'example'
return obj
},
{} as Record<string, unknown>
)
return `${originalDescription}
Table "${tableSchema.name}" available columns:
${columnList}
Pass the "data" parameter with an object like: ${JSON.stringify(dataExample)}`
}
// Default: just show columns
return `${originalDescription}
Table "${tableSchema.name}" columns:
${columnList}`
}
/**
* Enriches LLM tool parameters with table-specific information.
*
* @param llmSchema - The original LLM schema with properties and required fields
* @param tableSchema - The table schema with name and columns
* @param toolId - The tool identifier to determine operation type
* @returns Enriched schema with updated property descriptions and required fields
*/
export function enrichTableToolParameters(
llmSchema: { properties?: Record<string, any>; required?: string[] },
tableSchema: TableSchemaInfo,
toolId: string
): { properties: Record<string, any>; required: string[] } {
if (!tableSchema.columns || tableSchema.columns.length === 0) {
return {
properties: llmSchema.properties || {},
required: llmSchema.required || [],
}
}
const columnNames = tableSchema.columns.map((c) => c.name).join(', ')
const enrichedProperties = { ...llmSchema.properties }
const enrichedRequired = llmSchema.required ? [...llmSchema.required] : []
// Enrich filter parameter for filter-based operations
if (enrichedProperties.filter && FILTER_OPERATIONS.has(toolId)) {
enrichedProperties.filter = {
...enrichedProperties.filter,
description: `REQUIRED - query will fail without a filter. Construct filter from user's question using columns: ${columnNames}. Syntax: {"column": {"$eq": "value"}}`,
}
}
// Mark filter as required in schema for query operations
if (FILTER_OPERATIONS.has(toolId) && !enrichedRequired.includes('filter')) {
enrichedRequired.push('filter')
}
// Enrich limit parameter for query operations
if (enrichedProperties.limit && toolId === 'table_query_rows') {
enrichedProperties.limit = {
...enrichedProperties.limit,
description: `Maximum rows to return (min: 1, max: 1000, default: 100). Use limit=1000 to fetch all matching rows.`,
}
}
// Enrich data parameter for insert/update operations
if (enrichedProperties.data && DATA_OPERATIONS.has(toolId)) {
const exampleCols = tableSchema.columns.slice(0, 2)
const exampleData = exampleCols.reduce(
(obj: Record<string, unknown>, col: { name: string; type: string }) => {
obj[col.name] = col.type === 'number' ? 123 : col.type === 'boolean' ? true : 'value'
return obj
},
{} as Record<string, unknown>
)
enrichedProperties.data = {
...enrichedProperties.data,
description: `REQUIRED object containing row values. Use columns: ${columnNames}. Example value: ${JSON.stringify(exampleData)}`,
}
}
// Enrich rows parameter for batch insert
if (enrichedProperties.rows && toolId === 'table_batch_insert_rows') {
enrichedProperties.rows = {
...enrichedProperties.rows,
description: `REQUIRED. Array of row objects. Each object uses columns: ${columnNames}`,
}
}
return {
properties: enrichedProperties,
required: enrichedRequired,
}
}

View File

@@ -75,6 +75,33 @@ export const openaiProvider: ProviderConfig = {
}))
: undefined
// === DEBUG: Log full request details ===
logger.info('[OpenAIProvider] === FULL REQUEST DEBUG ===')
logger.info(`[OpenAIProvider] Messages: ${allMessages.length} total`)
for (const [i, m] of allMessages.entries()) {
const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
logger.info(`[OpenAIProvider] [${i}] ${m.role}: ${content?.substring(0, 150)}...`)
}
// Log tool definitions with formatted JSON
if (tools?.length) {
for (const tool of tools) {
logger.info(`[OpenAIProvider] Tool: ${tool.function.name}`)
logger.info(
`[OpenAIProvider] Description: ${tool.function.description?.substring(0, 200)}...`
)
logger.info(`[OpenAIProvider] Parameters:`)
const params = tool.function.parameters as any
if (params?.properties) {
for (const [key, val] of Object.entries(params.properties)) {
const desc = (val as any).description || ''
logger.info(`[OpenAIProvider] - ${key}: ${desc.substring(0, 100)}`)
}
}
}
}
logger.info('[OpenAIProvider] === END REQUEST DEBUG ===')
const payload: any = {
model: request.model,
messages: allMessages,
@@ -293,6 +320,17 @@ export const openaiProvider: ProviderConfig = {
try {
const toolArgs = JSON.parse(toolCall.function.arguments)
// === DEBUG: Log LLM tool call details ===
logger.info('[OpenAIProvider] === LLM TOOL CALL ===')
logger.info(`[OpenAIProvider] Tool: ${toolName}`)
logger.info(`[OpenAIProvider] Arguments: ${JSON.stringify(toolArgs, null, 2)}`)
if (toolName.startsWith('table_')) {
const filterStr = toolArgs.filter ? JSON.stringify(toolArgs.filter, null, 2) : 'NONE'
logger.info(`[OpenAIProvider] Filter: ${filterStr}`)
logger.info(`[OpenAIProvider] Limit: ${toolArgs.limit || 'default'}`)
}
const tool = request.tools?.find((t) => t.id === toolName)
if (!tool) {
@@ -300,9 +338,22 @@ export const openaiProvider: ProviderConfig = {
}
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
logger.info(`[OpenAIProvider] Executing ${toolName}...`)
const result = await executeTool(toolName, executionParams, true)
const toolCallEndTime = Date.now()
// === DEBUG: Log tool result ===
const resultString = JSON.stringify(result)
const sizeKB = Math.round(resultString.length / 1024)
logger.info(`[OpenAIProvider] === TOOL RESULT ===`)
logger.info(`[OpenAIProvider] Success: ${result.success}, Size: ${sizeKB}KB`)
if (result.output?.rows) {
logger.info(
`[OpenAIProvider] Rows returned: ${result.output.rows.length}, Total matching: ${result.output.totalCount}`
)
}
return {
toolCall,
toolName,
@@ -383,10 +434,17 @@ export const openaiProvider: ProviderConfig = {
success: result.success,
})
const toolMessageContent = JSON.stringify(resultContent)
const msgSizeKB = Math.round(toolMessageContent.length / 1024)
const estTokens = Math.round(toolMessageContent.length / 4)
logger.info(
`[OpenAIProvider] Adding ${toolName} result to conversation: ${msgSizeKB}KB (~${estTokens} tokens)`
)
currentMessages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(resultContent),
content: toolMessageContent,
})
}

View File

@@ -3,6 +3,7 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
import type { CompletionUsage } from 'openai/resources/completions'
import { env } from '@/lib/core/config/env'
import { isHosted } from '@/lib/core/config/feature-flags'
import { enrichTableToolDescription, enrichTableToolParameters } from '@/lib/table/llm-enrichment'
import { isCustomTool } from '@/executor/constants'
import {
getComputerUseModels,
@@ -420,9 +421,20 @@ export async function transformBlockTool(
getAllBlocks: () => any[]
getTool: (toolId: string) => any
getToolAsync?: (toolId: string) => Promise<any>
workspaceId?: string
workflowId?: string
executeTool?: (toolId: string, params: Record<string, any>) => Promise<any>
}
): Promise<ProviderToolConfig | null> {
const { selectedOperation, getAllBlocks, getTool, getToolAsync } = options
const {
selectedOperation,
getAllBlocks,
getTool,
getToolAsync,
workspaceId,
workflowId,
executeTool,
} = options
const blockDef = getAllBlocks().find((b: any) => b.type === block.type)
if (!blockDef) {
@@ -485,12 +497,60 @@ export async function transformBlockTool(
uniqueToolId = `${toolConfig.id}_${userProvidedParams.knowledgeBaseId}`
}
// Enrich table tool descriptions with schema information
let enrichedDescription = toolConfig.description
let enrichedLlmSchema = llmSchema
if (
toolId.startsWith('table_') &&
userProvidedParams.tableId &&
workspaceId &&
workflowId &&
executeTool
) {
try {
logger.info(`[transformBlockTool] Fetching schema for table ${userProvidedParams.tableId}`)
const schemaResult = await executeTool('table_get_schema', {
tableId: userProvidedParams.tableId,
_context: { workspaceId, workflowId },
})
if (schemaResult.success && schemaResult.output) {
const tableSchema = {
name: schemaResult.output.name,
columns: schemaResult.output.columns || [],
}
// Enrich description and parameters using lib/table utilities
enrichedDescription = enrichTableToolDescription(
toolConfig.description,
tableSchema,
toolId
)
const enrichedParams = enrichTableToolParameters(llmSchema, tableSchema, toolId)
enrichedLlmSchema = {
...llmSchema,
properties: enrichedParams.properties,
required:
enrichedParams.required.length > 0 ? enrichedParams.required : llmSchema.required,
}
logger.info(
`[transformBlockTool] Enriched ${toolId} with ${tableSchema.columns.length} columns`
)
} else {
logger.warn(`[transformBlockTool] Failed to fetch table schema: ${schemaResult.error}`)
}
} catch (error) {
logger.warn(`[transformBlockTool] Error fetching table schema:`, error)
}
}
return {
id: uniqueToolId,
name: toolConfig.name,
description: toolConfig.description,
description: enrichedDescription,
params: userProvidedParams,
parameters: llmSchema,
parameters: enrichedLlmSchema,
}
}