import 'package:flutter/widgets.dart'; import 'confirm_gate.dart'; import 'tool_definition.dart'; import 'tool_envelope.dart'; import 'tool_registry.dart'; /// Routes a single `FunctionCallResponse` from the LLM to the matching /// handler. See design `fn-tool_dispatcher.md`. /// /// `dispatch` never throws — every failure path returns a `ToolResult`. class ToolDispatcher { final ToolRegistry registry; final ConfirmGate confirmGate; ToolDispatcher({ required this.registry, ConfirmGate? confirmGate, }) : confirmGate = confirmGate ?? const ConfirmGate(); Future dispatch({ required String toolName, required Map rawArgs, required BuildContext? confirmContext, required ToolDeps deps, }) async { // 1. Lookup. final tool = registry.byName(toolName); if (tool == null) { return ToolErr('unknown_tool', '알 수 없는 도구: $toolName'); } // 2. Validate against schema. final validation = _validateArgs(tool.parametersSchema, rawArgs); if (validation != null) { return ToolErr('validation', '인자 오류: $validation'); } // 3. Destructive → Confirm gate. if (tool.isDestructive) { if (confirmContext == null) { return const ToolCancelled(); } final ok = await confirmGate.show(confirmContext, tool, rawArgs); if (!ok) return const ToolCancelled(); } // 4. Run handler. try { return await tool.handler(rawArgs, deps); } catch (e) { return ToolErr('handler_error', '도구 실행 실패: ${e.runtimeType}'); } } } /// Minimal JSON-schema-ish validator covering only what our tools use: /// - object root with `properties` + optional `required` /// - per-property `type` ∈ {string, integer, number, boolean, object, array} /// /// Returns null on success, a short error message on failure. Extra keys are /// allowed (model hallucination tolerated; logged at call site if needed). String? _validateArgs(Map schema, Map args) { final required = schema['required']; if (required is List) { for (final field in required) { if (field is String && !args.containsKey(field)) { return '필수 필드 \'$field\' 가 없습니다.'; } } } final props = schema['properties']; if (props is! Map) return null; for (final entry in args.entries) { final propSchema = props[entry.key]; if (propSchema is! Map) continue; // unknown key — tolerate final expected = propSchema['type']; if (expected is! String) continue; final v = entry.value; if (!_matchesType(expected, v)) { return '\'${entry.key}\' 타입 불일치 (기대=$expected, 실제=${v.runtimeType})'; } } return null; } bool _matchesType(String expected, dynamic v) { switch (expected) { case 'string': return v is String; case 'integer': return v is int; case 'number': return v is num; case 'boolean': return v is bool; case 'object': return v is Map; case 'array': return v is List; default: return true; // unknown type — passthrough } }