Skip to content

Commit cdcce02

Browse files
committed
Add read_file
1 parent f934141 commit cdcce02

5 files changed

Lines changed: 79 additions & 29 deletions

File tree

src/eca/features/chat.clj

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,8 @@
9494
user-prompt message
9595
all-tools (f.tools/all-tools @db* config)
9696
received-msgs* (atom "")
97-
add-to-history! (fn [msg & [append-to-last?]]
98-
(if append-to-last?
99-
(swap! db* update-in [:chats chat-id :messages] (fn [messages msg]
100-
(conj (pop messages)
101-
(update (last messages) :content str msg))) msg)
102-
(swap! db* update-in [:chats chat-id :messages] (fnil conj []) msg)))]
97+
add-to-history! (fn [msg]
98+
(swap! db* update-in [:chats chat-id :messages] (fnil conj []) msg))]
10399
(messenger/chat-content-received
104100
messenger
105101
{:chat-id chat-id

src/eca/features/tools.clj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
(defn native-definitions [config]
1616
(merge {}
1717
(when (get-in config [:nativeTools :filesystem :enabled])
18-
f.tools.filesystem/definitions)))
18+
(into
19+
{}
20+
(map (fn [[name tool]]
21+
[name (assoc tool :name name)]))
22+
f.tools.filesystem/definitions))))
1923

2024
(defn all-tools [db config]
2125
(let [native-tools (concat

src/eca/features/tools/filesystem.clj

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,26 @@
1515
(some #(fs/starts-with? path (shared/uri->filename (:uri %)))
1616
(:workspace-folders db)))
1717

18-
(defn ^:private list-allowed-directories [_arguments db]
19-
(single-text-content
20-
(str "Allowed directories:\n"
21-
(string/join "\n"
22-
(map (comp shared/uri->filename :uri) (:workspace-folders db))))))
23-
2418
(defn ^:private invalid-arguments [arguments validator]
2519
(first (keep (fn [[key pred error-msg]]
2620
(let [value (get arguments key)]
2721
(when-not (pred value)
28-
(single-text-content (string/replace error-msg (str "$" key) value)))))
22+
(single-text-content (string/replace error-msg (str "$" key) value) :error))))
2923
validator)))
3024

25+
(defn ^:private path-validations [db]
26+
[["path" fs/exists? "$path is not a valid path"]
27+
["path" (partial allowed-path? db) "Access denied - path $path outside allowed directories"]])
28+
29+
(defn ^:private list-allowed-directories [_arguments db]
30+
(single-text-content
31+
(str "Allowed directories:\n"
32+
(string/join "\n"
33+
(map (comp shared/uri->filename :uri) (:workspace-folders db))))))
34+
3135
(defn ^:private list-directory [arguments db]
3236
(let [path (delay (fs/canonicalize (get arguments "path")))]
33-
(or (invalid-arguments arguments [["path" fs/regular-file? "$path is not a valid path"]
34-
["path" (partial allowed-path? db) "Access denied - path $path outside allowed directories"]])
37+
(or (invalid-arguments arguments (path-validations db))
3538
(single-text-content
3639
(reduce
3740
(fn [out path]
@@ -42,23 +45,42 @@
4245
""
4346
(fs/list-dir @path))))))
4447

48+
(defn ^:private read-file [arguments db]
49+
(or (invalid-arguments arguments (concat (path-validations db)
50+
[["path" fs/readable? "File $path is not readable"]]))
51+
(single-text-content (slurp (fs/file (fs/canonicalize (get arguments "path")))))))
52+
4553
(def definitions
4654
{"list_allowed_directories"
47-
{:name "list_allowed_directories"
48-
:description (str "Returns the list of directories that this server is allowed to access."
55+
{:description (str "Returns the list of directories that this server is allowed to access. "
4956
"Use this to understand which directories are available before trying to access files.")
5057
:parameters {:type "object"
5158
:properties {}
5259
:required []}
5360
:handler #'list-allowed-directories}
5461
"list_directory"
55-
{:name "list_directory"
56-
:description (str "Get a detailed listing of all files and directories in a specified path. "
62+
{:description (str "Get a detailed listing of all files and directories in a specified path. "
5763
"Results clearly distinguish between files and directories with [FILE] and [DIR] "
5864
"prefixes. This tool is essential for understanding directory structure and "
5965
"finding specific files within a directory. Only works within workspace root.")
6066
:parameters {:type "object"
6167
:properties {"path" {:type "string"
62-
:description "The path to the directory to list."}}
68+
:description "The absolute path to the directory to list."}}
69+
:required ["path"]}
70+
:handler #'list-directory}
71+
"read_file"
72+
{:description (str "Read the complete contents of a file from the file system. "
73+
"Handles various text encodings and provides detailed error messages "
74+
"if the file cannot be read. Use this tool when you need to examine "
75+
"the contents of a single file. Use the 'head' parameter to read only "
76+
"the first N lines of a file, or the 'tail' parameter to read only "
77+
"the last N lines of a file. Only works within allowed directories.")
78+
:parameters {:type "object"
79+
:properties {"path" {:type "string"
80+
:description "The absolute path to the file to read."}
81+
"head" {:type "number"
82+
:description "If provided, returns only the first N lines of the file"}
83+
"tail" {:type "number"
84+
:description "If provided, returns only the last N lines of the file"}}
6385
:required ["path"]}
64-
:handler #'list-directory}})
86+
:handler #'read-file}})

src/eca/llm_providers/anthropic.clj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
:content [{:type "tool_use"
6666
:id (:id content)
6767
:name (:name content)
68-
:input (:arguments content)}]}
68+
:input (or (:arguments content) {})}]}
6969

7070
"tool_call_output"
7171
{:role "user"
@@ -75,7 +75,9 @@
7575
msg))
7676
past-messages)
7777
;; TODO add cache_control to last non thinking message
78-
{:role "user" :content user-prompt :cache_control {:type "ephemeral"}}))
78+
{:role "user" :content [{:type :text
79+
:text user-prompt
80+
:cache_control {:type "ephemeral"}}]}))
7981

