Class: SolidLoop::Middlewares::ToolCallXmlParser

Inherits:
Object
  • Object
show all
Defined in:
app/services/solid_loop/middlewares/tool_call_xml_parser.rb

Instance Method Summary collapse

Constructor Details

#initialize(app) ⇒ ToolCallXmlParser

Returns a new instance of ToolCallXmlParser.



4
5
6
# File 'app/services/solid_loop/middlewares/tool_call_xml_parser.rb', line 4

def initialize(app)
  @app = app
end

Instance Method Details

#call(env) ⇒ Object



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
# File 'app/services/solid_loop/middlewares/tool_call_xml_parser.rb', line 8

def call(env)
  # 1. Let the downstream pipeline run (ResponseParsing)
  @app.call(env)

  # 2. On the way back up, ResponseParsing has already created the message.
  # We check if there's a message, if it has content, and NO tool calls.
  msg = env.assistant_message
  return unless msg && msg.content.present? && msg.tool_calls.empty?

  extracted = extract_from_legacy_xml(msg.content, env.payload[:model])

  if extracted.any?
    # Revival, tool-call persistence and successor-job dispatch commit
    # atomically (same-DB transactional job backend): a crash or
    # rollback leaves neither a revived loop without jobs nor jobs for
    # rows that never committed.
    env.loop.transaction do
      revived = env.loop.transition_status(
        from: :completed,
        to: :running,
        expected_execution_token: nil,
        execution_token: env.execution_token
      )
      next unless revived

      # Save raw JSON representation just in case it's needed
      msg.update!(tool_calls_raw: extracted)

      extracted.each do |tc|
        begin
          args = JSON.parse(tc.dig("function", "arguments") || "{}")
          msg.tool_calls.create!(
            tool_call_id: tc["id"],
            function_name: tc.dig("function", "name"),
            arguments: args
          )
        rescue JSON::ParserError => e
          error_msg = "Invalid JSON in tool call arguments: #{e.message}. Raw: #{tc.dig('function', 'arguments')}"
          Rails.logger.error error_msg
          env.loop.messages.create!(
            role: "tool",
            tool_call_id: tc["id"],
            content: error_msg,
            status: "success"
          )
        end
      end

      # Explicit ordered scope — never rely on association default order.
      persisted_tool_calls = msg.tool_calls.ordered.to_a

      # Same emission policy as ResponseParsing: parallel agents dispatch
      # every call at once; sequential agents dispatch only the first and
      # let ResponseCreation chain the rest.
      calls_to_dispatch =
        if env.agent.sequential_tool_calls?
          persisted_tool_calls.first(1)
        else
          persisted_tool_calls
        end

      calls_to_dispatch.each do |tool_call|
        SolidLoop.enqueue!(SolidLoop::ToolExecutionJob, tool_call.id, env.execution_token)
      end
    end
  end
end