ruby-spacy 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bf4e97474302a2fffe019227e5afbb26c815036d13ec75bae6349f7186dff31
4
- data.tar.gz: 2cd4012d9545d0a5ad9d2ebb03d07e40ae54acdfb18f0c9132c8848cce5cfd8e
3
+ metadata.gz: b759c507a1cad8e726bd36ad39e4539894395407e2ac5d64a31b7b907a4e34d8
4
+ data.tar.gz: 0f02f9b8f6bcf39a49b50b1934905e72c0c13bb9652eee8fbaad3a3b7b58b7e7
5
5
  SHA512:
6
- metadata.gz: 208f43e0efa0334ec159fb56e836f315bd5469a3768e77e0f6b7fa3ec936e722546e0323cc7bf5790393cc2fc52318e172355b821b6f3c83268de3cbf35ba139
7
- data.tar.gz: 753515dcb7fb3e44ea2c4419e83baf9e0e8f9c6810a4f65cbfc3a631414b33019b5a0b4915c9a87edd8ac34fb8aac48673954c092c50e2e1cc152713bde48466
6
+ metadata.gz: 5b7d8078148e05c860f3ef698b5103cfca533fabfac5001c5afd12419efaae21d6492aa22fcebfba9d9d22bf8701fe2c6a21eb1ee879344cdbd5d897af9b8853
7
+ data.tar.gz: 4ffab4be8e0338e86c34aad0285cd9471a3d5c570fbf48b4214cdbe5ca5cc01c7d251d4f6d5edc778de2185d89fc3858e094c363499c81f89d5c3a61ab8859c2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.5.0 - 2026-07-21
4
+ ### Added
5
+ - `Language#with_llm(provider:)` — provider-neutral block-based LLM API
6
+ supporting `:openai`, `:anthropic` (Claude), and `:ollama` (local models)
7
+ - `AnthropicHelper` / `AnthropicClient` — Anthropic Messages API support
8
+ using only net/http (default model: `claude-sonnet-5`; requires
9
+ `ANTHROPIC_API_KEY`)
10
+ - `base_url:` option for OpenAI client/helper — works with any
11
+ OpenAI-compatible server (Ollama, LM Studio, llama.cpp server, vLLM,
12
+ OpenRouter, etc.)
13
+ - `schema:` option for `chat` — Structured Outputs on both providers;
14
+ returns a validated, parsed Ruby Hash
15
+ - New examples under `examples/llm/` (Anthropic, local Ollama, and a
16
+ spaCy-vs-LLM NER comparison with structured outputs)
17
+
18
+ ### Changed
19
+ - `temperature` is now sent only when explicitly specified; if a model
20
+ rejects it, the request is retried once without it. The model-name
21
+ heuristic (`temperature_unsupported?`) has been removed, so new models
22
+ work without gem updates. Note: previously a default of 0.7 was sent to
23
+ models that supported it (also from `Doc#openai_query` /
24
+ `Doc#openai_completion`); now the API default applies unless you pass
25
+ `temperature:` yourself
26
+ - Shared HTTP layer (`LLMClientBase`) extracted from `OpenAIClient`;
27
+ no behavior change for existing OpenAI usage
28
+ - Truncated responses (`finish_reason: length` / `stop_reason: max_tokens`)
29
+ now emit a warning; with `schema:`, unparseable (e.g. truncated) JSON
30
+ returns nil with a warning instead of raising `JSON::ParserError`
31
+ - LLM error messages are printed to stderr (`Kernel#warn`) instead of
32
+ stdout, keeping stdout clean for program output
33
+ - Default model names are now public constants:
34
+ `OpenAIClient::DEFAULT_MODEL`, `OpenAIClient::DEFAULT_EMBEDDINGS_MODEL`,
35
+ `AnthropicClient::DEFAULT_MODEL`
36
+
3
37
  ## 0.4.1 - 2026-07-19
4
38
  ### Fixed
5
39
  - Gem packaging: normalize file permissions at build time so packaged files
data/README.md CHANGED
@@ -11,14 +11,15 @@
11
11
  | ✅ | Named entity recognition |
12
12
  | ✅ | Syntactic dependency visualization |
13
13
  | ✅ | Access to pre-trained word vectors |
14
- | ✅ | OpenAI Chat/Completion/Embeddings API integration |
14
+ | ✅ | LLM integration: OpenAI, Anthropic (Claude), and local models |
15
15
 
16
- Current Version: `0.4.0`
16
+ Current Version: `0.5.0`
17
17
 
18
18
  - Ruby 4.0 supported
19
19
  - spaCy 3.8 supported
20
- - OpenAI GPT-5 API integration
21
- - Block-based OpenAI API with linguistic analysis
20
+ - Multi-provider LLM API: OpenAI, Anthropic (Claude), and local models via Ollama or any OpenAI-compatible server
21
+ - Structured outputs (JSON Schema) support
22
+ - Block-based LLM API with linguistic analysis
22
23
 
23
24
  ## Installation of Prerequisites
24
25
 
@@ -588,7 +589,9 @@ end
588
589
 
589
590
  ## OpenAI API Integration
590
591
 
591
- > ⚠️ This feature requires GPT-5 series models. Please refer to OpenAI's [API reference](https://platform.openai.com/docs/api-reference) for details. Note: GPT-5 models do not support the `temperature` parameter.
592
+ > ⚠️ This feature requires GPT-5 series models. Please refer to OpenAI's [API reference](https://platform.openai.com/docs/api-reference) for details.
593
+
594
+ > ℹ️ The `temperature` parameter is sent to the API only when you specify it explicitly. If the model does not support it (e.g., GPT-5 series and o-series models), the request is automatically retried once without it — no per-model configuration is needed.
592
595
 
593
596
  Easily leverage GPT models within ruby-spacy by using an OpenAI API key. When constructing prompts for the `Doc::openai_query` method, you can incorporate the following token properties of the document. These properties are retrieved through tool calls (made internally by GPT when necessary) and seamlessly integrated into your prompt. The available properties include:
594
597
 
@@ -903,6 +906,72 @@ nlp.with_openai do |ai|
903
906
  end
