ruby_llm-responses_api 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/lib/ruby_llm/providers/openai_responses/active_record_extension.rb +76 -0
- data/lib/ruby_llm/providers/openai_responses/background.rb +98 -0
- data/lib/ruby_llm/providers/openai_responses/base.rb +14 -0
- data/lib/ruby_llm/providers/openai_responses/built_in_tools.rb +184 -0
- data/lib/ruby_llm/providers/openai_responses/capabilities.rb +226 -0
- data/lib/ruby_llm/providers/openai_responses/chat.rb +265 -0
- data/lib/ruby_llm/providers/openai_responses/media.rb +114 -0
- data/lib/ruby_llm/providers/openai_responses/message_extension.rb +32 -0
- data/lib/ruby_llm/providers/openai_responses/model_registry.rb +257 -0
- data/lib/ruby_llm/providers/openai_responses/models.rb +48 -0
- data/lib/ruby_llm/providers/openai_responses/state.rb +56 -0
- data/lib/ruby_llm/providers/openai_responses/streaming.rb +128 -0
- data/lib/ruby_llm/providers/openai_responses/tools.rb +193 -0
- data/lib/ruby_llm/providers/openai_responses.rb +94 -0
- data/lib/ruby_llm-responses_api.rb +4 -0
- data/lib/rubyllm_responses_api.rb +44 -0
- metadata +177 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b83e9dbc3e924cceb9a1468e0da1271950bdbeb20d3bc0f3f2f859e3d17da565
|
|
4
|
+
data.tar.gz: 6ffbb8e1792cc333fb375b35a588f11d4d65d54a74182b95a0a3482ea716f39f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1178f162885b3fa9e7f084f183568789a3d08b3be0e27550f2aa8f7e069376a9a2165fc62f5a26ce8a8ec72ba4aebc4a5957ed9f83ab19ba9edc0b29eae0f82e
|
|
7
|
+
data.tar.gz: 7f922f72564bc21cee14d11e7a6d220a79519eb09906e6e1e17321d6e5db9bf424c9b18b1b8219d6a85eee096be9ec498c4da04d251bf10cfb190b1b5cc25fcb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-01-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of the RubyLLM Responses API provider
|
|
13
|
+
- Core chat completion support with Responses API format
|
|
14
|
+
- Streaming support with typed event handling
|
|
15
|
+
- Function calling (tool use) support
|
|
16
|
+
- Built-in tools support:
|
|
17
|
+
- Web Search (`web_search_preview`)
|
|
18
|
+
- Code Interpreter (`code_interpreter`)
|
|
19
|
+
- File Search (`file_search`)
|
|
20
|
+
- Image Generation (`image_generation`)
|
|
21
|
+
- MCP (Model Context Protocol) (`mcp`)
|
|
22
|
+
- Computer Use (`computer_use_preview`)
|
|
23
|
+
- Stateful conversation support via `previous_response_id` and `store`
|
|
24
|
+
- Background mode for long-running tasks
|
|
25
|
+
- Response polling and cancellation
|
|
26
|
+
- Message extension to support `response_id`
|
|
27
|
+
- Model capabilities for GPT-4o, GPT-4.1, and O-series models
|
|
28
|
+
- Media handling for images, PDFs, and audio
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Chris Hasinski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# RubyLLM Responses API
|
|
2
|
+
|
|
3
|
+
A [RubyLLM](https://github.com/crmne/ruby_llm) provider for OpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem 'ruby_llm-responses_api'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'ruby_llm-responses_api'
|
|
15
|
+
|
|
16
|
+
RubyLLM.configure do |config|
|
|
17
|
+
config.openai_api_key = ENV['OPENAI_API_KEY']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
chat = RubyLLM.chat(model: 'gpt-4o-mini', provider: :openai_responses)
|
|
21
|
+
response = chat.ask("Hello!")
|
|
22
|
+
puts response.content
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
All standard RubyLLM features work as expected (streaming, tools, vision, structured output).
|
|
26
|
+
|
|
27
|
+
## Stateful Conversations
|
|
28
|
+
|
|
29
|
+
Conversations automatically chain via `previous_response_id`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
chat = RubyLLM.chat(model: 'gpt-4o-mini', provider: :openai_responses)
|
|
33
|
+
chat.ask("My name is Alice.")
|
|
34
|
+
chat.ask("What's my name?") # => "Your name is Alice."
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Rails Persistence
|
|
38
|
+
|
|
39
|
+
For conversations that survive app restarts, add a migration:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
class AddResponseIdToMessages < ActiveRecord::Migration[7.0]
|
|
43
|
+
def change
|
|
44
|
+
add_column :messages, :response_id, :string
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then use normally:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# Day 1
|
|
53
|
+
chat = Chat.create!(model_id: 'gpt-4o-mini', provider: :openai_responses)
|
|
54
|
+
chat.ask("My name is Alice.")
|
|
55
|
+
|
|
56
|
+
# Day 2 (after restart)
|
|
57
|
+
chat = Chat.find(1)
|
|
58
|
+
chat.ask("What's my name?") # => "Alice"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Built-in Tools
|
|
62
|
+
|
|
63
|
+
The Responses API provides built-in tools that don't require custom implementation.
|
|
64
|
+
|
|
65
|
+
### Web Search
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
chat.with_params(tools: [{ type: 'web_search_preview' }])
|
|
69
|
+
chat.ask("Latest news about Ruby 3.4?")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Code Interpreter
|
|
73
|
+
|
|
74
|
+
Execute Python code in a sandbox:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
chat.with_params(tools: [{ type: 'code_interpreter' }])
|
|
78
|
+
chat.ask("Calculate the first 20 Fibonacci numbers and plot them")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### File Search
|
|
82
|
+
|
|
83
|
+
Search through uploaded files (requires vector store setup):
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
chat.with_params(tools: [{ type: 'file_search', vector_store_ids: ['vs_abc123'] }])
|
|
87
|
+
chat.ask("What does the documentation say about authentication?")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Combining Tools
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
chat.with_params(tools: [
|
|
94
|
+
{ type: 'web_search_preview' },
|
|
95
|
+
{ type: 'code_interpreter' }
|
|
96
|
+
])
|
|
97
|
+
chat.ask("Find the latest Bitcoin price and plot a chart")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Why Use the Responses API?
|
|
101
|
+
|
|
102
|
+
- **Built-in tools** - Web search, code execution, file search without custom implementation
|
|
103
|
+
- **Stateful conversations** - OpenAI stores context server-side via `previous_response_id`
|
|
104
|
+
- **Simpler multi-turn** - No need to send full message history on each request
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class OpenAIResponses
|
|
6
|
+
# Extends RubyLLM's ActiveRecord MessageMethods to support response_id persistence
|
|
7
|
+
# for stateful conversations with the OpenAI Responses API.
|
|
8
|
+
#
|
|
9
|
+
# This is automatically applied when Rails loads ActiveRecord.
|
|
10
|
+
# Just add a migration: add_column :messages, :response_id, :string
|
|
11
|
+
#
|
|
12
|
+
module MessageMethodsExtension
|
|
13
|
+
# Override to_llm to include response_id for Responses API support
|
|
14
|
+
def to_llm
|
|
15
|
+
cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
|
|
16
|
+
cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
|
|
17
|
+
resp_id = has_attribute?(:response_id) ? self[:response_id] : nil
|
|
18
|
+
|
|
19
|
+
RubyLLM::Message.new(
|
|
20
|
+
role: role.to_sym,
|
|
21
|
+
content: extract_content,
|
|
22
|
+
tool_calls: extract_tool_calls,
|
|
23
|
+
tool_call_id: extract_tool_call_id,
|
|
24
|
+
input_tokens: input_tokens,
|
|
25
|
+
output_tokens: output_tokens,
|
|
26
|
+
cached_tokens: cached,
|
|
27
|
+
cache_creation_tokens: cache_creation,
|
|
28
|
+
model_id: model_association&.model_id,
|
|
29
|
+
response_id: resp_id
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Extends RubyLLM's ActiveRecord ChatMethods to persist response_id
|
|
35
|
+
module ChatMethodsExtension
|
|
36
|
+
# Override persist_message_completion to also save response_id
|
|
37
|
+
def persist_message_completion(message)
|
|
38
|
+
super
|
|
39
|
+
|
|
40
|
+
# After the parent saves, update response_id if the column exists and message has one
|
|
41
|
+
return unless message
|
|
42
|
+
return unless message.respond_to?(:response_id) && message.response_id
|
|
43
|
+
return unless @message.has_attribute?(:response_id)
|
|
44
|
+
|
|
45
|
+
@message.update_column(:response_id, message.response_id)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@active_record_extensions_applied = false
|
|
50
|
+
|
|
51
|
+
# Apply ActiveRecord extensions for response_id persistence.
|
|
52
|
+
# Called automatically when ActiveRecord loads, or can be called manually.
|
|
53
|
+
def self.apply_active_record_extensions!
|
|
54
|
+
return if @active_record_extensions_applied
|
|
55
|
+
|
|
56
|
+
require 'ruby_llm/active_record/message_methods'
|
|
57
|
+
require 'ruby_llm/active_record/chat_methods'
|
|
58
|
+
|
|
59
|
+
RubyLLM::ActiveRecord::MessageMethods.prepend(MessageMethodsExtension)
|
|
60
|
+
RubyLLM::ActiveRecord::ChatMethods.prepend(ChatMethodsExtension)
|
|
61
|
+
|
|
62
|
+
@active_record_extensions_applied = true
|
|
63
|
+
rescue LoadError, NameError
|
|
64
|
+
# RubyLLM ActiveRecord support not available, skip silently
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Auto-apply extensions when ActiveRecord is loaded in Rails
|
|
72
|
+
if defined?(ActiveSupport.on_load)
|
|
73
|
+
ActiveSupport.on_load(:active_record) do
|
|
74
|
+
RubyLLM::Providers::OpenAIResponses.apply_active_record_extensions!
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class OpenAIResponses
|
|
6
|
+
# Background mode support for the OpenAI Responses API.
|
|
7
|
+
# Handles async responses with polling for long-running tasks.
|
|
8
|
+
module Background
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Status constants
|
|
12
|
+
QUEUED = 'queued'
|
|
13
|
+
IN_PROGRESS = 'in_progress'
|
|
14
|
+
COMPLETED = 'completed'
|
|
15
|
+
FAILED = 'failed'
|
|
16
|
+
CANCELLED = 'cancelled'
|
|
17
|
+
INCOMPLETE = 'incomplete'
|
|
18
|
+
|
|
19
|
+
TERMINAL_STATUSES = [COMPLETED, FAILED, CANCELLED, INCOMPLETE].freeze
|
|
20
|
+
PENDING_STATUSES = [QUEUED, IN_PROGRESS].freeze
|
|
21
|
+
|
|
22
|
+
# Add background mode to payload
|
|
23
|
+
# @param payload [Hash] The request payload
|
|
24
|
+
# @param background [Boolean] Whether to run in background mode
|
|
25
|
+
# @return [Hash] Updated payload
|
|
26
|
+
def apply_background_mode(payload, background: false)
|
|
27
|
+
payload[:background] = background if background
|
|
28
|
+
payload
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if response is still pending
|
|
32
|
+
# @param response [Hash] The API response
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def pending?(response)
|
|
35
|
+
status = response['status']
|
|
36
|
+
PENDING_STATUSES.include?(status)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if response is complete (terminal state)
|
|
40
|
+
# @param response [Hash] The API response
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def complete?(response)
|
|
43
|
+
status = response['status']
|
|
44
|
+
TERMINAL_STATUSES.include?(status)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if response was successful
|
|
48
|
+
# @param response [Hash] The API response
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def successful?(response)
|
|
51
|
+
response['status'] == COMPLETED
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if response failed
|
|
55
|
+
# @param response [Hash] The API response
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def failed?(response)
|
|
58
|
+
response['status'] == FAILED
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get response status
|
|
62
|
+
# @param response [Hash] The API response
|
|
63
|
+
# @return [String] The status
|
|
64
|
+
def status(response)
|
|
65
|
+
response['status']
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get error information if failed
|
|
69
|
+
# @param response [Hash] The API response
|
|
70
|
+
# @return [Hash, nil] Error information
|
|
71
|
+
def error_info(response)
|
|
72
|
+
response['error']
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# URL to retrieve a response by ID
|
|
76
|
+
# @param response_id [String] The response ID
|
|
77
|
+
# @return [String] The URL path
|
|
78
|
+
def retrieve_url(response_id)
|
|
79
|
+
"responses/#{response_id}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# URL to cancel a response
|
|
83
|
+
# @param response_id [String] The response ID
|
|
84
|
+
# @return [String] The URL path
|
|
85
|
+
def cancel_url(response_id)
|
|
86
|
+
"responses/#{response_id}/cancel"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# URL to list input items for a response
|
|
90
|
+
# @param response_id [String] The response ID
|
|
91
|
+
# @return [String] The URL path
|
|
92
|
+
def input_items_url(response_id)
|
|
93
|
+
"responses/#{response_id}/input_items"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# OpenAI Responses API provider for RubyLLM.
|
|
6
|
+
# Implements the new Responses API which provides built-in tools,
|
|
7
|
+
# stateful conversations, background mode, and MCP support.
|
|
8
|
+
#
|
|
9
|
+
# This base file defines the class structure before modules are loaded
|
|
10
|
+
# to avoid "superclass mismatch" errors.
|
|
11
|
+
class OpenAIResponses < Provider
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class OpenAIResponses
|
|
6
|
+
# Built-in tools support for the OpenAI Responses API.
|
|
7
|
+
# Provides configuration helpers and result parsing for:
|
|
8
|
+
# - Web Search
|
|
9
|
+
# - File Search
|
|
10
|
+
# - Code Interpreter
|
|
11
|
+
# - Image Generation
|
|
12
|
+
# - MCP (Model Context Protocol)
|
|
13
|
+
module BuiltInTools
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Web Search tool configuration
|
|
17
|
+
# @param search_context_size [String, nil] 'low', 'medium', or 'high'
|
|
18
|
+
# @param user_location [Hash, nil] { type: 'approximate', city: '...', country: '...' }
|
|
19
|
+
def web_search(search_context_size: nil, user_location: nil)
|
|
20
|
+
tool = { type: 'web_search_preview' }
|
|
21
|
+
tool[:search_context_size] = search_context_size if search_context_size
|
|
22
|
+
tool[:user_location] = user_location if user_location
|
|
23
|
+
tool
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# File Search tool configuration
|
|
27
|
+
# @param vector_store_ids [Array<String>] IDs of vector stores to search
|
|
28
|
+
# @param max_num_results [Integer, nil] Maximum results to return
|
|
29
|
+
# @param ranking_options [Hash, nil] Ranking configuration
|
|
30
|
+
def file_search(vector_store_ids:, max_num_results: nil, ranking_options: nil)
|
|
31
|
+
tool = {
|
|
32
|
+
type: 'file_search',
|
|
33
|
+
vector_store_ids: Array(vector_store_ids)
|
|
34
|
+
}
|
|
35
|
+
tool[:max_num_results] = max_num_results if max_num_results
|
|
36
|
+
tool[:ranking_options] = ranking_options if ranking_options
|
|
37
|
+
tool
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Code Interpreter tool configuration
|
|
41
|
+
# @param container_type [String] 'auto' or specific container type
|
|
42
|
+
def code_interpreter(container_type: 'auto')
|
|
43
|
+
{
|
|
44
|
+
type: 'code_interpreter',
|
|
45
|
+
container: { type: container_type }
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Image Generation tool configuration
|
|
50
|
+
# @param partial_images [Integer, nil] Number of partial images during streaming
|
|
51
|
+
def image_generation(partial_images: nil)
|
|
52
|
+
tool = { type: 'image_generation' }
|
|
53
|
+
tool[:partial_images] = partial_images if partial_images
|
|
54
|
+
tool
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# MCP (Model Context Protocol) tool configuration
|
|
58
|
+
# @param server_label [String] Label for the MCP server
|
|
59
|
+
# @param server_url [String] URL of the MCP server
|
|
60
|
+
# @param require_approval [String] 'never', 'always', or specific tool patterns
|
|
61
|
+
# @param allowed_tools [Array<String>, nil] List of allowed tool names
|
|
62
|
+
# @param headers [Hash, nil] Additional headers for the MCP server
|
|
63
|
+
def mcp(server_label:, server_url:, require_approval: 'never', allowed_tools: nil, headers: nil)
|
|
64
|
+
tool = {
|
|
65
|
+
type: 'mcp',
|
|
66
|
+
server_label: server_label,
|
|
67
|
+
server_url: server_url,
|
|
68
|
+
require_approval: require_approval
|
|
69
|
+
}
|
|
70
|
+
tool[:allowed_tools] = allowed_tools if allowed_tools
|
|
71
|
+
tool[:headers] = headers if headers
|
|
72
|
+
tool
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Computer Use tool configuration (preview)
|
|
76
|
+
# @param display_width [Integer] Display width in pixels
|
|
77
|
+
# @param display_height [Integer] Display height in pixels
|
|
78
|
+
# @param environment [String] 'browser' or 'mac' or 'windows' or 'ubuntu'
|
|
79
|
+
def computer_use(display_width:, display_height:, environment: 'browser')
|
|
80
|
+
{
|
|
81
|
+
type: 'computer_use_preview',
|
|
82
|
+
display_width: display_width,
|
|
83
|
+
display_height: display_height,
|
|
84
|
+
environment: environment
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Parse web search results from output
|
|
89
|
+
# @param output [Array] Response output array
|
|
90
|
+
# @return [Array<Hash>] Parsed search results with citations
|
|
91
|
+
def parse_web_search_results(output)
|
|
92
|
+
output
|
|
93
|
+
.select { |item| item['type'] == 'web_search_call' }
|
|
94
|
+
.map do |item|
|
|
95
|
+
{
|
|
96
|
+
id: item['id'],
|
|
97
|
+
status: item['status'],
|
|
98
|
+
results: parse_citations(item)
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parse file search results from output
|
|
104
|
+
# @param output [Array] Response output array
|
|
105
|
+
# @return [Array<Hash>] Parsed file search results
|
|
106
|
+
def parse_file_search_results(output)
|
|
107
|
+
output
|
|
108
|
+
.select { |item| item['type'] == 'file_search_call' }
|
|
109
|
+
.map do |item|
|
|
110
|
+
{
|
|
111
|
+
id: item['id'],
|
|
112
|
+
status: item['status'],
|
|
113
|
+
results: item['results'] || []
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Parse code interpreter results from output
|
|
119
|
+
# @param output [Array] Response output array
|
|
120
|
+
# @return [Array<Hash>] Parsed code interpreter results
|
|
121
|
+
def parse_code_interpreter_results(output)
|
|
122
|
+
output
|
|
123
|
+
.select { |item| item['type'] == 'code_interpreter_call' }
|
|
124
|
+
.map do |item|
|
|
125
|
+
{
|
|
126
|
+
id: item['id'],
|
|
127
|
+
code: item['code'],
|
|
128
|
+
results: item['results'] || [],
|
|
129
|
+
container_id: item['container_id']
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse image generation results from output
|
|
135
|
+
# @param output [Array] Response output array
|
|
136
|
+
# @return [Array<Hash>] Parsed image generation results
|
|
137
|
+
def parse_image_generation_results(output)
|
|
138
|
+
output
|
|
139
|
+
.select { |item| item['type'] == 'image_generation_call' }
|
|
140
|
+
.map do |item|
|
|
141
|
+
{
|
|
142
|
+
id: item['id'],
|
|
143
|
+
status: item['status'],
|
|
144
|
+
result: item['result']
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Extract all citations from message content
|
|
150
|
+
# @param content [Array] Message content array
|
|
151
|
+
# @return [Array<Hash>] All citations/annotations
|
|
152
|
+
def extract_citations(content)
|
|
153
|
+
return [] unless content.is_a?(Array)
|
|
154
|
+
|
|
155
|
+
content
|
|
156
|
+
.select { |c| c['type'] == 'output_text' }
|
|
157
|
+
.flat_map { |c| c['annotations'] || [] }
|
|
158
|
+
.map do |annotation|
|
|
159
|
+
{
|
|
160
|
+
type: annotation['type'],
|
|
161
|
+
text: annotation['text'],
|
|
162
|
+
url: annotation['url'],
|
|
163
|
+
title: annotation['title'],
|
|
164
|
+
start_index: annotation['start_index'],
|
|
165
|
+
end_index: annotation['end_index']
|
|
166
|
+
}.compact
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private_class_method def parse_citations(item)
|
|
171
|
+
return [] unless item['results']
|
|
172
|
+
|
|
173
|
+
item['results'].map do |result|
|
|
174
|
+
{
|
|
175
|
+
url: result['url'],
|
|
176
|
+
title: result['title'],
|
|
177
|
+
snippet: result['snippet']
|
|
178
|
+
}.compact
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|