extract method
Future<ToolExtraction>
extract(
- String request, {
- required List<
FunctionTool> functions, - ToolChoice choice = ToolChoice.auto,
- bool verify = false,
Turns request into calls against functions.
Costs one upstream message, two if the first reply had to be repaired
(measured at about 1 in 30) or if verify is on.
verify spends a second message re-reading the original request for a
condition the first pass dropped. That is the one real failure mode: a
request packing about six conditions loses one, and the result still
validates, so no amount of schema checking finds it. The audit recovered
the lost filter in measurement (2/4 to 3/4). Worth it for dense
multi-condition requests, wasteful for "the weather in Bogotá".
Implementation
Future<ToolExtraction> extract(
String request, {
required List<FunctionTool> functions,
ToolChoice choice = ToolChoice.auto,
bool verify = false,
}) async {
if (functions.isEmpty) {
throw ArgumentError.value(
functions, 'functions', 'declare at least one function');
}
final prompt = buildExtractorPrompt(functions, request, choice: choice);
// The choice narrows what the model is ALLOWED to have called, not just
// what it was asked to call: a pinned function authorises only itself, so
// a model that ignores the instruction and calls a different declared
// function now produces nothing, instead of a call that validated cleanly
// and ran the wrong thing.
final valid = allowedNames(functions, choice);
var raw = await _ask(prompt);
final envelope = parseToolEnvelope(raw, valid, functions: functions);
var spent = 1;
var notes = [...envelope.notes];
if (envelope is EnvelopeNeedInfo) {
return ToolInfoNeeded(
function: envelope.function,
missing: envelope.missing,
requests: spent,
notes: notes,
raw: raw,
);
}
var calls = envelope is EnvelopeCalls ? envelope.calls : null;
var errors =
calls == null ? const <String>[] : validateToolCalls(calls, functions);
// Repair runs only when the first pass produced something unusable, which
// measured at 1 in 30 — a safety net, not a second leg of the flow.
if (calls == null || errors.isNotEmpty) {
spent++;
final repaired = await _ask(buildRepairPrompt(prompt, raw, errors));
final second = parseToolEnvelope(repaired, valid, functions: functions);
if (second is EnvelopeNeedInfo) {
return ToolInfoNeeded(
function: second.function,
missing: second.missing,
requests: spent,
notes: [...notes, 'repaired'],
raw: repaired,
);
}
final secondCalls = second is EnvelopeCalls ? second.calls : null;
final secondErrors = secondCalls == null
? const <String>[]
: validateToolCalls(secondCalls, functions);
if (secondCalls != null && secondErrors.isEmpty) {
calls = secondCalls;
errors = const [];
raw = repaired;
notes = [...notes, 'repaired'];
} else {
calls = secondCalls ?? calls;
errors = secondErrors.isNotEmpty ? secondErrors : errors;
notes = [...notes, ...second.notes, 'repair-failed'];
}
}
if (calls == null) {
return NoToolCall(
requests: spent, errors: errors, notes: notes, raw: raw);
}
if (calls.isNotEmpty && verify) {
spent++;
final audited = await _ask(buildVerifyPrompt(functions, request, calls));
final third = parseToolEnvelope(audited, valid, functions: functions);
if (third is EnvelopeCalls) {
final auditedCalls = third.calls;
// Only accept the audit when it is at least as valid as what it
// replaces — an auditor that returns junk must not destroy a good
// first pass.
if (auditedCalls.isNotEmpty &&
validateToolCalls(auditedCalls, functions).isEmpty) {
calls = auditedCalls;
raw = audited;
notes = [...notes, 'verified'];
}
}
}
if (calls.isEmpty) {
return NoToolCall(
requests: spent, errors: errors, notes: notes, raw: raw);
}
return ToolCallsExtracted(calls, requests: spent, notes: notes, raw: raw);
}