904
907
  ```
905
908
 
909
+ ### Multi-provider LLM API
910
+
911
+ The `Language#with_llm` block API generalizes `with_openai` to multiple providers. The helper yielded to the block has the same `chat` interface for every provider.
912
+
913
+ **Anthropic (Claude):**
914
+
915
+ ```ruby
916
+ # Requires the ANTHROPIC_API_KEY environment variable
917
+ # Default model: claude-sonnet-5 (override with model: "...")
918
+ nlp.with_llm(provider: :anthropic) do |ai|
919
+ result = ai.chat(
920
+ system: "You are a linguistic analyst.",
921
+ user: doc.linguistic_summary
922
+ )
923
+ puts result
924
+ end
925
+ ```
926
+
927
+ **Local models via Ollama (no API key needed):**
928
+
929
+ ```ruby
930
+ # Requires a running Ollama server: https://ollama.com
931
+ nlp.with_llm(provider: :ollama, model: "llama3.2") do |ai|
932
+ puts ai.chat(user: doc.linguistic_summary)
933
+ end
934
+ ```
935
+
936
+ Any other OpenAI-compatible server (LM Studio, llama.cpp server, vLLM, OpenRouter, etc.) works via `base_url:`:
937
+
938
+ ```ruby
939
+ nlp.with_llm(provider: :openai, base_url: "http://localhost:1234/v1",
940
+ access_token: "not-needed", model: "your-model") do |ai|
941
+ puts ai.chat(user: "Hello!")
942
+ end
943
+ ```
944
+
945
+ **Structured outputs (`schema:`):** pass a JSON Schema and receive a validated, parsed Ruby Hash — useful for comparing LLM output with spaCy's analysis programmatically. Works with both `:openai` and `:anthropic`. Objects in the schema must set `additionalProperties: false`.
946
+
947
+ ```ruby
948
+ schema = {
949
+ type: "object",
950
+ properties: {
951
+ entities: {
952
+ type: "array",
953
+ items: {
954
+ type: "object",
955
+ properties: { text: { type: "string" }, label: { type: "string" } },
956
+ required: %w[text label],
957
+ additionalProperties: false
958
+ }
959
+ }
960
+ },
961
+ required: ["entities"],
962
+ additionalProperties: false
963
+ }
964
+
965
+ result = nlp.with_llm(provider: :openai) do |ai|
966
+ ai.chat(system: "Extract named entities.", user: doc.text, schema: schema)
967
+ end
968
+ result["entities"].each { |ent| puts "#{ent["text"]} (#{ent["label"]})" }
969
+ ```
970
+
971
+ **Note on `temperature`:** for all providers, `temperature` is omitted from requests unless you pass it explicitly (`ai.chat(user: "...", temperature: 0.3)`). Models that reject the parameter (e.g., GPT-5 series, o-series, and current Claude models) are automatically retried once without it, so any model works without per-model configuration.
972
+
973
+ See `examples/llm/` for complete scripts, including a spaCy-vs-LLM NER comparison.
974
+
906
975
  ## Advanced Usage
907
976
 
