feat(core): enhance tollcall validation with better error reporting
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
@@ -82,25 +83,9 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
|
|||||||
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
|
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix primitive action input for single-field tools
|
// validate and fix action input using tool schemas
|
||||||
// e.g. {"click_element_by_index": 2} → {"click_element_by_index": {"index": 2}}
|
|
||||||
if (resolvedArguments.action && tools) {
|
if (resolvedArguments.action && tools) {
|
||||||
const action = resolvedArguments.action
|
resolvedArguments.action = validateAction(resolvedArguments.action, tools)
|
||||||
const toolName = Object.keys(action)[0]
|
|
||||||
const value = action[toolName]
|
|
||||||
const schema = toolName && tools.get(toolName)?.inputSchema
|
|
||||||
|
|
||||||
if (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {
|
|
||||||
const requiredKey = Object.keys(schema.shape).find(
|
|
||||||
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
|
|
||||||
)
|
|
||||||
if (requiredKey) {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(`[normalizeResponse] #6: coercing primitive action input for "${toolName}"`)
|
|
||||||
)
|
|
||||||
resolvedArguments.action = { [toolName]: { [requiredKey]: value } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix incomplete formats
|
// fix incomplete formats
|
||||||
@@ -133,6 +118,55 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate action against tool schemas. Provides clear error messages
|
||||||
|
* instead of letting the union schema produce unreadable errors.
|
||||||
|
*
|
||||||
|
* Also coerces primitive inputs for single-field tools:
|
||||||
|
* e.g. `{"click_element_by_index": 2}` → `{"click_element_by_index": {"index": 2}}`
|
||||||
|
*/
|
||||||
|
function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
||||||
|
if (typeof action !== 'object' || action === null) return action
|
||||||
|
|
||||||
|
const toolName = Object.keys(action)[0]
|
||||||
|
if (!toolName) return action
|
||||||
|
|
||||||
|
const tool = tools.get(toolName)
|
||||||
|
if (!tool) {
|
||||||
|
const available = Array.from(tools.keys()).join(', ')
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
|
`Unknown action "${toolName}". Available: ${available}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = action[toolName]
|
||||||
|
const schema = tool.inputSchema
|
||||||
|
|
||||||
|
// coerce primitive input for single-field tools
|
||||||
|
if (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {
|
||||||
|
const requiredKey = Object.keys(schema.shape).find(
|
||||||
|
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
|
||||||
|
)
|
||||||
|
if (requiredKey) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(`[normalizeResponse] coercing primitive action input for "${toolName}"`)
|
||||||
|
)
|
||||||
|
value = { [requiredKey]: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = schema.safeParse(value)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
|
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [toolName]: result.data }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely parse JSON, return original input if not json.
|
* Safely parse JSON, return original input if not json.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { OpenAIClient } from './OpenAIClient'
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
||||||
import { InvokeError } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
|
|
||||||
export type { InvokeError, InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
export { InvokeError, InvokeErrorType }
|
||||||
|
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
||||||
|
|
||||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||||
// Runtime validation as defensive programming (types already guarantee these)
|
// Runtime validation as defensive programming (types already guarantee these)
|
||||||
|
|||||||
Reference in New Issue
Block a user