Source file tool.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
type context = { model : string; provider : Chatoyant_provider.Provider.id }
type call = { id : string; name : string; arguments : Chatoyant_runtime.Json.t }

type result = {
  id : string;
  ok : bool;
  value : Chatoyant_runtime.Json.t option;
  error : string option;
}

type t = {
  tool_name : string;
  tool_description : string;
  tool_parameters : Chatoyant_schema.Schema.field;
  tool_result_schema : Chatoyant_schema.Schema.field option;
  execute :
    context ->
    Chatoyant_runtime.Json.t ->
    (Chatoyant_runtime.Json.t, string) Stdlib.result;
}

let create ~name ~description ~parameters ?result_schema execute =
  {
    tool_name = name;
    tool_description = description;
    tool_parameters = parameters;
    tool_result_schema = result_schema;
    execute;
  }

let create_typed ~name ~description ~args ~result execute =
  create ~name ~description ~parameters:(Chatoyant_schema.Codec.schema args)
    ~result_schema:(Chatoyant_schema.Codec.schema result) (fun context json ->
      match Chatoyant_schema.Codec.decode args json with
      | Error message -> Error message
      | Ok args -> (
          match execute context args with
          | Error _ as err -> err
          | Ok value -> Ok (Chatoyant_schema.Codec.encode result value)))

let name tool = tool.tool_name
let description tool = tool.tool_description
let parameters tool = tool.tool_parameters

let json_schema tool =
  Chatoyant_schema.Schema.to_json_schema tool.tool_parameters

let to_provider_definition (tool : t) :
    Chatoyant_provider.Provider.tool_definition =
  {
    Chatoyant_provider.Provider.tool_name = tool.tool_name;
    tool_description = Some tool.tool_description;
    tool_parameters = json_schema tool;
    tool_strict = Some true;
  }

let failure id message = { id; ok = false; value = None; error = Some message }
let success id value = { id; ok = true; value = Some value; error = None }

let execute_call context call tool =
  if call.name <> tool.tool_name then
    failure call.id ("Unknown tool: " ^ call.name)
  else
    match
      Chatoyant_schema.Value.validate tool.tool_parameters call.arguments
    with
    | Error error ->
        failure call.id (Chatoyant_schema.Value.error_to_string error)
    | Ok () -> (
        match tool.execute context call.arguments with
        | Error message -> failure call.id message
        | Ok value -> (
            match tool.tool_result_schema with
            | None -> success call.id value
            | Some schema -> (
                match Chatoyant_schema.Value.validate schema value with
                | Ok () -> success call.id value
                | Error error ->
                    failure call.id
                      (Chatoyant_schema.Value.error_to_string error))))

let string value = Chatoyant_runtime.Json.String value

let add_opt name value fields =
  match value with None -> fields | Some value -> (name, value) :: fields

let call_to_json (call : call) =
  Chatoyant_runtime.Json.Object
    [
      ("id", string call.id);
      ("name", string call.name);
      ("arguments", call.arguments);
    ]

let result_to_json (result : result) =
  [ ("id", string result.id); ("ok", Chatoyant_runtime.Json.Bool result.ok) ]
  |> add_opt "value" result.value
  |> add_opt "error" (Option.map string result.error)
  |> List.rev
  |> fun fields -> Chatoyant_runtime.Json.Object fields

module Args = struct
  let ( let* ) result fn = Stdlib.Result.bind result fn

  let field name json =
    match Chatoyant_runtime.Json.field name json with
    | Some value -> Ok value
    | None -> Error ("missing field: " ^ name)

  let type_error name expected =
    Error ("expected field " ^ name ^ " to be " ^ expected)

  let string name json =
    let* value = field name json in
    match Chatoyant_runtime.Json.as_string value with
    | Some value -> Ok value
    | None -> type_error name "a string"

  let float name json =
    let* value = field name json in
    match Chatoyant_runtime.Json.as_float value with
    | Some value -> Ok value
    | None -> type_error name "a number"

  let int name json =
    let* value = field name json in
    match Chatoyant_runtime.Json.as_int value with
    | Some value -> Ok value
    | None -> type_error name "an integer"

  let bool name json =
    let* value = field name json in
    match Chatoyant_runtime.Json.as_bool value with
    | Some value -> Ok value
    | None -> type_error name "a boolean"

  let object_ name json =
    let* value = field name json in
    match Chatoyant_runtime.Json.as_object value with
    | Some value -> Ok value
    | None -> type_error name "an object"

  let list name json =
    let* value = field name json in
    match Chatoyant_runtime.Json.as_list value with
    | Some value -> Ok value
    | None -> type_error name "an array"

  let optional decode name json =
    match Chatoyant_runtime.Json.field name json with
    | None | Some Chatoyant_runtime.Json.Null -> Ok None
    | Some _ -> Stdlib.Result.map Option.some (decode name json)
end

module Json = struct
  let null = Chatoyant_runtime.Json.Null
  let string value = Chatoyant_runtime.Json.String value
  let float value = Chatoyant_runtime.Json.Float value
  let int value = Chatoyant_runtime.Json.Float (Float.of_int value)
  let bool value = Chatoyant_runtime.Json.Bool value
  let object_ fields = Chatoyant_runtime.Json.Object fields
  let array values = Chatoyant_runtime.Json.Array values
end