908
977
  ### Setting a Timeout
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # add path to ruby-spacy lib to load path
4
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
5
+
6
+ require "ruby-spacy"
7
+
8
+ # Requires the ANTHROPIC_API_KEY environment variable
9
+ # Default model: claude-sonnet-5 (override with model: "...")
10
+
11
+ nlp = Spacy::Language.new("en_core_web_sm")
12
+ doc = nlp.read("The cat sat on the mat while the dog slept under the table.")
13
+
14
+ result = nlp.with_llm(provider: :anthropic) do |ai|
15
+ ai.chat(
16
+ system: "You are a linguistics tutor. Explain the syntactic structure briefly.",
17
+ user: doc.linguistic_summary
18
+ )
19
+ end
20
+
21
+ puts result
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # add path to ruby-spacy lib to load path
4
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
5
+
6
+ require "ruby-spacy"
7
+
8
+ # Requires a running Ollama server (https://ollama.com):
9
+ # ollama pull llama3.2
10
+ # ollama serve
11
+ # No API key is needed.
12
+
13
+ nlp = Spacy::Language.new("en_core_web_sm")
14
+ doc = nlp.read("I sat on the bank of the river.")
15
+
16
+ result = nlp.with_llm(provider: :ollama, model: "llama3.2") do |ai|
17
+ ai.chat(
18
+ system: "Identify the meaning of the word 'bank' in one short sentence.",
19
+ user: doc.linguistic_summary
20
+ )
21
+ end
22
+
23
+ puts result
24
+
25
+ # Any OpenAI-compatible server works the same way via base_url, e.g.:
26
+ # nlp.with_llm(provider: :openai, base_url: "http://localhost:1234/v1",
27
+ # access_token: "not-needed", model: "your-model") { |ai| ... }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # add path to ruby-spacy lib to load path
4
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
5
+
6
+ require "ruby-spacy"
7
+
8
+ # Compares spaCy's named entity recognition with an LLM's, using structured
9
+ # outputs so the LLM's answer comes back as a validated Ruby Hash.
10
+ # Requires the OPENAI_API_KEY environment variable
11
+ # (or swap in provider: :anthropic with ANTHROPIC_API_KEY).
12
+
13
+ nlp = Spacy::Language.new("en_core_web_sm")
14
+ text = "Mr. Best flew to New York on Saturday morning to meet Tim Cook of Apple."
15
+ doc = nlp.read(text)
16
+
17
+ schema = {
18
+ type: "object",
19
+ properties: {
20
+ entities: {
21
+ type: "array",
22
+ items: {
23
+ type: "object",
24
+ properties: {
25
+ text: { type: "string" },
26
+ label: { type: "string", enum: %w[PERSON ORG GPE DATE TIME OTHER] }
27
+ },
28
+ required: %w[text label],
29
+ additionalProperties: false
30
+ }
31
+ }
32
+ },
33
+ required: ["entities"],
34
+ additionalProperties: false
35
+ }
36
+
37
+ llm_result = nlp.with_llm(provider: :openai) do |ai|
38
+ ai.chat(
39
+ system: "Extract all named entities from the user's text.",
40
+ user: text,
41
+ schema: schema
42
+ )
43
+ end
44
+
45
+ puts "spaCy entities:"
46
+ doc.ents.each { |ent| puts " #{ent.text} (#{ent.label})" }
47
+
48
+ puts "\nLLM entities:"
49
+ llm_result["entities"].each { |ent| puts " #{ent["text"]} (#{ent["label"]})" }
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "llm_client_base"
4
+
5
+ module Spacy
6
+ # A lightweight Anthropic Messages API client without external dependencies.
7
+ class AnthropicClient < LLMClientBase
8
+ # Default Anthropic API endpoint
9
+ API_ENDPOINT = "https://api.anthropic.com/v1"
10
+ # Messages API version header value
11
+ ANTHROPIC_VERSION = "2023-06-01"
12
+ # Default model for chat requests
13
+ DEFAULT_MODEL = "claude-sonnet-5"
14
+ # (see LLMClientBase::APIError)
15
+ APIError = LLMClientBase::APIError
16
+
17
+ # @param access_token [String] Anthropic API key
18
+ # @param timeout [Integer] request timeout in seconds
19
+ # @param base_url [String] API endpoint override
20
+ def initialize(access_token:, timeout: DEFAULT_TIMEOUT, base_url: API_ENDPOINT)
21
+ super(base_url: base_url, timeout: timeout)
22
+ @access_token = access_token
23
+ end
24
+
25
+ # Sends a Messages API request.
26
+ #
27
+ # The +temperature+ parameter is sent only when explicitly given. Current
28
+ # Claude models (Opus 4.7+) reject it; the request is then retried once
29
+ # without it.
30
+ #
31
+ # @param model [String] The model to use (e.g., "claude-sonnet-5")
32
+ # @param messages [Array<Hash>] The conversation messages
33
+ # @param system [String, nil] System prompt (top-level parameter)
34
+ # @param max_tokens [Integer] Maximum tokens in the response (required by the API)
35
+ # @param temperature [Float, nil] Sampling temperature (omitted when nil)
36
+ # @param output_config [Hash, nil] Output configuration, e.g.
37
+ # { format: { type: "json_schema", schema: {...} } } for structured outputs
38
+ # @return [Hash] The API response
39
+ def messages(model:, messages:, system: nil, max_tokens: 1000, temperature: nil, output_config: nil)
40
+ body = {
41
+ model: model,
42
+ max_tokens: max_tokens,
43
+ messages: messages
44
+ }
45
+ body[:system] = system if system
46
+ body[:temperature] = temperature unless temperature.nil?
47
+ body[:output_config] = output_config if output_config
48
+
49
+ post("/messages", body)
50
+ rescue APIError => e
51
+ raise unless body.key?(:temperature) && e.status_code == 400 && e.message.match?(/temperature/i)
52
+
53
+ warn "Warning: model does not support the temperature parameter; retrying without it"
54
+ post("/messages", body.reject { |key, _| key == :temperature })
55
+ end
56
+
57
+ private
58
+
59
+ def request_headers
60
+ {
61
+ "x-api-key" => @access_token,
62
+ "anthropic-version" => ANTHROPIC_VERSION
63
+ }
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spacy
4
+ # A helper class for Anthropic (Claude) API interactions, designed to work
5
+ # with spaCy's linguistic analysis via the block-based {Language#with_llm} API.
6
+ #
7
+ # Provides the same chat interface as {OpenAIHelper}, so code written against
8
+ # one provider works with the other. Anthropic does not offer an embeddings
9
+ # API; use the :openai or :ollama provider for embeddings.
10
+ #
11
+ # @example Basic usage with linguistic_summary
12
+ # nlp = Spacy::Language.new("en_core_web_sm")
13
+ # nlp.with_llm(provider: :anthropic) do |ai|
14
+ # doc = nlp.read("Apple Inc. was founded by Steve Jobs.")
15
+ # ai.chat(system: "Analyze the linguistic data.", user: doc.linguistic_summary)
16
+ # end
17
+ class AnthropicHelper
18
+ # @return [String] the default model for chat requests
19
+ attr_reader :model
20
+
21
+ # Creates a new AnthropicHelper instance.
22
+ # @param access_token [String, nil] Anthropic API key (defaults to ANTHROPIC_API_KEY env var)
23
+ # @param model [String] the default model for chat requests
24
+ # @param max_tokens [Integer] default maximum tokens in responses
25
+ # @param temperature [Float, nil] default sampling temperature (omitted from
26
+ # requests when nil; models that reject it are retried without it)
27
+ # @param base_url [String, nil] API endpoint override
28
+ def initialize(access_token: nil, model: AnthropicClient::DEFAULT_MODEL,
29
+ max_tokens: 1000, temperature: nil, base_url: nil)
30
+ @access_token = access_token || ENV["ANTHROPIC_API_KEY"]
31
+ raise "Error: ANTHROPIC_API_KEY is not set" unless @access_token
32
+
33
+ @model = model
34
+ @default_max_tokens = max_tokens
35
+ @default_temperature = temperature
36
+ @client = AnthropicClient.new(access_token: @access_token,
37
+ base_url: base_url || AnthropicClient::API_ENDPOINT)
38
+ end
39
+
40
+ # Sends a Messages API request.
41
+ #
42
+ # Provides convenient `system:` and `user:` keyword arguments as shortcuts.
43
+ # For multi-turn conversations, pass a full `messages:` array directly
44
+ # (the system prompt stays a separate `system:` argument — Anthropic takes
45
+ # it as a top-level parameter, not a message role).
46
+ #
47
+ # @param system [String, nil] system prompt
48
+ # @param user [String, nil] user message content (shortcut)
49
+ # @param messages [Array<Hash>, nil] full message array (overrides user:)
50
+ # @param model [String, nil] model override (defaults to instance model)
51
+ # @param max_tokens [Integer, nil] token limit override
52
+ # @param temperature [Float, nil] temperature override
53
+ # @param schema [Hash, nil] JSON Schema for structured outputs; when given,
54
+ # the model output is constrained to the schema and the parsed Hash is
55
+ # returned. Objects in the schema must set `additionalProperties: false`.
56
+ # @param raw [Boolean] if true, returns the full API response Hash instead of text
57
+ # @return [String, Hash, nil] the response text, parsed Hash (if schema:),
58
+ # full response Hash (if raw:), or nil on API error, refusal, or when
59
+ # the schema output cannot be parsed as JSON (e.g., truncated by the
60
+ # token limit)
61
+ def chat(system: nil, user: nil, messages: nil,
62
+ model: nil, max_tokens: nil, temperature: nil,
63
+ schema: nil, raw: false)
64
+ msgs = messages || (user ? [{ role: "user", content: user }] : [])
65
+ raise ArgumentError, "No messages provided. Use user:/messages:" if msgs.empty?
66
+
67
+ output_config = schema ? { format: { type: "json_schema", schema: schema } } : nil
68
+
69
+ response = @client.messages(
70
+ model: model || @model,
71
+ messages: msgs,
72
+ system: system,
73
+ max_tokens: max_tokens || @default_max_tokens,
74
+ temperature: temperature || @default_temperature,
75
+ output_config: output_config
76
+ )
77
+
78
+ return response if raw
79
+
80
+ # Safety classifiers can decline a request with HTTP 200 and an empty
81
+ # or partial content array.
82
+ if response["stop_reason"] == "refusal"
83
+ warn "Error: Anthropic API declined the request (stop_reason: refusal)"
84
+ return nil
85
+ end
86
+
87
+ if response["stop_reason"] == "max_tokens"
88
+ warn "Warning: response was truncated (stop_reason: max_tokens); consider increasing max_tokens"
89
+ end
90
+
91
+ text = Array(response["content"])
92
+ .select { |block| block["type"] == "text" }
93
+ .map { |block| block["text"] }
94
+ .join
95
+ schema ? parse_json_content(text) : text
96
+ rescue AnthropicClient::APIError => e
97
+ warn "Error: Anthropic API call failed - #{e.message}"
98
+ nil
99
+ end
100
+
101
+ # Anthropic does not provide an embeddings API.
102
+ # @raise [NotImplementedError] always
103
+ def embeddings(*)
104
+ raise NotImplementedError,
105
+ "Anthropic does not provide an embeddings API. " \
106
+ "Use with_llm(provider: :openai) or a local OpenAI-compatible server for embeddings."
107
+ end
108
+
109
+ private
110
+
111
+ def parse_json_content(text)
112
+ JSON.parse(text)
113
+ rescue JSON::ParserError
114
+ warn "Error: response is not valid JSON (possibly truncated output); returning nil"
115
+ nil
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "uri"
6
+ require "json"
7
+
8
+ module Spacy
9
+ # Shared HTTP layer for LLM API clients ({OpenAIClient}, {AnthropicClient}).
10
+ # Implements JSON POST requests with retry on rate limits (429/529) and
11
+ # transient network errors, using only net/http (no external dependencies).
12
+ class LLMClientBase
13
+ # Default request timeout in seconds
14
+ DEFAULT_TIMEOUT = 120
15
+ # Maximum number of retries for rate-limited requests and network errors
16
+ MAX_RETRIES = 3
17
+ # Base delay in seconds for exponential backoff between retries
18
+ BASE_RETRY_DELAY = 1
19
+
20
+ # Raised when an LLM API request fails after retries; carries the HTTP
21
+ # status code and the parsed response body when available.
22
+ class APIError < StandardError
23
+ attr_reader :status_code, :response_body
24
+
25
+ def initialize(message, status_code: nil, response_body: nil)
26
+ @status_code = status_code
27
+ @response_body = response_body
28
+ super(message)
29
+ end
30
+ end
31
+
32
+ def initialize(base_url:, timeout: DEFAULT_TIMEOUT)
33
+ @base_url = base_url.sub(%r{/\z}, "")
34
+ @timeout = timeout
35
+ end
36
+
37
+ private
38
+
39
+ # Subclasses provide authentication and API-specific headers.
40
+ # @return [Hash{String => String}]
41
+ def request_headers
42
+ {}
43
+ end
44
+
45
+ # Creates a certificate store with system CA certificates but without CRL checking.
46
+ # This avoids "unable to get certificate CRL" errors on some systems.
47
+ def default_cert_store
48
+ store = OpenSSL::X509::Store.new
49
+ store.set_default_paths
50
+ store
51
+ end
52
+
53
+ def post(path, body)
54
+ uri = URI.parse("#{@base_url}#{path}")
55
+ retries = 0
56
+
57
+ loop do
58
+ begin
59
+ http = Net::HTTP.new(uri.host, uri.port)
60
+ if uri.scheme == "https"
61
+ http.use_ssl = true
62
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
63
+ http.cert_store = default_cert_store
64
+ end
65
+ http.open_timeout = @timeout
66
+ http.read_timeout = @timeout
67
+
68
+ request = Net::HTTP::Post.new(uri.request_uri)
69
+ request["Content-Type"] = "application/json"
70
+ request_headers.each { |key, value| request[key] = value }
71
+ request.body = body.to_json
72
+
73
+ response = http.request(request)
74
+
75
+ # Handle rate limiting (429) and overload (529) before general response handling
76
+ if [429, 529].include?(response.code.to_i)
77
+ retries += 1
78
+ if retries <= MAX_RETRIES
79
+ retry_after = response["Retry-After"]&.to_f
80
+ delay = retry_after || (BASE_RETRY_DELAY * (2**(retries - 1)) + rand * 0.5)
81
+ sleep delay
82
+ next
83
+ end
84
+ raise APIError.new("Rate limited after #{MAX_RETRIES} retries",
85
+ status_code: response.code.to_i, response_body: response.body)
86
+ end
87
+
88
+ return handle_response(response)
89
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
90
+ retries += 1
91
+ if retries <= MAX_RETRIES
92
+ delay = BASE_RETRY_DELAY * (2**(retries - 1)) + rand * 0.5
93
+ sleep delay
94
+ next
95
+ end
96
+ raise APIError.new("Network error after #{MAX_RETRIES} retries: #{e.message}")
97
+ end
98
+ end
99
+ end
100
+
101
+ def handle_response(response)
102
+ body = JSON.parse(response.body)
103
+
104
+ case response.code.to_i
105
+ when 200
106
+ body
107
+ when 400..499
108
+ error_message = body.dig("error", "message") || "Client error"
109
+ raise APIError.new(error_message, status_code: response.code.to_i, response_body: body)
110
+ when 500..599
111
+ error_message = body.dig("error", "message") || "Server error"
112
+ raise APIError.new(error_message, status_code: response.code.to_i, response_body: body)
113
+ else
114
+ raise APIError.new("Unexpected response: #{response.code}", status_code: response.code.to_i, response_body: body)
115
+ end
116
+ rescue JSON::ParserError
117
+ raise APIError.new("Invalid JSON response", status_code: response.code.to_i, response_body: response.body)
118
+ end
119
+ end
120
+ end
@@ -1,42 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "openssl"
5
- require "uri"
6
- require "json"
3
+ require_relative "llm_client_base"
7
4
 
