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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -0
- data/README.md +411 -3
- data/VERSION +1 -1
- data/bin/asterism-games +8 -0
- data/lib/asterism_games/ai.rb +64 -0
- data/lib/asterism_games/commentate/ai_comment.rb +633 -0
- data/lib/asterism_games/commentate/assets/TILES_LICENSE.txt +42 -0
- data/lib/asterism_games/commentate/assets/commentate.css +100 -0
- data/lib/asterism_games/commentate/assets/commentate.js +735 -0
- data/lib/asterism_games/commentate/assets/index.html +26 -0
- data/lib/asterism_games/commentate/assets/tiles_hand.bmp +0 -0
- data/lib/asterism_games/commentate/assets/tiles_river.bmp +0 -0
- data/lib/asterism_games/commentate/mahjong.rb +400 -0
- data/lib/asterism_games/commentate/mahjong_events.rb +320 -0
- data/lib/asterism_games/commentate/mahjong_phrases.rb +535 -0
- data/lib/asterism_games/commentate/mahjong_voice.yml +93 -0
- data/lib/asterism_games/commentate/server.rb +204 -0
- data/lib/asterism_games/commentate/voice.rb +394 -0
- data/lib/asterism_games/commentate/voicevox.rb +110 -0
- data/lib/asterism_games/commentate.rb +223 -0
- data/lib/asterism_games/game.rb +19 -1
- data/lib/asterism_games/host.rb +140 -1
- data/lib/asterism_games/rooms.rb +7 -1
- metadata +19 -3
data/lib/asterism_games/ai.rb
CHANGED
|
@@ -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
|