diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fede934..a6c50612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- Fix openai-chat tool call + support for Mistral API #233 ## 0.87.1 diff --git a/src/eca/features/chat.clj b/src/eca/features/chat.clj index f99761f9..559e4f32 100644 --- a/src/eca/features/chat.clj +++ b/src/eca/features/chat.clj @@ -1021,21 +1021,31 @@ (run-pre-request-hooks! (assoc chat-ctx :message original-text))] (cond stop? (do (finish-chat-prompt! :idle chat-ctx) nil) - :else (let [last-user-idx (or (llm-util/find-last-user-msg-idx user-messages) - (dec (count user-messages))) - rewritten (if (and modify-allowed? - last-user-idx - final-prompt) + :else (let [last-user-idx (llm-util/find-last-user-msg-idx user-messages) + ;; preRequest additionalContext should ideally attach to the last user message, + ;; but some prompt sources may not contain a user role (e.g. prompt templates). + context-idx (or last-user-idx + (some-> user-messages seq count dec)) + rewritten (if (and modify-allowed? last-user-idx final-prompt) (assoc-in user-messages [last-user-idx :content 0 :text] final-prompt) user-messages) - with-contexts (if (seq additional-contexts) + with-contexts (cond + (and (seq additional-contexts) context-idx) (reduce (fn [msgs {:keys [hook-name content]}] - (update-in msgs [last-user-idx :content] + (update-in msgs [context-idx :content] #(conj (vec %) {:type :text :text (wrap-additional-context hook-name content)}))) rewritten additional-contexts) + + (seq additional-contexts) + (do (logger/warn logger-tag "Dropping preRequest additionalContext because no message index was found" + {:source-type source-type + :num-messages (count user-messages)}) + rewritten) + + :else rewritten)] with-contexts))) user-messages)] diff --git a/src/eca/features/prompt.clj b/src/eca/features/prompt.clj index 89bf2b79..bd4b429e 100644 --- a/src/eca/features/prompt.clj +++ b/src/eca/features/prompt.clj @@ -80,7 +80,6 @@ ""))) "" refined-contexts) - ;; TODO - should be refined contexts? (when startup-ctx (str "\n\n" startup-ctx "\n\n\n")) "")) diff --git a/src/eca/llm_providers/openai_chat.clj b/src/eca/llm_providers/openai_chat.clj index 89e23d4a..ccab3da2 100644 --- a/src/eca/llm_providers/openai_chat.clj +++ b/src/eca/llm_providers/openai_chat.clj @@ -396,9 +396,8 @@ ;; Atom to accumulate tool call data from streaming chunks. ;; OpenAI streams tool call arguments across multiple chunks, so we need to - ;; accumulate the partial JSON strings before parsing them. Keys are either - ;; index numbers for simple cases, or "index-id" composite keys for parallel - ;; tool calls that share the same index but have different IDs. + ;; accumulate partial JSON strings before parsing them. Keys are tool call + ;; indices (fallback: IDs) to keep chunks grouped for the active response. tool-calls* (atom {}) ;; Reasoning state machine: @@ -431,6 +430,12 @@ :content "" :buffer "") (on-reason {:status :started :id new-reason-id}))) + find-existing-tool-key (fn [tool-calls index id] + (some (fn [[k v]] (when (or (some-> id (= (:id v))) + (and (nil? (:id v)) + (some-> index (= (:index v))))) + k)) + tool-calls)) on-tools-called-wrapper (fn on-tools-called-wrapper [tools-to-call on-tools-called handle-response] (when-let [{:keys [new-messages]} (on-tools-called tools-to-call)] (let [pruned-messages (prune-history new-messages) @@ -449,9 +454,9 @@ :api-key api-key :url-relative-path url-relative-path :on-error wrapped-on-error - :on-stream (when stream? (fn [event data] (handle-response event data tool-calls* new-rid)))})))) + :on-stream (when stream? (fn [event data] (handle-response event data tool-calls*)))})))) - handle-response (fn handle-response [event data tool-calls* rid] + handle-response (fn handle-response [event data tool-calls*] (if (= event "stream-end") (do ;; Flush any leftover buffered content and finish reasoning if needed @@ -496,27 +501,32 @@ {name :name args :arguments} function ;; Extract Google Gemini thought signature if present thought-signature (get-in extra_content [:google :thought_signature]) - ;; Use RID as key to avoid collisions between API requests - tool-key (str rid "-" index) - ;; Create globally unique tool call ID for client - unique-id (when id (str rid "-" id))] - (when (and name unique-id) - (on-prepare-tool-call {:id unique-id - :full-name name - :arguments-text ""})) - (swap! tool-calls* update tool-key - (fn [existing] - (cond-> (or existing {:index index}) - unique-id (assoc :id unique-id) - name (assoc :full-name name) - args (update :arguments-text (fnil str "") args) - ;; Store thought signature for Google Gemini - thought-signature (assoc :external-id thought-signature)))) - (when-let [updated-tool-call (get @tool-calls* tool-key)] - (when (and (:id updated-tool-call) - (:full-name updated-tool-call) - args) - (on-prepare-tool-call (assoc updated-tool-call :arguments-text args))))))) + existing-key (find-existing-tool-key @tool-calls* index id) + existing (when existing-key (get @tool-calls* existing-key)) + tool-key (or existing-key index id)] + (if (nil? tool-key) + (logger/warn logger-tag "Received tool_call delta without index/id; ignoring" + {:tool-call tool-call}) + (do + (swap! tool-calls* update tool-key + (fn [existing] + (cond-> (or existing {:index index}) + (some? index) (assoc :index index) + (and id (nil? (:id existing))) (assoc :id id) + (and name (nil? (:full-name existing))) (assoc :full-name name) + args (update :arguments-text (fnil str "") args) + ;; Store thought signature for Google Gemini + thought-signature (assoc :external-id thought-signature)))) + (when-let [updated-tool-call (get @tool-calls* tool-key)] + ;; Streaming tool_calls may split metadata (id/name) and arguments across deltas. + ;; Emit prepare once we can correlate the call (id + full-name), on first id or args deltas, + ;; so :tool-prepare always precedes :tool-run in the tool-call state machine. + (when (and (:id updated-tool-call) + (:full-name updated-tool-call) + (or (nil? (:id existing)) args)) + (on-prepare-tool-call + (assoc updated-tool-call + :arguments-text (or args "")))))))))) ;; Process finish reason if present (but not tool_calls which is handled above) (when finish-reason ;; Flush any leftover buffered content before finishing @@ -541,4 +551,4 @@ :on-tools-called-wrapper on-tools-called-wrapper :on-error wrapped-on-error :on-stream (when stream? - (fn [event data] (handle-response event data tool-calls* rid)))}))) + (fn [event data] (handle-response event data tool-calls*)))})))