@animalabs/membrane 0.3.2 → 0.5.1
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/dist/formatters/anthropic-xml.d.ts +63 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -0
- package/dist/formatters/anthropic-xml.js +365 -0
- package/dist/formatters/anthropic-xml.js.map +1 -0
- package/dist/formatters/completions.d.ts +61 -0
- package/dist/formatters/completions.d.ts.map +1 -0
- package/dist/formatters/completions.js +224 -0
- package/dist/formatters/completions.js.map +1 -0
- package/dist/formatters/index.d.ts +8 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +7 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/native.d.ts +35 -0
- package/dist/formatters/native.d.ts.map +1 -0
- package/dist/formatters/native.js +261 -0
- package/dist/formatters/native.js.map +1 -0
- package/dist/formatters/types.d.ts +152 -0
- package/dist/formatters/types.d.ts.map +1 -0
- package/dist/formatters/types.js +7 -0
- package/dist/formatters/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +4 -0
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +65 -32
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/mock.d.ts +82 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +169 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/transforms/index.d.ts +0 -1
- package/dist/transforms/index.d.ts.map +1 -1
- package/dist/transforms/index.js +2 -1
- package/dist/transforms/index.js.map +1 -1
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/streaming.d.ts +6 -0
- package/dist/types/streaming.d.ts.map +1 -1
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +29 -13
- package/dist/utils/tool-parser.js.map +1 -1
- package/package.json +1 -1
- package/src/formatters/anthropic-xml.ts +490 -0
- package/src/formatters/completions.ts +343 -0
- package/src/formatters/index.ts +19 -0
- package/src/formatters/native.ts +340 -0
- package/src/formatters/types.ts +229 -0
- package/src/index.ts +3 -0
- package/src/membrane.ts +86 -45
- package/src/providers/anthropic.ts +3 -2
- package/src/providers/index.ts +7 -0
- package/src/providers/mock.ts +234 -0
- package/src/transforms/index.ts +2 -10
- package/src/types/config.ts +9 -1
- package/src/types/streaming.ts +9 -0
- package/src/utils/tool-parser.ts +32 -11
- package/src/transforms/prefill.ts +0 -574
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic XML Formatter
|
|
3
|
+
*
|
|
4
|
+
* Prefill-based formatting for Anthropic models using XML tool syntax.
|
|
5
|
+
* This is the "classic" membrane format with:
|
|
6
|
+
* - Participant: content format
|
|
7
|
+
* - <function_calls>/<function_results> for tools
|
|
8
|
+
* - <thinking> blocks for extended thinking
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
NormalizedMessage,
|
|
13
|
+
ContentBlock,
|
|
14
|
+
ToolDefinition,
|
|
15
|
+
ToolCall,
|
|
16
|
+
ToolResult,
|
|
17
|
+
} from '../types/index.js';
|
|
18
|
+
import type {
|
|
19
|
+
PrefillFormatter,
|
|
20
|
+
StreamParser,
|
|
21
|
+
BuildOptions,
|
|
22
|
+
BuildResult,
|
|
23
|
+
FormatterConfig,
|
|
24
|
+
ProviderMessage,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
import {
|
|
27
|
+
parseToolCalls as parseToolCallsXml,
|
|
28
|
+
formatToolResults as formatToolResultsXml,
|
|
29
|
+
parseAccumulatedIntoBlocks,
|
|
30
|
+
formatToolDefinitions,
|
|
31
|
+
type ToolDefinitionForPrompt,
|
|
32
|
+
} from '../utils/tool-parser.js';
|
|
33
|
+
import { IncrementalXmlParser } from '../utils/stream-parser.js';
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Configuration
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
export interface AnthropicXmlFormatterConfig extends FormatterConfig {
|
|
40
|
+
/**
|
|
41
|
+
* How to handle tool definitions:
|
|
42
|
+
* - 'xml': Inject into conversation as XML (prefill mode)
|
|
43
|
+
* - 'native': Pass to API as native tools
|
|
44
|
+
* Default: 'xml'
|
|
45
|
+
*/
|
|
46
|
+
toolMode?: 'xml' | 'native';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Where to inject tool definitions when toolMode is 'xml':
|
|
50
|
+
* - 'conversation': Inject into assistant content N messages from end
|
|
51
|
+
* - 'system': Inject into system prompt
|
|
52
|
+
* Default: 'conversation'
|
|
53
|
+
*/
|
|
54
|
+
toolInjectionMode?: 'conversation' | 'system';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Position to inject tools (from end of messages).
|
|
58
|
+
* Default: 10
|
|
59
|
+
*/
|
|
60
|
+
toolInjectionPosition?: number;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Message delimiter for base models (e.g., '</s>').
|
|
64
|
+
* Default: '' (none)
|
|
65
|
+
*/
|
|
66
|
+
messageDelimiter?: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Maximum participants to include in stop sequences.
|
|
70
|
+
* Default: 10
|
|
71
|
+
*/
|
|
72
|
+
maxParticipantsForStop?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Anthropic XML Formatter
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
export class AnthropicXmlFormatter implements PrefillFormatter {
|
|
80
|
+
readonly name = 'anthropic-xml';
|
|
81
|
+
readonly usesPrefill = true;
|
|
82
|
+
|
|
83
|
+
private config: Required<AnthropicXmlFormatterConfig>;
|
|
84
|
+
|
|
85
|
+
constructor(config: AnthropicXmlFormatterConfig = {}) {
|
|
86
|
+
this.config = {
|
|
87
|
+
toolMode: config.toolMode ?? 'xml',
|
|
88
|
+
toolInjectionMode: config.toolInjectionMode ?? 'conversation',
|
|
89
|
+
toolInjectionPosition: config.toolInjectionPosition ?? 10,
|
|
90
|
+
messageDelimiter: config.messageDelimiter ?? '',
|
|
91
|
+
maxParticipantsForStop: config.maxParticipantsForStop ?? 10,
|
|
92
|
+
unsupportedMedia: config.unsupportedMedia ?? 'error',
|
|
93
|
+
warnOnStrip: config.warnOnStrip ?? true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ==========================================================================
|
|
98
|
+
// REQUEST BUILDING
|
|
99
|
+
// ==========================================================================
|
|
100
|
+
|
|
101
|
+
buildMessages(messages: NormalizedMessage[], options: BuildOptions): BuildResult {
|
|
102
|
+
const {
|
|
103
|
+
assistantParticipant,
|
|
104
|
+
tools,
|
|
105
|
+
thinking,
|
|
106
|
+
systemPrompt,
|
|
107
|
+
promptCaching = false,
|
|
108
|
+
contextPrefix,
|
|
109
|
+
hasCacheMarker,
|
|
110
|
+
} = options;
|
|
111
|
+
|
|
112
|
+
const providerMessages: ProviderMessage[] = [];
|
|
113
|
+
const joiner = this.config.messageDelimiter ? '' : '\n';
|
|
114
|
+
|
|
115
|
+
// Track conversation state
|
|
116
|
+
let currentConversation: string[] = [];
|
|
117
|
+
let lastNonEmptyParticipant: string | null = null;
|
|
118
|
+
|
|
119
|
+
// Track cache marker state - everything BEFORE we see the marker gets cache_control
|
|
120
|
+
let passedCacheMarker = false;
|
|
121
|
+
let cacheMarkersApplied = 0;
|
|
122
|
+
|
|
123
|
+
// Calculate tool injection point
|
|
124
|
+
const totalMessages = messages.length;
|
|
125
|
+
const toolInjectionIndex = Math.max(0, totalMessages - this.config.toolInjectionPosition);
|
|
126
|
+
let toolsInjected = false;
|
|
127
|
+
const hasToolsForConversation =
|
|
128
|
+
this.config.toolMode === 'xml' &&
|
|
129
|
+
this.config.toolInjectionMode === 'conversation' &&
|
|
130
|
+
tools &&
|
|
131
|
+
tools.length > 0;
|
|
132
|
+
const toolsText = hasToolsForConversation ? this.formatToolsForInjection(tools!) : '';
|
|
133
|
+
|
|
134
|
+
// Build system content
|
|
135
|
+
let systemText = typeof systemPrompt === 'string' ? systemPrompt : '';
|
|
136
|
+
if (Array.isArray(systemPrompt)) {
|
|
137
|
+
systemText = systemPrompt
|
|
138
|
+
.filter((b): b is ContentBlock & { type: 'text' } => b.type === 'text')
|
|
139
|
+
.map(b => b.text)
|
|
140
|
+
.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Inject tools into system if configured
|
|
144
|
+
if (this.config.toolMode === 'xml' && this.config.toolInjectionMode === 'system' && tools?.length) {
|
|
145
|
+
const toolsXml = this.formatToolDefinitionsXml(tools);
|
|
146
|
+
systemText = this.injectToolsIntoSystem(systemText, toolsXml);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build system content with optional cache control
|
|
150
|
+
let systemContent: unknown;
|
|
151
|
+
if (systemText) {
|
|
152
|
+
const systemBlock: Record<string, unknown> = { type: 'text', text: systemText };
|
|
153
|
+
if (promptCaching) {
|
|
154
|
+
systemBlock.cache_control = { type: 'ephemeral' };
|
|
155
|
+
cacheMarkersApplied++;
|
|
156
|
+
}
|
|
157
|
+
systemContent = [systemBlock];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Add context prefix as first cached assistant message (for simulacrum seeding)
|
|
161
|
+
if (contextPrefix) {
|
|
162
|
+
const prefixBlock: Record<string, unknown> = { type: 'text', text: contextPrefix };
|
|
163
|
+
if (promptCaching) {
|
|
164
|
+
prefixBlock.cache_control = { type: 'ephemeral' };
|
|
165
|
+
cacheMarkersApplied++;
|
|
166
|
+
}
|
|
167
|
+
providerMessages.push({
|
|
168
|
+
role: 'assistant',
|
|
169
|
+
content: [prefixBlock],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Process messages
|
|
174
|
+
for (let i = 0; i < messages.length; i++) {
|
|
175
|
+
const message = messages[i];
|
|
176
|
+
if (!message) continue;
|
|
177
|
+
|
|
178
|
+
const isLastMessage = i === messages.length - 1;
|
|
179
|
+
const isAssistant = message.participant === assistantParticipant;
|
|
180
|
+
|
|
181
|
+
// Extract content
|
|
182
|
+
const { text, images, hasUnsupportedMedia } = this.extractContent(message.content, message.participant);
|
|
183
|
+
const hasImages = images.length > 0;
|
|
184
|
+
const isEmpty = !text.trim() && !hasImages;
|
|
185
|
+
|
|
186
|
+
// Handle unsupported media
|
|
187
|
+
if (hasUnsupportedMedia) {
|
|
188
|
+
if (this.config.unsupportedMedia === 'error') {
|
|
189
|
+
throw new Error(`AnthropicXmlFormatter does not support media in message from ${message.participant}. Configure unsupportedMedia: 'strip' to ignore.`);
|
|
190
|
+
} else if (this.config.warnOnStrip) {
|
|
191
|
+
console.warn(`[AnthropicXmlFormatter] Stripped unsupported media from message`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for tool results
|
|
196
|
+
const hasToolResult = message.content.some(c => c.type === 'tool_result');
|
|
197
|
+
|
|
198
|
+
// If message has images, flush and add as user turn
|
|
199
|
+
if (hasImages && !isEmpty) {
|
|
200
|
+
if (currentConversation.length > 0) {
|
|
201
|
+
providerMessages.push({
|
|
202
|
+
role: 'assistant',
|
|
203
|
+
content: currentConversation.join(joiner),
|
|
204
|
+
});
|
|
205
|
+
currentConversation = [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const userContent: unknown[] = [];
|
|
209
|
+
if (text) {
|
|
210
|
+
userContent.push({ type: 'text', text: `${message.participant}: ${text}` });
|
|
211
|
+
}
|
|
212
|
+
userContent.push(...images);
|
|
213
|
+
|
|
214
|
+
providerMessages.push({ role: 'user', content: userContent });
|
|
215
|
+
lastNonEmptyParticipant = message.participant;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Skip empty messages except last
|
|
220
|
+
if (isEmpty && !isLastMessage) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if this message has a cache marker - flush content BEFORE it with cache_control
|
|
225
|
+
if (hasCacheMarker && !passedCacheMarker && hasCacheMarker(message, i)) {
|
|
226
|
+
// Flush everything before this message WITH cache_control (if caching enabled)
|
|
227
|
+
if (currentConversation.length > 0) {
|
|
228
|
+
const content = currentConversation.join(joiner);
|
|
229
|
+
if (promptCaching) {
|
|
230
|
+
const contentBlock: Record<string, unknown> = { type: 'text', text: content };
|
|
231
|
+
contentBlock.cache_control = { type: 'ephemeral' };
|
|
232
|
+
cacheMarkersApplied++;
|
|
233
|
+
providerMessages.push({
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
content: [contentBlock],
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
providerMessages.push({
|
|
239
|
+
role: 'assistant',
|
|
240
|
+
content: content,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
currentConversation = [];
|
|
244
|
+
}
|
|
245
|
+
passedCacheMarker = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Inject tools before this message if at injection point
|
|
249
|
+
const shouldInjectHere = toolInjectionIndex > 0 ? i >= toolInjectionIndex : i === 0;
|
|
250
|
+
if (hasToolsForConversation && !toolsInjected && shouldInjectHere) {
|
|
251
|
+
currentConversation.push(toolsText);
|
|
252
|
+
toolsInjected = true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check bot continuation
|
|
256
|
+
const isBotMessage = message.participant === assistantParticipant;
|
|
257
|
+
const isContinuation = isBotMessage && lastNonEmptyParticipant === assistantParticipant && !hasToolResult;
|
|
258
|
+
|
|
259
|
+
if (isContinuation && isLastMessage) {
|
|
260
|
+
// Bot continuation - don't add prefix
|
|
261
|
+
continue;
|
|
262
|
+
} else if (isLastMessage && isEmpty) {
|
|
263
|
+
// Completion target - prefix added below
|
|
264
|
+
} else if (text) {
|
|
265
|
+
currentConversation.push(`${message.participant}: ${text}${this.config.messageDelimiter}`);
|
|
266
|
+
if (!hasToolResult) {
|
|
267
|
+
lastNonEmptyParticipant = message.participant;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Determine turn prefix
|
|
273
|
+
let turnPrefix: string;
|
|
274
|
+
if (thinking?.enabled) {
|
|
275
|
+
turnPrefix = `${assistantParticipant}: <thinking>`;
|
|
276
|
+
} else {
|
|
277
|
+
turnPrefix = `${assistantParticipant}:`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Flush remaining conversation
|
|
281
|
+
if (hasToolsForConversation && !toolsInjected) {
|
|
282
|
+
currentConversation.push(toolsText);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (currentConversation.length > 0) {
|
|
286
|
+
providerMessages.push({
|
|
287
|
+
role: 'assistant',
|
|
288
|
+
content: [...currentConversation, turnPrefix].join(joiner),
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
providerMessages.push({
|
|
292
|
+
role: 'assistant',
|
|
293
|
+
content: turnPrefix,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Build stop sequences
|
|
298
|
+
const stopSequences = this.buildStopSequences(messages, assistantParticipant, options);
|
|
299
|
+
|
|
300
|
+
// Native tools if configured
|
|
301
|
+
const nativeTools = this.config.toolMode === 'native' && tools?.length
|
|
302
|
+
? this.convertToNativeTools(tools)
|
|
303
|
+
: undefined;
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
messages: providerMessages,
|
|
307
|
+
systemContent,
|
|
308
|
+
assistantPrefill: typeof providerMessages[providerMessages.length - 1]?.content === 'string'
|
|
309
|
+
? providerMessages[providerMessages.length - 1]!.content as string
|
|
310
|
+
: undefined,
|
|
311
|
+
stopSequences,
|
|
312
|
+
nativeTools,
|
|
313
|
+
cacheMarkersApplied,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
formatToolResults(results: ToolResult[], options?: { thinking?: boolean }): string {
|
|
318
|
+
let xml = formatToolResultsXml(results);
|
|
319
|
+
if (options?.thinking) {
|
|
320
|
+
xml += '\n<thinking>';
|
|
321
|
+
}
|
|
322
|
+
return xml;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ==========================================================================
|
|
326
|
+
// RESPONSE PARSING
|
|
327
|
+
// ==========================================================================
|
|
328
|
+
|
|
329
|
+
createStreamParser(): StreamParser {
|
|
330
|
+
return new IncrementalXmlParser();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
parseToolCalls(content: string): ToolCall[] {
|
|
334
|
+
const result = parseToolCallsXml(content);
|
|
335
|
+
return result?.calls ?? [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
hasToolUse(content: string): boolean {
|
|
339
|
+
return /<(antml:)?function_calls>/.test(content);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
parseContentBlocks(content: string): ContentBlock[] {
|
|
343
|
+
const { blocks } = parseAccumulatedIntoBlocks(content);
|
|
344
|
+
return blocks;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ==========================================================================
|
|
348
|
+
// PRIVATE HELPERS
|
|
349
|
+
// ==========================================================================
|
|
350
|
+
|
|
351
|
+
private extractContent(
|
|
352
|
+
content: ContentBlock[],
|
|
353
|
+
participant: string
|
|
354
|
+
): { text: string; images: unknown[]; hasUnsupportedMedia: boolean } {
|
|
355
|
+
const parts: string[] = [];
|
|
356
|
+
const images: unknown[] = [];
|
|
357
|
+
let hasUnsupportedMedia = false;
|
|
358
|
+
|
|
359
|
+
for (const block of content) {
|
|
360
|
+
if (block.type === 'text') {
|
|
361
|
+
parts.push(block.text);
|
|
362
|
+
} else if (block.type === 'image') {
|
|
363
|
+
if (block.source.type === 'base64') {
|
|
364
|
+
images.push({
|
|
365
|
+
type: 'image',
|
|
366
|
+
source: {
|
|
367
|
+
type: 'base64',
|
|
368
|
+
media_type: block.source.mediaType,
|
|
369
|
+
data: block.source.data,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} else if (block.type === 'tool_use') {
|
|
374
|
+
parts.push(`${participant}>[${block.name}]: ${JSON.stringify(block.input)}`);
|
|
375
|
+
} else if (block.type === 'tool_result') {
|
|
376
|
+
const resultText = typeof block.content === 'string'
|
|
377
|
+
? block.content
|
|
378
|
+
: JSON.stringify(block.content);
|
|
379
|
+
parts.push(`${participant}<[tool_result]: ${resultText}`);
|
|
380
|
+
} else if (block.type === 'document' || block.type === 'audio') {
|
|
381
|
+
hasUnsupportedMedia = true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { text: parts.join('\n'), images, hasUnsupportedMedia };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private formatToolDefinitionsXml(tools: ToolDefinition[]): string {
|
|
389
|
+
const toolsForPrompt: ToolDefinitionForPrompt[] = tools.map((tool) => ({
|
|
390
|
+
name: tool.name,
|
|
391
|
+
description: tool.description,
|
|
392
|
+
parameters: Object.fromEntries(
|
|
393
|
+
Object.entries(tool.inputSchema.properties).map(([name, schema]) => [
|
|
394
|
+
name,
|
|
395
|
+
{
|
|
396
|
+
type: schema.type,
|
|
397
|
+
description: schema.description,
|
|
398
|
+
required: tool.inputSchema.required?.includes(name),
|
|
399
|
+
enum: schema.enum,
|
|
400
|
+
},
|
|
401
|
+
])
|
|
402
|
+
),
|
|
403
|
+
}));
|
|
404
|
+
|
|
405
|
+
return formatToolDefinitions(toolsForPrompt);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private formatToolsForInjection(tools: ToolDefinition[]): string {
|
|
409
|
+
const toolsXml = this.formatToolDefinitionsXml(tools);
|
|
410
|
+
|
|
411
|
+
// Assemble tags to avoid triggering stop sequences
|
|
412
|
+
const FUNC_CALLS_OPEN = '<' + 'function_calls>';
|
|
413
|
+
const FUNC_CALLS_CLOSE = '</' + 'function_calls>';
|
|
414
|
+
const INVOKE_OPEN = '<' + 'invoke name="';
|
|
415
|
+
const INVOKE_CLOSE = '</' + 'invoke>';
|
|
416
|
+
const PARAM_OPEN = '<' + 'parameter name="';
|
|
417
|
+
const PARAM_CLOSE = '</' + 'parameter>';
|
|
418
|
+
|
|
419
|
+
return `
|
|
420
|
+
<available_tools>
|
|
421
|
+
${toolsXml}
|
|
422
|
+
</available_tools>
|
|
423
|
+
|
|
424
|
+
When you want to use a tool, output:
|
|
425
|
+
${FUNC_CALLS_OPEN}
|
|
426
|
+
${INVOKE_OPEN}tool_name">
|
|
427
|
+
${PARAM_OPEN}param_name">value${PARAM_CLOSE}
|
|
428
|
+
${INVOKE_CLOSE}
|
|
429
|
+
${FUNC_CALLS_CLOSE}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private injectToolsIntoSystem(system: string, toolsXml: string): string {
|
|
433
|
+
const toolsSection = `
|
|
434
|
+
<available_tools>
|
|
435
|
+
${toolsXml}
|
|
436
|
+
</available_tools>
|
|
437
|
+
|
|
438
|
+
When you want to use a tool, output:
|
|
439
|
+
<function_calls>
|
|
440
|
+
<invoke name="tool_name">
|
|
441
|
+
<parameter name="param_name">value</parameter>
|
|
442
|
+
</invoke>
|
|
443
|
+
</function_calls>
|
|
444
|
+
`;
|
|
445
|
+
return system + '\n\n' + toolsSection;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private buildStopSequences(
|
|
449
|
+
messages: NormalizedMessage[],
|
|
450
|
+
assistantName: string,
|
|
451
|
+
options: BuildOptions
|
|
452
|
+
): string[] {
|
|
453
|
+
const sequences: string[] = [];
|
|
454
|
+
|
|
455
|
+
// Use option's maxParticipantsForStop, falling back to config
|
|
456
|
+
const maxParticipants = options.maxParticipantsForStop ?? this.config.maxParticipantsForStop;
|
|
457
|
+
|
|
458
|
+
// Collect unique participants (excluding assistant)
|
|
459
|
+
const participants = new Set<string>();
|
|
460
|
+
for (let i = messages.length - 1; i >= 0 && participants.size < maxParticipants; i--) {
|
|
461
|
+
const message = messages[i];
|
|
462
|
+
if (message && message.participant !== assistantName) {
|
|
463
|
+
participants.add(message.participant);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Participant-based stops
|
|
468
|
+
for (const participant of participants) {
|
|
469
|
+
sequences.push(`\n${participant}:`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Tool-related stop
|
|
473
|
+
sequences.push('</function_calls>');
|
|
474
|
+
|
|
475
|
+
// Add any additional stop sequences from options
|
|
476
|
+
if (options.additionalStopSequences?.length) {
|
|
477
|
+
sequences.push(...options.additionalStopSequences);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return sequences;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private convertToNativeTools(tools: ToolDefinition[]): unknown[] {
|
|
484
|
+
return tools.map(tool => ({
|
|
485
|
+
name: tool.name,
|
|
486
|
+
description: tool.description,
|
|
487
|
+
input_schema: tool.inputSchema,
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
}
|