package ocaml-ai-sdk

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file generate_text.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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
(** Extract text, reasoning, and tool calls from a content list. *)
let parse_content (content : Ai_provider.Content.t list) =
  let text = Buffer.create 256 in
  let reasoning = Buffer.create 256 in
  let tool_calls = ref [] in
  List.iter
    (fun (c : Ai_provider.Content.t) ->
      match c with
      | Text { text = t } ->
        if Buffer.length text > 0 then Buffer.add_char text '\n';
        Buffer.add_string text t
      | Reasoning { text = t; _ } ->
        if Buffer.length reasoning > 0 then Buffer.add_char reasoning '\n';
        Buffer.add_string reasoning t
      | Tool_call { tool_call_id; tool_name; args; _ } ->
        let args_json = Core_tool.safe_parse_json_args args in
        tool_calls := { Generate_text_result.tool_call_id; tool_name; args = args_json } :: !tool_calls
      | File _ -> ())
    content;
  Buffer.contents text, Buffer.contents reasoning, List.rev !tool_calls

let generate_text ~model ?system ?prompt ?messages ?tools ?(tool_choice : Ai_provider.Tool_choice.t option) ?output
  ?(max_steps = 1) ?stop_when ?max_output_tokens ?temperature ?top_p ?top_k ?stop_sequences ?seed ?headers
  ?provider_options ?on_step_finish ?(pending_tool_approvals = []) () =
  (* Build initial messages *)
  let initial_messages = Prompt_builder.resolve_messages ?system ?prompt ?messages () in
  let mode = Output.mode_of_output output in
  let tools = Option.value ~default:[] tools in
  let provider_tools = Prompt_builder.tools_to_provider tools in
  (* Step loop *)
  let rec loop ~current_messages ~steps ~total_usage ~all_tool_calls ~all_tool_results ~step_num =
    if step_num > max_steps then begin
      (* Exhausted steps - return what we have *)
      let last_step =
        match steps with
        | s :: _ -> s
        | [] ->
          {
            Generate_text_result.text = "";
            reasoning = "";
            tool_calls = [];
            tool_results = [];
            finish_reason = Ai_provider.Finish_reason.Error;
            usage = { input_tokens = 0; output_tokens = 0; total_tokens = None };
          }
      in
      let rev_steps = List.rev steps in
      Lwt.return
        {
          Generate_text_result.text = Generate_text_result.join_text rev_steps;
          reasoning = Generate_text_result.join_reasoning rev_steps;
          tool_calls = List.rev all_tool_calls;
          tool_results = List.rev all_tool_results;
          steps = rev_steps;
          finish_reason = last_step.finish_reason;
          usage = total_usage;
          response = { id = None; model = None; headers = []; body = `Null };
          warnings = [];
          output = None;
        }
    end
    else begin
      let opts =
        Prompt_builder.make_call_options ~messages:current_messages ~tools:provider_tools ?tool_choice ~mode
          ?max_output_tokens ?temperature ?top_p ?top_k ?stop_sequences ?seed ?provider_options ?headers ()
      in
      let%lwt result = Ai_provider.Language_model.generate model opts in
      let text, reasoning, tool_calls = parse_content result.content in
      let new_usage = Generate_text_result.add_usage total_usage result.usage in
      (* Check if we need to execute tools *)
      let has_tool_calls =
        match tool_calls with
        | [] -> false
        | _ :: _ -> true
      in
      let should_continue =
        has_tool_calls
        && step_num < max_steps
        &&
        match tool_choice with
        | Some Ai_provider.Tool_choice.None_ -> false
        | Some Auto | Some Required | Some (Specific _) | None -> true
      in
      if should_continue then begin
        let%lwt blocked_calls, executable_calls = Core_tool.evaluate_approvals ~tools tool_calls in
        let%lwt tool_results =
          Lwt_list.map_s
            (fun (tc : Generate_text_result.tool_call) ->
              Core_tool.execute_tool ~tools ~tool_call_id:tc.tool_call_id ~tool_name:tc.tool_name ~args:tc.args)
            executable_calls
        in
        let step : Generate_text_result.step =
          { text; reasoning; tool_calls; tool_results; finish_reason = result.finish_reason; usage = result.usage }
        in
        Option.iter (fun f -> f step) on_step_finish;
        match blocked_calls with
        | _ :: _ ->
          (* Some tools need approval — stop the loop after executing ready tools *)
          let all_steps = List.rev (step :: steps) in
          let parsed_output = Output.parse_output output all_steps in
          Lwt.return
            {
              Generate_text_result.text = Generate_text_result.join_text all_steps;
              reasoning = Generate_text_result.join_reasoning all_steps;
              tool_calls = List.rev (List.rev_append tool_calls all_tool_calls);
              tool_results = List.rev (List.rev_append tool_results all_tool_results);
              steps = all_steps;
              finish_reason = result.finish_reason;
              usage = new_usage;
              response = result.response;
              warnings = result.warnings;
              output = parsed_output;
            }
        | [] ->
          (* All tools executed — check stop conditions before continuing *)
          let%lwt stop_with_steps =
            match stop_when with
            | None -> Lwt.return_none
            | Some conditions ->
              let all_steps_so_far = List.rev (step :: steps) in
              let%lwt met = Stop_condition.is_met conditions ~steps:all_steps_so_far in
              Lwt.return (if met then Some all_steps_so_far else None)
          in
          (match stop_with_steps with
          | None ->
            let updated_messages =
              Prompt_builder.append_assistant_and_tool_results ~messages:current_messages
                ~assistant_content:result.content ~tool_results
            in
            loop ~current_messages:updated_messages ~steps:(step :: steps) ~total_usage:new_usage
              ~all_tool_calls:(List.rev_append tool_calls all_tool_calls)
              ~all_tool_results:(List.rev_append tool_results all_tool_results)
              ~step_num:(step_num + 1)
          | Some all_steps_so_far ->
            let parsed_output = Output.parse_output output all_steps_so_far in
            Lwt.return
              Generate_text_result.
                {
                  text = join_text all_steps_so_far;
                  reasoning = join_reasoning all_steps_so_far;
                  tool_calls = List.rev (List.rev_append tool_calls all_tool_calls);
                  tool_results = List.rev (List.rev_append tool_results all_tool_results);
                  steps = all_steps_so_far;
                  finish_reason = result.finish_reason;
                  usage = new_usage;
                  response = result.response;
                  warnings = result.warnings;
                  output = parsed_output;
                })
      end
      else begin
        (* Final step - no more tool calls *)
        let step : Generate_text_result.step =
          { text; reasoning; tool_calls; tool_results = []; finish_reason = result.finish_reason; usage = result.usage }
        in
        Option.iter (fun f -> f step) on_step_finish;
        let all_steps = List.rev (step :: steps) in
        let parsed_output = Output.parse_output output all_steps in
        Lwt.return
          {
            Generate_text_result.text = Generate_text_result.join_text all_steps;
            reasoning = Generate_text_result.join_reasoning all_steps;
            tool_calls = List.rev (List.rev_append tool_calls all_tool_calls);
            tool_results = List.rev all_tool_results;
            steps = all_steps;
            finish_reason = result.finish_reason;
            usage = new_usage;
            response = result.response;
            warnings = result.warnings;
            output = parsed_output;
          }
      end
    end
  in
  (* Execute pending tool approvals before the LLM step loop *)
  let%lwt start_messages, initial_steps, initial_tool_calls, initial_tool_results =
    match pending_tool_approvals with
    | [] -> Lwt.return (initial_messages, [], [], [])
    | approvals ->
      let%lwt tool_results =
        Lwt_list.map_s
          (fun (ta : Generate_text_result.pending_tool_approval) ->
            match ta.approved with
            | false ->
              Lwt.return
                {
                  Generate_text_result.tool_call_id = ta.tool_call_id;
                  tool_name = ta.tool_name;
                  result = Core_tool.denied_result;
                  is_error = false;
                }
            | true -> Core_tool.execute_tool ~tools ~tool_call_id:ta.tool_call_id ~tool_name:ta.tool_name ~args:ta.args)
          approvals
      in
      let tool_calls =
        List.map
          (fun (ta : Generate_text_result.pending_tool_approval) ->
            { Generate_text_result.tool_call_id = ta.tool_call_id; tool_name = ta.tool_name; args = ta.args })
          approvals
      in
      let step : Generate_text_result.step =
        {
          text = "";
          reasoning = "";
          tool_calls;
          tool_results;
          finish_reason = Ai_provider.Finish_reason.Tool_calls;
          usage = { input_tokens = 0; output_tokens = 0; total_tokens = Some 0 };
        }
      in
      Option.iter (fun f -> f step) on_step_finish;
      let tool_result_parts =
        List.map
          (fun (tr : Generate_text_result.tool_result) ->
            {
              Ai_provider.Prompt.tool_call_id = tr.tool_call_id;
              tool_name = tr.tool_name;
              result = tr.result;
              is_error = tr.is_error;
              content = [];
              provider_options = Ai_provider.Provider_options.empty;
            })
          tool_results
      in
      let updated_messages = initial_messages @ [ Ai_provider.Prompt.Tool { content = tool_result_parts } ] in
      Lwt.return (updated_messages, [ step ], tool_calls, tool_results)
  in
  loop ~current_messages:start_messages ~steps:(List.rev initial_steps)
    ~total_usage:{ input_tokens = 0; output_tokens = 0; total_tokens = Some 0 }
    ~all_tool_calls:initial_tool_calls ~all_tool_results:initial_tool_results
    ~step_num:(1 + List.length initial_steps)