Skip to content

Commit 40f7f5b

Browse files
committed
Improve tool calling to avoid stop LLM loop if any exception happens.
1 parent 76b5221 commit 40f7f5b

File tree

6 files changed

+120
-82
lines changed

6 files changed

+120
-82
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Avoid crash MCP start if doesn't support some capabilities.
6+
- Improve tool calling to avoid stop LLM loop if any exception happens.
67

78
## 0.17.0
89

src/eca/features/chat.clj

Lines changed: 11 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
(ns eca.features.chat
22
(:require
3-
[babashka.fs :as fs]
43
[cheshire.core :as json]
54
[clojure.set :as set]
65
[clojure.string :as string]
6+
[eca.features.context :as f.context]
77
[eca.features.index :as f.index]
88
[eca.features.prompt :as f.prompt]
99
[eca.features.rules :as f.rules]
@@ -18,29 +18,6 @@
1818

1919
(def ^:private logger-tag "[CHAT]")
2020

21-
(defn ^:private raw-contexts->refined [contexts db]
22-
(mapcat (fn [{:keys [type path lines-range uri]}]
23-
(case (name type)
24-
"file" [{:type :file
25-
:path path
26-
:partial (boolean lines-range)
27-
:content (llm-api/refine-file-context path lines-range)}]
28-
"directory" (->> (fs/glob path "**")
29-
(remove fs/directory?)
30-
(map (fn [path]
31-
(let [filename (str (fs/canonicalize path))]
32-
{:type :file
33-
:path filename
34-
:content (llm-api/refine-file-context filename nil)}))))
35-
"repoMap" [{:type :repoMap}]
36-
"mcpResource" (mapv
37-
(fn [{:keys [text]}]
38-
{:type :mcpResource
39-
:uri uri
40-
:content text})
41-
(:contents (f.mcp/get-resource! uri db)))))
42-
contexts))
43-
4421
(defn default-model [db config]
4522
(llm-api/default-model db config))
4623

@@ -144,7 +121,7 @@
144121
(let [db @db*
145122
manual-approval? (get-in config [:toolCall :manualApproval] false)
146123
rules (f.rules/all config (:workspace-folders db))
147-
refined-contexts (raw-contexts->refined contexts db)
124+
refined-contexts (f.context/raw-contexts->refined contexts db)
148125
repo-map* (delay (f.index/repo-map db {:as-string? true}))
149126
instructions (f.prompt/build-instructions refined-contexts rules repo-map* (or behavior (:chat-default-behavior db)) config)
150127
past-messages (get-in db [:chats chat-id :messages] [])
@@ -292,8 +269,12 @@
292269
{:keys [db*] :as chat-ctx}]
293270
(let [{:keys [arguments]} (first (filter #(= prompt (:name %)) (f.mcp/all-prompts @db*)))
294271
args-vals (zipmap (map :name arguments) args)
295-
{:keys [messages]} (f.mcp/get-prompt! prompt args-vals @db*)]
296-
(prompt-messages! messages false chat-ctx)))
272+
{:keys [messages error-message]} (f.prompt/get-prompt! prompt args-vals @db*)]
273+
(if error-message
274+
(send-content! chat-ctx :system
275+
{:type :text
276+
:text error-message})
277+
(prompt-messages! messages false chat-ctx))))
297278

298279
(defn ^:private handle-command! [{:keys [command]} {:keys [chat-id db* model] :as chat-ctx}]
299280
(let [db @db*]
@@ -355,38 +336,13 @@
355336
(defn tool-call-reject [{:keys [chat-id tool-call-id]} db*]
356337
(deliver (get-in @db* [:chats chat-id :tool-calls tool-call-id :approved?*]) false))
357338

358-
(defn ^:private contexts-for [root-filename query config]
359-
(let [all-files (fs/glob root-filename (str "**" (or query "") "**"))
360-
allowed-files (f.index/filter-allowed all-files root-filename config)]
361-
allowed-files))
362-
363339
(defn query-context
364340
[{:keys [query contexts chat-id]}
365341
db*
366342
config]
367-
(let [all-subfiles-and-dirs (into []
368-
(comp
369-
(map :uri)
370-
(map shared/uri->filename)
371-
(mapcat #(contexts-for % query config))
372-
(take 200) ;; for performance, user can always make query specific for better results.
373-
(map (fn [file-or-dir]
374-
{:type (if (fs/directory? file-or-dir)
375-
:directory
376-
:file)
377-
:path (str (fs/canonicalize file-or-dir))})))
378-
(:workspace-folders @db*))
379-
root-dirs (mapv (fn [{:keys [uri]}] {:type :directory
380-
:path (shared/uri->filename uri)})
381-
(:workspace-folders @db*))
382-
mcp-resources (mapv #(assoc % :type :mcpResource) (f.mcp/all-resources @db*))
383-
all-contexts (concat [{:type :repoMap}]
384-
root-dirs
385-
all-subfiles-and-dirs
386-
mcp-resources)]
387-
{:chat-id chat-id
388-
:contexts (set/difference (set all-contexts)
389-
(set contexts))}))
343+
{:chat-id chat-id
344+
:contexts (set/difference (set (f.context/all-contexts query db* config))
345+
(set contexts))})
390346

391347
(defn query-commands
392348
[{:keys [query chat-id]}

src/eca/features/context.clj

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
(ns eca.features.context
2+
(:require
3+
[babashka.fs :as fs]
4+
[eca.features.index :as f.index]
5+
[eca.features.tools.mcp :as f.mcp]
6+
[eca.llm-api :as llm-api]
7+
[eca.logger :as logger]
8+
[eca.shared :as shared]))
9+
10+
(set! *warn-on-reflection* true)
11+
12+
(def ^:private logger-tag "[CONTEXT]")
13+
14+
(defn raw-contexts->refined [contexts db]
15+
(mapcat (fn [{:keys [type path lines-range uri]}]
16+
(case (name type)
17+
"file" [{:type :file
18+
:path path
19+
:partial (boolean lines-range)
20+
:content (llm-api/refine-file-context path lines-range)}]
21+
"directory" (->> (fs/glob path "**")
22+
(remove fs/directory?)
23+
(map (fn [path]
24+
(let [filename (str (fs/canonicalize path))]
25+
{:type :file
26+
:path filename
27+
:content (llm-api/refine-file-context filename nil)}))))
28+
"repoMap" [{:type :repoMap}]
29+
"mcpResource" (try
30+
(mapv
31+
(fn [{:keys [text]}]
32+
{:type :mcpResource
33+
:uri uri
34+
:content text})
35+
(:contents (f.mcp/get-resource! uri db)))
36+
(catch Exception e
37+
(logger/warn logger-tag (format "Error getting MCP resource %s: %s" uri (.getMessage e)))
38+
[]))))
39+
contexts))
40+
41+
(defn ^:private contexts-for [root-filename query config]
42+
(let [all-files (fs/glob root-filename (str "**" (or query "") "**"))
43+
allowed-files (f.index/filter-allowed all-files root-filename config)]
44+
allowed-files))
45+
46+
(defn all-contexts [query config db*]
47+
(let [all-subfiles-and-dirs (into []
48+
(comp
49+
(map :uri)
50+
(map shared/uri->filename)
51+
(mapcat #(contexts-for % query config))
52+
(take 200) ;; for performance, user can always make query specific for better results.
53+
(map (fn [file-or-dir]
54+
{:type (if (fs/directory? file-or-dir)
55+
:directory
56+
:file)
57+
:path (str (fs/canonicalize file-or-dir))})))
58+
(:workspace-folders @db*))
59+
root-dirs (mapv (fn [{:keys [uri]}] {:type :directory
60+
:path (shared/uri->filename uri)})
61+
(:workspace-folders @db*))
62+
mcp-resources (mapv #(assoc % :type :mcpResource) (f.mcp/all-resources @db*))]
63+
(concat [{:type :repoMap}]
64+
root-dirs
65+
all-subfiles-and-dirs
66+
mcp-resources)))

src/eca/features/prompt.clj

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
(:require
33
[clojure.java.io :as io]
44
[clojure.string :as string]
5-
[eca.shared :refer [multi-str]]))
5+
[eca.features.tools.mcp :as f.mcp]
6+
[eca.logger :as logger]
7+
[eca.shared :refer [multi-str]])
8+
(:import
9+
[java.util Map]))
10+
11+
(set! *warn-on-reflection* true)
12+
13+
(def ^:private logger-tag "[PROMPT]")
614

715
(defn ^:private eca-prompt-template* [] (slurp (io/resource "eca_prompt.txt")))
816

@@ -43,3 +51,13 @@
4351
""
4452
refined-contexts)
4553
"</contexts>"))
54+
55+
(defn get-prompt! [^String name ^Map arguments db]
56+
(logger/info logger-tag (format "Calling prompt '%s' with args '%s'" name arguments))
57+
(try
58+
(let [result (f.mcp/get-prompt! name arguments db)]
59+
(logger/debug logger-tag "Prompt result: " result)
60+
result)
61+
(catch Exception e
62+
(logger/warn logger-tag (format "Error calling prompt %s: %s" name (.getMessage e)))
63+
{:error-message (str "Error calling prompt: " (.getMessage e))})))

src/eca/features/tools.clj

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,17 @@
5151
(defn call-tool! [^String name ^Map arguments db config]
5252
(logger/info logger-tag (format "Calling tool '%s' with args '%s'" name arguments))
5353
(let [arguments (update-keys arguments clojure.core/name)]
54-
(if-let [native-tool-handler (get-in (native-definitions db config) [name :handler])]
55-
(native-tool-handler arguments {:db db :config config})
56-
(f.mcp/call-tool! name arguments db))))
54+
(try
55+
(let [result (if-let [native-tool-handler (get-in (native-definitions db config) [name :handler])]
56+
(native-tool-handler arguments {:db db :config config})
57+
(f.mcp/call-tool! name arguments db))]
58+
(logger/debug logger-tag "Tool call result: " result)
59+
result)
60+
(catch Exception e
61+
(logger/warn logger-tag (format "Error calling tool %s: %s" name (.getMessage e)))
62+
{:error true
63+
:contents [{:type :text
64+
:text (str "Error calling tool: " (.getMessage e))}]}))))
5765

5866
(defn init-servers! [db* messenger config]
5967
(let [disabled-tools (set (get-in config [:disabledTools] []))

src/eca/features/tools/mcp.clj

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -203,17 +203,10 @@
203203
(when (some #(= name (:name %)) tools)
204204
client)))
205205
first)
206-
result (try
207-
(let [result (.callTool ^McpSyncClient mcp-client
208-
(McpSchema$CallToolRequest. name arguments))]
209-
{:error (.isError result)
210-
:contents (mapv ->content (.content result))})
211-
(catch Exception e
212-
{:error true
213-
:contents [{:type :text
214-
:text (.getMessage e)}]}))]
215-
(logger/debug logger-tag "ToolCall result: " result)
216-
result))
206+
result (.callTool ^McpSyncClient mcp-client
207+
(McpSchema$CallToolRequest. name arguments))]
208+
{:error (.isError result)
209+
:contents (mapv ->content (.content result))}))
217210

