asterism-games 0.17.1 → 0.19.1

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.
@@ -380,6 +380,70 @@ module AsterismGames
380
380
  end
381
381
  end
382
382
 
383
+ # A line an AI said (the commentary's one-liners, README "Commentary",
384
+ # "The AI's one-liners"): its text, the tokens as the API counted them
385
+ # ({"in", "out", "cache_write", "cache_read"}), how long it took and
386
+ # why it stopped (the API's stop_reason, e.g. "end_turn").
387
+ Said = Struct.new(:text, :tokens, :ms, :stop, keyword_init: true)
388
+
389
+ # Claude asked for a short line of free text (the commentary's
390
+ # one-liners): the fixed part as the system prompt (cached, so the
391
+ # calls after the first read it from the cache), the changing part as
392
+ # the user's message, the answer the text itself. One try, no retries:
393
+ # a line that comes late is worth nothing. The anthropic gem is
394
+ # loaded here, only when there is a key (it is not a dependency).
395
+ #
396
+ # Thinking is off and effort low: the answer is one or two sentences
397
+ # of 40 characters, and with adaptive thinking Haiku 5.5 spent up to
398
+ # 600 output tokens and 7 s thinking on an open question (the quiet
399
+ # scene). Off is accepted at effort low to high; the request has no
400
+ # tools and no structured output, so none of its limits applies.
401
+ # MAX_TOKENS fits two sentences of 60 characters (about 100 tokens)
402
+ # twice over; an answer cut there is dropped, its stop reason logged.
403
+ class Commentator
404
+ MAX_TOKENS = 200
405
+ THINKING = { type: :disabled }.freeze
406
+
407
+ attr_reader :model
408
+
409
+ def initialize(key, model:, effort: "low", base_url: nil)
410
+ require "anthropic"
411
+ kw = { api_key: key, max_retries: 0 }
412
+ kw[:base_url] = base_url if base_url
413
+ @client = ::Anthropic::Client.new(**kw)
414
+ @model = model
415
+ @effort = effort
416
+ end
417
+
418
+ def say(system:, user:, timeout: 8)
419
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
420
+ msg = @client.messages.create(
421
+ model: @model.to_sym,
422
+ max_tokens: MAX_TOKENS,
423
+ system_: [{ type: "text", text: system, cache_control: { type: "ephemeral" } }],
424
+ thinking: THINKING,
425
+ output_config: { effort: @effort.to_sym },
426
+ messages: [{ role: "user", content: user }],
427
+ request_options: { timeout: timeout }
428
+ )
429
+ u = msg.usage
430
+ tokens = { "in" => u.input_tokens.to_i, "out" => u.output_tokens.to_i,
431
+ "cache_write" => u.cache_creation_input_tokens.to_i, "cache_read" => u.cache_read_input_tokens.to_i }
432
+ stop = msg.stop_reason.to_s
433
+ raise TokenError.new("claude: refused (stop_reason refusal)", tokens) if stop == "refusal"
434
+ unless %w[end_turn stop_sequence].include?(stop)
435
+ raise TokenError.new("claude: cut (stop_reason #{stop}, out #{tokens['out']})", tokens)
436
+ end
437
+ text = msg.content.select { |b| b.type == :text }.map(&:text).join
438
+ Said.new(text: text, tokens: tokens, stop: stop,
439
+ ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round)
440
+ rescue ::Anthropic::Errors::APIStatusError => e
441
+ raise Error, "claude: HTTP #{e.status} #{e.class.name.split('::').last}"
442
+ rescue ::Anthropic::Errors::APIConnectionError => e
443
+ raise Error, "claude: #{e.class.name.split('::').last}"
444
+ end
445
+ end
446
+
383
447
  # A failure after the API counted tokens (they still cost).
384
448
  class TokenError < Error
385
449
  attr_reader :tokens