ruby-spacy 0.4.0 → 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 +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +133 -4
- data/Rakefile +13 -0
- data/examples/get_started/lexeme.rb +0 -0
- data/examples/get_started/linguistic_annotations.rb +0 -0
- data/examples/get_started/morphology.rb +0 -0
- data/examples/get_started/most_similar.rb +0 -0
- data/examples/get_started/named_entities.rb +0 -0
- data/examples/get_started/outputs/test_dep.svg +0 -0
- data/examples/get_started/outputs/test_dep_compact.svg +0 -0
- data/examples/get_started/outputs/test_ent.html +0 -0
- data/examples/get_started/pos_tags_and_dependencies.rb +0 -0
- data/examples/get_started/similarity.rb +0 -0
- data/examples/get_started/tokenization.rb +0 -0
- data/examples/get_started/visualizing_dependencies.rb +0 -0
- data/examples/get_started/visualizing_dependencies_compact.rb +0 -0
- data/examples/get_started/visualizing_named_entities.rb +0 -0
- data/examples/get_started/vocab.rb +0 -0
- data/examples/get_started/word_vectors.rb +0 -0
- data/examples/japanese/ancestors.rb +0 -0
- data/examples/japanese/entity_annotations_and_labels.rb +0 -0
- data/examples/japanese/information_extraction.rb +0 -0
- data/examples/japanese/lemmatization.rb +0 -0
- data/examples/japanese/most_similar.rb +0 -0
- data/examples/japanese/named_entity_recognition.rb +0 -0
- data/examples/japanese/navigating_parse_tree.rb +0 -0
- data/examples/japanese/noun_chunks.rb +0 -0
- data/examples/japanese/outputs/test_dep.svg +0 -0
- data/examples/japanese/outputs/test_ent.html +0 -0
- data/examples/japanese/pos_tagging.rb +0 -0
- data/examples/japanese/sentence_segmentation.rb +0 -0
- data/examples/japanese/similarity.rb +0 -0
- data/examples/japanese/tokenization.rb +0 -0
- data/examples/japanese/visualizing_dependencies.rb +0 -0
- data/examples/japanese/visualizing_named_entities.rb +0 -0
- data/examples/linguistic_features/ancestors.rb +0 -0
- data/examples/linguistic_features/entity_annotations_and_labels.rb +0 -0
- data/examples/linguistic_features/finding_a_verb_with_a_subject.rb +0 -0
- data/examples/linguistic_features/information_extraction.rb +0 -0
- data/examples/linguistic_features/iterating_children.rb +0 -0
- data/examples/linguistic_features/iterating_lefts_and_rights.rb +0 -0
- data/examples/linguistic_features/lemmatization.rb +0 -0
- data/examples/linguistic_features/named_entity_recognition.rb +0 -0
- data/examples/linguistic_features/navigating_parse_tree.rb +0 -0
- data/examples/linguistic_features/noun_chunks.rb +0 -0
- data/examples/linguistic_features/outputs/test_ent.html +0 -0
- data/examples/linguistic_features/pos_tagging.rb +0 -0
- data/examples/linguistic_features/retokenize_1.rb +0 -0
- data/examples/linguistic_features/retokenize_2.rb +0 -0
- data/examples/linguistic_features/rule_based_morphology.rb +0 -0
- data/examples/linguistic_features/sentence_segmentation.rb +0 -0
- data/examples/linguistic_features/similarity.rb +0 -0
- data/examples/linguistic_features/similarity_between_lexemes.rb +0 -0
- data/examples/linguistic_features/similarity_between_spans.rb +0 -0
- data/examples/linguistic_features/tokenization.rb +0 -0
- data/examples/llm/anthropic_chat.rb +21 -0
- data/examples/llm/local_llm_ollama.rb +27 -0
- data/examples/llm/structured_ner_comparison.rb +49 -0
- data/examples/openai_integration/openai_embeddings.rb +0 -0
- data/examples/rule_based_matching/creating_spans_from_matches.rb +0 -0
- data/examples/rule_based_matching/matcher.rb +0 -0
- data/lib/ruby-spacy/anthropic_client.rb +66 -0
- data/lib/ruby-spacy/anthropic_helper.rb +118 -0
- data/lib/ruby-spacy/llm_client_base.rb +120 -0
- data/lib/ruby-spacy/openai_client.rb +37 -107
- data/lib/ruby-spacy/openai_helper.rb +60 -16
- data/lib/ruby-spacy/version.rb +1 -1
- data/lib/ruby-spacy.rb +62 -14
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b759c507a1cad8e726bd36ad39e4539894395407e2ac5d64a31b7b907a4e34d8
|
|
4
|
+
data.tar.gz: 0f02f9b8f6bcf39a49b50b1934905e72c0c13bb9652eee8fbaad3a3b7b58b7e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b7d8078148e05c860f3ef698b5103cfca533fabfac5001c5afd12419efaae21d6492aa22fcebfba9d9d22bf8701fe2c6a21eb1ee879344cdbd5d897af9b8853
|
|
7
|
+
data.tar.gz: 4ffab4be8e0338e86c34aad0285cd9471a3d5c570fbf48b4214cdbe5ca5cc01c7d251d4f6d5edc778de2185d89fc3858e094c363499c81f89d5c3a61ab8859c2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
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
|
+
|
|
37
|
+
## 0.4.1 - 2026-07-19
|
|
38
|
+
### Fixed
|
|
39
|
+
- Gem packaging: normalize file permissions at build time so packaged files
|
|
40
|
+
are world-readable (0644 / 0755). Version 0.4.0 shipped 11 owner-only
|
|
41
|
+
(0600) files, which made the gem unusable after `sudo gem install`
|
|
42
|
+
|
|
43
|
+
## 0.4.0 - 2026-02-23
|
|
44
|
+
### Added
|
|
45
|
+
- `Language#with_openai` block API with `OpenAIHelper` for streamlined OpenAI integration
|
|
46
|
+
- `Doc#linguistic_summary` for JSON-formatted spaCy analysis output
|
|
47
|
+
- `Token#idx`, `Span#text`/`Span#to_s`, `Language#memory_zone` (spaCy 3.8+)
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- `Doc#ents` returns proper `Span` objects instead of raw Python objects
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- Model name validation in `Language#initialize` for security
|
|
54
|
+
- OpenAI client: temperature support for o-series models, 429 retry with
|
|
55
|
+
exponential backoff, client reuse, `dimensions`/`response_format` parameters,
|
|
56
|
+
tool call depth limit
|
|
57
|
+
- Improved `respond_to_missing?` across all wrapper classes
|
|
58
|
+
- Added `instance_variables_to_inspect` for Ruby 4.0+ compatibility
|
|
59
|
+
- Added `base64` gem dependency (required for Ruby 3.4+)
|
|
60
|
+
|
|
3
61
|
## 0.3.0 - 2025-01-06
|
|
4
62
|
### Added
|
|
5
63
|
- Ruby 4.0 support
|
data/README.md
CHANGED
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
| ✅ | Named entity recognition |
|
|
12
12
|
| ✅ | Syntactic dependency visualization |
|
|
13
13
|
| ✅ | Access to pre-trained word vectors |
|
|
14
|
-
| ✅ | OpenAI
|
|
14
|
+
| ✅ | LLM integration: OpenAI, Anthropic (Claude), and local models |
|
|
15
15
|
|
|
16
|
-
Current Version: `0.
|
|
16
|
+
Current Version: `0.5.0`
|
|
17
17
|
|
|
18
18
|
- Ruby 4.0 supported
|
|
19
19
|
- spaCy 3.8 supported
|
|
20
|
-
- OpenAI
|
|
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
|
|
21
23
|
|
|
22
24
|
## Installation of Prerequisites
|
|
23
25
|
|
|
@@ -587,7 +589,9 @@ end
|
|
|
587
589
|
|
|
588
590
|
## OpenAI API Integration
|
|
589
591
|
|
|
590
|
-
> ⚠️ This feature requires GPT-5 series models. Please refer to OpenAI's [API reference](https://platform.openai.com/docs/api-reference) for details.
|
|
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.
|
|
591
595
|
|
|
592
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:
|
|
593
597
|
|
|
@@ -843,6 +847,131 @@ Output:
|
|
|
843
847
|
...
|
|
844
848
|
```
|
|
845
849
|
|
|
850
|
+
### Block-based OpenAI API
|
|
851
|
+
|
|
852
|
+
The `Language#with_openai` block API provides a streamlined way to combine spaCy's linguistic analysis with OpenAI. The `Doc#linguistic_summary` method generates a JSON summary of spaCy's analysis (tokens, entities, noun chunks, etc.) that can be passed directly to the LLM as context.
|
|
853
|
+
|
|
854
|
+
**Basic usage:**
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
require "ruby-spacy"
|
|
858
|
+
|
|
859
|
+
nlp = Spacy::Language.new("en_core_web_sm")
|
|
860
|
+
doc = nlp.read("Apple Inc. was founded by Steve Jobs in California.")
|
|
861
|
+
|
|
862
|
+
nlp.with_openai(model: "gpt-5-mini") do |ai|
|
|
863
|
+
result = ai.chat(
|
|
864
|
+
system: "You are a linguistic analyst. Analyze the given linguistic data.",
|
|
865
|
+
user: doc.linguistic_summary
|
|
866
|
+
)
|
|
867
|
+
puts result
|
|
868
|
+
end
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Batch processing with `pipe`:**
|
|
872
|
+
|
|
873
|
+
```ruby
|
|
874
|
+
require "ruby-spacy"
|
|
875
|
+
|
|
876
|
+
nlp = Spacy::Language.new("en_core_web_sm")
|
|
877
|
+
texts = ["The bank approved the loan.", "I sat on the river bank."]
|
|
878
|
+
|
|
879
|
+
nlp.with_openai(model: "gpt-5-mini") do |ai|
|
|
880
|
+
nlp.pipe(texts).each do |doc|
|
|
881
|
+
result = ai.chat(
|
|
882
|
+
system: "Identify the meaning of 'bank' in one word based on the linguistic context.",
|
|
883
|
+
user: doc.linguistic_summary
|
|
884
|
+
)
|
|
885
|
+
puts "#{doc.text} => #{result}"
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
**Customizing linguistic summary:**
|
|
891
|
+
|
|
892
|
+
```ruby
|
|
893
|
+
# Include sentences and morphology, exclude noun chunks
|
|
894
|
+
summary = doc.linguistic_summary(
|
|
895
|
+
sections: [:text, :tokens, :entities, :sentences],
|
|
896
|
+
token_attributes: [:text, :lemma, :pos, :dep, :head, :morphology]
|
|
897
|
+
)
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**Embeddings:**
|
|
901
|
+
|
|
902
|
+
```ruby
|
|
903
|
+
nlp.with_openai do |ai|
|
|
904
|
+
vector = ai.embeddings("Hello world")
|
|
905
|
+
puts vector.length # => 1536
|
|
906
|
+
end
|
|
907
|
+
```
|
|
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
|
+
|
|
846
975
|
## Advanced Usage
|
|
847
976
|
|
|
848
977
|
### Setting a Timeout
|
data/Rakefile
CHANGED
|
@@ -10,3 +10,16 @@ Rake::TestTask.new(:test) do |t|
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
task default: :test
|
|
13
|
+
|
|
14
|
+
# Gem packaging preserves on-disk file modes; owner-only permissions here
|
|
15
|
+
# produce gems whose files are unreadable after a sudo install. Executable
|
|
16
|
+
# status is determined by path alone because local (Dropbox-synced) modes
|
|
17
|
+
# are unreliable.
|
|
18
|
+
task :normalize_permissions do
|
|
19
|
+
`git ls-files -z`.split("\x0").each do |f|
|
|
20
|
+
executable = f.start_with?("bin/", "exe/")
|
|
21
|
+
File.chmod(executable ? 0o755 : 0o644, f)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Rake::Task["build"].enhance([:normalize_permissions])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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"]})" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|