8082
(defn completion!
8183
[{:keys [model user-prompt temperature context max-tokens

test/eca/features/tools/filesystem_test.clj

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns eca.features.tools.filesystem-test
22
(:require
33
[babashka.fs :as fs]
4+
[clojure.string :as string]
45
[clojure.test :refer [deftest is testing]]
56
[eca.features.tools.filesystem :as f.tools.filesystem]
67
[eca.test-helper :as h]
@@ -20,20 +21,20 @@
2021
(testing "Invalid path"
2122
(is (match?
2223
{:contents [{:type :text
23-
:error false
24+
:error true
2425
:content "/foo/qux is not a valid path"}]}
2526
(with-redefs [fs/canonicalize (constantly (h/file-path "/foo/qux"))
26-
fs/regular-file? (constantly false)]
27+
fs/exists? (constantly false)]
2728
((get-in f.tools.filesystem/definitions ["list_directory" :handler])
2829
{"path" (h/file-path "/foo/qux")}
2930
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar/baz") :name "baz"}]})))))
3031
(testing "Unallowed dir"
3132
(is (match?
3233
{:contents [{:type :text
33-
:error false
34+
:error true
3435
:content "Access denied - path /foo/qux outside allowed directories"}]}
3536
(with-redefs [fs/canonicalize (constantly (h/file-path "/foo/qux"))
36-
fs/regular-file? (constantly true)]
37+
fs/exists? (constantly true)]
3738
((get-in f.tools.filesystem/definitions ["list_directory" :handler])
3839
{"path" (h/file-path "/foo/qux")}
3940
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar/baz") :name "baz"}]})))))
@@ -45,7 +46,7 @@
4546
"[DIR] %s\n")
4647
(h/file-path "/foo/bar/baz/some.clj")
4748
(h/file-path "/foo/bar/baz/qux"))}]}
48-
(with-redefs [fs/regular-file? (constantly true)
49+
(with-redefs [fs/exists? (constantly true)
4950
fs/starts-with? (constantly true)
5051
fs/list-dir (constantly [(fs/path (h/file-path "/foo/bar/baz/some.clj"))
5152
(fs/path (h/file-path "/foo/bar/baz/qux"))])
@@ -54,3 +55,28 @@
5455
((get-in f.tools.filesystem/definitions ["list_directory" :handler])
5556
{"path" (h/file-path "/foo/bar/baz")}
5657
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar/baz") :name "baz"}]}))))))
58+
59+
(deftest read-file-test
60+
(testing "Not readable path"
61+
(is (match?
62+
{:contents [{:type :text
63+
:error true
64+
:content "File /foo/qux is not readable"}]}
65+
(with-redefs [fs/exists? (constantly true)
66+
fs/readable? (constantly false)
67+
f.tools.filesystem/allowed-path? (constantly true)]
68+
((get-in f.tools.filesystem/definitions ["read_file" :handler])
69+
{"path" (h/file-path "/foo/qux")}
70+
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar/baz") :name "baz"}]})))))
71+
(testing "Readable path"
72+
(is (match?
73+
{:contents [{:type :text
74+
:error false
75+
:content "fooo"}]}
76+
(with-redefs [slurp (constantly "fooo")
77+
fs/exists? (constantly true)
78+
fs/readable? (constantly true)
79+
f.tools.filesystem/allowed-path? (constantly true)]
80+
((get-in f.tools.filesystem/definitions ["read_file" :handler])
81+
{"path" (h/file-path "/foo/qux")}
82+
{:workspace-folders [{:uri (h/file-uri "file:///foo/bar/baz") :name "baz"}]}))))))

0 commit comments

Comments
 (0)