hermes-client 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,429 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hermes_agent/client/entity"
4
+ require "hermes_agent/client/entities/session_headers"
5
+
6
+ module HermesAgent
7
+ class Client
8
+ module Entities
9
+ ##
10
+ # The token usage reported for a {Response} ({Response#usage}). Note the
11
+ # Responses API uses different field names than chat completions:
12
+ # `input_tokens`/`output_tokens` rather than
13
+ # `prompt_tokens`/`completion_tokens`.
14
+ #
15
+ class ResponseUsage < Entity
16
+ ##
17
+ # The number of tokens in the input.
18
+ # @return [Integer, nil]
19
+ #
20
+ def input_tokens
21
+ self["input_tokens"]
22
+ end
23
+
24
+ ##
25
+ # The number of tokens in the generated output.
26
+ # @return [Integer, nil]
27
+ #
28
+ def output_tokens
29
+ self["output_tokens"]
30
+ end
31
+
32
+ ##
33
+ # The total number of tokens used (input plus output).
34
+ # @return [Integer, nil]
35
+ #
36
+ def total_tokens
37
+ self["total_tokens"]
38
+ end
39
+ end
40
+
41
+ ##
42
+ # One content part within a message {ResponseOutputItem} (an entry of
43
+ # {ResponseOutputItem#content}), e.g. `{type: "output_text", text: "…"}`.
44
+ #
45
+ class ResponseContent < Entity
46
+ ##
47
+ # The content-part type, e.g. `"output_text"`.
48
+ # @return [String, nil]
49
+ #
50
+ def type
51
+ self["type"]
52
+ end
53
+
54
+ ##
55
+ # The text of the part (for an `output_text` part).
56
+ # @return [String, nil]
57
+ #
58
+ def text
59
+ self["text"]
60
+ end
61
+ end
62
+
63
+ ##
64
+ # One item in a {Response}'s `output` array. The output is heterogeneous:
65
+ # an item's `type` selects which readers are meaningful. Observed types
66
+ # are `"message"` (an assistant reply, with {#role} and {#content}),
67
+ # `"function_call"` (a tool invocation, with {#name}, {#arguments}, and
68
+ # {#call_id}), and `"function_call_output"` (a tool result, with
69
+ # {#call_id}, {#output}, and {#output_text}). Readers for fields that do
70
+ # not apply to the item's type return `nil`. {#id} and {#status} are
71
+ # populated only on items carried by streaming `response.output_item.*`
72
+ # events, not on items read off a final {Response}.
73
+ #
74
+ class ResponseOutputItem < Entity
75
+ ##
76
+ # The item type: `"message"`, `"function_call"`, or
77
+ # `"function_call_output"`.
78
+ # @return [String, nil]
79
+ #
80
+ def type
81
+ self["type"]
82
+ end
83
+
84
+ ##
85
+ # The item id (`"msg_…"`, `"fc_…"`, or `"fco_…"`). Present on items
86
+ # carried by streaming `response.output_item.*` events; `nil` for items
87
+ # read off a final {Response}'s `output` (the server omits it there).
88
+ # @return [String, nil]
89
+ #
90
+ def id
91
+ self["id"]
92
+ end
93
+
94
+ ##
95
+ # The item lifecycle status, e.g. `"in_progress"` or `"completed"`.
96
+ # Present on items carried by streaming `response.output_item.*` events;
97
+ # `nil` for items read off a final {Response}'s `output`. Like all such
98
+ # statuses it is a lifecycle marker, not a success signal.
99
+ # @return [String, nil]
100
+ #
101
+ def status
102
+ self["status"]
103
+ end
104
+
105
+ ##
106
+ # The author role of a `message` item, e.g. `"assistant"`.
107
+ # @return [String, nil]
108
+ #
109
+ def role
110
+ self["role"]
111
+ end
112
+
113
+ ##
114
+ # The content parts of a `message` item, each wrapped in a
115
+ # {ResponseContent}. Returns `nil` when the field is absent.
116
+ # @return [Array<ResponseContent>, nil]
117
+ #
118
+ def content
119
+ raw = self["content"]
120
+ return nil unless raw.is_a?(::Array)
121
+
122
+ raw.map { |part| ResponseContent.new(part) }
123
+ end
124
+
125
+ ##
126
+ # The tool name of a `function_call` item.
127
+ # @return [String, nil]
128
+ #
129
+ def name
130
+ self["name"]
131
+ end
132
+
133
+ ##
134
+ # The arguments of a `function_call` item, as the raw JSON string the
135
+ # server emitted (not parsed).
136
+ # @return [String, nil]
137
+ #
138
+ def arguments
139
+ self["arguments"]
140
+ end
141
+
142
+ ##
143
+ # The tool-call id linking a `function_call` to its
144
+ # `function_call_output`.
145
+ # @return [String, nil]
146
+ #
147
+ def call_id
148
+ self["call_id"]
149
+ end
150
+
151
+ ##
152
+ # The raw result of a `function_call_output` item, as the server
153
+ # emitted it. The shape differs by representation: a **raw JSON string**
154
+ # in non-streaming (POST/GET) bodies, but an **array of content parts**
155
+ # (`[{ "type" => "input_text", "text" => … }]`) in the streaming
156
+ # representation. Use {#output_text} for the result text regardless of
157
+ # shape.
158
+ # @return [String, Array<Hash>, nil]
159
+ #
160
+ def output
161
+ self["output"]
162
+ end
163
+
164
+ ##
165
+ # The result text of a `function_call_output` item, normalized across
166
+ # both {#output} shapes: the string itself when non-streaming, or the
167
+ # concatenated `text` of the content parts when streaming. Returns `nil`
168
+ # when there is no `output` (e.g. a non-output item).
169
+ # @return [String, nil]
170
+ #
171
+ def output_text
172
+ raw = self["output"]
173
+ return raw if raw.is_a?(::String)
174
+ return nil unless raw.is_a?(::Array)
175
+
176
+ raw.filter_map { |part| part["text"] if part.is_a?(::Hash) }.join
177
+ end
178
+
179
+ ##
180
+ # The assembled text of a `message` item: the concatenation of its
181
+ # `output_text` content parts. Returns `nil` for an item with no
182
+ # content (e.g. a tool item).
183
+ # @return [String, nil]
184
+ #
185
+ def text
186
+ parts = content
187
+ return nil unless parts
188
+
189
+ parts.filter_map { |part| part.text if part.type == "output_text" }.join
190
+ end
191
+ end
192
+
193
+ ##
194
+ # A response from the Responses API (`POST /v1/responses`,
195
+ # `GET /v1/responses/{id}`). The server persists conversation state, so a
196
+ # response can be retrieved later and chained from. Field readers are
197
+ # best-effort; {#to_h} remains the source of truth.
198
+ #
199
+ class Response < Entity
200
+ include SessionHeaders
201
+
202
+ ##
203
+ # Build a {Response} from a streamed turn's events. The terminal
204
+ # `response.completed` event carries the full final response object, so
205
+ # this takes the last `response` payload seen across the events (which
206
+ # is that terminal one — `response.created` carries an interim one).
207
+ # Returns a {Response} wrapping an empty payload if no event carried a
208
+ # `response` object.
209
+ #
210
+ # @param events [Array<ResponseStreamEvent>] The streamed events, in
211
+ # order.
212
+ # @param session_id [String, nil] The session id from the response
213
+ # headers, carried onto the assembled response.
214
+ # @param session_key [String, nil] The session key from the response
215
+ # headers, carried onto the assembled response.
216
+ # @return [Response]
217
+ #
218
+ def self.from_events(events, session_id: nil, session_key: nil)
219
+ payload = {}
220
+ events.each do |event|
221
+ raw = event["response"]
222
+ payload = raw if raw.is_a?(::Hash)
223
+ end
224
+ new(payload, session_id: session_id, session_key: session_key)
225
+ end
226
+
227
+ ##
228
+ # The response id, e.g. `"resp_…"`. Pass it as `previous_response_id` to
229
+ # chain a follow-up turn.
230
+ # @return [String, nil]
231
+ #
232
+ def id
233
+ self["id"]
234
+ end
235
+
236
+ ##
237
+ # The object type, `"response"`.
238
+ # @return [String, nil]
239
+ #
240
+ def object
241
+ self["object"]
242
+ end
243
+
244
+ ##
245
+ # The response status, e.g. `"completed"` or `"in_progress"`.
246
+ # @return [String, nil]
247
+ #
248
+ def status
249
+ self["status"]
250
+ end
251
+
252
+ ##
253
+ # When the response was created, as a Unix timestamp (seconds).
254
+ # @return [Integer, nil]
255
+ #
256
+ def created_at
257
+ self["created_at"]
258
+ end
259
+
260
+ ##
261
+ # The model that produced the response, e.g. `"hermes-test"`.
262
+ # @return [String, nil]
263
+ #
264
+ def model
265
+ self["model"]
266
+ end
267
+
268
+ ##
269
+ # The output items, each wrapped in a {ResponseOutputItem}. Returns
270
+ # `nil` when the field is absent.
271
+ # @return [Array<ResponseOutputItem>, nil]
272
+ #
273
+ def output
274
+ raw = self["output"]
275
+ return nil unless raw.is_a?(::Array)
276
+
277
+ raw.map { |item| ResponseOutputItem.new(item) }
278
+ end
279
+
280
+ ##
281
+ # The token usage, wrapped in a {ResponseUsage}. Returns `nil` when the
282
+ # field is absent.
283
+ # @return [ResponseUsage, nil]
284
+ #
285
+ def usage
286
+ raw = self["usage"]
287
+ raw.is_a?(::Hash) ? ResponseUsage.new(raw) : nil
288
+ end
289
+
290
+ ##
291
+ # The assistant's text: the concatenation of the text of every
292
+ # `message` output item, ignoring tool items. A convenience over
293
+ # {#output} for the common single-message case. Returns `nil` when
294
+ # there is no output.
295
+ # @return [String, nil]
296
+ #
297
+ def output_text
298
+ items = output
299
+ return nil unless items
300
+
301
+ items.select { |item| item.type == "message" }.filter_map(&:text).join
302
+ end
303
+ end
304
+
305
+ ##
306
+ # One event in a streamed Responses turn ({Resources::Responses#stream_create}).
307
+ #
308
+ # The Responses API emits **named** SSE events; each payload repeats the
309
+ # name in its {#type} and carries a 0-based {#sequence_number}. The
310
+ # observed sequence for a simple turn is `response.created` →
311
+ # `response.output_item.added` → `response.output_text.delta` (one per
312
+ # delta) → `response.output_text.done` → `response.output_item.done` →
313
+ # `response.completed` (terminal; there is no `[DONE]` sentinel). Which
314
+ # readers are meaningful depends on {#type}; the rest return `nil`.
315
+ #
316
+ class ResponseStreamEvent < Entity
317
+ ##
318
+ # The event type, e.g. `"response.output_text.delta"` or
319
+ # `"response.completed"`.
320
+ # @return [String, nil]
321
+ #
322
+ def type
323
+ self["type"]
324
+ end
325
+
326
+ ##
327
+ # The 0-based sequence number of this event within the turn.
328
+ # @return [Integer, nil]
329
+ #
330
+ def sequence_number
331
+ self["sequence_number"]
332
+ end
333
+
334
+ ##
335
+ # The incremental text on a `response.output_text.delta` event.
336
+ # @return [String, nil]
337
+ #
338
+ def delta
339
+ self["delta"]
340
+ end
341
+
342
+ ##
343
+ # The assembled text on a `response.output_text.done` event.
344
+ # @return [String, nil]
345
+ #
346
+ def text
347
+ self["text"]
348
+ end
349
+
350
+ ##
351
+ # The id of the output item a text delta/done event applies to
352
+ # (`"msg_…"`).
353
+ # @return [String, nil]
354
+ #
355
+ def item_id
356
+ self["item_id"]
357
+ end
358
+
359
+ ##
360
+ # The index of the output item this event applies to.
361
+ # @return [Integer, nil]
362
+ #
363
+ def output_index
364
+ self["output_index"]
365
+ end
366
+
367
+ ##
368
+ # The index of the content part within the item this event applies to.
369
+ # @return [Integer, nil]
370
+ #
371
+ def content_index
372
+ self["content_index"]
373
+ end
374
+
375
+ ##
376
+ # The nested response object on a `response.created` or
377
+ # `response.completed` event, wrapped in a {Response}. Returns `nil` on
378
+ # events that carry no response object.
379
+ # @return [Response, nil]
380
+ #
381
+ def response
382
+ raw = self["response"]
383
+ raw.is_a?(::Hash) ? Response.new(raw) : nil
384
+ end
385
+
386
+ ##
387
+ # The nested output item on a `response.output_item.added` or
388
+ # `response.output_item.done` event, wrapped in a
389
+ # {ResponseOutputItem}. Returns `nil` on events that carry no item.
390
+ # @return [ResponseOutputItem, nil]
391
+ #
392
+ def item
393
+ raw = self["item"]
394
+ raw.is_a?(::Hash) ? ResponseOutputItem.new(raw) : nil
395
+ end
396
+ end
397
+
398
+ ##
399
+ # The result of deleting a response (`DELETE /v1/responses/{id}`):
400
+ # `{id, object: "response", deleted: true}`.
401
+ #
402
+ class ResponseDeletion < Entity
403
+ ##
404
+ # The id of the deleted response.
405
+ # @return [String, nil]
406
+ #
407
+ def id
408
+ self["id"]
409
+ end
410
+
411
+ ##
412
+ # The object type, `"response"`.
413
+ # @return [String, nil]
414
+ #
415
+ def object
416
+ self["object"]
417
+ end
418
+
419
+ ##
420
+ # Whether the response was deleted.
421
+ # @return [boolean, nil]
422
+ #
423
+ def deleted?
424
+ self["deleted"]
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end