|
7 | 7 | [eca.db :as db] |
8 | 8 | [eca.features.commands :as f.commands] |
9 | 9 | [eca.features.context :as f.context] |
| 10 | + [eca.features.hooks :as f.hooks] |
10 | 11 | [eca.features.index :as f.index] |
11 | 12 | [eca.features.login :as f.login] |
12 | 13 | [eca.features.prompt :as f.prompt] |
|
33 | 34 | :role role |
34 | 35 | :content content})) |
35 | 36 |
|
36 | | -(defn finish-chat-prompt! [status {:keys [chat-id db* metrics on-finished-side-effect] :as chat-ctx}] |
| 37 | +(defn ^:private notify-before-hook-action! [chat-ctx {:keys [id name type]}] |
| 38 | + (send-content! chat-ctx :system |
| 39 | + {:type :hookActionStarted |
| 40 | + :action-type type |
| 41 | + :name name |
| 42 | + :id id})) |
| 43 | + |
| 44 | +(defn ^:private notify-after-hook-action! [chat-ctx {:keys [id name output error status type]}] |
| 45 | + (send-content! chat-ctx :system |
| 46 | + {:type :hookActionFinished |
| 47 | + :action-type type |
| 48 | + :id id |
| 49 | + :name name |
| 50 | + :status status |
| 51 | + :output output |
| 52 | + :error error})) |
| 53 | + |
| 54 | +(defn finish-chat-prompt! [status {:keys [message chat-id db* metrics config on-finished-side-effect] :as chat-ctx}] |
37 | 55 | (swap! db* assoc-in [:chats chat-id :status] status) |
| 56 | + (f.hooks/trigger-if-matches! :postPrompt |
| 57 | + {:chat-id chat-id |
| 58 | + :prompt message} |
| 59 | + {:on-before-action (partial notify-before-hook-action! chat-ctx) |
| 60 | + :on-after-action (partial notify-after-hook-action! chat-ctx)} |
| 61 | + @db* |
| 62 | + config) |
38 | 63 | (send-content! chat-ctx :system |
39 | 64 | {:type :progress |
40 | 65 | :state :finished}) |
|
153 | 178 | {:status :rejected |
154 | 179 | :actions [:send-toolCallRejected]} |
155 | 180 |
|
| 181 | + [:execution-approved :hook-rejected] |
| 182 | + {:status :rejected |
| 183 | + :actions [:set-decision-reason]} |
| 184 | + |
156 | 185 | [:execution-approved :execution-start] |
157 | 186 | {:status :executing |
158 | 187 | :actions [:set-start-time :add-future :send-toolCallRunning :send-progress]} |
|
163 | 192 |
|
164 | 193 | [:cleanup :cleanup-finished] |
165 | 194 | {:status :completed |
166 | | - :actions [:destroy-all-resources :remove-all-resources :remove-all-promises :remove-future]} |
| 195 | + :actions [:destroy-all-resources :remove-all-resources :remove-all-promises :remove-future :trigger-post-tool-call-hook]} |
167 | 196 |
|
168 | 197 | [:executing :resources-created] |
169 | 198 | {:status :executing |
|
304 | 333 | :details (:details event-data) |
305 | 334 | :summary (:summary event-data)))) |
306 | 335 |
|
| 336 | + :trigger-post-tool-call-hook |
| 337 | + (let [tool-call-state (get-tool-call-state @db* (:chat-id chat-ctx) tool-call-id)] |
| 338 | + (f.hooks/trigger-if-matches! |
| 339 | + :postToolCall |
| 340 | + {:chat-id (:chat-id chat-ctx) |
| 341 | + :tool-name (:name tool-call-state) |
| 342 | + :server (:server tool-call-state) |
| 343 | + :arguments (:arguments tool-call-state)} |
| 344 | + {:on-before-action (partial notify-before-hook-action! chat-ctx) |
| 345 | + :on-after-action (partial notify-after-hook-action! chat-ctx)} |
| 346 | + @db* |
| 347 | + (:config chat-ctx))) |
| 348 | + |
307 | 349 | ;; Actions on parts of the state |
308 | 350 | :deliver-approval-false |
309 | 351 | (deliver (get-in @db* [:chats (:chat-id chat-ctx) :tool-calls tool-call-id :approved?*]) |
|
581 | 623 | (when-not (string/blank? @received-msgs*) |
582 | 624 | (add-to-history! {:role "assistant" :content [{:type :text :text @received-msgs*}]}) |
583 | 625 | (reset! received-msgs* "")) |
584 | | - (let [any-rejected-tool-call?* (atom false)] |
| 626 | + (let [any-rejected-tool-call?* (atom nil)] |
585 | 627 | (run! (fn do-tool-call [{:keys [id name arguments] :as tool-call}] |
586 | 628 | (let [approved?* (promise) ; created here, stored in the state. |
| 629 | + hook-approved?* (atom true) |
587 | 630 | details (f.tools/tool-call-details-before-invocation name arguments) |
588 | 631 | summary (f.tools/tool-call-summary all-tools name arguments config) |
589 | 632 | origin (tool-name->origin name all-tools) |
|
616 | 659 | :text "Tool call rejected by user config"}}) |
617 | 660 | (logger/warn logger-tag "Unknown value of approval in config" |
618 | 661 | {:approval approval :tool-call-id id}))) |
619 | | - ;; Execute each tool call concurrently |
620 | | - (if @approved?* ;TODO: Should there be a timeout here? If so, what would be the state transitions? |
| 662 | + ;; TODO: Should there be a timeout here? If so, what would be the state transitions? |
| 663 | + @approved?* ;; wait for user respond before checking hook |
| 664 | + (f.hooks/trigger-if-matches! :preToolCall |
| 665 | + {:chat-id chat-id |
| 666 | + :tool-name name |
| 667 | + :server server |
| 668 | + :arguments arguments} |
| 669 | + {:on-before-action (partial notify-before-hook-action! chat-ctx) |
| 670 | + :on-after-action (fn [result] |
| 671 | + (when (= 2 (:status result)) |
| 672 | + (transition-tool-call! db* chat-ctx id :hook-rejected |
| 673 | + {:reason {:code :hook-rejected |
| 674 | + :text (str "Tool call rejected by hook, output: " (:output result))}}) |
| 675 | + (reset! hook-approved?* false)) |
| 676 | + (notify-after-hook-action! chat-ctx result))} |
| 677 | + db |
| 678 | + config) |
| 679 | + (if (and @approved?* @hook-approved?*) |
621 | 680 | ;; assert: In :execution-approved or :stopping or :cleanup |
622 | 681 | (when-not (#{:stopping :cleanup} (:status (get-tool-call-state @db* chat-id id))) |
623 | 682 | (assert-chat-not-stopped! chat-ctx) |
|
695 | 754 | :content (assoc tool-call :output {:error true |
696 | 755 | :contents [{:text text |
697 | 756 | :type :text}]})}) |
698 | | - (reset! any-rejected-tool-call?* true) |
| 757 | + (reset! any-rejected-tool-call?* code) |
699 | 758 | (transition-tool-call! db* chat-ctx id :send-reject |
700 | 759 | {:origin origin |
701 | 760 | :name name |
|
741 | 800 | :ex-data (ex-data t) |
742 | 801 | :message (.getMessage t) |
743 | 802 | :cause (.getCause t)}))))))) |
744 | | - (if @any-rejected-tool-call?* |
| 803 | + (if-let [reason-code @any-rejected-tool-call?*] |
745 | 804 | (do |
746 | | - (send-content! chat-ctx :system |
747 | | - {:type :text |
748 | | - :text "Tell ECA what to do differently for the rejected tool(s)"}) |
749 | | - (add-to-history! {:role "user" :content [{:type :text |
750 | | - :text "I rejected one or more tool calls with the following reason"}]}) |
| 805 | + (if (= :hook-rejected reason-code) |
| 806 | + (do |
| 807 | + (send-content! chat-ctx :system |
| 808 | + {:type :text |
| 809 | + :text "Tool rejected by hook"}) |
| 810 | + (add-to-history! {:role "user" :content [{:type :text |
| 811 | + :text "A user hook rejected one or more tool calls with the following reason"}]})) |
| 812 | + (do |
| 813 | + (send-content! chat-ctx :system |
| 814 | + {:type :text |
| 815 | + :text "Tell ECA what to do differently for the rejected tool(s)"}) |
| 816 | + (add-to-history! {:role "user" :content [{:type :text |
| 817 | + :text "I rejected one or more tool calls with the following reason"}]}))) |
751 | 818 | (finish-chat-prompt! :idle chat-ctx) |
752 | 819 | nil) |
753 | 820 | {:new-messages (get-in @db* [:chats chat-id :messages])}))) |
|
882 | 949 | expanded-prompt-contexts |
883 | 950 | image-contents)}] |
884 | 951 | chat-ctx {:chat-id chat-id |
| 952 | + :message message |
885 | 953 | :contexts contexts |
886 | 954 | :behavior selected-behavior |
887 | 955 | :behavior-config behavior-config |
|
892 | 960 | :metrics metrics |
893 | 961 | :config config |
894 | 962 | :messenger messenger} |
895 | | - decision (message->decision message)] |
| 963 | + decision (message->decision message) |
| 964 | + hook-outputs* (atom []) |
| 965 | + _ (f.hooks/trigger-if-matches! :prePrompt |
| 966 | + {:chat-id chat-id |
| 967 | + :prompt message} |
| 968 | + {:on-before-action (partial notify-before-hook-action! chat-ctx) |
| 969 | + :on-after-action (fn [result] |
| 970 | + (when (= 0 (:status result)) |
| 971 | + (reset! hook-outputs* (:outputs result))) |
| 972 | + (notify-after-hook-action! chat-ctx result))} |
| 973 | + db |
| 974 | + config) |
| 975 | + user-messages (if (seq @hook-outputs*) |
| 976 | + (update-in user-messages [0 :content 0 :text] str " " (string/join "\n" @hook-outputs*)) |
| 977 | + user-messages)] |
896 | 978 | (swap! db* assoc-in [:chats chat-id :status] :running) |
897 | 979 | (send-content! chat-ctx :user {:type :text |
898 | 980 | :text (str message "\n")}) |
|
0 commit comments