@pedrofariasx/qwenproxy 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -13
- package/package.json +1 -1
- package/src/api/server.ts +0 -2
- package/src/cache/memory-cache.ts +3 -2
- package/src/routes/chat.ts +123 -77
- package/src/routes/upload.ts +4 -4
- package/src/services/playwright.ts +1 -0
- package/src/services/qwen.ts +22 -13
- package/src/tools/parser.ts +10 -13
- package/src/utils/context-truncation.ts +0 -5
- package/src/linter/extraction-engine.ts +0 -165
- package/src/linter/index.ts +0 -258
- package/src/linter/repair-normalize.ts +0 -245
- package/src/linter/safety-gate.ts +0 -219
- package/src/linter/streaming-state-machine.ts +0 -252
- package/src/linter/structural-parser.ts +0 -352
- package/src/linter/types.ts +0 -74
- package/src/tests/linter.test.ts +0 -151
- package/src/tests/parallel.test.ts +0 -42
- package/src/tests/structureVerification.test.ts +0 -176
- package/src/tools/ast.ts +0 -15
- package/src/tools/coercion.ts +0 -67
- package/src/tools/confidence.ts +0 -48
- package/src/tools/detector.ts +0 -40
- package/src/tools/executor.ts +0 -236
- package/src/tools/pipeline.ts +0 -122
- package/src/tools/registry-runtime.ts +0 -34
- package/src/tools/repair.ts +0 -42
- package/src/tools/validator.ts +0 -33
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Structure Verification Tests
|
|
3
|
-
* Verifies that the identified issues are correct and the logic works as expected.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, beforeEach } from 'node:test'
|
|
7
|
-
import assert from 'node:assert'
|
|
8
|
-
|
|
9
|
-
import { UltraToolCallLinter } from '../linter/index.js'
|
|
10
|
-
import type { ParseResult, RawToolCandidate } from '../linter/types.js'
|
|
11
|
-
|
|
12
|
-
// Helper functions to match the old bar.ts API using direct instantiation
|
|
13
|
-
let globalLinter: UltraToolCallLinter | null = null
|
|
14
|
-
|
|
15
|
-
function getGlobalLinter(): UltraToolCallLinter {
|
|
16
|
-
if (!globalLinter) globalLinter = new UltraToolCallLinter()
|
|
17
|
-
return globalLinter
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function configure(config: { registry?: any; strictMode?: boolean; enableSecurityGate?: boolean; maxRecoveryAttempts?: number; minConfidenceThreshold?: number }): void {
|
|
21
|
-
globalLinter = new UltraToolCallLinter(config)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseText(input: string): ParseResult {
|
|
25
|
-
return getGlobalLinter().parseText(input)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function extract(input: string): RawToolCandidate[] {
|
|
29
|
-
return getGlobalLinter().extract(input)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function repair(input: string): string {
|
|
33
|
-
return getGlobalLinter().repair(input)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Test 2: Verify CandidateSpan is defined but unused
|
|
37
|
-
type TestCandidateSpan = {
|
|
38
|
-
startIndex: number
|
|
39
|
-
endIndex: number | null
|
|
40
|
-
rawContent: string
|
|
41
|
-
sourceHint: string
|
|
42
|
-
isComplete: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Test 3: Verify ParseResult structure
|
|
46
|
-
|
|
47
|
-
describe('Structure Verification Tests', () => {
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
configure({ strictMode: false, minConfidenceThreshold: 0.1 })
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
describe('Direct import from index.js verification', () => {
|
|
53
|
-
it('should verify UltraToolCallLinter methods work correctly', () => {
|
|
54
|
-
const linter = new UltraToolCallLinter()
|
|
55
|
-
|
|
56
|
-
assert.strictEqual(typeof linter.parse, 'function')
|
|
57
|
-
assert.strictEqual(typeof linter.push, 'function')
|
|
58
|
-
assert.strictEqual(typeof linter.extract, 'function')
|
|
59
|
-
assert.strictEqual(typeof linter.repair, 'function')
|
|
60
|
-
assert.strictEqual(typeof linter.parseText, 'function')
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('should verify parseText works through foo -> bar -> index chain', () => {
|
|
64
|
-
const input = '{"tool":"search","input":{"query":"test"}}'
|
|
65
|
-
const result = parseText(input)
|
|
66
|
-
|
|
67
|
-
assert.ok(Array.isArray(result.toolCalls))
|
|
68
|
-
assert.ok(typeof result.confidence === 'number')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should verify extract works through foo -> bar -> index chain', () => {
|
|
72
|
-
const input = '{"tool":"search","input":{"query":"test"}}'
|
|
73
|
-
const candidates = extract(input)
|
|
74
|
-
|
|
75
|
-
assert.ok(Array.isArray(candidates))
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('should verify repair works through foo -> bar -> index chain', () => {
|
|
79
|
-
const input = "{'tool': 'search'}"
|
|
80
|
-
const repaired = repair(input)
|
|
81
|
-
|
|
82
|
-
assert.ok(typeof repaired === 'string')
|
|
83
|
-
assert.ok(repaired.includes('search'))
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
describe('Unused type: CandidateSpan verification', () => {
|
|
88
|
-
it('CandidateSpan type exists in types.ts but is not used anywhere', () => {
|
|
89
|
-
// This test documents that CandidateSpan is defined but unused
|
|
90
|
-
const exampleSpan: TestCandidateSpan = {
|
|
91
|
-
startIndex: 0,
|
|
92
|
-
endIndex: 10,
|
|
93
|
-
rawContent: '{"tool":"test"}',
|
|
94
|
-
sourceHint: 'json',
|
|
95
|
-
isComplete: true
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
assert.ok(exampleSpan.startIndex === 0)
|
|
99
|
-
assert.ok(exampleSpan.isComplete === true)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('ParseResult structure verification', () => {
|
|
104
|
-
it('ParseResult has all required fields', () => {
|
|
105
|
-
const parseResult: ParseResult = {
|
|
106
|
-
text: 'test',
|
|
107
|
-
toolCalls: [],
|
|
108
|
-
errors: [],
|
|
109
|
-
confidence: 0.5
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
assert.ok(Array.isArray(parseResult.toolCalls))
|
|
113
|
-
assert.ok(typeof parseResult.confidence === 'number')
|
|
114
|
-
assert.ok(Array.isArray(parseResult.errors))
|
|
115
|
-
assert.ok(typeof parseResult.text === 'string')
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('parseText returns ParseResult with all required fields', () => {
|
|
119
|
-
const input = '{"tool":"search","input":{"query":"test"}}'
|
|
120
|
-
const result = parseText(input)
|
|
121
|
-
|
|
122
|
-
// Should have all ParserResult fields + confidence
|
|
123
|
-
assert.ok(typeof result.text === 'string')
|
|
124
|
-
assert.ok(Array.isArray(result.toolCalls))
|
|
125
|
-
assert.ok(Array.isArray(result.errors))
|
|
126
|
-
assert.ok(typeof result.confidence === 'number')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('src/tools/ directory isolation verification', () => {
|
|
131
|
-
it('tools/registry.ts has syntax error (Map initialization)', async () => {
|
|
132
|
-
// This test documents that tools/registry.ts has a bug:
|
|
133
|
-
// const toolRegistry: Map<string, ToolRegistration> = Map()
|
|
134
|
-
// Should be: new Map()
|
|
135
|
-
// The file is unused by the main codebase anyway
|
|
136
|
-
|
|
137
|
-
const registryModule = await import('../tools/registry.js').catch(() => null)
|
|
138
|
-
|
|
139
|
-
// If the module can't be imported due to syntax error, that confirms the issue
|
|
140
|
-
if (registryModule === null) {
|
|
141
|
-
assert.ok(true, 'Registry module has syntax error as expected')
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('tools/parser.ts StreamingToolParser is used by main codebase', async () => {
|
|
146
|
-
// This verifies that StreamingToolParser IS actually used
|
|
147
|
-
const { StreamingToolParser } = await import('../tools/parser.js')
|
|
148
|
-
|
|
149
|
-
assert.ok(typeof StreamingToolParser === 'function')
|
|
150
|
-
|
|
151
|
-
const parser = new StreamingToolParser()
|
|
152
|
-
assert.ok(typeof parser.feed === 'function')
|
|
153
|
-
})
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
describe('LinterConfig duplication verification', () => {
|
|
157
|
-
it('LinterConfig in index.ts duplicates constructor param type from bar.ts', () => {
|
|
158
|
-
// bar.ts defines inline config type:
|
|
159
|
-
// { registry?: ToolRegistry; strictMode?: boolean; ... }
|
|
160
|
-
//
|
|
161
|
-
// index.ts defines LinterConfig interface:
|
|
162
|
-
// { registry?: ToolRegistry; strictMode?: boolean; ... }
|
|
163
|
-
//
|
|
164
|
-
// These are functionally identical but defined separately
|
|
165
|
-
|
|
166
|
-
const linter = new UltraToolCallLinter({
|
|
167
|
-
strictMode: true,
|
|
168
|
-
enableSecurityGate: false,
|
|
169
|
-
maxRecoveryAttempts: 5,
|
|
170
|
-
minConfidenceThreshold: 0.5
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
assert.ok(linter instanceof UltraToolCallLinter)
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
})
|
package/src/tools/ast.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { JsonSchema } from './types.js';
|
|
2
|
-
|
|
3
|
-
export interface ToolCallAST {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
arguments: unknown;
|
|
7
|
-
raw: string;
|
|
8
|
-
confidence: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ToolDefinition {
|
|
12
|
-
name: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
schema: JsonSchema;
|
|
15
|
-
}
|
package/src/tools/coercion.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { JsonSchema } from './types.js';
|
|
2
|
-
|
|
3
|
-
export function coerceArguments(args: unknown, schema: JsonSchema): unknown {
|
|
4
|
-
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
5
|
-
return args;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const coerced: Record<string, unknown> = {};
|
|
9
|
-
const properties = schema.properties || {};
|
|
10
|
-
|
|
11
|
-
for (const [key, value] of Object.entries(args as Record<string, unknown>)) {
|
|
12
|
-
const propSchema = properties[key];
|
|
13
|
-
if (propSchema && typeof propSchema === 'object') {
|
|
14
|
-
coerced[key] = coerceValue(value, propSchema);
|
|
15
|
-
} else {
|
|
16
|
-
coerced[key] = value;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return coerced;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function coerceValue(value: unknown, schema: JsonSchema): unknown {
|
|
24
|
-
if (typeof value !== 'string') {
|
|
25
|
-
if (Array.isArray(value) && schema.type === 'array' && schema.items) {
|
|
26
|
-
return value.map(item => coerceValue(item, schema.items!));
|
|
27
|
-
}
|
|
28
|
-
if (typeof value === 'object' && value !== null && schema.type === 'object' && schema.properties) {
|
|
29
|
-
return coerceArguments(value, { type: 'object', properties: schema.properties!, required: schema.required });
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const trimmed = value.trim();
|
|
35
|
-
|
|
36
|
-
if (schema.type === 'boolean') {
|
|
37
|
-
if (trimmed.toLowerCase() === 'true') return true;
|
|
38
|
-
if (trimmed.toLowerCase() === 'false') return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (schema.type === 'integer' || schema.type === 'number') {
|
|
42
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
43
|
-
const num = Number(trimmed);
|
|
44
|
-
if (!Number.isNaN(num)) {
|
|
45
|
-
return schema.type === 'integer' ? Math.trunc(num) : num;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (schema.type === 'array' && trimmed.startsWith('[')) {
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(trimmed);
|
|
53
|
-
} catch {
|
|
54
|
-
// fall through
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (schema.type === 'object' && trimmed.startsWith('{')) {
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(trimmed);
|
|
61
|
-
} catch {
|
|
62
|
-
// fall through
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return value;
|
|
67
|
-
}
|
package/src/tools/confidence.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { ToolCallAST, ToolDefinition } from './ast.js';
|
|
2
|
-
|
|
3
|
-
export interface ConfidenceResult {
|
|
4
|
-
score: number;
|
|
5
|
-
reasons: string[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function calculateConfidence(ast: ToolCallAST, toolDef: ToolDefinition): ConfidenceResult {
|
|
9
|
-
let score = 0.0;
|
|
10
|
-
const reasons: string[] = [];
|
|
11
|
-
|
|
12
|
-
if (ast.name && toolDef.name === ast.name) {
|
|
13
|
-
score += 0.4;
|
|
14
|
-
reasons.push('Tool name matches registry');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (ast.arguments && typeof ast.arguments === 'object' && !Array.isArray(ast.arguments)) {
|
|
18
|
-
score += 0.3;
|
|
19
|
-
reasons.push('Arguments are a valid object');
|
|
20
|
-
} else if (typeof ast.arguments === 'string') {
|
|
21
|
-
try {
|
|
22
|
-
JSON.parse(ast.arguments as string);
|
|
23
|
-
score += 0.15;
|
|
24
|
-
reasons.push('Arguments are a valid JSON string');
|
|
25
|
-
} catch {
|
|
26
|
-
reasons.push('Arguments are an invalid JSON string');
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
reasons.push('Arguments are missing or invalid type');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const schema = toolDef.schema as any;
|
|
33
|
-
if (schema && schema.required && Array.isArray(schema.required)) {
|
|
34
|
-
const args = (ast.arguments as Record<string, unknown>) || {};
|
|
35
|
-
const hasAllRequired = schema.required.every((field: string) => field in args);
|
|
36
|
-
if (hasAllRequired) {
|
|
37
|
-
score += 0.3;
|
|
38
|
-
reasons.push('All required fields are present');
|
|
39
|
-
} else {
|
|
40
|
-
reasons.push('Missing required fields');
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
score += 0.3;
|
|
44
|
-
reasons.push('No required fields to validate');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { score: Math.min(score, 1.0), reasons };
|
|
48
|
-
}
|
package/src/tools/detector.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { robustParseJSON } from '../utils/json.js';
|
|
2
|
-
|
|
3
|
-
export interface DetectedTool {
|
|
4
|
-
raw: string;
|
|
5
|
-
extracted: Record<string, unknown>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const TOOL_CALL_REGEX = /\b[^>]*>([\s\S]*?)<\/tool_call>/gi;
|
|
9
|
-
|
|
10
|
-
export function detectToolCalls(text: string): DetectedTool[] {
|
|
11
|
-
const results: DetectedTool[] = [];
|
|
12
|
-
|
|
13
|
-
let match;
|
|
14
|
-
while ((match = TOOL_CALL_REGEX.exec(text)) !== null) {
|
|
15
|
-
const raw = match[0];
|
|
16
|
-
const content = match[1].trim();
|
|
17
|
-
const parsed = robustParseJSON(content);
|
|
18
|
-
if (parsed && typeof parsed === 'object') {
|
|
19
|
-
results.push({ raw, extracted: parsed });
|
|
20
|
-
} else {
|
|
21
|
-
results.push({ raw, extracted: { _rawContent: content } });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (results.length === 0) {
|
|
26
|
-
const parsed = robustParseJSON(text);
|
|
27
|
-
if (parsed && typeof parsed === 'object') {
|
|
28
|
-
if (
|
|
29
|
-
'name' in parsed ||
|
|
30
|
-
'tool' in parsed ||
|
|
31
|
-
'function' in parsed ||
|
|
32
|
-
'function_call' in parsed
|
|
33
|
-
) {
|
|
34
|
-
results.push({ raw: text.trim(), extracted: parsed });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return results;
|
|
40
|
-
}
|
package/src/tools/executor.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* File: executor.ts
|
|
3
|
-
* Project: qwenproxy
|
|
4
|
-
* Execution loop for tool calling - agentic loop that handles
|
|
5
|
-
* send -> tool calls -> execute -> re-send until completion
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
-
import type { ParsedToolCall, ToolCallResult, ToolContext } from './types';
|
|
10
|
-
import { SchemaValidationError } from './schema.js';
|
|
11
|
-
import { registry } from './registry';
|
|
12
|
-
import { robustParseJSON } from '../utils/json.js';
|
|
13
|
-
|
|
14
|
-
export interface ExecutionLoopConfig {
|
|
15
|
-
maxTurns?: number;
|
|
16
|
-
debug?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface LoopTurnResult {
|
|
20
|
-
toolCalls: ParsedToolCall[];
|
|
21
|
-
toolResults: ToolCallResult[];
|
|
22
|
-
content: string | null;
|
|
23
|
-
finishReason: string | null;
|
|
24
|
-
turn: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type LLMSendFunction = (
|
|
28
|
-
messages: unknown[],
|
|
29
|
-
tools: unknown[] | undefined,
|
|
30
|
-
model: string
|
|
31
|
-
) => Promise<LLMResponse>;
|
|
32
|
-
|
|
33
|
-
export interface LLMResponse {
|
|
34
|
-
content: string | null;
|
|
35
|
-
toolCalls: ParsedToolCall[];
|
|
36
|
-
finishReason: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const TOOL_START_TAG = '<' + 'tool_call>';
|
|
40
|
-
const TOOL_END_TAG = '</' + 'tool_call>';
|
|
41
|
-
|
|
42
|
-
export function parseToolCallsFromContent(content: string): {
|
|
43
|
-
textContent: string;
|
|
44
|
-
toolCalls: ParsedToolCall[];
|
|
45
|
-
} {
|
|
46
|
-
const toolCalls: ParsedToolCall[] = [];
|
|
47
|
-
let remaining = content;
|
|
48
|
-
let textContent = '';
|
|
49
|
-
|
|
50
|
-
while (true) {
|
|
51
|
-
const startIdx = remaining.indexOf(TOOL_START_TAG);
|
|
52
|
-
if (startIdx === -1) {
|
|
53
|
-
textContent += remaining;
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
textContent += remaining.substring(0, startIdx);
|
|
58
|
-
|
|
59
|
-
const endIdx = remaining.indexOf(TOOL_END_TAG, startIdx + TOOL_START_TAG.length);
|
|
60
|
-
if (endIdx === -1) {
|
|
61
|
-
textContent += remaining.substring(startIdx);
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const jsonStr = remaining
|
|
66
|
-
.substring(startIdx + TOOL_START_TAG.length, endIdx)
|
|
67
|
-
.trim();
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const parsed = robustParseJSON(jsonStr);
|
|
71
|
-
if (!parsed) throw new Error('Failed to parse JSON');
|
|
72
|
-
|
|
73
|
-
toolCalls.push({
|
|
74
|
-
id: 'call_' + uuidv4(),
|
|
75
|
-
name: parsed.name || '',
|
|
76
|
-
arguments: parsed.arguments
|
|
77
|
-
? (typeof parsed.arguments === 'string' ? JSON.parse(parsed.arguments) : parsed.arguments)
|
|
78
|
-
: (() => {
|
|
79
|
-
const { name, ...rest } = parsed;
|
|
80
|
-
return rest;
|
|
81
|
-
})(),
|
|
82
|
-
});
|
|
83
|
-
} catch (e) {
|
|
84
|
-
textContent += TOOL_START_TAG + jsonStr + TOOL_END_TAG;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
remaining = remaining.substring(endIdx + TOOL_END_TAG.length);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return { textContent: textContent.trim(), toolCalls };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function executeToolCalls(
|
|
94
|
-
toolCalls: ParsedToolCall[],
|
|
95
|
-
context: ToolContext
|
|
96
|
-
): Promise<ToolCallResult[]> {
|
|
97
|
-
return await Promise.all(
|
|
98
|
-
toolCalls.map(async (tc) => {
|
|
99
|
-
try {
|
|
100
|
-
if (!registry.has(tc.name)) {
|
|
101
|
-
return {
|
|
102
|
-
toolCallId: tc.id,
|
|
103
|
-
name: tc.name,
|
|
104
|
-
result: JSON.stringify({ error: `Unknown tool: '${tc.name}'` }),
|
|
105
|
-
isError: true,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const result = await registry.execute(tc.name, tc.arguments, context);
|
|
110
|
-
return {
|
|
111
|
-
toolCallId: tc.id,
|
|
112
|
-
name: tc.name,
|
|
113
|
-
result,
|
|
114
|
-
isError: false,
|
|
115
|
-
};
|
|
116
|
-
} catch (err: unknown) {
|
|
117
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
-
const isValidation = err instanceof SchemaValidationError;
|
|
119
|
-
return {
|
|
120
|
-
toolCallId: tc.id,
|
|
121
|
-
name: tc.name,
|
|
122
|
-
result: JSON.stringify({
|
|
123
|
-
error: isValidation ? 'Schema validation failed' : 'Tool execution error',
|
|
124
|
-
details: message,
|
|
125
|
-
...(isValidation ? { path: (err as SchemaValidationError).path } : {}),
|
|
126
|
-
}),
|
|
127
|
-
isError: true,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function buildToolMessage(result: ToolCallResult): Record<string, unknown> {
|
|
135
|
-
return {
|
|
136
|
-
role: 'tool',
|
|
137
|
-
tool_call_id: result.toolCallId,
|
|
138
|
-
content: result.result,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function buildAssistantToolCallMessage(
|
|
143
|
-
content: string | null,
|
|
144
|
-
toolCalls: ParsedToolCall[]
|
|
145
|
-
): Record<string, unknown> {
|
|
146
|
-
return {
|
|
147
|
-
role: 'assistant',
|
|
148
|
-
content: content || null,
|
|
149
|
-
tool_calls: toolCalls.map((tc) => ({
|
|
150
|
-
id: tc.id,
|
|
151
|
-
type: 'function',
|
|
152
|
-
function: {
|
|
153
|
-
name: tc.name,
|
|
154
|
-
arguments: typeof tc.arguments === 'string'
|
|
155
|
-
? tc.arguments
|
|
156
|
-
: JSON.stringify(tc.arguments),
|
|
157
|
-
},
|
|
158
|
-
})),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function runExecutionLoop(
|
|
163
|
-
sendToLLM: LLMSendFunction,
|
|
164
|
-
messages: unknown[],
|
|
165
|
-
model: string,
|
|
166
|
-
config: ExecutionLoopConfig = {}
|
|
167
|
-
): Promise<string> {
|
|
168
|
-
const maxTurns = config.maxTurns ?? 10;
|
|
169
|
-
const debug = config.debug ?? false;
|
|
170
|
-
|
|
171
|
-
const tools = registry.listNames().length > 0
|
|
172
|
-
? registry.toOpenAITools()
|
|
173
|
-
: undefined;
|
|
174
|
-
|
|
175
|
-
for (let turn = 0; turn < maxTurns; turn++) {
|
|
176
|
-
if (debug) {
|
|
177
|
-
console.log(`[executor] Turn ${turn + 1}/${maxTurns}, messages: ${messages.length}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const response = await sendToLLM(messages, tools, model);
|
|
181
|
-
|
|
182
|
-
const hasStructuredToolCalls = response.toolCalls && response.toolCalls.length > 0;
|
|
183
|
-
let parsedFromContent: { textContent: string; toolCalls: ParsedToolCall[] } | null = null;
|
|
184
|
-
|
|
185
|
-
if (!hasStructuredToolCalls && response.content) {
|
|
186
|
-
parsedFromContent = parseToolCallsFromContent(response.content);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const effectiveToolCalls = hasStructuredToolCalls
|
|
190
|
-
? response.toolCalls
|
|
191
|
-
: parsedFromContent?.toolCalls || [];
|
|
192
|
-
|
|
193
|
-
const effectiveContent = parsedFromContent
|
|
194
|
-
? parsedFromContent.textContent
|
|
195
|
-
: response.content;
|
|
196
|
-
|
|
197
|
-
if (effectiveToolCalls.length === 0) {
|
|
198
|
-
if (debug) {
|
|
199
|
-
console.log('[executor] No tool calls, loop complete');
|
|
200
|
-
}
|
|
201
|
-
return effectiveContent || '';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const context: ToolContext = {
|
|
205
|
-
messages,
|
|
206
|
-
turn,
|
|
207
|
-
model,
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
if (debug) {
|
|
211
|
-
console.log(
|
|
212
|
-
`[executor] Executing ${effectiveToolCalls.length} tool calls:`,
|
|
213
|
-
effectiveToolCalls.map((tc) => tc.name)
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const toolResults = await executeToolCalls(effectiveToolCalls, context);
|
|
218
|
-
|
|
219
|
-
messages.push(buildAssistantToolCallMessage(effectiveContent, effectiveToolCalls));
|
|
220
|
-
|
|
221
|
-
for (const result of toolResults) {
|
|
222
|
-
messages.push(buildToolMessage(result));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (debug) {
|
|
226
|
-
console.log(
|
|
227
|
-
`[executor] Tool results:`,
|
|
228
|
-
toolResults.map((r) => ({ name: r.name, isError: r.isError }))
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
throw new Error(
|
|
234
|
-
`Execution loop exceeded maximum turns (${maxTurns}). The agent may be stuck in a cycle.`
|
|
235
|
-
);
|
|
236
|
-
}
|
package/src/tools/pipeline.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { ToolCallAST, ToolDefinition } from './ast.js';
|
|
2
|
-
import { detectToolCalls } from './detector.js';
|
|
3
|
-
import { repairToolCall } from './repair.js';
|
|
4
|
-
import { RequestToolRegistry } from './registry-runtime.js';
|
|
5
|
-
import { coerceArguments } from './coercion.js';
|
|
6
|
-
import { validateToolCall, type ValidationResult } from './validator.js';
|
|
7
|
-
import { calculateConfidence } from './confidence.js';
|
|
8
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
-
|
|
10
|
-
export interface PipelineResult {
|
|
11
|
-
textContent: string;
|
|
12
|
-
toolCalls: ToolCallAST[];
|
|
13
|
-
errors: Array<{
|
|
14
|
-
toolName?: string;
|
|
15
|
-
code: string;
|
|
16
|
-
message: string;
|
|
17
|
-
details?: any;
|
|
18
|
-
}>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const CONFIDENCE_THRESHOLD = 0.7;
|
|
22
|
-
|
|
23
|
-
export function processToolCalls(
|
|
24
|
-
text: string,
|
|
25
|
-
requestTools: unknown[]
|
|
26
|
-
): PipelineResult {
|
|
27
|
-
const registry = new RequestToolRegistry(requestTools);
|
|
28
|
-
const detected = detectToolCalls(text);
|
|
29
|
-
|
|
30
|
-
const toolCalls: ToolCallAST[] = [];
|
|
31
|
-
const errors: PipelineResult['errors'] = [];
|
|
32
|
-
let textContent = text;
|
|
33
|
-
|
|
34
|
-
for (const det of detected) {
|
|
35
|
-
while (textContent.includes(det.raw)) {
|
|
36
|
-
textContent = textContent.replace(det.raw, '').trim();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
for (const det of detected) {
|
|
41
|
-
const repaired = repairToolCall(det.extracted);
|
|
42
|
-
if (!repaired) {
|
|
43
|
-
errors.push({
|
|
44
|
-
code: 'MALFORMED_TOOL_CALL',
|
|
45
|
-
message: 'Could not repair or identify tool call structure',
|
|
46
|
-
details: det.extracted,
|
|
47
|
-
});
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!registry.has(repaired.name)) {
|
|
52
|
-
errors.push({
|
|
53
|
-
toolName: repaired.name,
|
|
54
|
-
code: 'UNKNOWN_TOOL',
|
|
55
|
-
message: `Tool '${repaired.name}' is not registered or provided in the request`,
|
|
56
|
-
});
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const toolDef = registry.get(repaired.name)!;
|
|
61
|
-
const coercedArgs = coerceArguments(repaired.arguments, toolDef.schema);
|
|
62
|
-
|
|
63
|
-
const ast: ToolCallAST = {
|
|
64
|
-
id: `call_${uuidv4()}`,
|
|
65
|
-
name: repaired.name,
|
|
66
|
-
arguments: coercedArgs,
|
|
67
|
-
raw: det.raw,
|
|
68
|
-
confidence: 0.0,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const validation: ValidationResult = validateToolCall(ast, toolDef);
|
|
72
|
-
const confidenceResult = calculateConfidence(ast, toolDef);
|
|
73
|
-
ast.confidence = confidenceResult.score;
|
|
74
|
-
|
|
75
|
-
if (!validation.valid) {
|
|
76
|
-
if (validation.missingFields.length > 0) {
|
|
77
|
-
errors.push({
|
|
78
|
-
toolName: ast.name,
|
|
79
|
-
code: 'MISSING_REQUIRED_FIELD',
|
|
80
|
-
message: `Missing required fields: ${validation.missingFields.join(', ')}`,
|
|
81
|
-
details: validation.errors,
|
|
82
|
-
});
|
|
83
|
-
} else {
|
|
84
|
-
errors.push({
|
|
85
|
-
toolName: ast.name,
|
|
86
|
-
code: 'SCHEMA_VALIDATION_FAILED',
|
|
87
|
-
message: 'Arguments do not match the tool schema',
|
|
88
|
-
details: validation.errors,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (ast.confidence >= CONFIDENCE_THRESHOLD) {
|
|
95
|
-
toolCalls.push(ast);
|
|
96
|
-
} else {
|
|
97
|
-
errors.push({
|
|
98
|
-
toolName: ast.name,
|
|
99
|
-
code: 'LOW_CONFIDENCE',
|
|
100
|
-
message: `Tool call confidence ${ast.confidence} is below threshold ${CONFIDENCE_THRESHOLD}`,
|
|
101
|
-
details: confidenceResult.reasons,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
textContent,
|
|
108
|
-
toolCalls,
|
|
109
|
-
errors,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function formatOpenAIToolCalls(toolCalls: ToolCallAST[]): any[] {
|
|
114
|
-
return toolCalls.map(tc => ({
|
|
115
|
-
id: tc.id,
|
|
116
|
-
type: 'function',
|
|
117
|
-
function: {
|
|
118
|
-
name: tc.name,
|
|
119
|
-
arguments: JSON.stringify(tc.arguments),
|
|
120
|
-
},
|
|
121
|
-
}));
|
|
122
|
-
}
|