8
5
  module Spacy
9
- # A lightweight OpenAI API client with tools support for GPT-5 series models.
6
+ # A lightweight OpenAI API client with tools support.
10
7
  # This client implements the chat completions and embeddings endpoints
11
8
  # without external dependencies.
12
- class OpenAIClient
9
+ #
10
+ # A custom +base_url+ makes it work with any OpenAI-compatible server
11
+ # (Ollama, LM Studio, llama.cpp server, vLLM, OpenRouter, etc.).
12
+ class OpenAIClient < LLMClientBase
13
+ # Default OpenAI API endpoint
13
14
  API_ENDPOINT = "https://api.openai.com/v1"
14
- DEFAULT_TIMEOUT = 120
15
- MAX_RETRIES = 3
16
- BASE_RETRY_DELAY = 1
17
-
18
- class APIError < StandardError
19
- attr_reader :status_code, :response_body
20
-
21
- def initialize(message, status_code: nil, response_body: nil)
22
- @status_code = status_code
23
- @response_body = response_body
24
- super(message)
25
- end
26
- end
27
-
28
- def initialize(access_token:, timeout: DEFAULT_TIMEOUT)
15
+ # Default model for chat requests
16
+ DEFAULT_MODEL = "gpt-5-mini"
17
+ # Default model for embeddings requests
18
+ DEFAULT_EMBEDDINGS_MODEL = "text-embedding-3-small"
19
+ # (see LLMClientBase::APIError)
20
+ APIError = LLMClientBase::APIError
21
+
22
+ # @param access_token [String] API key ("ollama" or any placeholder for local servers)
23
+ # @param timeout [Integer] request timeout in seconds
24
+ # @param base_url [String] API endpoint; include the version path
25
+ # (e.g., "http://localhost:11434/v1" for Ollama)
26
+ def initialize(access_token:, timeout: DEFAULT_TIMEOUT, base_url: API_ENDPOINT)
27
+ super(base_url: base_url, timeout: timeout)
29
28
  @access_token = access_token
