Skip to content

Commit 7fedcbe

Browse files
committed
Support /resume a specific chat.
1 parent 369da48 commit 7fedcbe

File tree

5 files changed

+127
-57
lines changed

5 files changed

+127
-57
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Support `/resume` a specific chat.
6+
57
## 0.70.6
68

79
- Fix `openai-chat` api not following `completionUrlRelativePath`.

src/eca/db.clj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
(def ^:private logger-tag "[DB]")
1717

18-
(def version 4)
18+
(def version 5)
1919

2020
(defonce initial-db
2121
{:client-info {}
@@ -122,8 +122,10 @@
122122
(-> (select-keys db [:chats])
123123
(update :chats (fn [chats]
124124
(into {}
125-
(map (fn [[k v]]
126-
[k (dissoc v :tool-calls)]))
125+
(comp
126+
(filter #(seq (:messages (second %))))
127+
(map (fn [[k v]]
128+
[k (dissoc v :tool-calls)])))
127129
chats)))))
128130

129131
(defn ^:private normalize-db-for-global-write [db]

src/eca/features/chat.clj

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
(send-content! chat-ctx :system
6666
{:type :progress
6767
:state :finished})
68+
(when-not (get-in @db* [:chats chat-id :created-at])
69+
(swap! db* assoc-in [:chats chat-id :created-at] (System/currentTimeMillis)))
6870
(when on-finished-side-effect
6971
(on-finished-side-effect))
7072
(db/update-workspaces-cache! @db* metrics))
@@ -569,7 +571,10 @@
569571
(swap! db* assoc-in [:chats chat-id :title] title)
570572
(send-content! chat-ctx :system (assoc-some
571573
{:type :metadata}
572-
:title title))))))
574+
:title title))
575+
;; user prompt responded faster than title was generated
576+
(when (= :idle (get-in @db* [:chats chat-id :status]))
577+
(db/update-workspaces-cache! @db* metrics))))))
573578
(send-content! chat-ctx :system {:type :progress
574579
:state :running
575580
:text "Waiting model"})
@@ -696,7 +701,8 @@
696701
(partial get-tool-call-state @db* chat-id id)
697702
(partial transition-tool-call! db* chat-ctx id))
698703
details (f.tools/tool-call-details-after-invocation name arguments details result)
699-
{:keys [start-time]} (get-tool-call-state @db* chat-id id)]
704+
{:keys [start-time]} (get-tool-call-state @db* chat-id id)
705+
total-time-ms (- (System/currentTimeMillis) start-time)]
700706
(add-to-history! {:role "tool_call"
701707
:content (assoc tool-call
702708
:details details
@@ -707,6 +713,7 @@
707713
:content (assoc tool-call
708714
:error (:error result)
709715
:output result
716+
:total-time-ms total-time-ms
710717
:details details
711718
:summary summary
712719
:origin origin
@@ -722,7 +729,7 @@
722729
:arguments arguments
723730
:error (:error result)
724731
:outputs (:contents result)
725-
:total-time-ms (- (System/currentTimeMillis) start-time)
732+
:total-time-ms total-time-ms
726733
:progress-text "Generating"
727734
:details details
728735
:summary summary})
@@ -733,7 +740,7 @@
733740
:arguments arguments
734741
:error (:error result)
735742
:outputs (:contents result)
736-
:total-time-ms (- (System/currentTimeMillis) start-time)
743+
:total-time-ms total-time-ms
737744
:reason :user-stop
738745
:details details
739746
:summary summary})
@@ -834,13 +841,14 @@
834841
{:type :reasonText
835842
:id id
836843
:text text}))
837-
:finished (do
844+
:finished (let [total-time-ms (- (System/currentTimeMillis) (get-in @reasonings* [id :start-time]))]
838845
(add-to-history! {:role "reason" :content {:id id
839846
:external-id external-id
847+
:total-time-ms total-time-ms
840848
:text (get-in @reasonings* [id :text])}})
841849
(send-content! chat-ctx :assistant
842850
{:type :reasonFinished
843-
:total-time-ms (- (System/currentTimeMillis) (get-in @reasonings* [id :start-time]))
851+
:total-time-ms total-time-ms
844852
:id id}))
845853
nil))
846854
:on-error (fn [{:keys [message exception]}]
@@ -865,42 +873,54 @@
865873
(case role
866874
("user"
867875
"system"
868-
"assistant") (reduce
869-
(fn [m content]
870-
(case (:type content)
871-
:text (assoc m
872-
:type :text
873-
:text (str (:text m) "\n" (:text content)))
874-
m))
875-
{}
876-
message-content)
877-
"tool_call" {:type :toolCallPrepare
878-
:origin (:origin message-content)
879-
:name (:name message-content)
880-
:server (:server message-content)
881-
:arguments-text ""
882-
:id (:id message-content)}
883-
"tool_call_output" {:type :toolCalled
884-
:origin (:origin message-content)
885-
:name (:name message-content)
886-
:server (:server message-content)
887-
:arguments (:arguments message-content)
888-
:error (:error message-content)
889-
:id (:id message-content)
890-
:outputs (:contents (:output message-content))}
891-
"reason" {:id (:id message-content)
892-
:external-id (:external-id message-content)
893-
:text (:text message-content)}))
876+
"assistant") [(reduce
877+
(fn [m content]
878+
(case (:type content)
879+
:text (assoc m
880+
:type :text
881+
:text (str (:text m) "\n" (:text content)))
882+
m))
883+
{}
884+
message-content)]
885+
"tool_call" [{:type :toolCallPrepare
886+
:origin (:origin message-content)
887+
:name (:name message-content)
888+
:server (:server message-content)
889+
:arguments-text ""
890+
:id (:id message-content)}]
891+
"tool_call_output" [{:type :toolCalled
892+
:origin (:origin message-content)
893+
:name (:name message-content)
894+
:server (:server message-content)
895+
:arguments (:arguments message-content)
896+
:total-time-ms (:total-time-ms message-content)
897+
:error (:error message-content)
898+
:id (:id message-content)
899+
:outputs (:contents (:output message-content))}]
900+
"reason" [{:type :reasonStarted
901+
:id (:id message-content)}
902+
{:type :reasonText
903+
:id (:id message-content)
904+
:text (:text message-content)}
905+
{:type :reasonFinished
906+
:id (:id message-content)
907+
:total-time-ms (:total-time-ms message-content)}]))
894908

