142 lines
5.3 KiB
JavaScript
142 lines
5.3 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Example & Test: Tool error handling and LLM self-correction
|
||
*
|
||
* Demonstrates:
|
||
* 1. A tool throwing an Error (e.g. invalid parameter format).
|
||
* 2. LLMOrbiter catching the error, emitting a 'tool_error' event, and
|
||
* formatting the error back to the LLM in the tool message.
|
||
* 3. The LLM inspecting the error feedback and retrying the tool call with corrected arguments.
|
||
* 4. Successful run completion.
|
||
*
|
||
* Usage:
|
||
* node examples/tool-error-retry.js
|
||
* node examples/tool-error-retry.js --model deepseek/deepseek-v4-flash-0731
|
||
*/
|
||
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import dotenv from 'dotenv';
|
||
|
||
import { LLMOrbiter, OpenRouterProvider, defineTool, ToolError } from '../src/index.js';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||
|
||
const args = process.argv.slice(2);
|
||
function getArg(flag) {
|
||
const idx = args.indexOf(flag);
|
||
return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
|
||
}
|
||
|
||
const apiKey = getArg('--key') || process.env.OPENROUTER_API_KEY;
|
||
const model = getArg('--model') || 'deepseek/deepseek-v4-flash-0731';
|
||
|
||
if (!apiKey) {
|
||
console.error('Error: Missing API key. Set OPENROUTER_API_KEY in examples/.env or pass --key <api-key>');
|
||
process.exit(1);
|
||
}
|
||
|
||
// ─── Tool with error throwing ───────────────────────────────────────────────────
|
||
|
||
let callCount = 0;
|
||
|
||
const orderLookupTool = defineTool({
|
||
name: 'lookup_order',
|
||
description: 'Look up an order by order ID.',
|
||
parameters: {
|
||
type: 'object',
|
||
required: ['orderId'],
|
||
additionalProperties: false,
|
||
properties: {
|
||
orderId: {
|
||
type: 'string',
|
||
description: 'The order ID to check',
|
||
},
|
||
},
|
||
},
|
||
invoke: async ({ orderId }) => {
|
||
callCount++;
|
||
// Enforce the requirement that order IDs must start with "ORD-"
|
||
if (!orderId.startsWith('ORD-')) {
|
||
throw new ToolError(
|
||
`Invalid order ID "${orderId}". Order IDs must start with the prefix "ORD-", e.g. "ORD-${orderId}". Please correct the ID and retry.`
|
||
);
|
||
}
|
||
|
||
return {
|
||
orderId,
|
||
status: 'Delivered',
|
||
item: 'Mechanical Keyboard',
|
||
trackingNumber: 'TRK-98765432',
|
||
};
|
||
},
|
||
});
|
||
|
||
// ─── Main ───────────────────────────────────────────────────────────────────────
|
||
|
||
async function main() {
|
||
console.log('🧪 Starting Tool Error & Retry Test...\n');
|
||
|
||
const provider = new OpenRouterProvider({ apiKey });
|
||
const events = [];
|
||
|
||
const orbiter = new LLMOrbiter(provider, {
|
||
onStatus: (status) => {
|
||
events.push({ type: 'status', data: status });
|
||
console.log(`⚡ ${status}`);
|
||
},
|
||
onToolCall: (call) => {
|
||
events.push({ type: 'tool_call', data: call });
|
||
console.log(`🔧 Tool call: ${call.name}(${JSON.stringify(call.args)})`);
|
||
},
|
||
onToolError: (err) => {
|
||
events.push({ type: 'tool_error', data: err });
|
||
console.log(`❌ Tool error:`, err.error);
|
||
},
|
||
onToolResult: (res) => {
|
||
events.push({ type: 'tool_result', data: res });
|
||
console.log(`📦 Tool result:`, res.result);
|
||
},
|
||
onUsage: (usage) => {
|
||
events.push({ type: 'usage', data: usage });
|
||
console.log(`📊 LLM call usage:`, usage);
|
||
},
|
||
});
|
||
|
||
const result = await orbiter.run({
|
||
prompt: 'Please check the status of order 54321. If the tool reports an error, fix the ID according to the error message and call the tool again.',
|
||
model,
|
||
tools: [orderLookupTool],
|
||
});
|
||
|
||
console.log('\n💬 Final Response:\n', result.content);
|
||
|
||
// ─── Test Assertions ─────────────────────────────────────────────────────────
|
||
console.log('\n─── Test Results ───');
|
||
|
||
const errorEvent = events.find((e) => e.type === 'tool_error');
|
||
const resultEvent = events.find((e) => e.type === 'tool_result');
|
||
const toolCalls = events.filter((e) => e.type === 'tool_call');
|
||
|
||
const hasError = !!errorEvent;
|
||
const hasRetry = toolCalls.length >= 2;
|
||
const hasResult = !!resultEvent;
|
||
const hasContent = !!result.content;
|
||
|
||
console.log(hasError ? '✅ PASS: Tool error was thrown and caught' : '❌ FAIL: Tool error was not emitted');
|
||
console.log(hasRetry ? '✅ PASS: LLM called the tool again with correction' : '❌ FAIL: Tool was not called again');
|
||
console.log(hasResult ? '✅ PASS: Corrected tool call succeeded' : '❌ FAIL: Tool never succeeded');
|
||
console.log(hasContent ? '✅ PASS: LLM generated final answer from result' : '❌ FAIL: No final response');
|
||
|
||
if (hasError && hasRetry && hasResult && hasContent) {
|
||
console.log('\n🎉 ALL TESTS PASSED: Tool error and retry workflow verified successfully!\n');
|
||
} else {
|
||
console.error('\n⚠️ Some assertions failed.\n');
|
||
process.exitCode = 1;
|
||
}
|
||
}
|
||
|
||
main().catch(console.error);
|