kohagi 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 +15 -0
- data/LICENSE +21 -0
- data/README.md +112 -0
- data/lib/kohagi/client.rb +156 -0
- data/lib/kohagi/errors.rb +32 -0
- data/lib/kohagi/exit_code.rb +13 -0
- data/lib/kohagi/result.rb +26 -0
- data/lib/kohagi/summary.rb +24 -0
- data/lib/kohagi/summary_line.rb +39 -0
- data/lib/kohagi/version.rb +5 -0
- data/lib/kohagi.rb +15 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4beb0ae3b0422e028e52f1e04c90e414f740bf412ef4a871d928f7e760730861
|
|
4
|
+
data.tar.gz: 7ac174c73b676551ba7ce105e396d1b133e298663d00193467520bb0a162d82e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2262d1a3cbb528dbe35615e1e548a7d4604c08635d3ea3a0211187c8d1fc12a1e940d5a80d2535a403d8e3b5fdfa071382ab2757efe2ce0c6a1921330d3adb8d
|
|
7
|
+
data.tar.gz: 312e26729165f64e008efaa94b83aff99430206ead9219a8a8d2e0fae67505ae385a93fe084cb42ac39107680b71d59cbcdb4a5d9358cba634a535073cc758bf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-07-24
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `Kohagi::Client` — builds the kohagi command from 1:1 CLI-flag options,
|
|
8
|
+
spawns it deadlock-safely (writer thread + concurrent stderr drain), and
|
|
9
|
+
streams `Kohagi::Result` records back.
|
|
10
|
+
- Exit codes as outcomes: 0/2 return a `Kohagi::Summary` (`ok?` / `partial?`),
|
|
11
|
+
1 raises `Kohagi::FatalError`, 3 raises `Kohagi::UnsupportedDeviceError` for
|
|
12
|
+
the CoreML → CPU fallback.
|
|
13
|
+
- `--report-tokens` surfaced as `Result#truncated?` / `#n_tokens` and the
|
|
14
|
+
summary's `truncated` count.
|
|
15
|
+
- `Client#with` for option overrides (the fallback pattern).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 takahashim
|
|
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,112 @@
|
|
|
1
|
+
# Kohagi (Ruby)
|
|
2
|
+
|
|
3
|
+
A Ruby client for [Kohagi](https://github.com/takahashim/kohagi), a local CLI for generating sentence embeddings.
|
|
4
|
+
|
|
5
|
+
It runs the `kohagi` binary and safely handles its input and output streams.
|
|
6
|
+
It also reports execution errors and returns embeddings associated with their input IDs.
|
|
7
|
+
|
|
8
|
+
This gem handles only communication with the CLI, converting `{id, text}` records into `{id, embedding}` results.
|
|
9
|
+
How the embeddings are stored, validated, or used is left to your application.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
The `kohagi` binary must be available on `PATH`, or specified with the `bin:` option.
|
|
14
|
+
|
|
15
|
+
See the [Kohagi installation instructions](https://github.com/takahashim/kohagi#install).
|
|
16
|
+
|
|
17
|
+
This gem ships no native code and does not bundle the `kohagi` binary.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem "kohagi"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "kohagi"
|
|
29
|
+
|
|
30
|
+
client = Kohagi::Client.new(prefix: "検索文書: ")
|
|
31
|
+
|
|
32
|
+
records = [
|
|
33
|
+
{ id: 1, text: "夏目漱石『吾輩は猫である』…" },
|
|
34
|
+
{ id: 2, text: "太宰治『人間失格』…" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Streaming: each result is yielded as it arrives, so memory stays flat on any corpus. Returns a Summary.
|
|
38
|
+
summary = client.embed(records) do |r|
|
|
39
|
+
store(r.id, r.embedding) # e.g. write to a pgvector column
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
summary.dim # => 512
|
|
43
|
+
summary.out # => 2 (records embedded)
|
|
44
|
+
summary.skipped # => 0
|
|
45
|
+
summary.truncated # => 0 (records that ran past --max-seq-length)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Without a block the results are collected onto the summary:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
summary = client.embed(records)
|
|
52
|
+
summary.results.each { |r| store(r.id, r.embedding) }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Configuration
|
|
56
|
+
|
|
57
|
+
Each option maps directly to a `kohagi` CLI flag. Kohagi does not use a configuration file.
|
|
58
|
+
|
|
59
|
+
| Option | CLI flag or behavior |
|
|
60
|
+
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `bin:` | Executable path. Defaults to `"kohagi"`. |
|
|
62
|
+
| `model_id:` | `--model-id` |
|
|
63
|
+
| `model_path:` and `tokenizer_path:` | `--model-path` and `--tokenizer-path` for offline use. These take precedence over `model_id:`. |
|
|
64
|
+
| `prefix:` | `--prefix` |
|
|
65
|
+
| `pooling:` | `--pooling` |
|
|
66
|
+
| `device:` | `--device` (`"cpu"`, `"metal"`, or `"coreml"`) |
|
|
67
|
+
| `coreml_dir:`, `coreml_model_id:`, and `coreml_prefer:` | Corresponding `--coreml-*` flags |
|
|
68
|
+
| `max_seq_length:`, `batch_size:`, and `precision:` | Corresponding CLI flags |
|
|
69
|
+
| `normalize:` | Passes `--no-normalize` when `false`. Normalization is enabled by default. |
|
|
70
|
+
| `report_tokens:` | Passes `--report-tokens`, adding `n_tokens` and `truncated?` to each result. |
|
|
71
|
+
| `logger:` | A callable that receives `kohagi`'s standard error output after each run. |
|
|
72
|
+
|
|
73
|
+
### Truncation
|
|
74
|
+
|
|
75
|
+
Text longer than `max_seq_length` is truncated before embedding.
|
|
76
|
+
The default limit is 512 tokens.
|
|
77
|
+
|
|
78
|
+
The summary always includes the number of truncated inputs.
|
|
79
|
+
With `report_tokens: true`, each result also includes `n_tokens` and `truncated?`.
|
|
80
|
+
You can use this information to process truncated inputs separately:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
client = Kohagi::Client.new(report_tokens: true)
|
|
84
|
+
|
|
85
|
+
client.embed(records) do |result|
|
|
86
|
+
chunk_and_reembed(result.id) if result.truncated?
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Exit codes
|
|
91
|
+
|
|
92
|
+
| `kohagi` exit code | Result |
|
|
93
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
+
| 0 | Returns a `Summary` for which `summary.ok?` is true. |
|
|
95
|
+
| 2 | Returns a `Summary` for which `summary.partial?` is true. Some input lines were skipped, but the returned output is valid. |
|
|
96
|
+
| 1 | Raises `Kohagi::FatalError`. |
|
|
97
|
+
| 3 | Raises `Kohagi::UnsupportedDeviceError`. |
|
|
98
|
+
|
|
99
|
+
Exit code 3 indicates that the Core ML backend cannot handle the request.
|
|
100
|
+
You can retry using the CPU:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
begin
|
|
104
|
+
client.embed(records, &block)
|
|
105
|
+
rescue Kohagi::UnsupportedDeviceError
|
|
106
|
+
client.with(device: "cpu").embed(records, &block)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Kohagi
|
|
7
|
+
# Drives the kohagi binary's stdin/stdout JSONL protocol.
|
|
8
|
+
#
|
|
9
|
+
# kohagi is config-file-less, so every option here maps 1:1 to a CLI flag.
|
|
10
|
+
# The client owns only the transport: building the command, spawning the
|
|
11
|
+
# process without deadlocking on the pipe buffer, mapping exit codes to
|
|
12
|
+
# outcomes, and parsing the records back. The domain (which texts, what to do
|
|
13
|
+
# with the vectors) stays with the caller.
|
|
14
|
+
#
|
|
15
|
+
# client = Kohagi::Client.new(prefix: "検索文書: ", report_tokens: true)
|
|
16
|
+
# summary = client.embed(records) { |r| store(r.id, r.embedding) }
|
|
17
|
+
#
|
|
18
|
+
class Client
|
|
19
|
+
DEFAULTS = { bin: "kohagi", normalize: true, report_tokens: false }.freeze
|
|
20
|
+
|
|
21
|
+
# See the README for the full option list. Unknown keys are ignored, so a
|
|
22
|
+
# newer kohagi flag can be threaded through as `extra_flags`.
|
|
23
|
+
def initialize(**options)
|
|
24
|
+
@options = DEFAULTS.merge(options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A copy with some options overridden — for the exit-3 → CPU fallback:
|
|
28
|
+
#
|
|
29
|
+
# rescue Kohagi::UnsupportedDeviceError
|
|
30
|
+
# client.with(device: "cpu").embed(records, &block)
|
|
31
|
+
def with(**overrides)
|
|
32
|
+
self.class.new(**@options.merge(overrides))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The argv kohagi is launched with. Pure and public, so a caller can assert
|
|
36
|
+
# on it without spawning a process.
|
|
37
|
+
def command
|
|
38
|
+
o = @options
|
|
39
|
+
cmd = [o.fetch(:bin)]
|
|
40
|
+
cmd.concat(model_flags(o))
|
|
41
|
+
cmd.concat(["--prefix", o[:prefix]]) if o[:prefix]
|
|
42
|
+
cmd.concat(device_flags(o))
|
|
43
|
+
{
|
|
44
|
+
"--pooling" => o[:pooling], "--max-seq-length" => o[:max_seq_length],
|
|
45
|
+
"--batch-size" => o[:batch_size], "--precision" => o[:precision]
|
|
46
|
+
}.each { |flag, val| cmd.concat([flag, val.to_s]) unless val.nil? }
|
|
47
|
+
cmd << "--no-normalize" unless o.fetch(:normalize)
|
|
48
|
+
cmd << "--report-tokens" if o.fetch(:report_tokens)
|
|
49
|
+
cmd.concat(Array(o[:extra_flags]))
|
|
50
|
+
cmd
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Embed `records` (each `{id:, text:}`). With a block, yields a Result per
|
|
54
|
+
# record as it arrives, so memory stays flat on any corpus; without one, the
|
|
55
|
+
# results are collected into the returned Summary. Returns a Summary either
|
|
56
|
+
# way.
|
|
57
|
+
#
|
|
58
|
+
# Exit codes become outcomes: 0 and 2 return normally (2 = some lines were
|
|
59
|
+
# skipped, but the output you received is valid); 1 raises FatalError; 3
|
|
60
|
+
# raises UnsupportedDeviceError so the caller can retry on
|
|
61
|
+
# `with(device: "cpu")`.
|
|
62
|
+
def embed(records, &block)
|
|
63
|
+
collected = block ? nil : []
|
|
64
|
+
emit = block || ->(r) { collected << r }
|
|
65
|
+
dim = nil
|
|
66
|
+
out = 0
|
|
67
|
+
|
|
68
|
+
Open3.popen3(*command) do |stdin, stdout, stderr, wait|
|
|
69
|
+
# Write from one thread and read from this one — writing the whole
|
|
70
|
+
# corpus before reading anything can deadlock both processes once the
|
|
71
|
+
# pipe buffer fills. Drain stderr concurrently for the same reason.
|
|
72
|
+
writer = Thread.new do
|
|
73
|
+
records.each { |rec| stdin.puts(JSON.generate(id: rec.fetch(:id), text: rec.fetch(:text))) }
|
|
74
|
+
rescue Errno::EPIPE
|
|
75
|
+
# kohagi exited before reading all input (e.g. exit 3, detected up
|
|
76
|
+
# front). The process exit code carries the real reason; the broken
|
|
77
|
+
# pipe here is a symptom, not the error to surface.
|
|
78
|
+
ensure
|
|
79
|
+
begin
|
|
80
|
+
stdin.close
|
|
81
|
+
rescue Errno::EPIPE
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
errors = Thread.new { stderr.read }
|
|
86
|
+
|
|
87
|
+
stdout.each_line do |line|
|
|
88
|
+
result = Result.from_json(line)
|
|
89
|
+
dim ||= result.embedding&.size
|
|
90
|
+
out += 1
|
|
91
|
+
emit.call(result)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
writer.join
|
|
95
|
+
stderr_text = errors.value
|
|
96
|
+
@options[:logger]&.call(stderr_text) unless stderr_text.to_s.empty?
|
|
97
|
+
code = wait.value.exitstatus
|
|
98
|
+
raise_for(code, stderr_text)
|
|
99
|
+
|
|
100
|
+
return build_summary(stderr_text, sent: records.size, out: out, dim: dim, code: code, results: collected)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def raise_for(code, stderr)
|
|
107
|
+
case code
|
|
108
|
+
when ExitCode::OK, ExitCode::PARTIAL then nil
|
|
109
|
+
when ExitCode::UNSUPPORTED then raise UnsupportedDeviceError.new(stderr, exit_code: code)
|
|
110
|
+
else raise FatalError.new(stderr, exit_code: code)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Combine kohagi's reported counts with what we counted from the stream,
|
|
115
|
+
# preferring kohagi's numbers and falling back to the stream when a field
|
|
116
|
+
# (or the whole summary line) is absent.
|
|
117
|
+
def build_summary(stderr, sent:, out:, dim:, code:, results:)
|
|
118
|
+
reported = SummaryLine.parse(stderr)
|
|
119
|
+
Summary.new(
|
|
120
|
+
model: reported&.model,
|
|
121
|
+
dim: reported&.dim || dim,
|
|
122
|
+
sent: sent,
|
|
123
|
+
out: reported&.out || out,
|
|
124
|
+
skipped: reported&.skipped || (sent - out),
|
|
125
|
+
truncated: reported&.truncated,
|
|
126
|
+
exit_code: code,
|
|
127
|
+
results: results
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Local weights win over a Hub id; both absent lets kohagi use its default.
|
|
132
|
+
def model_flags(o)
|
|
133
|
+
if o[:model_path] && o[:tokenizer_path]
|
|
134
|
+
["--model-path", o[:model_path].to_s, "--tokenizer-path", o[:tokenizer_path].to_s]
|
|
135
|
+
elsif o[:model_id]
|
|
136
|
+
["--model-id", o[:model_id].to_s]
|
|
137
|
+
else
|
|
138
|
+
[]
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# --device, plus the converted-model pointer CoreML needs (--coreml-dir
|
|
143
|
+
# wins over --coreml-model-id, matching kohagi).
|
|
144
|
+
def device_flags(o)
|
|
145
|
+
return [] unless o[:device]
|
|
146
|
+
|
|
147
|
+
flags = ["--device", o[:device].to_s]
|
|
148
|
+
return flags unless o[:device].to_s == "coreml"
|
|
149
|
+
|
|
150
|
+
flags.concat(["--coreml-dir", o[:coreml_dir].to_s]) if o[:coreml_dir]
|
|
151
|
+
flags.concat(["--coreml-model-id", o[:coreml_model_id].to_s]) if o[:coreml_model_id] && !o[:coreml_dir]
|
|
152
|
+
flags.concat(["--coreml-prefer", o[:coreml_prefer].to_s]) if o[:coreml_prefer]
|
|
153
|
+
flags
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kohagi
|
|
4
|
+
# Base for every error the client raises. Carries the process exit code and
|
|
5
|
+
# the captured stderr so the caller can log or branch on them.
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
attr_reader :exit_code, :stderr
|
|
8
|
+
|
|
9
|
+
def initialize(stderr = nil, exit_code: nil)
|
|
10
|
+
@stderr = stderr
|
|
11
|
+
@exit_code = exit_code
|
|
12
|
+
super(build_message(stderr, exit_code))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def build_message(stderr, code)
|
|
18
|
+
base = "kohagi exited #{code}"
|
|
19
|
+
tail = stderr.to_s.lines.last(3).map(&:chomp).reject(&:empty?).join(" / ")
|
|
20
|
+
tail.empty? ? base : "#{base}: #{tail}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Exit 1: model load failure, bad flags, I/O error. Nothing to retry.
|
|
25
|
+
class FatalError < Error; end
|
|
26
|
+
|
|
27
|
+
# Exit 3: the requested CoreML backend cannot serve the request (built without
|
|
28
|
+
# the feature, no converted model given, or a sequence past the largest
|
|
29
|
+
# bucket). Detected before any input is read, so no output was produced — the
|
|
30
|
+
# caller can retry on `client.with(device: "cpu")`.
|
|
31
|
+
class UnsupportedDeviceError < Error; end
|
|
32
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kohagi
|
|
4
|
+
# The exit codes kohagi uses (see PROTOCOL.md), named in one place so the
|
|
5
|
+
# raise-vs-return decision (Client#raise_for) and the outcome classification
|
|
6
|
+
# (Summary#ok? / #partial?) can't drift apart.
|
|
7
|
+
module ExitCode
|
|
8
|
+
OK = 0 # every record embedded
|
|
9
|
+
PARTIAL = 2 # finished, but some input lines were skipped; output valid
|
|
10
|
+
FATAL = 1 # model load failure, bad flags, I/O error
|
|
11
|
+
UNSUPPORTED = 3 # the requested CoreML backend cannot serve the request
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Kohagi
|
|
6
|
+
# One output record: the echoed id and its vector, plus the optional
|
|
7
|
+
# `--report-tokens` fields. `id` is whatever JSON value was sent, unchanged.
|
|
8
|
+
Result = Struct.new(:id, :embedding, :n_tokens, :truncated, keyword_init: true) do
|
|
9
|
+
# True only when kohagi reported this text as truncated (needs
|
|
10
|
+
# `report_tokens: true`); nil/false otherwise.
|
|
11
|
+
def truncated?
|
|
12
|
+
truncated == true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Parse one JSONL line from kohagi's stdout.
|
|
16
|
+
def self.from_json(line)
|
|
17
|
+
row = JSON.parse(line)
|
|
18
|
+
new(
|
|
19
|
+
id: row["id"],
|
|
20
|
+
embedding: row["embedding"],
|
|
21
|
+
n_tokens: row["n_tokens"],
|
|
22
|
+
truncated: row["truncated"]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kohagi
|
|
4
|
+
# The run's outcome: kohagi's reported counts (see SummaryLine) combined by the
|
|
5
|
+
# client with what actually streamed back. A plain data holder — parsing and
|
|
6
|
+
# composition live in SummaryLine and Client, not here.
|
|
7
|
+
#
|
|
8
|
+
# `truncated` is nil against a kohagi old enough not to report it.
|
|
9
|
+
Summary = Struct.new(
|
|
10
|
+
:model, :dim, :sent, :out, :skipped, :truncated, :exit_code, :results,
|
|
11
|
+
keyword_init: true
|
|
12
|
+
) do
|
|
13
|
+
# Every record embedded.
|
|
14
|
+
def ok?
|
|
15
|
+
exit_code == ExitCode::OK
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Finished, but some input lines were skipped. The received output is still
|
|
19
|
+
# valid; investigate stderr and resend the skipped records.
|
|
20
|
+
def partial?
|
|
21
|
+
exit_code == ExitCode::PARTIAL
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kohagi
|
|
4
|
+
# kohagi's one-line run summary from stderr, parsed into a value object:
|
|
5
|
+
#
|
|
6
|
+
# kohagi: model=cl-nagoya/ruri-v3-130m dim=512 in=2141 out=2141 skipped=0 truncated=3
|
|
7
|
+
#
|
|
8
|
+
# `SummaryLine.parse` returns an instance, or nil when stderr carries no such
|
|
9
|
+
# line, so the caller can fall back to values it computed from the stream. A
|
|
10
|
+
# field the line omits (an older kohagi has no `truncated`) reads as nil.
|
|
11
|
+
# Knowing kohagi's stderr text format lives here, not in the Summary data.
|
|
12
|
+
class SummaryLine
|
|
13
|
+
MARKER = "kohagi: model="
|
|
14
|
+
MODEL = /\bmodel=(?<value>\S+)/
|
|
15
|
+
NUMERIC = %i[dim out skipped truncated].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :model, :dim, :out, :skipped, :truncated
|
|
18
|
+
|
|
19
|
+
# The parsed summary line in `stderr`, or nil if there isn't one.
|
|
20
|
+
def self.parse(stderr)
|
|
21
|
+
line = stderr.to_s.lines.reverse.find { |l| l.include?(MARKER) }
|
|
22
|
+
line && new(line)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(line)
|
|
26
|
+
@model = capture(line, MODEL)
|
|
27
|
+
NUMERIC.each do |name|
|
|
28
|
+
value = capture(line, /\b#{name}=(?<value>\d+)\b/)
|
|
29
|
+
instance_variable_set("@#{name}", value&.to_i)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def capture(line, regexp)
|
|
36
|
+
line.match(regexp)&.[](:value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/kohagi.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "kohagi/version"
|
|
4
|
+
require_relative "kohagi/exit_code"
|
|
5
|
+
require_relative "kohagi/errors"
|
|
6
|
+
require_relative "kohagi/result"
|
|
7
|
+
require_relative "kohagi/summary_line"
|
|
8
|
+
require_relative "kohagi/summary"
|
|
9
|
+
require_relative "kohagi/client"
|
|
10
|
+
|
|
11
|
+
# Ruby client for the kohagi local sentence-embedding CLI.
|
|
12
|
+
# See https://github.com/takahashim/kohagi/blob/main/PROTOCOL.md for the
|
|
13
|
+
# stdin/stdout protocol this wraps.
|
|
14
|
+
module Kohagi
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kohagi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- takahashim
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Drives the kohagi binary's stdin/stdout JSONL protocol from Ruby: builds the
|
|
14
|
+
command, spawns it without deadlocking on the pipe buffer, maps exit codes to
|
|
15
|
+
outcomes, and returns id-tagged embeddings. Shells out to an installed kohagi
|
|
16
|
+
binary and ships no native code of its own.
|
|
17
|
+
email:
|
|
18
|
+
- takahashimm@gmail.com
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- CHANGELOG.md
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.md
|
|
26
|
+
- lib/kohagi.rb
|
|
27
|
+
- lib/kohagi/client.rb
|
|
28
|
+
- lib/kohagi/errors.rb
|
|
29
|
+
- lib/kohagi/exit_code.rb
|
|
30
|
+
- lib/kohagi/result.rb
|
|
31
|
+
- lib/kohagi/summary.rb
|
|
32
|
+
- lib/kohagi/summary_line.rb
|
|
33
|
+
- lib/kohagi/version.rb
|
|
34
|
+
homepage: https://github.com/takahashim/kohagi-ruby
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
source_code_uri: https://github.com/takahashim/kohagi-ruby
|
|
39
|
+
changelog_uri: https://github.com/takahashim/kohagi-ruby/blob/main/CHANGELOG.md
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '3.0'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 4.0.16
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Ruby client for the kohagi local sentence-embedding CLI
|
|
58
|
+
test_files: []
|