Skip to content

Commit 396d7fb

Browse files
authored
Merge pull request #244 from editor-code-assistant/improve-context-handling
Context: skip missing @file refs (reduce prompt noise)
2 parents 3a61bc6 + 9b4fc9d commit 396d7fb

File tree

6 files changed

+114
-69
lines changed

6 files changed

+114
-69
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
- Fix openai-chat tool call + support for Mistral API #233
5+
- Skip missing/unreadable @file references when building context
56

67
## 0.87.1
78

src/eca/features/context.clj

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,34 @@
2727
([path visited]
2828
(if (contains? visited path)
2929
[]
30-
(let [root-content (llm-api/refine-file-context path nil)
31-
visited' (conj visited path)
32-
at-mentions (extract-at-mentions root-content)
33-
parent-dir (str (fs/parent path))
34-
resolved-paths (map (fn [mention]
35-
(cond
36-
;; Absolute path
37-
(string/starts-with? mention "/")
38-
(str (fs/canonicalize (fs/file mention)))
30+
(if-let [root-content (llm-api/refine-file-context path nil)]
31+
(let [visited' (conj visited path)
32+
at-mentions (extract-at-mentions root-content)
33+
parent-dir (str (fs/parent path))
34+
resolved-paths (map (fn [mention]
35+
(cond
36+
;; Absolute path
37+
(string/starts-with? mention "/")
38+
(str (fs/canonicalize (fs/file mention)))
3939

40-
;; Relative path (./... or ../...)
41-
(or (string/starts-with? mention "./")
42-
(string/starts-with? mention "../"))
43-
(str (fs/canonicalize (fs/file parent-dir mention)))
40+
;; Relative path (./... or ../...)
41+
(or (string/starts-with? mention "./")
42+
(string/starts-with? mention "../"))
43+
(str (fs/canonicalize (fs/file parent-dir mention)))
4444

45-
;; Simple filename, relative to current file's directory
46-
:else
47-
(str (fs/canonicalize (fs/file parent-dir mention)))))
48-
at-mentions)
49-
;; Deduplicate resolved paths
50-
unique-paths (distinct resolved-paths)
51-
;; Recursively parse all mentioned files
52-
nested-results (mapcat #(parse-agents-file % visited') unique-paths)]
53-
(concat [{:type :agents-file
54-
:path path
55-
:content root-content}]
56-
nested-results)))))
45+
;; Simple filename, relative to current file's directory
46+
:else
47+
(str (fs/canonicalize (fs/file parent-dir mention)))))
48+
at-mentions)
49+
;; Deduplicate resolved paths
50+
unique-paths (distinct resolved-paths)
51+
;; Recursively parse all mentioned files
52+
nested-results (mapcat #(parse-agents-file % visited') unique-paths)]
53+
(concat [{:type :agents-file
54+
:path path
55+
:content root-content}]
56+
nested-results))
57+
[]))))
5758