895909
(defn ^:private handle-command! [{:keys [command args]} chat-ctx]
896910
(let [{:keys [type on-finished-side-effect] :as result} (f.commands/handle-command! command args chat-ctx)]
897911
(case type
898912
:chat-messages (do
899-
(doseq [[chat-id messages] (:chats result)]
913+
(doseq [[chat-id {:keys [messages title]}] (:chats result)]
900914
(doseq [message messages]
901-
(send-content! (assoc chat-ctx :chat-id chat-id)
902-
(:role message)
903-
(message-content->chat-content (:role message) (:content message)))))
915+
(let [new-chat-ctx (assoc chat-ctx :chat-id chat-id)]
916+
(doseq [chat-content (message-content->chat-content (:role message) (:content message))]
917+
(send-content! new-chat-ctx
918+
(:role message)
919+
chat-content))
920+
(when title
921+
(send-content! new-chat-ctx :system (assoc-some
922+
{:type :metadata}
923+
:title title))))))
904924
(finish-chat-prompt! :idle chat-ctx))
905925
:new-chat-status (finish-chat-prompt! (:status result) chat-ctx)
906926
:send-prompt (prompt-messages! [{:role "user" :content (:prompt result)}] (assoc chat-ctx :on-finished-side-effect on-finished-side-effect))

src/eca/features/commands.clj

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
[eca.features.prompt :as f.prompt]
1111
[eca.features.tools.mcp :as f.mcp]
1212
[eca.llm-api :as llm-api]
13+
[eca.logger :as logger]
1314
[eca.messenger :as messenger]
1415
[eca.secrets :as secrets]
1516
[eca.shared :as shared :refer [multi-str update-some]])
1617
(:import
18+
[clojure.lang PersistentVector]
1719
[java.lang ProcessHandle]))
1820

