slidict 0.4.0 → 0.4.2

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: a9f6b8851999b92437addc0186d6a047c1c8317145940987514df479e02fabc2
4
- data.tar.gz: fb42f714400e77cbc97931d105cef01a8b54776c0ce67db152ce6ce475136caf
3
+ metadata.gz: 829a5806efaa7c29d773818f0683fa3c0525bebfe883bd32261ba29f5a649a5e
4
+ data.tar.gz: e45e42541ba0940a883f470f7d03941df71b5825c0c9b93471fe3b1c991b0de5
5
5
  SHA512:
6
- metadata.gz: 88ad47ca34c0e2d7bcf99f118905ba3b7b7f8df571aa4e118080552659564bc2bb7d79d007c3c67a9dc4ebd291e415e6cf423f93299f013485856737e54f7e77
7
- data.tar.gz: 781cd459d6c82e62a29bbc8702bea3c7f9d4125d78341e2b13723608ded41e9131af456b87dbfc61525dbc7ebab91da9729fc4465e3b852c0298a96f1e277223
6
+ metadata.gz: ff04a4475b51083d1c8ed884ad10f924000bb6c47adb0947d6c365fa3dd81d83eb09b94b280794bed9d2ef1001b559589c9241d92b0ccc005a7969065a814eb1
7
+ data.tar.gz: a0385b86027802eb018c4c9bda5fc26ee8030df9eee449879bd5dc299faa32382df90e5b8c707b528c846018d1fa8f28b3861ce299ed8d58f7d5ad125bc29b15
@@ -1,4 +1,4 @@
1
- name-template: 'v$NEXT_PATCH_VERSION'
1
+ name-template: 'Unreleased'
2
2
  tag-template: 'v$NEXT_PATCH_VERSION'
3
3
  template: |
4
4
  ## Changes
@@ -28,6 +28,8 @@ module Slidict
28
28
  return lint(options[:args]) if options[:command] == "lint"
29
29
 
30
30
  config = build_config(options)
31
+ return print_available_models(config) if config.llm_enabled? && config.model.nil?
32
+
31
33
  client = llm_client_for(config)
32
34
  return 1 if client && !verify_connection(client)
33
35
 
@@ -155,6 +157,21 @@ module Slidict
155
157
  )
156
158
  end
157
159
 
160
+ def print_available_models(config)
161
+ client = Llm::Client.new(base_url: config.base_url, api_key: config.api_key, model: nil)
162
+ models = client.list_models
163
+ if models.empty?
164
+ @output.puts "No models available at #{config.base_url}"
165
+ else
166
+ @output.puts "Available models (specify one with --llm-model NAME or SLIDICT_LLM_MODEL=NAME):"
167
+ models.each { |m| @output.puts " #{m}" }
168
+ end
169
+ 0
170
+ rescue Llm::Client::Error => e
171
+ @output.puts "Error: LLM request failed (#{e.message})"
172
+ 1
173
+ end
174
+
158
175
  def llm_client_for(config)
159
176
  return nil unless config.llm_enabled?
160
177
 
@@ -284,7 +301,7 @@ module Slidict
284
301
  --llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL).
285
302
  When omitted, the built-in slide template is used instead.
286
303
  --llm-api-key KEY API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)
287
- --llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL, default: gpt-4o-mini)
304
+ --llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL); omit to list available models
288
305
  --no-llm Skip the LLM call and use the built-in slide template
289
306
  --publish Publish the generated slides to slidict.io as a draft
290
307
  (requires `slidict auth`; creates a new slide, or edits
@@ -31,12 +31,13 @@ module Slidict
31
31
  def run_lint(options)
32
32
  config = build_config(options)
33
33
  return llm_required unless config.llm_enabled?
34
+ return print_available_models(config) if config.model.nil?
34
35
 
35
36
  print_findings(lint(options, config))
36
37
  end
37
38
 
38
39
  def lint(options, config)
39
- @linter_factory.call(config).lint(File.read(options[:path]), format: format_for(options))
40
+ @linter_factory.call(config).lint(File.read(options[:path]), format: format_for(options), translate: options[:translate])
40
41
  end
41
42
 
42
43
  def print_usage_error(error)
@@ -70,6 +71,7 @@ module Slidict
70
71
  when "--llm-base-url" then options[:llm_base_url] = fetch_value!(args, arg)
71
72
  when "--llm-api-key" then options[:llm_api_key] = fetch_value!(args, arg)
72
73
  when "--llm-model" then options[:llm_model] = fetch_value!(args, arg)
74
+ when "--translate" then options[:translate] = fetch_value!(args, arg)
73
75
  else raise ArgumentError, "unknown option #{arg}"
74
76
  end
75
77
  end
@@ -111,6 +113,20 @@ module Slidict
111
113
  1
112
114
  end
113
115
 
116
+ def print_available_models(config)
117
+ client = Llm::Client.new(base_url: config.base_url, api_key: config.api_key, model: nil)
118
+ models = client.list_models
119
+ if models.empty?
120
+ @output.puts "No models available at #{config.base_url}"
121
+ else
122
+ @output.puts "Available models (specify one with --llm-model NAME or SLIDICT_LLM_MODEL=NAME):"
123
+ models.each { |m| @output.puts " #{m}" }
124
+ end
125
+ 0
126
+ rescue Llm::Client::Error => e
127
+ print_error(e)
128
+ end
129
+
114
130
  def llm_required
115
131
  @output.puts "Error: lint requires an LLM endpoint (--llm-base-url or SLIDICT_LLM_BASE_URL)"
116
132
  1
@@ -123,7 +139,8 @@ module Slidict
123
139
  --format FORMAT markdown or asciidoc (default: auto-detected from extension)
124
140
  --llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL)