5859
(defn agents-file-contexts
5960
"Search for AGENTS.md file both in workspaceRoot and global config dir.
@@ -75,30 +76,35 @@
7576
(mapcat #(parse-agents-file (str %))))))
7677

7778
(defn ^:private file->refined-context [path lines-range]
78-
(let [ext (string/lower-case (or (fs/extension path) ""))]
79-
(if (contains? #{"png" "jpg" "jpeg" "gif" "webp"} ext)
80-
{:type :image
81-
:media-type (case ext
82-
"jpg" "image/jpeg"
83-
(str "image/" ext))
84-
:base64 (.encodeToString (Base64/getEncoder)
85-
(fs/read-all-bytes (fs/file path)))
86-
:path path}
87-
(assoc-some
88-
{:type :file
89-
:path path
90-
:content (llm-api/refine-file-context path lines-range)}
91-
:lines-range lines-range))))
79+
(if (fs/readable? path)
80+
(let [ext (string/lower-case (or (fs/extension path) ""))]
81+
(if (contains? #{"png" "jpg" "jpeg" "gif" "webp"} ext)
82+
{:type :image
83+
:media-type (case ext
84+
"jpg" "image/jpeg"
85+
(str "image/" ext))
86+
:base64 (.encodeToString (Base64/getEncoder)
87+
(fs/read-all-bytes (fs/file path)))
88+
:path path}
89+
(when-let [content (llm-api/refine-file-context path lines-range)]
90+
(assoc-some
91+
{:type :file
92+
:path path
93+
:content content}
94+
:lines-range lines-range))))
95+
(logger/warn logger-tag "File not found or unreadable at" path)))
9296

9397
(defn raw-contexts->refined [contexts db]
9498
(mapcat (fn [{:keys [type path lines-range position uri]}]
9599
(case (name type)
96-
"file" [(file->refined-context path lines-range)]
100+
"file" (if-let [ctx (file->refined-context path lines-range)]
101+
[ctx]
102+
[])
97103
"directory" (->> (fs/glob path "**")
98104
(remove fs/directory?)
99-
(map (fn [path]
100-
(let [filename (str (fs/canonicalize path))]
101-
(file->refined-context filename nil)))))
105+
(keep (fn [path]
106+
(let [filename (str (fs/canonicalize path))]
107+
(file->refined-context filename nil)))))
102108
"repoMap" [{:type :repoMap}]
103109
"cursor" [{:type :cursor
104110
:path path
@@ -122,16 +128,16 @@
122128
[prompt db]
123129
(let [;; Capture @<path> with optional :L<start>-L<end>
124130
context-pattern #"@([^\s:]+)(?::L(\d+)-L(\d+))?"
125-
matches (re-seq context-pattern prompt)]
126-
(when (seq matches)
127-
(let [raw-contexts (mapv (fn [[_ path s e]]
128-
(assoc-some {:type "file"
129-
:path path}
130-
:lines-range (when (and s e)
131-
{:start (Integer/parseInt s)
132-
:end (Integer/parseInt e)})))
133-
matches)]
134-
(raw-contexts->refined raw-contexts db)))))
131+
matches (re-seq context-pattern prompt)
132+
raw-contexts (mapv (fn [[_ path s e]]
133+
(assoc-some {:type "file"
134+
:path path}
135+
:lines-range (when (and s e)
136+
{:start (Integer/parseInt s)
137+
:end (Integer/parseInt e)})))
138+
matches)]
139+
(when (seq raw-contexts)
140+
(raw-contexts->refined raw-contexts db))))
135141

136142
(defn ^:private all-files-from* [root-filename] (fs/glob root-filename "**"))
137143
(def ^:private all-files-from (memoize all-files-from*))

src/eca/features/prompt.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
content)
6969
(format "<file path=\"%s\">%s</file>\n\n" path content))
7070
:agents-file (multi-str
71-
(format "<agents-file description=\"Instructions following AGENTS.md spec.\" path=\"%s\">" path)
71+
(format "<agents-file description=\"Primary System Directives & Coding Standards.\" path=\"%s\">" path)
7272
content
7373
"</agents-file>\n\n")
7474
:repoMap (format "<repoMap description=\"Workspaces structure in a tree view, spaces represent file hierarchy\" >%s</repoMap>\n\n" @repo-map*)

src/eca/llm_api.clj

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@
2020

2121
(def ^:private logger-tag "[LLM-API]")
2222

23-
;; TODO ask LLM for the most relevant parts of the path
2423
(defn refine-file-context [path lines-range]
2524
(cond
2625
(not (fs/exists? path))
27-
"File not found"
28-
26+
(logger/warn logger-tag "File not found at" path)
2927
(not (fs/readable? path))
30-
"File not readable"
31-
28+
(logger/warn logger-tag "Unable to read file at" path)
3229
:else
3330
(let [content (slurp path)]
3431
(if lines-range

test/eca/features/chat_test.clj

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(ns eca.features.chat-test
22
(:require
3+
[babashka.fs :as fs]
34
[clojure.string :as string]
45
[clojure.test :refer [deftest is testing]]
56
[eca.features.chat :as f.chat]
@@ -193,9 +194,32 @@
193194
(deftest contexts-in-prompt-test
194195
(testing "When prompt contains @file we add a user message"
195196
(h/reset-components!)
196-
(let [{:keys [chat-id]}
197+
(with-redefs [fs/readable? (constantly true)
198+
llm-api/refine-file-context (constantly "Mocked file content")]
199+
(let [{:keys [chat-id]}
200+
(prompt!
201+
{:message "Check @/path/to/file please"}
202+
{:all-tools-mock (constantly [])
203+
:api-mock
204+
(fn [{:keys [on-first-response-received
205+
on-message-received]}]
206+
(on-first-response-received {:type :text :text "On it..."})
207+
(on-message-received {:type :text :text "On it..."})
208+
(on-message-received {:type :finish}))})]
209+
(is (match?
210+
{chat-id {:id chat-id
211+
:messages [{:role "user"
212+
:content [{:type :text :text "Check @/path/to/file please"}
213+
{:type :text :text (m/pred #(string/includes? % "<file path"))}]}
214+
{:role "assistant" :content [{:type :text :text "On it..."}]}]}}
215+
(:chats (h/db)))))))
216+
(testing "When prompt contains @missing-file we do not add context noise"
217+
(h/reset-components!)
218+
(let [missing-file "definitely-does-not-exist-eca-test-ctx.md"
219+
msg (str "Check @" missing-file " please")
220+
{:keys [chat-id]}
197221
(prompt!
198-
{:message "Check @/path/to/file please"}
222+
{:message msg}
199223
{:all-tools-mock (constantly [])
200224
:api-mock
201225
(fn [{:keys [on-first-response-received
@@ -205,14 +229,13 @@
205229
(on-message-received {:type :finish}))})]
206230
(is (match?
207231
{chat-id {:id chat-id
208-
:messages [{:role "user" :content [{:type :text :text "Check @/path/to/file please"}
209-
{:type :text :text (m/pred #(string/includes? % "<file path"))}]}
232+
:messages [{:role "user" :content [{:type :text :text msg}]}
210233
{:role "assistant" :content [{:type :text :text "On it..."}]}]}}
211234
(:chats (h/db))))
212235
(is (match?
213236
{:chat-content-received
214237
[{:chat-id chat-id
215-
:content {:type :text :text "Check @/path/to/file please\n"}
238+
:content {:type :text :text (str msg "\n")}
216239
:role :user}
217240
{:chat-id chat-id
218241
:content {:type :progress :state :running :text "Waiting model"}
@@ -585,9 +608,9 @@
585608
;; Rollback to second message (keep first 2 messages, remove last 2)
586609
(h/reset-messenger!)
587610
(is (= {} (f.chat/rollback-chat
588-
{:chat-id chat-id
589-
:include ["messages" "tools"]
590-
:content-id second-content-id} (h/db*) (h/messenger))))
611+
{:chat-id chat-id
612+
:include ["messages" "tools"]
613+
:content-id second-content-id} (h/db*) (h/messenger))))
591614

592615
;; Verify messages after content-id are removed (keeps messages before content-id)
593616
(is (match?

test/eca/features/context_test.clj

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,21 +321,39 @@
321321
:path b-file
322322
:content b-content}])
323323
(#'f.context/parse-agents-file a-file)))))))
324+
(testing "Missing referenced file is ignored"
325+
(let [a-file (h/file-path "/fake/AGENTS.md")
326+
a-content (multi-str
327+
"- do foo"
328+
"- check @missing.md for missing things")
329+
missing-file (h/file-path "/fake/missing.md")]
330+
(with-redefs [llm-api/refine-file-context (fn [p _l]
331+
(condp = p
332+
a-file a-content
333+
missing-file nil))]
334+
(is (match?
335+
(m/in-any-order
336+
[{:type :agents-file
337+
:path a-file
338+
:content a-content}])
339+
(#'f.context/parse-agents-file a-file))))))
324340

325341
(deftest contexts-str-from-prompt-test
326342
(testing "not context mention"
327343
(is (match?
328344
nil
329345
(f.context/contexts-str-from-prompt "check /path/to/file" (h/db)))))
330346
(testing "Context mention"
331-
(with-redefs [llm-api/refine-file-context (constantly "Some content")]
347+
(with-redefs [fs/readable? (constantly true)
348+
llm-api/refine-file-context (constantly "Some content")]
332349
(is (match?
333350
[{:type :file
334351
:path "/path/to/file"
335352
:content "Some content"}]
336353
(f.context/contexts-str-from-prompt "check @/path/to/file" (h/db))))))
337354
(testing "Context mention with lines range"
338-
(with-redefs [llm-api/refine-file-context (constantly "Some content")]
355+
(with-redefs [fs/readable? (constantly true)
356+
llm-api/refine-file-context (constantly "Some content")]
339357
(is (match?
340358
[{:type :file
341359
:path "/path/to/file"

0 commit comments

Comments
 (0)