Skip to content

Commit f0db8bc

Browse files
committed
Add websearch capability
1 parent ed01c3b commit f0db8bc

File tree

8 files changed

+99
-45
lines changed

8 files changed

+99
-45
lines changed

docs/protocol.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ interface ChatContentReceivedParams {
393393
*/
394394
type ChatContent =
395395
| TextContent
396+
| URLContent
396397
| ProgressContent
397398
| FileChangeContent
398399
| MCPToolCallContent;
@@ -421,6 +422,23 @@ interface TextContent {
421422
}];
422423
}
423424

425+
/**
426+
* URL content message from the LLM
427+
*/
428+
interface URLContent {
429+
type: 'url';
430+
431+
/**
432+
* The URL title
433+
*/
434+
title: string;
435+
436+
/**
437+
* The URL link
438+
*/
439+
url: string;
440+
}
441+
424442
/**
425443
* Details about the progress of the chat completion.
426444
*/

src/eca/db.clj

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
:chats {}
1010
:chat-behaviors ["agent" "chat"]
1111
:chat-default-behavior "chat"
12-
:models {"o4-mini" {:tools true}
13-
"gpt-4.1" {:tools true}
14-
"claude-sonnet-4-0" {:tools true}
15-
"claude-opus-4-0" {:tools true}
16-
"claude-3-5-haiku-latest" {:tools true}} ;; + ollama local models
12+
:models {"o4-mini" {:mcp-tools true
13+
:web-search false}
14+
"gpt-4.1" {:mcp-tools true
15+
:web-search true}
16+
"claude-sonnet-4-0" {:mcp-tools true
17+
:web-search true}
18+
"claude-opus-4-0" {:mcp-tools true
19+
:web-search true}
20+
"claude-3-5-haiku-latest" {:mcp-tools true
21+
:web-search true}} ;; + ollama local models
1722
:default-model "o4-mini" ;; unless a ollama model is running.
1823
})
1924

src/eca/features/chat.clj

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"</contexts>"))
5050

5151
(defn default-model [db]
52-
(if-let [ollama-model (first (filter #(string/starts-with? % config/ollama-model-prefix) (:models db)))]
52+
(if-let [ollama-model (first (filter #(string/starts-with? % config/ollama-model-prefix) (vals (:models db))))]
5353
ollama-model
5454
(:default-model db)))
5555

@@ -116,6 +116,7 @@
116116
:text "Waiting model"}})
117117
(llm-api/complete!
118118
{:model chosen-model
119+
:model-config (get-in db [:models chosen-model])
119120
:user-prompt user-prompt
120121
:context context-str
121122
:past-messages past-messages
@@ -130,28 +131,37 @@
130131
:content {:type :progress
131132
:state :running
132133
:text "Generating"}}))
133-
:on-message-received (fn [{:keys [message finish-reason]}]
134-
(when message
135-
(swap! received-msgs* str message)
136-
(messenger/chat-content-received
137-
messenger
138-
{:chat-id chat-id
139-
:request-id request-id
140-
:role :assistant
141-
:content {:type :text
142-
:text message}}))
143-
(when finish-reason
144-
(swap! db* update-in [:chats chat-id :messages]
145-
(fnil conj [])
146-
{:role "assistant"
147-
:content @received-msgs*})
148-
(messenger/chat-content-received
149-
messenger
150-
{:chat-id chat-id
151-
:request-id request-id
152-
:role :system
153-
:content {:type :progress
154-
:state :finished}})))
134+
:on-message-received (fn [{:keys [type] :as msg}]
135+
(case type
136+
:text (do
137+
(swap! received-msgs* str (:text msg))
138+
(messenger/chat-content-received
139+
messenger
140+
{:chat-id chat-id
141+
:request-id request-id
142+
:role :assistant
143+
:content {:type :text
144+
:text (:text msg)}}))
145+
:url (messenger/chat-content-received
146+
messenger
147+
{:chat-id chat-id
148+
:request-id request-id
149+
:role :assistant
150+
:content {:type :url
151+
:title (:title msg)
152+
:url (:url msg)}})
153+
:finish (do
154+
(swap! db* update-in [:chats chat-id :messages]
155+
(fnil conj [])
156+
{:role "assistant"
157+
:content @received-msgs*})
158+
(messenger/chat-content-received
159+
messenger
160+
{:chat-id chat-id
161+
:request-id request-id
162+
:role :system
163+
:content {:type :progress
164+
:state :finished}}))))
155165
:on-tool-called (fn [{:keys [name arguments]}]
156166
(messenger/chat-content-received
157167
messenger

src/eca/handlers.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
(swap! db* update :models merge
1616
(reduce
1717
(fn [models {:keys [model]}]
18-
(assoc models (str config/ollama-model-prefix model) {:tools (get-in config [:ollama :use-tools] false)}))
18+
(assoc models
19+
(str config/ollama-model-prefix model)
20+
{:mcp-tools (get-in config [:ollama :use-tools] false)}))
1921
{}
2022
ollama-models)))))
2123

src/eca/llm_api.clj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
:type "function"))
2222