30
- @timeout = timeout
31
29
  end
32
30
 
33
31
  # Sends a chat completion request with optional tools support.
34
- # Note: GPT-5 series and o-series models do not support the temperature parameter.
32
+ #
33
+ # The +temperature+ parameter is sent only when explicitly given. If the
34
+ # model rejects it (e.g., GPT-5 series and o-series models), the request
35
+ # is retried once without it.
35
36
  #
36
37
  # @param model [String] The model to use (e.g., "gpt-5-mini")
37
38
  # @param messages [Array<Hash>] The conversation messages
38
39
  # @param max_completion_tokens [Integer] Maximum tokens in the response
39
- # @param temperature [Float, nil] Sampling temperature (ignored for models that don't support it)
40
+ # @param temperature [Float, nil] Sampling temperature (omitted when nil)
40
41
  # @param tools [Array<Hash>, nil] Tool definitions for function calling
41
42
  # @param tool_choice [String, Hash, nil] Tool selection strategy
42
43
  # @param response_format [Hash, nil] Response format specification (e.g., { type: "json_object" })
@@ -47,11 +48,7 @@ module Spacy
47
48
  messages: messages,
48
49
  max_completion_tokens: max_completion_tokens
49
50
  }
50
-
51
- # GPT-5 series and o-series models do not support temperature parameter
52
- unless temperature_unsupported?(model)
53
- body[:temperature] = temperature || 0.7
54
- end
51
+ body[:temperature] = temperature unless temperature.nil?
55
52
 
56
53
  if tools && !tools.empty?
57
54
  body[:tools] = tools
@@ -60,16 +57,7 @@ module Spacy
60
57
 
61
58
  body[:response_format] = response_format if response_format
62
59
 
63
- post("/chat/completions", body)
64
- end
65
-
66
- # Checks if the model does not support the temperature parameter.
67
- # This includes GPT-5 series and o-series (o1, o3, o4-mini, etc.) models.
68
- # @param model [String] The model name
69
- # @return [Boolean]
70
- def temperature_unsupported?(model)
71
- name = model.to_s
72
- name.start_with?("gpt-5") || name.match?(/\Ao\d/)
60
+ post_with_temperature_fallback("/chat/completions", body)
73
61
  end
74
62
 
75
63
  # Sends an embeddings request.
@@ -90,77 +78,19 @@ module Spacy
90
78
 
91
79
  private
92
80
 
93
- # Creates a certificate store with system CA certificates but without CRL checking.
94
- # This avoids "unable to get certificate CRL" errors on some systems.
95
- def default_cert_store
96
- store = OpenSSL::X509::Store.new
97
- store.set_default_paths
98
- store
81
+ def request_headers
82
+ { "Authorization" => "Bearer #{@access_token}" }
99
83
  end
100
84
 
101
- def post(path, body)
102
- uri = URI.parse("#{API_ENDPOINT}#{path}")
103
- retries = 0
85
+ # Models that do not accept the temperature parameter reject it with a 400;
86
+ # retry once without it so callers need no per-model knowledge.
87
+ def post_with_temperature_fallback(path, body)
88
+ post(path, body)
89
+ rescue APIError => e
90
+ raise unless body.key?(:temperature) && e.status_code == 400 && e.message.match?(/temperature/i)
104
91
 