125
141
  --llm-api-key KEY API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)
126
- --llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL, default: gpt-4o-mini)
142
+ --llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL); omit to list available models
143
+ --translate LANG Translate findings into the given language (e.g. Japanese)
127
144
  -h, --help Show this help
128
145
  HELP
129
146
  0
@@ -6,7 +6,7 @@ module Slidict
6
6
 
7
7
  attr_reader :base_url, :api_key, :model
8
8
 
9
- def initialize(base_url: nil, api_key: nil, model: DEFAULT_MODEL, enabled: true)
9
+ def initialize(base_url: nil, api_key: nil, model: nil, enabled: true)
10
10
  @base_url = base_url
11
11
  @api_key = api_key
12
12
  @model = model
@@ -17,7 +17,7 @@ module Slidict
17
17
  new(
18
18
  base_url: env["SLIDICT_LLM_BASE_URL"],
19
19
  api_key: env["SLIDICT_LLM_API_KEY"],
20
- model: env["SLIDICT_LLM_MODEL"] || DEFAULT_MODEL
20
+ model: env["SLIDICT_LLM_MODEL"]
21
21
  )
22
22
  end
23
23
 
@@ -11,11 +11,11 @@ module Slidict
11
11
  @client = client
12
12
  end
13
13
 
14
- def lint(content, format: "markdown")
14
+ def lint(content, format: "markdown", translate: nil)
15
15
  slides = SlideParser.parse(content, format: format)
16
16
  raise Error, "no slides found in the given file" if slides.empty?
17
17
 
18
- @client.lint_slides(slides)
18
+ @client.lint_slides(slides, translate: translate)
19
19
  end
20
20
  end
21
21
  end
@@ -21,12 +21,18 @@ module Slidict
21
21
  # Checks that the endpoint is reachable before the (slower, more
22
22
  # expensive) chat completion request is made. Raises Error on failure.
23
23
  def verify_connection!
24
- uri = endpoint_uri("models")
25
- request = Net::HTTP::Get.new(uri)
26
- request["Authorization"] = "Bearer #{@api_key}"
27
- response = perform_request(uri, request)
24
+ response = get_models_response
25
+ raise Error, "#{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
26
+ end
28
27
 
28
+ # Returns a sorted array of model IDs available at the endpoint.
29
+ def list_models
30
+ response = get_models_response
29
31
  raise Error, "#{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
32
+
33
+ Array(JSON.parse(response.body)["data"]).map { |m| m["id"] }.sort
34
+ rescue JSON::ParserError => e
35
+ raise Error, "could not parse models response: #{e.message}"
30
36
  end
31
37
 
32
38
  def generate_slides(deck)
@@ -37,13 +43,20 @@ module Slidict
37
43
  # slide_texts is an array of slide bodies (1-indexed by position) as
38
44
  # produced by Slidict::Lint::SlideParser. Returns an array of
39
45
  # Slidict::Lint::Finding.
40
- def lint_slides(slide_texts)
41
- content = chat_completion(lint_prompt_for(slide_texts))
46
+ def lint_slides(slide_texts, translate: nil)
47
+ content = chat_completion(lint_prompt_for(slide_texts, translate: translate))
42
48
  findings_from(content)
43
49
  end
44
50
 
45
51
  private
46
52
 
53
+ def get_models_response
54
+ uri = endpoint_uri("models")
55
+ request = Net::HTTP::Get.new(uri)
56
+ request["Authorization"] = "Bearer #{@api_key}"
57
+ perform_request(uri, request)
58
+ end
59
+
47
60
  def prompt_for(deck)
48
61
  <<~PROMPT
49
62
  You are an assistant that designs presentation slide outlines.
@@ -93,9 +106,10 @@ module Slidict
93
106
  JSON array. Return an empty array [] if you find no issues.
94
107
  PROMPT
95
108
 
96
- def lint_prompt_for(slide_texts)
109
+ def lint_prompt_for(slide_texts, translate: nil)
97
110
  numbered = slide_texts.each_with_index.map { |text, i| "--- Slide #{i + 1} ---\n#{text}" }.join("\n\n")
98
- format(LINT_PROMPT_TEMPLATE, numbered: numbered)
111
+ prompt = format(LINT_PROMPT_TEMPLATE, numbered: numbered)
112
+ translate ? "#{prompt}\nWrite each message field in #{translate}." : prompt
99
113
  end
100
114
 
101
115
  def findings_from(content)
@@ -190,7 +204,7 @@ module Slidict
190
204
  return candidate if candidate
191
205
  end
192
206
 
193
- raise Error, "no JSON array found in model response"
207
+ raise Error, "no JSON array found in model response\nThe model may be too small to follow the required output format. Try using a larger model."
194
208
  end
195
209
 
196
210
  def json_array_from(content, start)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slidict
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slidict
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Abe