extract method

Future<ToolExtraction> extract(
  1. String request, {
  2. required List<FunctionTool> functions,
  3. ToolChoice choice = ToolChoice.auto,
  4. 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);
}