105
- loop do
106
- begin
107
- http = Net::HTTP.new(uri.host, uri.port)
108
- http.use_ssl = true
109
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
110
- http.cert_store = default_cert_store
111
- http.open_timeout = @timeout
112
- http.read_timeout = @timeout
113
-
114
- request = Net::HTTP::Post.new(uri.path)
115
- request["Content-Type"] = "application/json"
116
- request["Authorization"] = "Bearer #{@access_token}"
117
- request.body = body.to_json
118
-
119
- response = http.request(request)
120
-
121
- # Handle 429 rate limiting before general response handling
122
- if response.code.to_i == 429
123
- retries += 1
124
- if retries <= MAX_RETRIES
125
- retry_after = response["Retry-After"]&.to_f
126
- delay = retry_after || (BASE_RETRY_DELAY * (2**(retries - 1)) + rand * 0.5)
127
- sleep delay
128
- next
129
- end
130
- raise APIError.new("Rate limited after #{MAX_RETRIES} retries",
131
- status_code: 429, response_body: response.body)
132
- end
133
-
134
- return handle_response(response)
135
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
136
- retries += 1
137
- if retries <= MAX_RETRIES
138
- delay = BASE_RETRY_DELAY * (2**(retries - 1)) + rand * 0.5
139
- sleep delay
140
- next
141
- end
142
- raise APIError.new("Network error after #{MAX_RETRIES} retries: #{e.message}")
143
- end
144
- end
145
- end
146
-
147
- def handle_response(response)
148
- body = JSON.parse(response.body)
149
-
150
- case response.code.to_i
151
- when 200
152
- body
153
- when 400..499
154
- error_message = body.dig("error", "message") || "Client error"
155
- raise APIError.new(error_message, status_code: response.code.to_i, response_body: body)
156
- when 500..599
157
- error_message = body.dig("error", "message") || "Server error"
158
- raise APIError.new(error_message, status_code: response.code.to_i, response_body: body)
159
- else
160
- raise APIError.new("Unexpected response: #{response.code}", status_code: response.code.to_i, response_body: body)
161
- end
162
- rescue JSON::ParserError
163
- raise APIError.new("Invalid JSON response", status_code: response.code.to_i, response_body: response.body)
92
+ warn "Warning: model does not support the temperature parameter; retrying without it"
93
+ post(path, body.reject { |key, _| key == :temperature })
164
94
  end
165
95
  end
166
96
  end
@@ -2,7 +2,11 @@
2
2
 
3
3
  module Spacy
4
4
  # A helper class for OpenAI API interactions, designed to work with spaCy's
5
- # linguistic analysis via the block-based {Language#with_openai} API.
5
+ # linguistic analysis via the block-based {Language#with_llm} /
6
+ # {Language#with_openai} API.
7
+ #
8
+ # With a custom +base_url+, this helper also works with any OpenAI-compatible
9
+ # server such as Ollama, LM Studio, llama.cpp server, or vLLM.
6
10
  #
7
11
  # @example Basic usage with linguistic_summary
8
12
  # nlp = Spacy::Language.new("en_core_web_sm")
@@ -10,6 +14,11 @@ module Spacy
10
14
  # doc = nlp.read("Apple Inc. was founded by Steve Jobs.")
11
15
  # ai.chat(system: "Analyze the linguistic data.", user: doc.linguistic_summary)
12
16
  # end
17
+ #
18
+ # @example Local model via Ollama
19
+ # nlp.with_llm(provider: :ollama, model: "llama3.2") do |ai|
20
+ # ai.chat(user: "Say hello.")
21
+ # end
13
22
  class OpenAIHelper
14
23
  # @return [String] the default model for chat requests
15
24
  attr_reader :model
@@ -18,19 +27,25 @@ module Spacy
18
27
  # @param access_token [String, nil] OpenAI API key (defaults to OPENAI_API_KEY env var)
19
28
  # @param model [String] the default model for chat requests
20
29
  # @param max_completion_tokens [Integer] default maximum tokens in responses
21
- # @param temperature [Float] default sampling temperature
22
- def initialize(access_token: nil, model: "gpt-5-mini",
23
- max_completion_tokens: 1000, temperature: 0.7)
30
+ # @param max_tokens [Integer, nil] alias for max_completion_tokens (takes precedence)
31
+ # @param temperature [Float, nil] default sampling temperature (omitted from
32
+ # requests when nil; models that reject it are retried without it)
33
+ # @param base_url [String, nil] OpenAI-compatible API endpoint
34
+ # (e.g., "http://localhost:11434/v1" for Ollama)
35
+ def initialize(access_token: nil, model: OpenAIClient::DEFAULT_MODEL,
36
+ max_completion_tokens: 1000, max_tokens: nil,
37
+ temperature: nil, base_url: nil)
24
38
  @access_token = access_token || ENV["OPENAI_API_KEY"]
25
39
  raise "Error: OPENAI_API_KEY is not set" unless @access_token
26
40
 
27
41
  @model = model
28
- @default_max_completion_tokens = max_completion_tokens
42
+ @default_max_completion_tokens = max_tokens || max_completion_tokens
29
43
  @default_temperature = temperature
30
- @client = OpenAIClient.new(access_token: @access_token)
44
+ @client = OpenAIClient.new(access_token: @access_token,
45
+ base_url: base_url || OpenAIClient::API_ENDPOINT)
31
46
  end
32
47
 
33
- # Sends a chat completion request to OpenAI.
48
+ # Sends a chat completion request.
34
49
  #
35
50
  # Provides convenient `system:` and `user:` keyword arguments as shortcuts
36
51
  # for building simple message arrays. For more complex conversations, pass
@@ -41,41 +56,63 @@ module Spacy
41
56
  # @param messages [Array<Hash>, nil] full message array (overrides system:/user:)
42
57
  # @param model [String, nil] model override (defaults to instance model)
43
58
  # @param max_completion_tokens [Integer, nil] token limit override
59
+ # @param max_tokens [Integer, nil] alias for max_completion_tokens (takes precedence)
44
60
  # @param temperature [Float, nil] temperature override
45
61
  # @param response_format [Hash, nil] response format (e.g., { type: "json_object" })
62
+ # @param schema [Hash, nil] JSON Schema for structured outputs; when given,
63
+ # the model output is constrained to the schema and the parsed Hash is
64
+ # returned. Objects in the schema must set `additionalProperties: false`
65
+ # and list all properties in `required`.
46
66
  # @param raw [Boolean] if true, returns the full API response Hash instead of text
47
- # @return [String, Hash, nil] the response text, full response Hash (if raw:), or nil on error
67
+ # @return [String, Hash, nil] the response text, parsed Hash (if schema:),
68
+ # full response Hash (if raw:), or nil on API error or when the schema
69
+ # output cannot be parsed as JSON (e.g., truncated by the token limit)
48
70
  def chat(system: nil, user: nil, messages: nil,
49
- model: nil, max_completion_tokens: nil,
50
- temperature: nil, response_format: nil, raw: false)
71
+ model: nil, max_completion_tokens: nil, max_tokens: nil,
72
+ temperature: nil, response_format: nil, schema: nil, raw: false)
51
73
  msgs = messages || build_messages(system: system, user: user)
52
74
  raise ArgumentError, "No messages provided. Use system:/user: or messages:" if msgs.empty?
53
75
 