218211
(defn all-prompts [db]
219212
(into []
@@ -233,25 +226,21 @@
233226
(when (some #(= name (:name %)) prompts)
234227
client)))
235228
first)
236-
prompt (.getPrompt ^McpSyncClient mcp-client (McpSchema$GetPromptRequest. name arguments))
237-
result {:description (.description prompt)
238-
:messages (mapv (fn [^McpSchema$PromptMessage message]
239-
{:role (string/lower-case (str (.role message)))
240-
:content [(->content (.content message))]})
241-
(.messages prompt))}]
242-
(logger/debug logger-tag "Prompt result:" result)
243-
result))
229+
prompt (.getPrompt ^McpSyncClient mcp-client (McpSchema$GetPromptRequest. name arguments))]
230+
{:description (.description prompt)
231+
:messages (mapv (fn [^McpSchema$PromptMessage message]
232+
{:role (string/lower-case (str (.role message)))
233+
:content [(->content (.content message))]})
234+
(.messages prompt))}))
244235

245236
(defn get-resource! [^String uri db]
246237
(let [mcp-client (->> (vals (:mcp-clients db))
247238
(keep (fn [{:keys [client resources]}]
248239
(when (some #(= uri (:uri %)) resources)
249240
client)))
250241
first)
251-
resource (.readResource ^McpSyncClient mcp-client (McpSchema$ReadResourceRequest. uri))
252-
result {:contents (mapv ->resource-content (.contents resource))}]
253-
(logger/debug logger-tag "Resource result:" result)
254-
result))
242+
resource (.readResource ^McpSyncClient mcp-client (McpSchema$ReadResourceRequest. uri))]
243+
{:contents (mapv ->resource-content (.contents resource))}))
255244

256245
(defn shutdown! [db*]
257246
(doseq [[_name {:keys [client]}] (:mcp-clients @db*)]

0 commit comments

Comments
 (0)