Compare commits
5 Commits
python-sec
...
llm-inside
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c8227968 | ||
|
|
a88af46db1 | ||
|
|
2d22131160 | ||
|
|
eff3a66185 | ||
|
|
065100e728 |
@@ -2082,6 +2082,7 @@
|
||||
warehouse-schema
|
||||
warehouses-rest
|
||||
enterprise/dependencies
|
||||
enterprise/llm
|
||||
enterprise/transforms
|
||||
enterprise/transforms-python}}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(deferred-tru "The OpenAI Model (e.g. ''gpt-4'', ''gpt-3.5-turbo'')")
|
||||
:encryption :no
|
||||
:visibility :settings-manager
|
||||
:default "gpt-4-turbo-preview"
|
||||
:default "gpt-4.1-mini"
|
||||
:export? false
|
||||
:doc false)
|
||||
|
||||
@@ -26,6 +26,13 @@
|
||||
:export? false
|
||||
:doc false)
|
||||
|
||||
(defsetting ee-anthropic-api-key
|
||||
(deferred-tru "The Anthropic API Key used in Metabase Enterprise.")
|
||||
:encryption :no
|
||||
:visibility :settings-manager
|
||||
:export? false
|
||||
:doc false)
|
||||
|
||||
(defsetting ee-ai-features-enabled
|
||||
(deferred-tru "Enable AI features.")
|
||||
:type :boolean
|
||||
|
||||
111
enterprise/backend/src/metabase_enterprise/metabot_v3/self.clj
Normal file
111
enterprise/backend/src/metabase_enterprise/metabot_v3/self.clj
Normal file
@@ -0,0 +1,111 @@
|
||||
(ns metabase-enterprise.metabot-v3.self
|
||||
"TODO:
|
||||
- done: figure out request cancellation
|
||||
- done: async tool calls appended to the same xf results
|
||||
- done: usage tracking - they'll go through and we can do whatever we want later on
|
||||
- done: test structured output
|
||||
- done: add anthropic api support to see how hard is to be heterogeneous
|
||||
- figure out what's lacking compared to ai-service"
|
||||
(:require
|
||||
[clojure.core.async :as a]
|
||||
[metabase-enterprise.llm.settings :as llm]
|
||||
[metabase-enterprise.metabot-v3.self.claude :as claude]
|
||||
[metabase-enterprise.metabot-v3.self.core :as core]
|
||||
[metabase-enterprise.metabot-v3.self.openai :as openai]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.malli :as mu]
|
||||
[potemkin :as p]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(p/import-vars [claude
|
||||
claude-raw
|
||||
claude->aisdk-xf])
|
||||
|
||||
(p/import-vars [core
|
||||
chan?
|
||||
sse-chan
|
||||
aisdk-xf
|
||||
tool-executor-rff])
|
||||
|
||||
(p/import-vars [openai
|
||||
openai-raw
|
||||
openai->aisdk-xf
|
||||
openai])
|
||||
|
||||
(comment
|
||||
(llm/ee-openai-api-key)
|
||||
(llm/ee-ai-features-enabled)
|
||||
(def sys
|
||||
"You MUST call tools for time or currency questions. If asked 'what time' or 'convert X to Y', do not guess—always call the relevant tool first.")
|
||||
|
||||
(def usr
|
||||
"What time is it right now in Europe/Kyiv, and convert 100 EUR to UAH.")
|
||||
|
||||
(def q (a/<!! (a/into [] (openai/openai-raw {:messages [{:role "system" :content sys}
|
||||
{:role "user" :content usr}]
|
||||
:tools (vals TOOLS)})))))
|
||||
|
||||
;;; tools
|
||||
|
||||
(mu/defn get-time
|
||||
"Return current time for a given IANA timezone."
|
||||
[{:keys [tz]} :- [:map {:closed true}
|
||||
[:tz [:string {:description "IANA timezone, e.g. Europe/Bucharest"}]]]]
|
||||
(str (java.time.ZonedDateTime/now (java.time.ZoneId/of tz))))
|
||||
|
||||
(mu/defn convert-currency
|
||||
"Convert an amount between two ISO currencies using a dummy rate."
|
||||
[{:keys [amount from to]} :- [:map {:closed true}
|
||||
[:amount :float]
|
||||
[:from :string]
|
||||
[:to :string]]]
|
||||
(Thread/sleep 500) ;; we're doing some request to some far away service
|
||||
(let [rate (if (= [from to] ["EUR" "USD"]) 1.16 1.0)]
|
||||
{:amount amount
|
||||
:from from
|
||||
:to to
|
||||
:rate rate
|
||||
:converted (* amount rate)}))
|
||||
|
||||
(mu/defn analyze-data-trend
|
||||
"Analyze a data trend by calling back to the LLM for natural language insights.
|
||||
This demonstrates a recursive LLM call pattern commonly used in agentic workflows."
|
||||
[{:keys [metric values period]} :- [:map {:closed true}
|
||||
[:metric [:string {:description "The metric being analyzed, e.g. 'revenue', 'users'"}]]
|
||||
[:values [:vector {:description "Time series values"} number?]]
|
||||
[:period [:string {:description "Time period, e.g. 'Q1 2025', 'last 6 months'"}]]]]
|
||||
;; Simulate calling back to LLM with a mini-prompt
|
||||
(let [prompt (format "Analyze this %s trend over %s: %s. Provide a 1-2 sentence insight highlighting key patterns."
|
||||
metric period (pr-str values))]
|
||||
(openai/openai {:messages [{:role "user" :content prompt}]})))
|
||||
|
||||
(def TOOLS
|
||||
"All the defined tools"
|
||||
(u/index-by
|
||||
#(-> % meta :name name)
|
||||
[#'get-time
|
||||
#'convert-currency
|
||||
#'analyze-data-trend]))
|
||||
|
||||
(comment
|
||||
(map tool->openai (vals TOOLS)))
|
||||
|
||||
(comment
|
||||
(def q (a/<!! (a/into [] (analyze-data-trend {:metric "revenue"
|
||||
:values [100.0 120.0 145.0 160.0]
|
||||
:period "Q1 2025"}))))
|
||||
(def w (into [] (tool-executor-rff TOOLS) q))
|
||||
(def e (into [] aisdk-xf w))
|
||||
|
||||
(def q (a/<!! (a/into [] (openai-raw
|
||||
{:system "You are a data analysis assistant. When users provide time-series data and ask for insights, use the analyze-data-trend tool to generate interpretations. Always call the tool rather than making up your own analysis."
|
||||
:input [{:role "user" :content "Can you analyze these trends? Revenue for Q1: [50000, 55000, 58000, 62000] and customer count: [100, 110, 105, 115]. What story do these numbers tell?"}]
|
||||
:tools (vals metabase-enterprise.metabot-v3.self/TOOLS)}))))
|
||||
|
||||
(def q (a/<!! (a/into [] (claude-raw
|
||||
{:input [{:role "user" :content "Can you tell me currencies of three northmost American countries?"}]
|
||||
:schema [:map
|
||||
[:currencies [:sequential [:map
|
||||
[:country [:string {:description "Three-letter code"}]]
|
||||
[:currency [:string {:description "Three-letter code"}]]]]]]})))))
|
||||
@@ -0,0 +1,155 @@
|
||||
(ns metabase-enterprise.metabot-v3.self.claude
|
||||
(:require
|
||||
[clj-http.client :as http]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.string :as str]
|
||||
[malli.json-schema :as mjs]
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.llm.settings :as llm]
|
||||
[metabase-enterprise.metabot-v3.self.core :as core]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.json :as json]
|
||||
[metabase.util.malli :as mu]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defn claude->aisdk-xf
|
||||
"Translates Claude /v1/messages streaming events into AI SDK v5 protocol format.
|
||||
|
||||
https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol
|
||||
|
||||
Claude Streaming Format:
|
||||
- Each event: {:type \"message_start\" :message {...}}
|
||||
{:type \"content_block_start\" :index 0 :content_block {:type \"text\"}}
|
||||
{:type \"content_block_delta\" :index 0 :delta {:type \"text_delta\" :text \"...\"}}
|
||||
{:type \"content_block_stop\" :index 0}
|
||||
{:type \"message_delta\" :delta {:stop_reason \"end_turn\"}}
|
||||
{:type \"message_stop\"}
|
||||
|
||||
AI SDK v5 Format (SSE protocol):
|
||||
- Message parts: {:type :start, :messageId ...}
|
||||
- Part types: start, text-start, text-delta, text-end, finish-step, finish
|
||||
- Ends with: 'data: [DONE]\\n'"
|
||||
[rf]
|
||||
(let [current-type (volatile! nil)
|
||||
current-id (volatile! nil)
|
||||
message-id (volatile! nil)
|
||||
payload (volatile! {})]
|
||||
(fn
|
||||
([result]
|
||||
(cond-> result
|
||||
;; close up latest type if incomplete
|
||||
@current-type (rf (merge {:type (case @current-type
|
||||
:text :text-end
|
||||
:tool_use :tool-input-available)}
|
||||
@payload))
|
||||
true (rf)))
|
||||
([result {t :type :keys [message content_block delta usage]}]
|
||||
(let [block-type (when content_block
|
||||
(keyword (:type content_block)))
|
||||
chunk-id (or (:id content_block)
|
||||
#_(when (= @current-id index)
|
||||
(:id @payload))
|
||||
@current-id
|
||||
(core/mkid))]
|
||||
(cond-> result
|
||||
;; start of message
|
||||
(= t "message_start") (-> (rf {:type :start :messageId (:id message)})
|
||||
(u/prog1
|
||||
(vreset! message-id (:id message))))
|
||||
;; start of new content block
|
||||
(= t "content_block_start") (-> (u/prog1
|
||||
(vreset! current-type block-type)
|
||||
(vreset! current-id chunk-id)
|
||||
(vreset! payload
|
||||
(case block-type
|
||||
:text {:id chunk-id}
|
||||
:tool_use {:toolCallId chunk-id
|
||||
:toolName (:name content_block)}
|
||||
nil)))
|
||||
(rf (merge (case block-type
|
||||
:text {:type :text-start}
|
||||
:tool_use {:type :tool-input-start})
|
||||
@payload)))
|
||||
|
||||
;; content block delta
|
||||
(= t "content_block_delta") (rf (case (:type delta)
|
||||
"text_delta" {:type :text-delta
|
||||
:id (:id @payload)
|
||||
:delta (:text delta)}
|
||||
"input_json_delta" {:type :tool-input-delta
|
||||
:toolCallId (:toolCallId @payload)
|
||||
:inputTextDelta (:partial_json delta)}))
|
||||
|
||||
;; end of content block
|
||||
(= t "content_block_stop") (-> (rf (merge {:type (case @current-type
|
||||
:text :text-end
|
||||
:tool_use :tool-input-available)}
|
||||
@payload))
|
||||
(u/prog1
|
||||
(vreset! current-type nil)
|
||||
(vreset! current-id nil)
|
||||
(vreset! payload {})))
|
||||
;; message delta (usage info)
|
||||
(= t "message_delta") (cond->
|
||||
usage (rf {:type :usage
|
||||
:usage {:promptTokens (:input_tokens usage)
|
||||
:completionTokens (:output_tokens usage)}
|
||||
:id @message-id}))
|
||||
|
||||
;; end of message
|
||||
(= t "message_stop") identity))))))
|
||||
|
||||
(defn- tool->claude [tool]
|
||||
(let [{:keys [doc schema] :as tool} (if (map? tool) tool (meta tool))
|
||||
[_:=> [_:cat params] _out] schema
|
||||
doc (if (str/starts-with? doc "Inputs: ")
|
||||
;; strip that stuff we're appending in mu/defn
|
||||
(second (str/split doc #"\n\n " 2))
|
||||
doc)]
|
||||
{:name (name (:name tool))
|
||||
:description doc
|
||||
:input_schema (mjs/transform params {:additionalProperties false})}))
|
||||
|
||||
(mu/defn claude-raw
|
||||
"Perform a request to Claude API"
|
||||
[{:keys [model system input tools schema]
|
||||
:or {model "claude-sonnet-4-5-20250929"
|
||||
input [{:role "user" :content "Hello"}]}}
|
||||
:- [:map
|
||||
[:model {:optional true} :string]
|
||||
[:system {:optional true} :string]
|
||||
[:input {:optional true} [:vector [:map
|
||||
[:role [:enum "user" "assistant"]]
|
||||
[:content :string]]]]
|
||||
[:tools {:optional true} [:sequential [:fn var?]]]
|
||||
[:schema {:optional true} :any]]]
|
||||
(assert (llm/ee-anthropic-api-key) "No Anthropic API key!")
|
||||
(let [req (cond-> {:model model
|
||||
:max_tokens 4096
|
||||
:stream true
|
||||
:messages input}
|
||||
system (assoc :system system)
|
||||
(seq tools) (assoc :tools (mapv tool->claude tools))
|
||||
schema (assoc :tool_choice {:type "tool"
|
||||
:name "structured_output"}
|
||||
:tools [{:name "structured_output"
|
||||
:description "Output structured data"
|
||||
:input_schema (mjs/transform (mut/closed-schema schema))}]))]
|
||||
(try
|
||||
(let [res (http/post "https://api.anthropic.com/v1/messages"
|
||||
{:as :stream
|
||||
:headers {"x-api-key" (llm/ee-anthropic-api-key)
|
||||
"anthropic-version" "2023-06-01"
|
||||
"content-type" "application/json"}
|
||||
:body (json/encode req)})]
|
||||
(core/sse-chan (:body res)))
|
||||
(catch Exception e
|
||||
(if-let [res (ex-data e)]
|
||||
(throw (ex-info (.getMessage e) (json/decode-body res)))
|
||||
(throw e))))))
|
||||
|
||||
(comment
|
||||
(def q (a/<!! (a/into [] (claude-raw {:input [{:role "user" :content "How are you feeling today?"}]}))))
|
||||
|
||||
(into [] (comp claude->aisdk-xf core/aisdk-xf) q))
|
||||
@@ -0,0 +1,170 @@
|
||||
(ns metabase-enterprise.metabot-v3.self.core
|
||||
(:require
|
||||
[clojure.core.async :as a]
|
||||
[clojure.core.async.impl.protocols :as impl]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.json :as json]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defn mkid
|
||||
"Generate a random id"
|
||||
[]
|
||||
(str "mb-" (random-uuid)))
|
||||
|
||||
(defn chan?
|
||||
"Checks if argument is a `core.async/chan`"
|
||||
[x]
|
||||
(satisfies? impl/Channel x))
|
||||
|
||||
;; we're using channels because of backpressure and ability to close them
|
||||
(defn sse-chan
|
||||
"Turn an SSE InputStream into a channel of `data` strings.
|
||||
Closes the channel on EOF."
|
||||
[^java.io.Closeable input]
|
||||
(let [out (a/chan 64)]
|
||||
(.start
|
||||
(doto (Thread/ofVirtual) (.name "sse-reader"))
|
||||
(fn []
|
||||
;; NOTE: parsing is oriented towards OpenAI-like SSE stream, where there are only `data: ...` lines with json.
|
||||
;; Feel free to adjust once we have better understanding.
|
||||
(with-open [r (io/reader input)]
|
||||
(loop []
|
||||
(when-let [line (.readLine ^java.io.BufferedReader r)]
|
||||
(cond
|
||||
(= line "data: [DONE]") nil
|
||||
;; this `when` is doing the heavy lifting here: `>!!` will return `false` if the channel is closed:
|
||||
;; - which exits the loop
|
||||
;; - which exits `with-open`
|
||||
;; - which closes the `io/reader`
|
||||
;; - which closes the underlying `InputStream`
|
||||
;; and closes the http connection, which should stop the LLM
|
||||
(str/starts-with? line "data: ") (when (a/>!! out (json/decode+kw (subs line 6)))
|
||||
(recur))
|
||||
:else (recur)))))
|
||||
(a/close! out)))
|
||||
out))
|
||||
|
||||
;;; AISDK5
|
||||
|
||||
(defn- aisdk-chunks->part [[chunk :as chunks]]
|
||||
(case (:type chunk)
|
||||
:start {:type :start
|
||||
:id (:messageId chunk)}
|
||||
:usage chunk
|
||||
:text-start {:type :text
|
||||
:id (:id chunk)
|
||||
:text (->> (map :delta chunks)
|
||||
(str/join ""))}
|
||||
:tool-input-start {:type :tool-input
|
||||
:id (:toolCallId chunk)
|
||||
:function (:toolName chunk)
|
||||
:arguments (->> (map :inputTextDelta chunks)
|
||||
(str/join "")
|
||||
json/decode+kw)}
|
||||
:tool-output-available {:type :tool-output
|
||||
:id (:toolCallId chunk)
|
||||
:function (:toolName chunk)
|
||||
:result (:result chunk)
|
||||
:error (:error chunk)}))
|
||||
|
||||
(defn aisdk-xf
|
||||
"Collect a stream of AI SDK v5 messages into a list of parts (joins by id)."
|
||||
[rf]
|
||||
;; FIXME: logic relies on chunks pieces not being interleaved which won't hold for long if we will use
|
||||
;; `tool-executor-rff`
|
||||
(let [current-id (volatile! nil)
|
||||
acc (volatile! [])]
|
||||
(fn
|
||||
([result]
|
||||
(cond-> result
|
||||
(seq @acc) (rf (aisdk-chunks->part @acc))
|
||||
true rf))
|
||||
([result chunk]
|
||||
(let [chunk-id (or (:id chunk)
|
||||
(:toolCallId chunk))]
|
||||
(if (not= chunk-id @current-id)
|
||||
(u/prog1 (cond-> result
|
||||
(seq @acc) (rf (aisdk-chunks->part @acc)))
|
||||
(vreset! current-id chunk-id)
|
||||
(vreset! acc [chunk]))
|
||||
(u/prog1 result
|
||||
(vswap! acc conj chunk))))))))
|
||||
|
||||
;;; Fancy tool executor guy
|
||||
|
||||
(defn tool-executor-rff
|
||||
"Transducer that intercepts tool calls, executes them asynchronously, and appends results at the end.
|
||||
|
||||
- Passes all chunks through unchanged
|
||||
- When `:tool-input-start` appears with a tool name from TOOLS, starts tracking that tool call
|
||||
- Collects arguments from `:tool-input-delta` chunks
|
||||
- When `:tool-input-available` appears, fires off async tool execution
|
||||
- At completion, appends all tool results to the stream
|
||||
|
||||
NOTE: AISDK5 has only `:tool-output-available` that's sync in nature, so for channels returned (presumably from
|
||||
LLMS) we just directly append them to our stream."
|
||||
[tools]
|
||||
(fn [rf]
|
||||
;; id -> [chunks...] at first, id -> part + :chan later
|
||||
(let [active-tools (volatile! {})]
|
||||
(fn
|
||||
([result]
|
||||
(let [merged (a/merge (keep :chan (vals @active-tools)))]
|
||||
(rf (reduce rf result (take-while some? (repeatedly #(let [v (a/<!! merged)] v)))))))
|
||||
|
||||
([result chunk]
|
||||
(case (:type chunk)
|
||||
:tool-input-start ; start collecting chunks if tool is known
|
||||
(let [tool-name (:toolName chunk)
|
||||
tool-call-id (:toolCallId chunk)]
|
||||
(when (contains? tools tool-name)
|
||||
(vswap! active-tools assoc tool-call-id [chunk]))
|
||||
(rf result chunk))
|
||||
|
||||
:tool-input-delta ; append a chunk if we're interested in this tool
|
||||
(let [tool-call-id (:toolCallId chunk)]
|
||||
(when (contains? @active-tools tool-call-id)
|
||||
(vswap! active-tools update tool-call-id conj chunk))
|
||||
(rf result chunk))
|
||||
|
||||
;; grab a tool, execute and send everything to a channel that's going to be examined in
|
||||
;; reducing arity up there
|
||||
:tool-input-available
|
||||
(let [tool-name (:toolName chunk)
|
||||
tool-call-id (:toolCallId chunk)]
|
||||
(when-let [chunks (seq (get @active-tools tool-call-id))]
|
||||
(let [tool-fn (get tools tool-name)
|
||||
chan (a/chan 10)]
|
||||
(vswap! active-tools assoc tool-call-id {:chan chan})
|
||||
(future
|
||||
(try
|
||||
;; time to combine those chunks in a sensible representation;
|
||||
;; we're doing that in a `future` because we're saving some μs (haven't measured yet)
|
||||
(let [{:keys [arguments]
|
||||
:as tool-call} (into {} aisdk-xf chunks)
|
||||
_ (vswap! active-tools update tool-call-id merge tool-call)
|
||||
result (tool-fn arguments)
|
||||
done-ch (a/chan)]
|
||||
(if (chan? result)
|
||||
(do
|
||||
(a/pipeline 1 chan (remove #(#{:start :finish} (:type %))) result done-ch)
|
||||
(a/<!! done-ch)) ;; wait until pipeline stops
|
||||
(a/>!! chan {:type :tool-output-available
|
||||
:toolCallId tool-call-id
|
||||
:toolName tool-name
|
||||
:result result})))
|
||||
(catch Exception e
|
||||
(a/>!! chan {:type :tool-output-available
|
||||
:toolCallId tool-call-id
|
||||
:toolName tool-name
|
||||
:error {:message (.getMessage e)
|
||||
:type (str (type e))}}))
|
||||
(finally
|
||||
(a/close! chan))))))
|
||||
(rf result chunk))
|
||||
|
||||
;; default
|
||||
(rf result chunk)))))))
|
||||
@@ -0,0 +1,166 @@
|
||||
(ns metabase-enterprise.metabot-v3.self.openai
|
||||
(:require
|
||||
[clj-http.client :as http]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.string :as str]
|
||||
[malli.json-schema :as mjs]
|
||||
[malli.util :as mut]
|
||||
[metabase-enterprise.llm.settings :as llm]
|
||||
[metabase-enterprise.metabot-v3.self.core :as core]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.json :as json]
|
||||
[metabase.util.malli :as mu]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defn openai->aisdk-xf
|
||||
"Translates OpenAI /v1/responses streaming events into AI SDK v5 protocol format.
|
||||
|
||||
https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol
|
||||
|
||||
OpenAI Responses Format:
|
||||
- Each event: {:type \"response.output_text.delta\" :delta ...}
|
||||
{:type \"response.output_item.added\" :item {:id :type :output ...}}
|
||||
|
||||
AI SDK v5 Format (SSE protocol):
|
||||
- Message parts: {:type :start, :messageId ...}
|
||||
- Part types: start, text-start, text-delta, text-end, finish-step, finish
|
||||
- Ends with: 'data: [DONE]\\n'"
|
||||
[rf]
|
||||
;; we've got lots of state since aisdk has lots of start/stop/etc messages that raw openai does not
|
||||
(let [current-type (volatile! nil)
|
||||
current-id (volatile! nil)
|
||||
payload (volatile! {})]
|
||||
;; some notes about the approach:
|
||||
;; - most of message types carry similar payload, like id for messages, or id+name for tool calls
|
||||
;; - this trick with u/prog1 was chosen deliberately, a few approaches were made and they all looked worse
|
||||
;; - most of dispatch is inlined rather than separated as multimethods is not an overlook to make function
|
||||
;; smaller, I'd rather contain this hairyness in a single piece while it's possible
|
||||
(fn
|
||||
([result]
|
||||
(cond-> result
|
||||
;; in case the response was incomplete we'll close up latest type
|
||||
@current-type (rf (merge {:type (case @current-type
|
||||
:text :text-end
|
||||
:function_call :tool-input-available)}
|
||||
@payload))
|
||||
true (rf)))
|
||||
([result {t :type :keys [response item delta] :as chunk}]
|
||||
(let [middle (second (str/split t #"\."))
|
||||
chunk-type (case middle
|
||||
"output_item" (case (:type item)
|
||||
"message" :text
|
||||
(keyword (:type item)))
|
||||
"content_part" :text
|
||||
"output_text" :text
|
||||
"function_call_arguments" :function_call
|
||||
(keyword middle))
|
||||
chunk-id (or (case chunk-type
|
||||
;; chunks that have natural id in API response go here
|
||||
:function_call (:call_id item)
|
||||
:text (:id chunk)
|
||||
nil)
|
||||
@current-id
|
||||
(core/mkid))]
|
||||
(cond-> result
|
||||
(= t "response.created") (rf {:type :start :messageId (:id response)})
|
||||
;; time to finish previous chunk
|
||||
;; this logic will skip most of the *.done types, but they seem to be always followed by one of those two?
|
||||
(or (= t "response.output_item.done")
|
||||
(and @current-id
|
||||
(not= chunk-id
|
||||
@current-id))) (-> (rf (merge {:type (case @current-type
|
||||
:text :text-end
|
||||
:function_call :tool-input-available)}
|
||||
@payload))
|
||||
(u/prog1
|
||||
(vreset! current-type nil)
|
||||
(vreset! current-id nil)
|
||||
(vreset! payload {})))
|
||||
;; start of a new chunk
|
||||
(= t "response.output_item.added") (-> (u/prog1
|
||||
(vreset! current-type chunk-type)
|
||||
(vreset! current-id chunk-id)
|
||||
(vreset! payload
|
||||
(case @current-type
|
||||
;; no :type in payloads since we'll use that for finish msg too
|
||||
:text {:id chunk-id}
|
||||
:function_call {:toolCallId chunk-id
|
||||
:toolName (:name item)}
|
||||
nil)))
|
||||
(rf (merge (case @current-type
|
||||
:text {:type :text-start}
|
||||
:function_call {:type :tool-input-start})
|
||||
@payload)))
|
||||
;; just a middle of a chunk
|
||||
delta (rf (case @current-type
|
||||
:text {:type :text-delta
|
||||
:id @current-id
|
||||
:delta delta}
|
||||
:function_call {:type :tool-input-delta
|
||||
:toolCallId (:toolCallId @payload)
|
||||
:inputTextDelta delta}))
|
||||
(= (:type chunk)
|
||||
"response.completed") (rf {:type :usage
|
||||
:usage (:usage response)
|
||||
;; non-standard extension, not in AISDK5
|
||||
:id (:id response)})))))))
|
||||
|
||||
(defn- tool->openai [tool]
|
||||
(let [{:keys [doc schema] :as tool} (if (map? tool) tool (meta tool))
|
||||
[_:=> [_:cat params] _out] schema
|
||||
doc (if (str/starts-with? doc "Inputs: ")
|
||||
;; strip that stuff we're appending in mu/defn
|
||||
(second (str/split doc #"\n\n " 2))
|
||||
doc)]
|
||||
{:type "function"
|
||||
:name (name (:name tool))
|
||||
:description doc
|
||||
:parameters (mjs/transform params {:additionalProperties false})}))
|
||||
|
||||
(mu/defn openai-raw
|
||||
"Perform a request to OpenAI"
|
||||
[{:keys [model system input tools schema]
|
||||
:or {model "gpt-4.1-mini"
|
||||
input [{:role "system" :content "Just tell something to a user"}]}}
|
||||
:- [:map
|
||||
[:model {:optional true} :string]
|
||||
[:system {:optional true} :string]
|
||||
[:input {:optional true} [:vector [:map
|
||||
[:role [:enum "user" "assistant"]]
|
||||
[:content :string]]]]
|
||||
[:tools {:optional true} [:sequential [:fn var?]]]
|
||||
;; malli schema expected here
|
||||
;; TODO: check it's a `:map`
|
||||
[:schema {:optional true} :any]]]
|
||||
(assert (llm/ee-openai-api-key) "No OpenAI API key!")
|
||||
(let [req {:model model
|
||||
:stream true
|
||||
:store false
|
||||
:instructions system
|
||||
:input input
|
||||
:tool_choice (when (seq tools) "auto")
|
||||
:tools (when (seq tools) (mapv tool->openai tools))
|
||||
:text (when schema
|
||||
{:format {:type "json_schema"
|
||||
:strict true
|
||||
:name "schema"
|
||||
;; OpenAI insists on `{additionalProperties false}`
|
||||
:schema (mjs/transform (mut/closed-schema schema))}})}]
|
||||
(try
|
||||
(let [res (http/post "https://api.openai.com/v1/responses"
|
||||
{:as :stream
|
||||
:headers {"Authorization" (str "Bearer " (llm/ee-openai-api-key))
|
||||
"Content-Type" "application/json"}
|
||||
:body (json/encode req)})]
|
||||
(core/sse-chan (:body res)))
|
||||
(catch Exception e
|
||||
(if-let [res (ex-data e)]
|
||||
(throw (ex-info (.getMessage e) (json/decode-body res)))
|
||||
(throw e))))))
|
||||
|
||||
(defn openai
|
||||
"Perform a request to OpenAI"
|
||||
[& args]
|
||||
;; pipe returns destination channel \o/
|
||||
(a/pipe (apply openai-raw args) (a/chan 64 openai->aisdk-xf)))
|
||||
@@ -0,0 +1,284 @@
|
||||
(ns metabase-enterprise.metabot-v3.self-test
|
||||
(:require
|
||||
[clj-http.client :as http]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :refer :all]
|
||||
[metabase-enterprise.metabot-v3.self :as self]
|
||||
[metabase.util :as u]
|
||||
[metabase.util.json :as json]
|
||||
[metabase.util.malli :as mu]
|
||||
[ring.adapter.jetty :as jetty]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defn json-resource [fname]
|
||||
(-> (io/resource fname)
|
||||
slurp
|
||||
json/decode+kw))
|
||||
|
||||
(defn make-sse ^String [v]
|
||||
(->> (map json/encode v)
|
||||
(map #(str "data: " % "\n\n"))
|
||||
(str/join)))
|
||||
|
||||
(defn parts->aisdk [parts]
|
||||
(mapcat
|
||||
(fn [{:keys [id] :as part}]
|
||||
(case (:type part)
|
||||
:start [{:type :start :messageId id}]
|
||||
:usage [part]
|
||||
:text (concat
|
||||
[{:type :text-start :id id}]
|
||||
(for [bit (partition-all 5 (:text part))]
|
||||
{:type :text-delta :id id :delta (str/join bit)})
|
||||
[{:type :text-end :id id}])
|
||||
:tool-input (concat
|
||||
[{:type :tool-input-start :toolName (:function part) :toolCallId id}]
|
||||
(for [bit (partition-all 5 (json/encode (:arguments part)))]
|
||||
{:type :tool-input-delta :toolCallId id :inputTextDelta (str/join bit)})
|
||||
[{:type :tool-input-available :toolName (:function part) :toolCallId id}])))
|
||||
parts))
|
||||
|
||||
;;; utils tests
|
||||
|
||||
(defn- chan-seq!
|
||||
"Convert core.async channel to a lazy seq, blocking; just for tests"
|
||||
[chan]
|
||||
(lazy-seq
|
||||
(when-some [chunk (a/<!! chan)]
|
||||
(cons chunk (chan-seq! chan)))))
|
||||
|
||||
(deftest sse-chan-test
|
||||
(testing "sse-chan actually produces chan with lines"
|
||||
(let [data ["just a test" "to make you jealous"]
|
||||
istream (io/input-stream (.getBytes (make-sse data)))
|
||||
chan (self/sse-chan istream)]
|
||||
(is (= data
|
||||
(chan-seq! chan))))))
|
||||
|
||||
(deftest sse-chan-stops-response-test
|
||||
(let [cnt (atom 30)
|
||||
handler (fn [_req]
|
||||
(let [out (java.io.PipedOutputStream.)
|
||||
in (java.io.PipedInputStream. out 65536)]
|
||||
(future
|
||||
(try
|
||||
(dotimes [i @cnt]
|
||||
(.write out (.getBytes (make-sse [(str "msg-" i)])))
|
||||
(.flush out)
|
||||
(swap! cnt dec)
|
||||
(Thread/sleep 10))
|
||||
(catch Exception _)
|
||||
(finally
|
||||
(try (.close out)
|
||||
(catch Exception _)))))
|
||||
{:status 200
|
||||
:headers {"Content-Type" "text/event-stream"
|
||||
"Cache-Control" "no-cache"}
|
||||
:body in}))
|
||||
server (jetty/run-jetty handler {:port 0
|
||||
:join? false
|
||||
:output-buffer-size 1})
|
||||
url (str "http://localhost:" (.. server getURI getPort))]
|
||||
(try
|
||||
(let [res (http/request {:method :post :url url :as :stream})
|
||||
chan (self/sse-chan (:body res))]
|
||||
(is (= "msg-0" (a/<!! chan)))
|
||||
(is (= "msg-1" (a/<!! chan)))
|
||||
(a/close! chan)
|
||||
;; we're waiting for 10 in the handler, so 30 should be enough for propagation
|
||||
(Thread/sleep 30)
|
||||
;; it's been 25-26 when I tested this, if it every becomes flaky maybe decrease the number?
|
||||
(is (> @cnt 20) "SHOULD have stopped writing when channel closed"))
|
||||
(finally
|
||||
(.stop server)))))
|
||||
|
||||
;;; conversion tests
|
||||
|
||||
(deftest openai-conv-test
|
||||
(testing "tools calls are mapped well"
|
||||
(is (= {:start 1
|
||||
:tool-input-start 2
|
||||
:tool-input-delta 34
|
||||
:tool-input-available 2
|
||||
:usage 1}
|
||||
(->> (json-resource "llm/openai-tool-calls.json")
|
||||
(into [] self/openai->aisdk-xf)
|
||||
(map :type)
|
||||
frequencies)))
|
||||
(is (=? [{:type :start}
|
||||
{:type :tool-input :function "analyze-data-trend" :arguments {}}
|
||||
{:type :tool-input :function "analyze-data-trend" :arguments {}}
|
||||
{:type :usage :usage {:total_tokens 225}}]
|
||||
(->> (json-resource "llm/openai-tool-calls.json")
|
||||
(into [] (comp self/openai->aisdk-xf self/aisdk-xf))))))
|
||||
(testing "structured output is parsed too"
|
||||
(is (= {:start 1
|
||||
;; TODO: we're representing it as just text but this needs to be improved maybe?
|
||||
:text-start 1
|
||||
:text-delta 32
|
||||
:text-end 1
|
||||
:usage 1}
|
||||
(->> (json-resource "llm/openai-structured-output.json")
|
||||
(into [] self/openai->aisdk-xf)
|
||||
(map :type)
|
||||
frequencies)))
|
||||
(is (=? [{:type :start}
|
||||
{:type :text :text string?}
|
||||
{:type :usage :usage {:total_tokens 109}}]
|
||||
(->> (json-resource "llm/openai-structured-output.json")
|
||||
(into [] (comp self/openai->aisdk-xf self/aisdk-xf)))))))
|
||||
|
||||
(deftest claude-conv-test
|
||||
(testing "text is mapped well"
|
||||
(is (= {:start 1
|
||||
:text-start 1
|
||||
:text-delta 6
|
||||
:text-end 1
|
||||
:usage 1}
|
||||
(->> (json-resource "llm/claude-text.json")
|
||||
(into [] self/claude->aisdk-xf)
|
||||
(map :type)
|
||||
frequencies)))
|
||||
(is (=? [{:type :start :id string?}
|
||||
{:type :text :id string? :text string?}
|
||||
{:type :usage :id string? :usage {:promptTokens 13}}]
|
||||
(->> (json-resource "llm/claude-text.json")
|
||||
(into [] (comp self/claude->aisdk-xf self/aisdk-xf))))))
|
||||
(testing "tool input (also structured output) is mapped well"
|
||||
(is (= {:start 1
|
||||
:tool-input-start 1
|
||||
:tool-input-delta 15
|
||||
:tool-input-available 1
|
||||
:usage 1}
|
||||
(->> (json-resource "llm/claude-tool-input.json")
|
||||
(into [] self/claude->aisdk-xf)
|
||||
(map :type)
|
||||
frequencies)))
|
||||
(is (=? [{:type :start}
|
||||
{:type :tool-input :arguments {:currencies [{:country "CAN" :currency "CAD"}
|
||||
{:country "USA" :currency "USD"}
|
||||
{:country "MEX" :currency "MXN"}]}}
|
||||
;; TODO: convert usage to common format
|
||||
{:type :usage :usage {:promptTokens 737}}]
|
||||
(->> (json-resource "llm/claude-tool-input.json")
|
||||
(into [] (comp self/claude->aisdk-xf self/aisdk-xf)))))))
|
||||
|
||||
;;; tool executor
|
||||
|
||||
(mu/defn get-time
|
||||
"Return current time for a given IANA timezone."
|
||||
[{:keys [tz]} :- [:map {:closed true}
|
||||
[:tz [:string {:description "IANA timezone, e.g. Europe/Bucharest"}]]]]
|
||||
(str (java.time.ZonedDateTime/now (java.time.ZoneId/of tz))))
|
||||
|
||||
(mu/defn convert-currency
|
||||
"Convert an amount between two ISO currencies using a dummy rate."
|
||||
[{:keys [amount from to]} :- [:map {:closed true}
|
||||
[:amount :float]
|
||||
[:from :string]
|
||||
[:to :string]]]
|
||||
(let [rate (if (= [from to] ["EUR" "USD"]) 1.16 1.0)]
|
||||
{:amount amount
|
||||
:from from
|
||||
:to to
|
||||
:rate rate
|
||||
:converted (* amount rate)}))
|
||||
|
||||
(mu/defn mock-llm
|
||||
"Return aisdk-formatted results in a core.async channel"
|
||||
[{:keys [id input]} :- [:map {:closed true}
|
||||
[:id :string]
|
||||
[:input :string]]]
|
||||
(a/to-chan!! (parts->aisdk
|
||||
[{:type :start :id "mock-1"}
|
||||
{:type :text :id id :text input}])))
|
||||
|
||||
(def TOOLS
|
||||
"All the defined tools"
|
||||
(u/index-by
|
||||
#(-> % meta :name name)
|
||||
[#'get-time
|
||||
#'convert-currency
|
||||
#'mock-llm]))
|
||||
|
||||
(deftest ^:parallel async-tool-executor-rff-test
|
||||
(testing "tool-executor-rff passes through all chunks unchanged"
|
||||
(let [chunks (parts->aisdk
|
||||
[{:type :start :id "msg-123"}
|
||||
{:type :text :id "text-1" :text "Hello world"}])
|
||||
result (into [] (self/tool-executor-rff TOOLS) chunks)]
|
||||
(is (= chunks result)
|
||||
"Non-tool chunks should pass through unchanged")))
|
||||
|
||||
(testing "tool-executor-rff executes tool calls and appends results"
|
||||
(let [chunks (parts->aisdk
|
||||
[{:type :start :id "msg-123"}
|
||||
{:type :tool-input :id "call-1" :function "get-time" :arguments {:tz "Europe/Kyiv"}}
|
||||
{:type :usage :usage {:total_tokens 100}}])
|
||||
result (into [] (self/tool-executor-rff TOOLS) chunks)]
|
||||
(is (= (count chunks) (dec (count result)))
|
||||
"Should have original chunks plus one tool result")
|
||||
(is (= chunks (take (count chunks) result))
|
||||
"Original chunks should be unchanged")
|
||||
(let [tool-result (last result)]
|
||||
(is (=? {:type :tool-output-available
|
||||
:toolCallId "call-1"
|
||||
:toolName "get-time"
|
||||
:result string?}
|
||||
tool-result)))))
|
||||
|
||||
(testing "tool-executor-rff handles multiple concurrent tool calls"
|
||||
(let [chunks (parts->aisdk
|
||||
[{:type :start :id "msg-456"}
|
||||
{:type :tool-input :id "call-1" :function "get-time" :arguments {:tz "Europe/Kyiv"}}
|
||||
{:type :tool-input :id "call-2" :function "convert-currency" :arguments {:amount 100, :from "EUR", :to "USD"}}])
|
||||
result (into [] (self/tool-executor-rff TOOLS) chunks)]
|
||||
(is (= (+ (count chunks) 2) (count result))
|
||||
"Should have original chunks plus two tool results")
|
||||
(let [tool-results (take-last 2 result)]
|
||||
(is (every? #(= :tool-output-available (:type %)) tool-results)
|
||||
"Last two chunks should be tool outputs")
|
||||
(is (= #{"call-1" "call-2"}
|
||||
(set (map :toolCallId tool-results)))))))
|
||||
|
||||
(testing "tool-executor-rff handles tools returning channels"
|
||||
(let [llm-id "wut-1"
|
||||
input "Little bits and pieces"
|
||||
chunks (parts->aisdk
|
||||
[{:type :start :id "msg-666"}
|
||||
{:type :tool-input :id "call-1" :function "mock-llm" :arguments {:input input
|
||||
:id llm-id}}])
|
||||
result (into [] (self/tool-executor-rff TOOLS) chunks)]
|
||||
(is (= 1 (count (filter #(= :start (:type %)) result)))
|
||||
"Just the first start is left in the stream")
|
||||
(is (< 3 (count (filter #(= llm-id (:id %)) result)))
|
||||
"We get output from our 'llm' in little pieces")
|
||||
(is (= {:type :text
|
||||
:id llm-id
|
||||
:text input}
|
||||
(last (into [] self/aisdk-xf result))))))
|
||||
|
||||
(testing "tool-executor-rff handles tool execution errors gracefully"
|
||||
(let [chunks (parts->aisdk
|
||||
[{:type :start :id "msg-789"}
|
||||
{:type :tool-input :id "call-err" :function "get-time" :arguments {:tz "Invalid/Timezone"}}])
|
||||
result (into [] (self/tool-executor-rff TOOLS) chunks)]
|
||||
(is (= (count chunks) (dec (count result))))
|
||||
(let [tool-result (last result)]
|
||||
(is (=? {:type :tool-output-available
|
||||
:toolCallId "call-err"
|
||||
:toolName "get-time"
|
||||
:error {:message string?
|
||||
:type string?}}
|
||||
tool-result)))))
|
||||
|
||||
(testing "tool-executor-rff ignores unknown tool names"
|
||||
(let [chunks (parts->aisdk
|
||||
[{:type :start :id "msg-789"}
|
||||
{:type :tool-input :id "call-1" :function "unknown-tool" :arguments {:foo :bar}}])
|
||||
result (into [] (self/tool-executor-rff TOOLS) chunks)]
|
||||
(is (= chunks result)
|
||||
"Unknown tools should be ignored, chunks pass through unchanged"))))
|
||||
@@ -5,7 +5,8 @@
|
||||
[cheshire.core :as cheshire]
|
||||
[cheshire.factory]
|
||||
[cheshire.generate :as json.generate]
|
||||
[clojure.java.io :as io])
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str])
|
||||
(:import
|
||||
(com.fasterxml.jackson.core JsonGenerator)
|
||||
(java.io InputStream Reader)))
|
||||
@@ -89,3 +90,12 @@
|
||||
"Decode a value from a JSON from a string, InputStream, or Reader, keywordizing map keys."
|
||||
[source]
|
||||
(decode source true))
|
||||
|
||||
(defn decode-body
|
||||
"Given a response map, decodes body if headers indicate it's a JSON response, or just slurps if it's not a string."
|
||||
[res]
|
||||
(let [ctype (get-in res [:headers "content-type"])]
|
||||
(cond
|
||||
(some-> ctype (str/starts-with? "application/json")) (update res :body decode+kw)
|
||||
(string? (:body res)) res
|
||||
:else (update res :body slurp))))
|
||||
|
||||
91
test_resources/llm/claude-text.json
Normal file
91
test_resources/llm/claude-text.json
Normal file
@@ -0,0 +1,91 @@
|
||||
[ {
|
||||
"type" : "message_start",
|
||||
"message" : {
|
||||
"model" : "claude-sonnet-4-5-20250929",
|
||||
"id" : "msg_01NSbAHc6fiJS5e1c6UPdKho",
|
||||
"type" : "message",
|
||||
"role" : "assistant",
|
||||
"content" : [ ],
|
||||
"stop_reason" : null,
|
||||
"stop_sequence" : null,
|
||||
"usage" : {
|
||||
"input_tokens" : 13,
|
||||
"cache_creation_input_tokens" : 0,
|
||||
"cache_read_input_tokens" : 0,
|
||||
"cache_creation" : {
|
||||
"ephemeral_5m_input_tokens" : 0,
|
||||
"ephemeral_1h_input_tokens" : 0
|
||||
},
|
||||
"output_tokens" : 2,
|
||||
"service_tier" : "standard"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_start",
|
||||
"index" : 0,
|
||||
"content_block" : {
|
||||
"type" : "text",
|
||||
"text" : ""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "text_delta",
|
||||
"text" : "I don"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "text_delta",
|
||||
"text" : "'t experience feelings in the way you "
|
||||
}
|
||||
}, {
|
||||
"type" : "ping"
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "text_delta",
|
||||
"text" : "do, but I'm functioning well and ready to help!"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "text_delta",
|
||||
"text" : " I'm curious though – how are *you* feeling today? Is"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "text_delta",
|
||||
"text" : " there something specific on your mind or something"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "text_delta",
|
||||
"text" : " I can assist you with?"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_stop",
|
||||
"index" : 0
|
||||
}, {
|
||||
"type" : "message_delta",
|
||||
"delta" : {
|
||||
"stop_reason" : "end_turn",
|
||||
"stop_sequence" : null
|
||||
},
|
||||
"usage" : {
|
||||
"input_tokens" : 13,
|
||||
"cache_creation_input_tokens" : 0,
|
||||
"cache_read_input_tokens" : 0,
|
||||
"output_tokens" : 52
|
||||
}
|
||||
}, {
|
||||
"type" : "message_stop"
|
||||
} ]
|
||||
154
test_resources/llm/claude-tool-input.json
Normal file
154
test_resources/llm/claude-tool-input.json
Normal file
@@ -0,0 +1,154 @@
|
||||
[ {
|
||||
"type" : "message_start",
|
||||
"message" : {
|
||||
"model" : "claude-sonnet-4-5-20250929",
|
||||
"id" : "msg_01JaVDFooYCJgjDYdVv7UZuA",
|
||||
"type" : "message",
|
||||
"role" : "assistant",
|
||||
"content" : [ ],
|
||||
"stop_reason" : null,
|
||||
"stop_sequence" : null,
|
||||
"usage" : {
|
||||
"input_tokens" : 737,
|
||||
"cache_creation_input_tokens" : 0,
|
||||
"cache_read_input_tokens" : 0,
|
||||
"cache_creation" : {
|
||||
"ephemeral_5m_input_tokens" : 0,
|
||||
"ephemeral_1h_input_tokens" : 0
|
||||
},
|
||||
"output_tokens" : 18,
|
||||
"service_tier" : "standard"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_start",
|
||||
"index" : 0,
|
||||
"content_block" : {
|
||||
"type" : "tool_use",
|
||||
"id" : "toolu_012iDFpxaJuqVBQEqVVyfu6T",
|
||||
"name" : "structured_output",
|
||||
"input" : { }
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : ""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "{\"currenc"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "ies\": [{\"cou"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "ntry\":\"CA"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "N\",\"curr"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "ency\":\"CAD\""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "},{\"country"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "\":\"USA\",\""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "cur"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "rency\":\""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "USD\"},{\"coun"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "try\":\""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "MEX\","
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : "\"currency\""
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_delta",
|
||||
"index" : 0,
|
||||
"delta" : {
|
||||
"type" : "input_json_delta",
|
||||
"partial_json" : ":\"MXN\"}]}"
|
||||
}
|
||||
}, {
|
||||
"type" : "content_block_stop",
|
||||
"index" : 0
|
||||
}, {
|
||||
"type" : "message_delta",
|
||||
"delta" : {
|
||||
"stop_reason" : "tool_use",
|
||||
"stop_sequence" : null
|
||||
},
|
||||
"usage" : {
|
||||
"input_tokens" : 737,
|
||||
"cache_creation_input_tokens" : 0,
|
||||
"cache_read_input_tokens" : 0,
|
||||
"output_tokens" : 82
|
||||
}
|
||||
}, {
|
||||
"type" : "message_stop"
|
||||
} ]
|
||||
579
test_resources/llm/openai-structured-output.json
Normal file
579
test_resources/llm/openai-structured-output.json
Normal file
@@ -0,0 +1,579 @@
|
||||
[ {
|
||||
"type" : "response.created",
|
||||
"sequence_number" : 0,
|
||||
"response" : {
|
||||
"previous_response_id" : null,
|
||||
"tool_choice" : "auto",
|
||||
"top_logprobs" : 0,
|
||||
"store" : false,
|
||||
"top_p" : 1.0,
|
||||
"service_tier" : "auto",
|
||||
"tools" : [ ],
|
||||
"instructions" : null,
|
||||
"background" : false,
|
||||
"reasoning" : {
|
||||
"effort" : null,
|
||||
"summary" : null
|
||||
},
|
||||
"output" : [ ],
|
||||
"usage" : null,
|
||||
"safety_identifier" : null,
|
||||
"prompt_cache_key" : null,
|
||||
"incomplete_details" : null,
|
||||
"status" : "in_progress",
|
||||
"id" : "resp_0ca34af04d29538e0168fb881f3a2881a09bcbafa7555d1d3c",
|
||||
"max_output_tokens" : null,
|
||||
"max_tool_calls" : null,
|
||||
"truncation" : "disabled",
|
||||
"error" : null,
|
||||
"user" : null,
|
||||
"metadata" : { },
|
||||
"object" : "response",
|
||||
"temperature" : 1.0,
|
||||
"created_at" : 1761314847,
|
||||
"parallel_tool_calls" : true,
|
||||
"text" : {
|
||||
"format" : {
|
||||
"type" : "json_schema",
|
||||
"description" : null,
|
||||
"name" : "schema",
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"currencies" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"country" : {
|
||||
"description" : "Three-letter code",
|
||||
"type" : "string"
|
||||
},
|
||||
"currency" : {
|
||||
"description" : "Three-letter code",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "country", "currency" ],
|
||||
"additionalProperties" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" : [ "currencies" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
},
|
||||
"verbosity" : "medium"
|
||||
},
|
||||
"model" : "gpt-4.1-mini-2025-04-14"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.in_progress",
|
||||
"sequence_number" : 1,
|
||||
"response" : {
|
||||
"previous_response_id" : null,
|
||||
"tool_choice" : "auto",
|
||||
"top_logprobs" : 0,
|
||||
"store" : false,
|
||||
"top_p" : 1.0,
|
||||
"service_tier" : "auto",
|
||||
"tools" : [ ],
|
||||
"instructions" : null,
|
||||
"background" : false,
|
||||
"reasoning" : {
|
||||
"effort" : null,
|
||||
"summary" : null
|
||||
},
|
||||
"output" : [ ],
|
||||
"usage" : null,
|
||||
"safety_identifier" : null,
|
||||
"prompt_cache_key" : null,
|
||||
"incomplete_details" : null,
|
||||
"status" : "in_progress",
|
||||
"id" : "resp_0ca34af04d29538e0168fb881f3a2881a09bcbafa7555d1d3c",
|
||||
"max_output_tokens" : null,
|
||||
"max_tool_calls" : null,
|
||||
"truncation" : "disabled",
|
||||
"error" : null,
|
||||
"user" : null,
|
||||
"metadata" : { },
|
||||
"object" : "response",
|
||||
"temperature" : 1.0,
|
||||
"created_at" : 1761314847,
|
||||
"parallel_tool_calls" : true,
|
||||
"text" : {
|
||||
"format" : {
|
||||
"type" : "json_schema",
|
||||
"description" : null,
|
||||
"name" : "schema",
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"currencies" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"country" : {
|
||||
"description" : "Three-letter code",
|
||||
"type" : "string"
|
||||
},
|
||||
"currency" : {
|
||||
"description" : "Three-letter code",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "country", "currency" ],
|
||||
"additionalProperties" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" : [ "currencies" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
},
|
||||
"verbosity" : "medium"
|
||||
},
|
||||
"model" : "gpt-4.1-mini-2025-04-14"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.output_item.added",
|
||||
"sequence_number" : 2,
|
||||
"output_index" : 0,
|
||||
"item" : {
|
||||
"id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"type" : "message",
|
||||
"status" : "in_progress",
|
||||
"content" : [ ],
|
||||
"role" : "assistant"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.content_part.added",
|
||||
"sequence_number" : 3,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"part" : {
|
||||
"type" : "output_text",
|
||||
"annotations" : [ ],
|
||||
"logprobs" : [ ],
|
||||
"text" : ""
|
||||
}
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 4,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "{\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "Ul3wymizN27SES"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 5,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "curr",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "Q6rHkmKGWCPS"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 6,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "encies",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "D0iDA126nN"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 7,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":[",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "aGjRa82Tg8MTM"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 8,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "{\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "mfsWjRGPWxhpk3"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 9,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "country",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "NxYi9q5cE"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 10,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "q2K5KIYOEb5ce"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 11,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "USA",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "MnmCqcS11BaIo"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 12,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\",\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "HmTcwAx03927C"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 13,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "currency",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "TIgq0xXu"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 14,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "aoHmfLEtAafDV"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 15,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "USD",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "gaNsBVlT07Nwi"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 16,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\"},{\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "THpEpCEhXhV"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 17,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "country",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "ATA5uOovr"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 18,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "0pAAmxK3eZ4ow"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 19,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "CAN",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "I88ezjGjyZQDr"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 20,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\",\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "xSxLuYIaUNlPu"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 21,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "currency",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "MurBe8Ck"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 22,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "CM2RiZkK3bXo3"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 23,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "CAD",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "oIjELbQAJSyD4"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 24,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\"},{\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "q3PC1t5fNOq"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 25,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "country",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "cucS4dtLC"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 26,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "T6oan7ahr1c46"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 27,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "M",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "YiZ6s9ypSEltXFR"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 28,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "EX",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "0B7uMO2Em77Sgn"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 29,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\",\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "W7zyWPzWxcbwa"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 30,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "currency",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "64jnAhOj"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 31,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\":\"",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "YYZy7RJ0lkG9z"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 32,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "MX",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "zdj5CpYRcRK9kP"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 33,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "N",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "X7gGiXXowmCDOo0"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 34,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "\"}",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "6uGb0m2YCH0QGQ"
|
||||
}, {
|
||||
"type" : "response.output_text.delta",
|
||||
"sequence_number" : 35,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"delta" : "]}",
|
||||
"logprobs" : [ ],
|
||||
"obfuscation" : "Aeoh74yeT3aEGi"
|
||||
}, {
|
||||
"type" : "response.output_text.done",
|
||||
"sequence_number" : 36,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"text" : "{\"currencies\":[{\"country\":\"USA\",\"currency\":\"USD\"},{\"country\":\"CAN\",\"currency\":\"CAD\"},{\"country\":\"MEX\",\"currency\":\"MXN\"}]}",
|
||||
"logprobs" : [ ]
|
||||
}, {
|
||||
"type" : "response.content_part.done",
|
||||
"sequence_number" : 37,
|
||||
"item_id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"output_index" : 0,
|
||||
"content_index" : 0,
|
||||
"part" : {
|
||||
"type" : "output_text",
|
||||
"annotations" : [ ],
|
||||
"logprobs" : [ ],
|
||||
"text" : "{\"currencies\":[{\"country\":\"USA\",\"currency\":\"USD\"},{\"country\":\"CAN\",\"currency\":\"CAD\"},{\"country\":\"MEX\",\"currency\":\"MXN\"}]}"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.output_item.done",
|
||||
"sequence_number" : 38,
|
||||
"output_index" : 0,
|
||||
"item" : {
|
||||
"id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"type" : "message",
|
||||
"status" : "completed",
|
||||
"content" : [ {
|
||||
"type" : "output_text",
|
||||
"annotations" : [ ],
|
||||
"logprobs" : [ ],
|
||||
"text" : "{\"currencies\":[{\"country\":\"USA\",\"currency\":\"USD\"},{\"country\":\"CAN\",\"currency\":\"CAD\"},{\"country\":\"MEX\",\"currency\":\"MXN\"}]}"
|
||||
} ],
|
||||
"role" : "assistant"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.completed",
|
||||
"sequence_number" : 39,
|
||||
"response" : {
|
||||
"previous_response_id" : null,
|
||||
"tool_choice" : "auto",
|
||||
"top_logprobs" : 0,
|
||||
"store" : false,
|
||||
"top_p" : 1.0,
|
||||
"service_tier" : "default",
|
||||
"tools" : [ ],
|
||||
"instructions" : null,
|
||||
"background" : false,
|
||||
"reasoning" : {
|
||||
"effort" : null,
|
||||
"summary" : null
|
||||
},
|
||||
"output" : [ {
|
||||
"id" : "msg_0ca34af04d29538e0168fb88203d6481a09f06bfa2b66cc16a",
|
||||
"type" : "message",
|
||||
"status" : "completed",
|
||||
"content" : [ {
|
||||
"type" : "output_text",
|
||||
"annotations" : [ ],
|
||||
"logprobs" : [ ],
|
||||
"text" : "{\"currencies\":[{\"country\":\"USA\",\"currency\":\"USD\"},{\"country\":\"CAN\",\"currency\":\"CAD\"},{\"country\":\"MEX\",\"currency\":\"MXN\"}]}"
|
||||
} ],
|
||||
"role" : "assistant"
|
||||
} ],
|
||||
"usage" : {
|
||||
"input_tokens" : 76,
|
||||
"input_tokens_details" : {
|
||||
"cached_tokens" : 0
|
||||
},
|
||||
"output_tokens" : 33,
|
||||
"output_tokens_details" : {
|
||||
"reasoning_tokens" : 0
|
||||
},
|
||||
"total_tokens" : 109
|
||||
},
|
||||
"safety_identifier" : null,
|
||||
"prompt_cache_key" : null,
|
||||
"incomplete_details" : null,
|
||||
"status" : "completed",
|
||||
"id" : "resp_0ca34af04d29538e0168fb881f3a2881a09bcbafa7555d1d3c",
|
||||
"max_output_tokens" : null,
|
||||
"max_tool_calls" : null,
|
||||
"truncation" : "disabled",
|
||||
"error" : null,
|
||||
"user" : null,
|
||||
"metadata" : { },
|
||||
"object" : "response",
|
||||
"temperature" : 1.0,
|
||||
"created_at" : 1761314847,
|
||||
"parallel_tool_calls" : true,
|
||||
"text" : {
|
||||
"format" : {
|
||||
"type" : "json_schema",
|
||||
"description" : null,
|
||||
"name" : "schema",
|
||||
"schema" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"currencies" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"country" : {
|
||||
"description" : "Three-letter code",
|
||||
"type" : "string"
|
||||
},
|
||||
"currency" : {
|
||||
"description" : "Three-letter code",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "country", "currency" ],
|
||||
"additionalProperties" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required" : [ "currencies" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
},
|
||||
"verbosity" : "medium"
|
||||
},
|
||||
"model" : "gpt-4.1-mini-2025-04-14"
|
||||
}
|
||||
} ]
|
||||
641
test_resources/llm/openai-tool-calls.json
Normal file
641
test_resources/llm/openai-tool-calls.json
Normal file
@@ -0,0 +1,641 @@
|
||||
[ {
|
||||
"type" : "response.created",
|
||||
"sequence_number" : 0,
|
||||
"response" : {
|
||||
"previous_response_id" : null,
|
||||
"tool_choice" : "auto",
|
||||
"top_logprobs" : 0,
|
||||
"store" : false,
|
||||
"top_p" : 1.0,
|
||||
"service_tier" : "auto",
|
||||
"tools" : [ {
|
||||
"type" : "function",
|
||||
"description" : "Return current time for a given IANA timezone.",
|
||||
"name" : "get-time",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"tz" : {
|
||||
"description" : "IANA timezone, e.g. Europe/Bucharest",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "tz" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
}, {
|
||||
"type" : "function",
|
||||
"description" : "Convert an amount between two ISO currencies using a dummy rate.",
|
||||
"name" : "convert-currency",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"amount" : {
|
||||
"type" : "number"
|
||||
},
|
||||
"from" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"to" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "amount", "from", "to" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
}, {
|
||||
"type" : "function",
|
||||
"description" : "Analyze a data trend by calling back to the LLM for natural language insights.\n This demonstrates a recursive LLM call pattern commonly used in agentic workflows.",
|
||||
"name" : "analyze-data-trend",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"metric" : {
|
||||
"description" : "The metric being analyzed, e.g. 'revenue', 'users'",
|
||||
"type" : "string"
|
||||
},
|
||||
"values" : {
|
||||
"description" : "Time series values",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "number"
|
||||
}
|
||||
},
|
||||
"period" : {
|
||||
"description" : "Time period, e.g. 'Q1 2025', 'last 6 months'",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "metric", "values", "period" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
} ],
|
||||
"instructions" : "You are a data analysis assistant. When users provide time-series data and ask for insights, use the analyze-data-trend tool to generate interpretations. Always call the tool rather than making up your own analysis.",
|
||||
"background" : false,
|
||||
"reasoning" : {
|
||||
"effort" : null,
|
||||
"summary" : null
|
||||
},
|
||||
"output" : [ ],
|
||||
"usage" : null,
|
||||
"safety_identifier" : null,
|
||||
"prompt_cache_key" : null,
|
||||
"incomplete_details" : null,
|
||||
"status" : "in_progress",
|
||||
"id" : "resp_0a93101cc19fa9880168fb5bd1d7d481a0a2913223de357104",
|
||||
"max_output_tokens" : null,
|
||||
"max_tool_calls" : null,
|
||||
"truncation" : "disabled",
|
||||
"error" : null,
|
||||
"user" : null,
|
||||
"metadata" : { },
|
||||
"object" : "response",
|
||||
"temperature" : 1.0,
|
||||
"created_at" : 1761303506,
|
||||
"parallel_tool_calls" : true,
|
||||
"text" : {
|
||||
"format" : {
|
||||
"type" : "text"
|
||||
},
|
||||
"verbosity" : "medium"
|
||||
},
|
||||
"model" : "gpt-4.1-mini-2025-04-14"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.in_progress",
|
||||
"sequence_number" : 1,
|
||||
"response" : {
|
||||
"previous_response_id" : null,
|
||||
"tool_choice" : "auto",
|
||||
"top_logprobs" : 0,
|
||||
"store" : false,
|
||||
"top_p" : 1.0,
|
||||
"service_tier" : "auto",
|
||||
"tools" : [ {
|
||||
"type" : "function",
|
||||
"description" : "Return current time for a given IANA timezone.",
|
||||
"name" : "get-time",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"tz" : {
|
||||
"description" : "IANA timezone, e.g. Europe/Bucharest",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "tz" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
}, {
|
||||
"type" : "function",
|
||||
"description" : "Convert an amount between two ISO currencies using a dummy rate.",
|
||||
"name" : "convert-currency",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"amount" : {
|
||||
"type" : "number"
|
||||
},
|
||||
"from" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"to" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "amount", "from", "to" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
}, {
|
||||
"type" : "function",
|
||||
"description" : "Analyze a data trend by calling back to the LLM for natural language insights.\n This demonstrates a recursive LLM call pattern commonly used in agentic workflows.",
|
||||
"name" : "analyze-data-trend",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"metric" : {
|
||||
"description" : "The metric being analyzed, e.g. 'revenue', 'users'",
|
||||
"type" : "string"
|
||||
},
|
||||
"values" : {
|
||||
"description" : "Time series values",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "number"
|
||||
}
|
||||
},
|
||||
"period" : {
|
||||
"description" : "Time period, e.g. 'Q1 2025', 'last 6 months'",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "metric", "values", "period" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
} ],
|
||||
"instructions" : "You are a data analysis assistant. When users provide time-series data and ask for insights, use the analyze-data-trend tool to generate interpretations. Always call the tool rather than making up your own analysis.",
|
||||
"background" : false,
|
||||
"reasoning" : {
|
||||
"effort" : null,
|
||||
"summary" : null
|
||||
},
|
||||
"output" : [ ],
|
||||
"usage" : null,
|
||||
"safety_identifier" : null,
|
||||
"prompt_cache_key" : null,
|
||||
"incomplete_details" : null,
|
||||
"status" : "in_progress",
|
||||
"id" : "resp_0a93101cc19fa9880168fb5bd1d7d481a0a2913223de357104",
|
||||
"max_output_tokens" : null,
|
||||
"max_tool_calls" : null,
|
||||
"truncation" : "disabled",
|
||||
"error" : null,
|
||||
"user" : null,
|
||||
"metadata" : { },
|
||||
"object" : "response",
|
||||
"temperature" : 1.0,
|
||||
"created_at" : 1761303506,
|
||||
"parallel_tool_calls" : true,
|
||||
"text" : {
|
||||
"format" : {
|
||||
"type" : "text"
|
||||
},
|
||||
"verbosity" : "medium"
|
||||
},
|
||||
"model" : "gpt-4.1-mini-2025-04-14"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.output_item.added",
|
||||
"sequence_number" : 2,
|
||||
"output_index" : 0,
|
||||
"item" : {
|
||||
"id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"type" : "function_call",
|
||||
"status" : "in_progress",
|
||||
"arguments" : "",
|
||||
"call_id" : "call_9lJIfuuW5IAKOb4kVfdqkoDJ",
|
||||
"name" : "analyze-data-trend"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 3,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "{",
|
||||
"obfuscation" : "FfJORXg2oHG3AnS"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 4,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\"metric",
|
||||
"obfuscation" : "OOeCNg38A"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 5,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\":",
|
||||
"obfuscation" : "3vZHLcv3Axrwrf"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 6,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\"re",
|
||||
"obfuscation" : "dlTxs2T5bkyLy"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 7,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "venue",
|
||||
"obfuscation" : "KqGxPzytiD9"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 8,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\",",
|
||||
"obfuscation" : "FDXUoSyY2EQhnC"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 9,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\"values",
|
||||
"obfuscation" : "P8mXBHkzH"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 10,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\":[",
|
||||
"obfuscation" : "TOqgWWYWzFSot"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 11,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "50000,",
|
||||
"obfuscation" : "QjrxQbMdS5"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 12,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "55000,",
|
||||
"obfuscation" : "6KC3WM2QpB"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 13,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "58000,",
|
||||
"obfuscation" : "qwbVBFQL9a"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 14,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "62000],",
|
||||
"obfuscation" : "jIIrAVxqC"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 15,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\"period",
|
||||
"obfuscation" : "MjNe3ga8A"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 16,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\":",
|
||||
"obfuscation" : "hlb4qNhA2ESqoi"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 17,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\"Q",
|
||||
"obfuscation" : "SVkKv2AYdXJO8r"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 18,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "1",
|
||||
"obfuscation" : "njRtFFC57dLndFB"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 19,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"delta" : "\"}",
|
||||
"obfuscation" : "h2JUplPrVIeMrm"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.done",
|
||||
"sequence_number" : 20,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"output_index" : 0,
|
||||
"arguments" : "{\"metric\":\"revenue\",\"values\":[50000,55000,58000,62000],\"period\":\"Q1\"}"
|
||||
}, {
|
||||
"type" : "response.output_item.done",
|
||||
"sequence_number" : 21,
|
||||
"output_index" : 0,
|
||||
"item" : {
|
||||
"id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"type" : "function_call",
|
||||
"status" : "completed",
|
||||
"arguments" : "{\"metric\":\"revenue\",\"values\":[50000,55000,58000,62000],\"period\":\"Q1\"}",
|
||||
"call_id" : "call_9lJIfuuW5IAKOb4kVfdqkoDJ",
|
||||
"name" : "analyze-data-trend"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.output_item.added",
|
||||
"sequence_number" : 22,
|
||||
"output_index" : 1,
|
||||
"item" : {
|
||||
"id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"type" : "function_call",
|
||||
"status" : "in_progress",
|
||||
"arguments" : "",
|
||||
"call_id" : "call_vuD5KmaxyQ55WRKCMUZOJ4hz",
|
||||
"name" : "analyze-data-trend"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 23,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "{",
|
||||
"obfuscation" : "hPFTrRLvzBmokZu"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 24,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\"metric",
|
||||
"obfuscation" : "Adu9kuY84"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 25,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\":",
|
||||
"obfuscation" : "g1AXeVsaO2RqGC"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 26,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\"customer",
|
||||
"obfuscation" : "Qaz7W75"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 27,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : " count",
|
||||
"obfuscation" : "wfjVeuKAo8"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 28,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\",",
|
||||
"obfuscation" : "gfOmpFPNJUwnaq"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 29,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\"values",
|
||||
"obfuscation" : "OJWErZx0t"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 30,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\":[",
|
||||
"obfuscation" : "tNU3yBfiZND2w"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 31,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "100,",
|
||||
"obfuscation" : "tF1hoVFzosb5"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 32,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "110,",
|
||||
"obfuscation" : "8Fqgor9b46Oo"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 33,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "105,",
|
||||
"obfuscation" : "FgQ1QmrIyit2"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 34,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "115],",
|
||||
"obfuscation" : "sId2VdDMuFT"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 35,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\"period",
|
||||
"obfuscation" : "8s7X915fx"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 36,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\":",
|
||||
"obfuscation" : "heVDgIklzhMVMK"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 37,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\"Q",
|
||||
"obfuscation" : "q0Rm2YZL0sAMF5"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 38,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "1",
|
||||
"obfuscation" : "i3kuywBLI7Q47C1"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.delta",
|
||||
"sequence_number" : 39,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"delta" : "\"}",
|
||||
"obfuscation" : "5hBqIqzHLLRtbw"
|
||||
}, {
|
||||
"type" : "response.function_call_arguments.done",
|
||||
"sequence_number" : 40,
|
||||
"item_id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"output_index" : 1,
|
||||
"arguments" : "{\"metric\":\"customer count\",\"values\":[100,110,105,115],\"period\":\"Q1\"}"
|
||||
}, {
|
||||
"type" : "response.output_item.done",
|
||||
"sequence_number" : 41,
|
||||
"output_index" : 1,
|
||||
"item" : {
|
||||
"id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"type" : "function_call",
|
||||
"status" : "completed",
|
||||
"arguments" : "{\"metric\":\"customer count\",\"values\":[100,110,105,115],\"period\":\"Q1\"}",
|
||||
"call_id" : "call_vuD5KmaxyQ55WRKCMUZOJ4hz",
|
||||
"name" : "analyze-data-trend"
|
||||
}
|
||||
}, {
|
||||
"type" : "response.completed",
|
||||
"sequence_number" : 42,
|
||||
"response" : {
|
||||
"previous_response_id" : null,
|
||||
"tool_choice" : "auto",
|
||||
"top_logprobs" : 0,
|
||||
"store" : false,
|
||||
"top_p" : 1.0,
|
||||
"service_tier" : "default",
|
||||
"tools" : [ {
|
||||
"type" : "function",
|
||||
"description" : "Return current time for a given IANA timezone.",
|
||||
"name" : "get-time",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"tz" : {
|
||||
"description" : "IANA timezone, e.g. Europe/Bucharest",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "tz" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
}, {
|
||||
"type" : "function",
|
||||
"description" : "Convert an amount between two ISO currencies using a dummy rate.",
|
||||
"name" : "convert-currency",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"amount" : {
|
||||
"type" : "number"
|
||||
},
|
||||
"from" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"to" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "amount", "from", "to" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
}, {
|
||||
"type" : "function",
|
||||
"description" : "Analyze a data trend by calling back to the LLM for natural language insights.\n This demonstrates a recursive LLM call pattern commonly used in agentic workflows.",
|
||||
"name" : "analyze-data-trend",
|
||||
"parameters" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"metric" : {
|
||||
"description" : "The metric being analyzed, e.g. 'revenue', 'users'",
|
||||
"type" : "string"
|
||||
},
|
||||
"values" : {
|
||||
"description" : "Time series values",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "number"
|
||||
}
|
||||
},
|
||||
"period" : {
|
||||
"description" : "Time period, e.g. 'Q1 2025', 'last 6 months'",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required" : [ "metric", "values", "period" ],
|
||||
"additionalProperties" : false
|
||||
},
|
||||
"strict" : true
|
||||
} ],
|
||||
"instructions" : "You are a data analysis assistant. When users provide time-series data and ask for insights, use the analyze-data-trend tool to generate interpretations. Always call the tool rather than making up your own analysis.",
|
||||
"background" : false,
|
||||
"reasoning" : {
|
||||
"effort" : null,
|
||||
"summary" : null
|
||||
},
|
||||
"output" : [ {
|
||||
"id" : "fc_0a93101cc19fa9880168fb5bd37fe481a09db247297868375d",
|
||||
"type" : "function_call",
|
||||
"status" : "completed",
|
||||
"arguments" : "{\"metric\":\"revenue\",\"values\":[50000,55000,58000,62000],\"period\":\"Q1\"}",
|
||||
"call_id" : "call_9lJIfuuW5IAKOb4kVfdqkoDJ",
|
||||
"name" : "analyze-data-trend"
|
||||
}, {
|
||||
"id" : "fc_0a93101cc19fa9880168fb5bd4078881a082a3ce11fa151383",
|
||||
"type" : "function_call",
|
||||
"status" : "completed",
|
||||
"arguments" : "{\"metric\":\"customer count\",\"values\":[100,110,105,115],\"period\":\"Q1\"}",
|
||||
"call_id" : "call_vuD5KmaxyQ55WRKCMUZOJ4hz",
|
||||
"name" : "analyze-data-trend"
|
||||
} ],
|
||||
"usage" : {
|
||||
"input_tokens" : 139,
|
||||
"input_tokens_details" : {
|
||||
"cached_tokens" : 0
|
||||
},
|
||||
"output_tokens" : 86,
|
||||
"output_tokens_details" : {
|
||||
"reasoning_tokens" : 0
|
||||
},
|
||||
"total_tokens" : 225
|
||||
},
|
||||
"safety_identifier" : null,
|
||||
"prompt_cache_key" : null,
|
||||
"incomplete_details" : null,
|
||||
"status" : "completed",
|
||||
"id" : "resp_0a93101cc19fa9880168fb5bd1d7d481a0a2913223de357104",
|
||||
"max_output_tokens" : null,
|
||||
"max_tool_calls" : null,
|
||||
"truncation" : "disabled",
|
||||
"error" : null,
|
||||
"user" : null,
|
||||
"metadata" : { },
|
||||
"object" : "response",
|
||||
"temperature" : 1.0,
|
||||
"created_at" : 1761303506,
|
||||
"parallel_tool_calls" : true,
|
||||
"text" : {
|
||||
"format" : {
|
||||
"type" : "text"
|
||||
},
|
||||
"verbosity" : "medium"
|
||||
},
|
||||
"model" : "gpt-4.1-mini-2025-04-14"
|
||||
}
|
||||
} ]
|
||||
Reference in New Issue
Block a user