76
+ if schema
77
+ response_format = {
78
+ type: "json_schema",
79
+ json_schema: { name: "response", strict: true, schema: schema }
80
+ }
81
+ end
82
+
54
83
  response = @client.chat(
55
84
  model: model || @model,
56
85
  messages: msgs,
57
- max_completion_tokens: max_completion_tokens || @default_max_completion_tokens,
86
+ max_completion_tokens: max_tokens || max_completion_tokens || @default_max_completion_tokens,
58
87
  temperature: temperature || @default_temperature,
59
88
  response_format: response_format
60
89
  )
61
90
 
62
- raw ? response : response.dig("choices", 0, "message", "content")
91
+ return response if raw
92
+
93
+ choice = response.dig("choices", 0)
94
+ if choice&.dig("finish_reason") == "length"
95
+ warn "Warning: response was truncated (finish_reason: length); consider increasing max_completion_tokens"
96
+ end
97
+
98
+ content = choice&.dig("message", "content")
99
+ schema && content ? parse_json_content(content) : content
63
100
  rescue OpenAIClient::APIError => e
64
- puts "Error: OpenAI API call failed - #{e.message}"
101
+ warn "Error: OpenAI API call failed - #{e.message}"
65
102
  nil
66
103
  end
67
104
 
68
- # Generates text embeddings using OpenAI's embeddings API.
105
+ # Generates text embeddings using the embeddings API.
69
106
  #
70
107
  # @param text [String] the text to embed
71
108
  # @param model [String] the embeddings model
72
109
  # @param dimensions [Integer, nil] number of dimensions (nil uses model default)
73
110
  # @return [Array<Float>, nil] the embedding vector, or nil on error
74
- def embeddings(text, model: "text-embedding-3-small", dimensions: nil)
111
+ def embeddings(text, model: OpenAIClient::DEFAULT_EMBEDDINGS_MODEL, dimensions: nil)
75
112
  response = @client.embeddings(model: model, input: text, dimensions: dimensions)
76
113
  response.dig("data", 0, "embedding")
77
114
  rescue OpenAIClient::APIError => e
78
- puts "Error: OpenAI API call failed - #{e.message}"
115
+ warn "Error: OpenAI API call failed - #{e.message}"
79
116
  nil
80
117
  end
81
118
 
@@ -87,5 +124,12 @@ module Spacy
87
124
  msgs << { role: "user", content: user } if user
88
125
  msgs
89
126
  end
127
+
128
+ def parse_json_content(text)
129
+ JSON.parse(text)
130
+ rescue JSON::ParserError
131
+ warn "Error: response is not valid JSON (possibly truncated output); returning nil"
132
+ nil
133
+ end
90
134
  end
91
135
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Spacy
4
4
  # The version number of the module
5
- VERSION = "0.4.1"
5
+ VERSION = "0.5.0"
6
6
  end
data/lib/ruby-spacy.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ruby-spacy/version"
4
+ require_relative "ruby-spacy/llm_client_base"
4
5
  require_relative "ruby-spacy/openai_client"
6
+ require_relative "ruby-spacy/anthropic_client"
5
7
  require_relative "ruby-spacy/openai_helper"
8
+ require_relative "ruby-spacy/anthropic_helper"
6
9
  require "numpy"
7
10
  require "pycall"
8
11
  require "timeout"
@@ -310,7 +313,8 @@ module Spacy
310
313
  # @param access_token [String, nil] OpenAI API key (defaults to OPENAI_API_KEY env var)
311
314
  # @param max_completion_tokens [Integer] Maximum tokens in the response
312
315
  # @param max_tokens [Integer] Alias for max_completion_tokens (deprecated, for backward compatibility)
313
- # @param temperature [Float] Sampling temperature (ignored for GPT-5 models)
316
+ # @param temperature [Float, nil] Sampling temperature (omitted from requests
317
+ # when nil; models that reject it are retried without it)
314
318
  # @param model [String] The model to use (default: gpt-5-mini)
315
319
  # @param messages [Array<Hash>] Conversation history (for recursive tool calls). Note: this array is modified in place when tool calls occur.
316
320
  # @param prompt [String, nil] System prompt for the query
@@ -318,8 +322,8 @@ module Spacy
318
322
  def openai_query(access_token: nil,
319
323
  max_completion_tokens: nil,
320
324
  max_tokens: nil,
321
- temperature: 0.7,
322
- model: "gpt-5-mini",
325
+ temperature: nil,
326
+ model: OpenAIClient::DEFAULT_MODEL,
323
327
  messages: [],
324
328
  prompt: nil,
325
329
  response_format: nil,
@@ -420,7 +424,7 @@ module Spacy
420
424
  message["content"]
421
425
  end
422
426
  rescue OpenAIClient::APIError => e
423
- puts "Error: OpenAI API call failed - #{e.message}"
427
+ warn "Error: OpenAI API call failed - #{e.message}"
424
428
  nil
425
429
  end
426
430
 
@@ -429,10 +433,11 @@ module Spacy
429
433
  # @param access_token [String, nil] OpenAI API key (defaults to OPENAI_API_KEY env var)
430
434
  # @param max_completion_tokens [Integer] Maximum tokens in the response
431
435
  # @param max_tokens [Integer] Alias for max_completion_tokens (deprecated, for backward compatibility)
432
- # @param temperature [Float] Sampling temperature (ignored for GPT-5 models)
436
+ # @param temperature [Float, nil] Sampling temperature (omitted from requests
437
+ # when nil; models that reject it are retried without it)
433
438
  # @param model [String] The model to use (default: gpt-5-mini)
434
439
  # @return [String, nil] The completed text
435
- def openai_completion(access_token: nil, max_completion_tokens: nil, max_tokens: nil, temperature: 0.7, model: "gpt-5-mini")
440
+ def openai_completion(access_token: nil, max_completion_tokens: nil, max_tokens: nil, temperature: nil, model: OpenAIClient::DEFAULT_MODEL)
436
441
  # Support both max_completion_tokens and max_tokens for backward compatibility
437
442
  max_completion_tokens ||= max_tokens || 1000
438
443
 
@@ -450,7 +455,7 @@ module Spacy
450
455
  )
451
456
  response.dig("choices", 0, "message", "content")
452
457
  rescue OpenAIClient::APIError => e
453
- puts "Error: OpenAI API call failed - #{e.message}"
458
+ warn "Error: OpenAI API call failed - #{e.message}"
454
459
  nil