1921
(set! *warn-on-reflection* true)
@@ -96,8 +98,8 @@
9698
:arguments [{:name "additional-input"}]}
9799
{:name "resume"
98100
:type :native
99-
:description "Resume the chats from this session workspaces."
100-
:arguments []}
101+
:description "Resume the specified chat-id. Blank to list chats or 'latest'."
102+
:arguments [{:name "chat-id"}]}
101103
{:name "config"
102104
:type :native
103105
:description "Show ECA config for troubleshooting."
@@ -230,13 +232,49 @@
230232
metrics)
231233
{:type :new-chat-status
232234
:status :login})
233-
"resume" (let [chats (:chats db)]
234-
;; Override current chat with first chat
235-
(when-let [first-chat (second (first chats))]
236-
(swap! db* assoc-in [:chats chat-id] first-chat)
237-
;; TODO support multiple chats update
238-
{:type :chat-messages
239-
:chats {chat-id (:messages first-chat)}}))
235+
"resume" (let [chats (into {}
236+
(filter #(not= chat-id (first %)))
237+
(:chats db))
238+
chats-ids (vec (sort-by #(:created-at (get chats %)) (keys chats)))
239+
selected-chat-id (try (if (= "latest" (first args))
240+
(count chats-ids)
241+
(parse-long (first args)))
242+
(catch Exception _ nil))
243+
selected-chat-id (when selected-chat-id (nth chats-ids (dec selected-chat-id) nil))
244+
chat-message-fn (fn [text]
245+
{:type :chat-messages
246+
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text text}]}]}}})]
247+
(cond
248+
(empty? chats)
249+
(chat-message-fn "No past chats found to resume")
250+
251+
(string/blank? (first args))
252+
(chat-message-fn (multi-str
253+
"Chats:"
254+
""
255+
"ID - Created at - Title"
256+
(reduce
257+
(fn [s chat-id]
258+
(let [chat (get chats chat-id)]
259+
(str s (format "%s - %s - %s\n"
260+
(inc (.indexOf ^PersistentVector chats-ids chat-id))
261+
(shared/ms->presentable-date (:created-at chat) "dd/MM/yyyy HH:mm")
262+
(or (:title chat) (format "No chat title (%s user messages)" (count (filter #(= "user" (:role %))
263+
(:messages chat)))))))))
264+
""
265+
chats-ids)
266+
"Run `/resume <chat-id>` or `/resume latest`"))
267+
268+
(not selected-chat-id)
269+
(chat-message-fn "Chat ID not found.")
270+
271+
:else
272+
(let [chat (get chats selected-chat-id)]
273+
(swap! db* assoc-in [:chats chat-id] chat)
274+
{:type :chat-messages
275+
:chats {chat-id {:title (:title chat)
276+
:messages (concat [{:role "system" :content [{:type :text :text (str "Resuming chat: " selected-chat-id)}]}]
277+
(:messages chat))}}})))
240278
"costs" (let [total-input-tokens (get-in db [:chats chat-id :total-input-tokens] 0)
241279
total-input-cache-creation-tokens (get-in db [:chats chat-id :total-input-cache-creation-tokens] nil)
242280
total-input-cache-read-tokens (get-in db [:chats chat-id :total-input-cache-read-tokens] nil)
@@ -250,13 +288,13 @@
250288
(str "Total output tokens: " total-output-tokens)
251289
(str "Total cost: $" (shared/tokens->cost total-input-tokens total-input-cache-creation-tokens total-input-cache-read-tokens total-output-tokens model-capabilities)))]
252290
{:type :chat-messages
253-
:chats {chat-id [{:role "system" :content [{:type :text :text text}]}]}})
291+
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text text}]}]}}})
254292
"config" {:type :chat-messages
255-
:chats {chat-id [{:role "system" :content [{:type :text :text (with-out-str (pprint/pprint config))}]}]}}
293+
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text (with-out-str (pprint/pprint config))}]}]}}}
256294
"doctor" {:type :chat-messages
257-
:chats {chat-id [{:role "system" :content [{:type :text :text (doctor-msg db config)}]}]}}
295+
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text (doctor-msg db config)}]}]}}}
258296
"repo-map-show" {:type :chat-messages
259-
:chats {chat-id [{:role "system" :content [{:type :text :text (f.index/repo-map db config {:as-string? true})}]}]}}
297+
:chats {chat-id {:messages [{:role "system" :content [{:type :text :text (f.index/repo-map db config {:as-string? true})}]}]}}}
260298
"prompt-show" (let [full-prompt (str "Instructions:\n" instructions "\n"
261299
"Prompt:\n" (reduce
262300
(fn [s {:keys [content]}]
@@ -269,9 +307,9 @@
269307
""
270308
user-messages))]
271309
{:type :chat-messages
272-
:chats {chat-id [{:role "system"
273-
:content [{:type :text
274-
:text full-prompt}]}]}})
310+
:chats {chat-id {:messages [{:role "system"
311+
:content [{:type :text
312+
:text full-prompt}]}]}}})
275313

276314
;; else check if a custom command
277315
(if-let [custom-command-prompt (get-custom-command command args custom-cmds)]

src/eca/shared.clj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
[clojure.walk :as walk])
88
(:import
99
[java.net URI]
10-
[java.nio.file Paths]))
10+
[java.nio.file Paths]
11+
[java.time ZoneOffset]
12+
[java.time.format DateTimeFormatter]
13+
[java.time Instant]))
1114

1215
(set! *warn-on-reflection* true)
1316

@@ -67,7 +70,7 @@
6770
(if (next kvs)
6871
(recur ret (first kvs) (second kvs) (nnext kvs))
6972
(throw (IllegalArgumentException.
70-
"assoc-some expects even number of arguments after map/vector, found odd number")))
73+
"assoc-some expects even number of arguments after map/vector, found odd number")))
7174
ret))))
7275

7376
(defn update-some
@@ -177,3 +180,8 @@
177180
(mf file (safe-mtime file)))
178181
([file & args]
179182
(apply mf file (safe-mtime file) args)))))
183+
184+
(defn ms->presentable-date [^Long ms ^String pattern]
185+
(when ms
186+
(.format (.atOffset (Instant/ofEpochMilli ms) ZoneOffset/UTC)
187+
(DateTimeFormatter/ofPattern pattern))))

0 commit comments

Comments
 (0)