2323
(defn complete!
24-
[{:keys [model context user-prompt config on-first-message-received
24+
[{:keys [model model-config context user-prompt config on-first-message-received
2525
on-message-received on-error on-tool-called
2626
past-messages mcp-tools]}]
2727
(let [first-message-received* (atom false)
@@ -30,7 +30,9 @@
3030
(reset! first-message-received* true)
3131
(apply on-first-message-received args))
3232
(apply on-message-received args))
33-
tools (map mcp-tool->llm-tool mcp-tools)]
33+
tools (when (:mcp-tools model-config)
34+
(map mcp-tool->llm-tool mcp-tools))
35+
web-search (:web-search model-config)]
3436
(cond
3537
(contains? #{"o4-mini" "gpt-4.1"} model)
3638
(llm-providers.openai/completion!
@@ -39,6 +41,7 @@
3941
:user-prompt user-prompt
4042
:past-messages past-messages
4143
:tools tools
44+
:web-search web-search
4245
:api-key (:openai-api-key config)}
4346
{:on-message-received on-message-received-wrapper
4447
:on-error on-error
@@ -53,6 +56,7 @@
5356
:user-prompt user-prompt
5457
:past-messages past-messages
5558
:tools tools
59+
:web-search web-search
5660
:api-key (:anthropic-api-key config)}
5761
{:on-message-received on-message-received-wrapper
5862
:on-error on-error

src/eca/llm_providers/anthropic.clj

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212

1313
(def ^:private url "https://api.anthropic.com/v1/messages")
1414

15-
(defn ^:private ->tools [tools]
16-
(mapv (fn [tool]
17-
(assoc (select-keys tool [:name :description])
18-
:input_schema (:parameters tool))) tools))
15+
(defn ^:private ->tools [tools web-search]
16+
(cond->
17+
(mapv (fn [tool]
18+
(assoc (select-keys tool [:name :description])
19+
:input_schema (:parameters tool))) tools)
20+
web-search (conj {:type "web_search_20250305"
21+
:name "web_search"
22+
:max_uses 10})))
1923

2024
(defn ^:private base-request! [{:keys [body api-key on-error on-response]}]
2125
(let [api-key (or api-key
@@ -45,7 +49,7 @@
4549

4650
(defn completion!
4751
[{:keys [model user-prompt temperature context max-tokens
48-
api-key past-messages tools]
52+
api-key past-messages tools web-search]
4953
:or {max-tokens 1024
5054
temperature 1.0}}
5155
{:keys [on-message-received on-error on-tool-called]}]
@@ -56,13 +60,14 @@
5660
:temperature temperature
5761
;; TODO support :thinking
5862
:stream true
59-
:tools (->tools tools)
63+
:tools (->tools tools web-search)
6064
:system context}
6165
content-block* (atom nil)
6266
on-response-fn (fn handle-response [event data]
6367
(case event
6468
"content_block_delta" (case (-> data :delta :type)
65-
"text_delta" (on-message-received {:message (-> data :delta :text)})
69+
"text_delta" (on-message-received {:type :text
70+
:text (-> data :delta :text)})
6671
"input_json_delta" (swap! content-block* update-in [(:index data) :input-json] str (-> data :delta :partial_json))
6772
(logger/warn "Unkown response delta type" (-> data :delta :type)))
6873
"content_block_start" (case (-> data :content_block :type)
@@ -91,7 +96,8 @@
9196
:api-key api-key
9297
:on-error on-error
9398
:on-response handle-response}))))
94-
"end_turn" (on-message-received {:finish-reason (-> data :delta :stop_reason)})
99+
"end_turn" (on-message-received {:type :finish
100+
:finish-reason (-> data :delta :stop_reason)})
95101
nil)
96102
nil))]
97103
(base-request!

src/eca/llm_providers/ollama.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@
7575
(let [{:keys [message done_reason]} data]
7676
(on-message-received
7777
(cond-> {}
78-
message (assoc :message (:content message))
79-
done_reason (assoc :finish-reason done_reason)))))]
78+
message (assoc :type :text :text (:content message))
79+
done_reason (assoc :type :finish :finish-reason done_reason)))))]
8080
(base-completion-request!
8181
{:url url
8282
:body body

src/eca/llm_providers/openai.clj

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
(fn [e]
3838
(on-error {:exception e})))))
3939

40-
(defn completion! [{:keys [model user-prompt context temperature api-key past-messages tools]
40+
(defn completion! [{:keys [model user-prompt context temperature api-key past-messages tools web-search]
4141
:or {temperature 1.0}}
4242
{:keys [on-message-received on-error on-tool-called]}]
4343
(let [input (conj past-messages {:role "user" :content user-prompt})
@@ -46,11 +46,13 @@
4646
:user (str (System/getProperty "user.name") "@ECA")
4747
:instructions context
4848
:temperature temperature
49-
:tools tools
49+
:tools (cond-> tools
50+
web-search (conj {:type "web_search_preview"}))
5051
:stream true}
5152
on-response-fn (fn handle-response [event data]
5253
(case event
53-
"response.output_text.delta" (on-message-received {:message (:delta data)})
54+
"response.output_text.delta" (on-message-received {:type :text
55+
:text (:delta data)})
5456
"response.output_item.done" (when (= "function_call" (:type (:item data)))
5557
(let [function-name (-> data :item :name)
5658
function-args (-> data :item :arguments)
@@ -72,8 +74,15 @@
7274
:api-key api-key
7375
:on-error on-error
7476
:on-response handle-response})))
77+
"response.output_text.annotation.added" (case (-> data :annotation :type)
78+
"url_citation" (on-message-received
79+
{:type :url
80+
:title (-> data :annotation :title)
81+
:url (-> data :annotation :url)})
82+
nil)
7583
"response.completed" (when-not (= "function_call" (-> data :response :output last :type))
76-
(on-message-received {:finish-reason (-> data :response :status)}))
84+
(on-message-received {:type :finish
85+
:finish-reason (-> data :response :status)}))
7786
nil))]
7887
(base-request!
7988
{:body body

0 commit comments

Comments
 (0)