455
460
  end
456
461
 
@@ -460,12 +465,12 @@ module Spacy
460
465
  # @param model [String] The embeddings model (default: text-embedding-3-small)
461
466
  # @param dimensions [Integer, nil] The number of dimensions for the output embeddings (nil uses model default)
462
467
  # @return [Array<Float>, nil] The embedding vector
463
- def openai_embeddings(access_token: nil, model: "text-embedding-3-small", dimensions: nil)
468
+ def openai_embeddings(access_token: nil, model: OpenAIClient::DEFAULT_EMBEDDINGS_MODEL, dimensions: nil)
464
469
  client = openai_client(access_token)
465
470
  response = client.embeddings(model: model, input: @text, dimensions: dimensions)
466
471
  response.dig("data", 0, "embedding")
467
472
  rescue OpenAIClient::APIError => e
468
- puts "Error: OpenAI API call failed - #{e.message}"
473
+ warn "Error: OpenAI API call failed - #{e.message}"
469
474
  nil
470
475
  end
471
476
 
@@ -618,14 +623,56 @@ module Spacy
618
623
  end
619
624
  end
620
625
 
621
- # Yields an {OpenAIHelper} instance for making OpenAI API calls within a block.
626
+ # Yields a provider-specific LLM helper for making API calls within a block.
622
627
  # The helper is configured once and reused for all calls within the block,
623
628
  # making it efficient for batch processing with {#pipe}.
624
629
  #
630
+ # Providers:
631
+ # - +:openai+ — OpenAI API (or any OpenAI-compatible endpoint via +base_url:+).
632
+ # Yields an {OpenAIHelper}.
633
+ # - +:anthropic+ — Anthropic (Claude) Messages API. Yields an {AnthropicHelper}.
634
+ # - +:ollama+ — shortcut for a local Ollama server
635
+ # (+base_url: "http://localhost:11434/v1"+, no API key needed).
636
+ # Yields an {OpenAIHelper}.
637
+ #
638
+ # @param provider [Symbol] :openai (default), :anthropic, or :ollama
639
+ # @param opts [Hash] helper options (access_token:, model:, max_tokens:,
640
+ # temperature:, base_url:, ...) — see {OpenAIHelper#initialize} and
641
+ # {AnthropicHelper#initialize}
642
+ # @yield [OpenAIHelper, AnthropicHelper] the helper instance for making API calls
643
+ # @return [Object] the block's return value
644
+ # @example Claude
645
+ # nlp.with_llm(provider: :anthropic) do |ai|
646
+ # ai.chat(system: "Analyze.", user: doc.linguistic_summary)
647
+ # end
648
+ # @example Local model via Ollama
649
+ # nlp.with_llm(provider: :ollama, model: "llama3.2") do |ai|
650
+ # ai.chat(user: "Say hello.")
651
+ # end
652
+ def with_llm(provider: :openai, **opts)
653
+ helper = case provider.to_sym
654
+ when :openai
655
+ OpenAIHelper.new(**opts)
656
+ when :anthropic
657
+ AnthropicHelper.new(**opts)
658
+ when :ollama
659
+ OpenAIHelper.new(**{ base_url: "http://localhost:11434/v1",
660
+ access_token: "ollama" }.merge(opts))
661
+ else
662
+ raise ArgumentError, "Unknown LLM provider: #{provider} (use :openai, :anthropic, or :ollama)"
663
+ end
664
+ yield helper
665
+ end
666
+
667
+ # Yields an {OpenAIHelper} instance for making OpenAI API calls within a block.
668
+ # Equivalent to {#with_llm} with +provider: :openai+.
669
+ #
625
670
  # @param access_token [String, nil] OpenAI API key (defaults to OPENAI_API_KEY env var)
626
671
  # @param model [String] the default model for chat requests
627
672
  # @param max_completion_tokens [Integer] default maximum tokens in responses
628
- # @param temperature [Float] default sampling temperature
673
+ # @param temperature [Float, nil] default sampling temperature (omitted from
674
+ # requests when nil)
675
+ # @param base_url [String, nil] OpenAI-compatible API endpoint override
629
676
  # @yield [OpenAIHelper] the helper instance for making API calls
630
677
  # @return [Object] the block's return value
631
678
  # @example Batch processing with pipe
@@ -634,13 +681,14 @@ module Spacy
634
681
  # ai.chat(system: "Analyze.", user: doc.linguistic_summary)
635
682
  # end
636
683
  # end
637
- def with_openai(access_token: nil, model: "gpt-5-mini",
638
- max_completion_tokens: 1000, temperature: 0.7)
684
+ def with_openai(access_token: nil, model: OpenAIClient::DEFAULT_MODEL,
685
+ max_completion_tokens: 1000, temperature: nil, base_url: nil)
639
686
  helper = OpenAIHelper.new(
640
687
  access_token: access_token,
641
688
  model: model,
642
689
  max_completion_tokens: max_completion_tokens,
643
- temperature: temperature
690
+ temperature: temperature,
691
+ base_url: base_url
644
692
  )
645
693
  yield helper
646
694
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-spacy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoichiro Hasebe
@@ -208,6 +208,9 @@ files:
208
208
  - examples/linguistic_features/similarity_between_lexemes.rb
209
209
  - examples/linguistic_features/similarity_between_spans.rb
210
210
  - examples/linguistic_features/tokenization.rb
211
+ - examples/llm/anthropic_chat.rb
212
+ - examples/llm/local_llm_ollama.rb
213
+ - examples/llm/structured_ner_comparison.rb
211
214
  - examples/openai_integration/openai_completion.rb
212
215
  - examples/openai_integration/openai_embeddings.rb
213
216
  - examples/openai_integration/openai_query_1.rb
@@ -217,6 +220,9 @@ files:
217
220
  - examples/rule_based_matching/creating_spans_from_matches.rb
218
221
  - examples/rule_based_matching/matcher.rb
219
222
  - lib/ruby-spacy.rb
223
+ - lib/ruby-spacy/anthropic_client.rb
224
+ - lib/ruby-spacy/anthropic_helper.rb
225
+ - lib/ruby-spacy/llm_client_base.rb
220
226
  - lib/ruby-spacy/openai_client.rb
221
227
  - lib/ruby-spacy/openai_helper.rb
222
228
  - lib/ruby-spacy/version.rb