turnkit 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 +9 -0
- data/LICENSE.md +21 -0
- data/README.md +193 -0
- data/lib/turnkit/adapters/ruby_llm.rb +104 -0
- data/lib/turnkit/agent.rb +73 -0
- data/lib/turnkit/budget.rb +48 -0
- data/lib/turnkit/client.rb +9 -0
- data/lib/turnkit/clock.rb +11 -0
- data/lib/turnkit/conversation.rb +77 -0
- data/lib/turnkit/error.rb +8 -0
- data/lib/turnkit/generators/turnkit/install/templates/conversation.rb +10 -0
- data/lib/turnkit/generators/turnkit/install/templates/create_turnkit_tables.rb +82 -0
- data/lib/turnkit/generators/turnkit/install/templates/initializer.rb +14 -0
- data/lib/turnkit/generators/turnkit/install/templates/message.rb +11 -0
- data/lib/turnkit/generators/turnkit/install/templates/tool_execution.rb +10 -0
- data/lib/turnkit/generators/turnkit/install/templates/turn.rb +14 -0
- data/lib/turnkit/generators/turnkit/install_generator.rb +44 -0
- data/lib/turnkit/id.rb +19 -0
- data/lib/turnkit/memory_store.rb +130 -0
- data/lib/turnkit/message.rb +67 -0
- data/lib/turnkit/message_projection.rb +27 -0
- data/lib/turnkit/rails/railtie.rb +9 -0
- data/lib/turnkit/record.rb +116 -0
- data/lib/turnkit/result.rb +19 -0
- data/lib/turnkit/skill.rb +23 -0
- data/lib/turnkit/store.rb +24 -0
- data/lib/turnkit/stores/active_record_store.rb +190 -0
- data/lib/turnkit/sub_agent_tool.rb +38 -0
- data/lib/turnkit/tool.rb +63 -0
- data/lib/turnkit/tool_call.rb +28 -0
- data/lib/turnkit/tool_execution.rb +28 -0
- data/lib/turnkit/tool_runner.rb +90 -0
- data/lib/turnkit/turn.rb +142 -0
- data/lib/turnkit/usage.rb +28 -0
- data/lib/turnkit/version.rb +5 -0
- data/lib/turnkit.rb +56 -0
- metadata +100 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turnkit
|
|
4
|
+
class ToolExecution < ApplicationRecord
|
|
5
|
+
self.table_name = "<%= table_prefix %>_tool_executions"
|
|
6
|
+
|
|
7
|
+
belongs_to :turn, class_name: "Turnkit::Turn", foreign_key: :turn_uid, primary_key: :uid, inverse_of: :tool_executions
|
|
8
|
+
has_many :messages, class_name: "Turnkit::Message", foreign_key: :tool_execution_uid, primary_key: :uid, dependent: :nullify
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Turnkit
|
|
4
|
+
class Turn < ApplicationRecord
|
|
5
|
+
self.table_name = "<%= table_prefix %>_turns"
|
|
6
|
+
|
|
7
|
+
belongs_to :conversation, class_name: "Turnkit::Conversation", foreign_key: :conversation_uid, primary_key: :uid, inverse_of: :turns
|
|
8
|
+
belongs_to :parent_turn, class_name: "Turnkit::Turn", foreign_key: :parent_turn_uid, primary_key: :uid, optional: true
|
|
9
|
+
belongs_to :parent_tool_execution, class_name: "Turnkit::ToolExecution", foreign_key: :parent_tool_execution_uid, primary_key: :uid, optional: true
|
|
10
|
+
|
|
11
|
+
has_many :messages, class_name: "Turnkit::Message", foreign_key: :turn_uid, primary_key: :uid, dependent: :nullify, inverse_of: :turn
|
|
12
|
+
has_many :tool_executions, class_name: "Turnkit::ToolExecution", foreign_key: :turn_uid, primary_key: :uid, dependent: :destroy, inverse_of: :turn
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module TurnKit
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("install/templates", __dir__)
|
|
12
|
+
|
|
13
|
+
class_option :table_prefix, type: :string, default: "turnkit", desc: "Database table prefix."
|
|
14
|
+
|
|
15
|
+
def copy_initializer
|
|
16
|
+
template "initializer.rb", "config/initializers/turnkit.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_models
|
|
20
|
+
template "conversation.rb", "app/models/turnkit/conversation.rb"
|
|
21
|
+
template "turn.rb", "app/models/turnkit/turn.rb"
|
|
22
|
+
template "message.rb", "app/models/turnkit/message.rb"
|
|
23
|
+
template "tool_execution.rb", "app/models/turnkit/tool_execution.rb"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def copy_migration
|
|
27
|
+
migration_template "create_turnkit_tables.rb", "db/migrate/create_turnkit_tables.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.next_migration_number(dirname)
|
|
31
|
+
if ActiveRecord::Base.timestamped_migrations
|
|
32
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
33
|
+
else
|
|
34
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
def table_prefix
|
|
40
|
+
options[:table_prefix]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/turnkit/id.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
module Id
|
|
5
|
+
PREFIXES = {
|
|
6
|
+
conversation: "conv",
|
|
7
|
+
message: "msg",
|
|
8
|
+
turn: "turn",
|
|
9
|
+
tool_execution: "tool"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def generate(type)
|
|
15
|
+
prefix = PREFIXES.fetch(type)
|
|
16
|
+
"#{prefix}_#{SecureRandom.hex(12)}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class MemoryStore < Store
|
|
5
|
+
def initialize
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
@conversations = {}
|
|
8
|
+
@turns = {}
|
|
9
|
+
@messages = {}
|
|
10
|
+
@tool_executions = {}
|
|
11
|
+
@message_sequences = Hash.new(0)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_conversation(attributes)
|
|
15
|
+
record = Record.conversation(attributes)
|
|
16
|
+
|
|
17
|
+
@mutex.synchronize { @conversations[record.fetch("id")] = record }
|
|
18
|
+
record.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_conversation(id)
|
|
22
|
+
@mutex.synchronize { duplicate(@conversations.fetch(id)) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def next_message_sequence(conversation_id)
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
@message_sequences[conversation_id] += 1
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def latest_message_sequence(conversation_id)
|
|
32
|
+
@mutex.synchronize { @message_sequences[conversation_id].to_i }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def append_message(attributes)
|
|
36
|
+
attrs = stringify(attributes)
|
|
37
|
+
attrs["sequence"] ||= next_message_sequence(attrs.fetch("conversation_id"))
|
|
38
|
+
message = Record.message(attrs)
|
|
39
|
+
@mutex.synchronize { @messages[message.fetch("id")] = message }
|
|
40
|
+
duplicate(message)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def list_messages(conversation_id, through_sequence: nil, turn_id: nil)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
rows = @messages.values.select { |message| message["conversation_id"] == conversation_id }
|
|
46
|
+
rows = rows.select { |message| message["sequence"].to_i <= through_sequence.to_i || message["turn_id"] == turn_id } if through_sequence
|
|
47
|
+
rows.sort_by { |message| [ message["sequence"].to_i, message["created_at"].to_f, message["id"] ] }.map { |message| duplicate(message) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_turn(attributes)
|
|
52
|
+
record = Record.turn(attributes)
|
|
53
|
+
|
|
54
|
+
@mutex.synchronize { @turns[record.fetch("id")] = record }
|
|
55
|
+
duplicate(record)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def load_turn(id)
|
|
59
|
+
@mutex.synchronize { duplicate(@turns.fetch(id)) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update_turn(id, attributes)
|
|
63
|
+
attrs = Record.turn_update(attributes)
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
record = @turns.fetch(id)
|
|
66
|
+
record.merge!(attrs.merge("updated_at" => Clock.now))
|
|
67
|
+
duplicate(record)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def list_turns(root_turn_id: nil, conversation_id: nil)
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
rows = @turns.values
|
|
74
|
+
rows = rows.select { |turn| turn["root_turn_id"] == root_turn_id } if root_turn_id
|
|
75
|
+
rows = rows.select { |turn| turn["conversation_id"] == conversation_id } if conversation_id
|
|
76
|
+
rows.sort_by { |turn| [ turn["created_at"].to_f, turn["id"] ] }.map { |turn| duplicate(turn) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_tool_execution(attributes)
|
|
81
|
+
record = Record.tool_execution(attributes)
|
|
82
|
+
|
|
83
|
+
@mutex.synchronize { @tool_executions[record.fetch("id")] = record }
|
|
84
|
+
duplicate(record)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_tool_execution(id)
|
|
88
|
+
@mutex.synchronize { duplicate(@tool_executions.fetch(id)) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def update_tool_execution(id, attributes)
|
|
92
|
+
attrs = Record.tool_execution_update(attributes)
|
|
93
|
+
@mutex.synchronize do
|
|
94
|
+
record = @tool_executions.fetch(id)
|
|
95
|
+
record.merge!(attrs.merge("updated_at" => Clock.now))
|
|
96
|
+
duplicate(record)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def list_tool_executions(turn_id:)
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
@tool_executions.values
|
|
103
|
+
.select { |execution| execution["turn_id"] == turn_id }
|
|
104
|
+
.sort_by { |execution| [ execution["created_at"].to_f, execution["id"] ] }
|
|
105
|
+
.map { |execution| duplicate(execution) }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def find_stale_turns(before:)
|
|
110
|
+
@mutex.synchronize do
|
|
111
|
+
@turns.values.select do |turn|
|
|
112
|
+
%w[pending running].include?(turn["status"]) && stale_anchor(turn) && stale_anchor(turn) < before
|
|
113
|
+
end.map { |turn| duplicate(turn) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
def stringify(hash)
|
|
119
|
+
hash.transform_keys(&:to_s)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def duplicate(value)
|
|
123
|
+
Marshal.load(Marshal.dump(value))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def stale_anchor(turn)
|
|
127
|
+
turn["heartbeat_at"] || turn["started_at"] || turn["created_at"]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class Message
|
|
5
|
+
ROLES = %w[user assistant tool].freeze
|
|
6
|
+
KINDS = %w[text tool_call tool_result].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :id, :conversation_id, :turn_id, :role, :kind, :sequence
|
|
9
|
+
attr_reader :content, :text, :tool_execution_id, :provider_message_id, :metadata, :created_at
|
|
10
|
+
|
|
11
|
+
def initialize(attributes = {})
|
|
12
|
+
attrs = stringify(attributes)
|
|
13
|
+
@id = attrs["id"] || Id.generate(:message)
|
|
14
|
+
@conversation_id = attrs.fetch("conversation_id")
|
|
15
|
+
@turn_id = attrs["turn_id"]
|
|
16
|
+
@role = attrs.fetch("role").to_s
|
|
17
|
+
@kind = attrs.fetch("kind", "text").to_s
|
|
18
|
+
@sequence = attrs.fetch("sequence").to_i
|
|
19
|
+
@content = normalize_content(attrs["content"] || attrs["text"])
|
|
20
|
+
@text = attrs["text"] || extract_text(@content)
|
|
21
|
+
@tool_execution_id = attrs["tool_execution_id"]
|
|
22
|
+
@provider_message_id = attrs["provider_message_id"]
|
|
23
|
+
@metadata = attrs["metadata"] || {}
|
|
24
|
+
@created_at = attrs["created_at"] || Clock.now
|
|
25
|
+
|
|
26
|
+
validate!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{
|
|
31
|
+
"id" => id,
|
|
32
|
+
"conversation_id" => conversation_id,
|
|
33
|
+
"turn_id" => turn_id,
|
|
34
|
+
"role" => role,
|
|
35
|
+
"kind" => kind,
|
|
36
|
+
"sequence" => sequence,
|
|
37
|
+
"content" => content,
|
|
38
|
+
"text" => text,
|
|
39
|
+
"tool_execution_id" => tool_execution_id,
|
|
40
|
+
"provider_message_id" => provider_message_id,
|
|
41
|
+
"metadata" => metadata,
|
|
42
|
+
"created_at" => created_at
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
def stringify(hash)
|
|
48
|
+
hash.transform_keys(&:to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def normalize_content(value)
|
|
52
|
+
return value if value.is_a?(Array)
|
|
53
|
+
|
|
54
|
+
[ { "type" => "text", "text" => value.to_s } ]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_text(blocks)
|
|
58
|
+
Array(blocks).filter_map { |block| block.is_a?(Hash) ? block["text"] || block[:text] : nil }.join("\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate!
|
|
62
|
+
raise ArgumentError, "unknown role: #{role}" unless ROLES.include?(role)
|
|
63
|
+
raise ArgumentError, "unknown kind: #{kind}" unless KINDS.include?(kind)
|
|
64
|
+
raise ArgumentError, "sequence must be positive" unless sequence.positive?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class MessageProjection
|
|
5
|
+
def self.for(messages)
|
|
6
|
+
messages.map { |message| new(message).to_h }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(message)
|
|
10
|
+
@message = message
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
case message.kind
|
|
15
|
+
when "tool_call"
|
|
16
|
+
{ role: :assistant, content: message.text, tool_calls: message.metadata.fetch("tool_calls", []) }
|
|
17
|
+
when "tool_result"
|
|
18
|
+
{ role: :tool, content: message.text, tool_call_id: message.metadata["tool_call_id"] }
|
|
19
|
+
else
|
|
20
|
+
{ role: message.role.to_sym, content: message.text }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
attr_reader :message
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
module Record
|
|
5
|
+
TURN_STATUSES = %w[pending running completed failed cancelled stale].freeze
|
|
6
|
+
TOOL_EXECUTION_STATUSES = %w[pending running completed failed cancelled].freeze
|
|
7
|
+
|
|
8
|
+
TURN_UPDATE_KEYS = %w[status options usage cost error output_text started_at heartbeat_at completed_at].freeze
|
|
9
|
+
TOOL_EXECUTION_UPDATE_KEYS = %w[status result error started_at completed_at].freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def conversation(attributes)
|
|
14
|
+
attrs = stringify(attributes)
|
|
15
|
+
now = Clock.now
|
|
16
|
+
{
|
|
17
|
+
"id" => attrs["id"] || Id.generate(:conversation),
|
|
18
|
+
"agent_name" => attrs["agent_name"],
|
|
19
|
+
"model" => attrs["model"],
|
|
20
|
+
"subject" => attrs["subject"],
|
|
21
|
+
"metadata" => attrs["metadata"] || {},
|
|
22
|
+
"created_at" => attrs["created_at"] || now,
|
|
23
|
+
"updated_at" => attrs["updated_at"] || now
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def message(attributes)
|
|
28
|
+
Message.new(attributes).to_h
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def turn(attributes)
|
|
32
|
+
attrs = stringify(attributes)
|
|
33
|
+
id = attrs["id"] || Id.generate(:turn)
|
|
34
|
+
status = attrs["status"] || "pending"
|
|
35
|
+
assert_status!(status, TURN_STATUSES, "turn")
|
|
36
|
+
now = Clock.now
|
|
37
|
+
{
|
|
38
|
+
"id" => id,
|
|
39
|
+
"conversation_id" => attrs.fetch("conversation_id"),
|
|
40
|
+
"agent_name" => attrs["agent_name"],
|
|
41
|
+
"parent_turn_id" => attrs["parent_turn_id"],
|
|
42
|
+
"parent_tool_execution_id" => attrs["parent_tool_execution_id"],
|
|
43
|
+
"root_turn_id" => attrs["root_turn_id"] || id,
|
|
44
|
+
"context_message_sequence" => attrs["context_message_sequence"].to_i,
|
|
45
|
+
"status" => status,
|
|
46
|
+
"model" => attrs["model"],
|
|
47
|
+
"options" => attrs["options"] || {},
|
|
48
|
+
"usage" => attrs["usage"] || {},
|
|
49
|
+
"cost" => attrs["cost"],
|
|
50
|
+
"error" => attrs["error"],
|
|
51
|
+
"output_text" => attrs["output_text"],
|
|
52
|
+
"started_at" => attrs["started_at"],
|
|
53
|
+
"heartbeat_at" => attrs["heartbeat_at"],
|
|
54
|
+
"completed_at" => attrs["completed_at"],
|
|
55
|
+
"created_at" => attrs["created_at"] || now,
|
|
56
|
+
"updated_at" => attrs["updated_at"] || now
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def tool_execution(attributes)
|
|
61
|
+
attrs = stringify(attributes)
|
|
62
|
+
status = attrs["status"] || "pending"
|
|
63
|
+
assert_status!(status, TOOL_EXECUTION_STATUSES, "tool execution")
|
|
64
|
+
now = Clock.now
|
|
65
|
+
{
|
|
66
|
+
"id" => attrs["id"] || Id.generate(:tool_execution),
|
|
67
|
+
"turn_id" => attrs.fetch("turn_id"),
|
|
68
|
+
"tool_call_id" => attrs.fetch("tool_call_id"),
|
|
69
|
+
"tool_name" => attrs.fetch("tool_name"),
|
|
70
|
+
"status" => status,
|
|
71
|
+
"arguments" => attrs["arguments"] || {},
|
|
72
|
+
"result" => attrs["result"],
|
|
73
|
+
"error" => attrs["error"],
|
|
74
|
+
"started_at" => attrs["started_at"],
|
|
75
|
+
"completed_at" => attrs["completed_at"],
|
|
76
|
+
"created_at" => attrs["created_at"] || now,
|
|
77
|
+
"updated_at" => attrs["updated_at"] || now
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def turn_update(attributes)
|
|
82
|
+
update(attributes, TURN_UPDATE_KEYS, TURN_STATUSES, "turn")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def tool_execution_update(attributes)
|
|
86
|
+
update(attributes, TOOL_EXECUTION_UPDATE_KEYS, TOOL_EXECUTION_STATUSES, "tool execution")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def stringify(hash)
|
|
90
|
+
hash.transform_keys(&:to_s)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def subject_pair(subject)
|
|
94
|
+
case subject
|
|
95
|
+
when Hash
|
|
96
|
+
attrs = stringify(subject)
|
|
97
|
+
[ attrs["type"] || attrs["class"], attrs["id"] ]
|
|
98
|
+
else
|
|
99
|
+
[ subject&.class&.name, subject&.respond_to?(:id) ? subject.id : nil ]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def assert_status!(status, allowed, name)
|
|
104
|
+
raise ArgumentError, "unknown #{name} status: #{status}" unless allowed.include?(status.to_s)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def update(attributes, allowed_keys, allowed_statuses, name)
|
|
108
|
+
attrs = stringify(attributes)
|
|
109
|
+
unknown = attrs.keys - allowed_keys
|
|
110
|
+
raise ArgumentError, "unknown #{name} update attributes: #{unknown.join(", ")}" if unknown.any?
|
|
111
|
+
|
|
112
|
+
assert_status!(attrs["status"], allowed_statuses, name) if attrs.key?("status")
|
|
113
|
+
attrs
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :text, :tool_calls, :usage, :model, :finish_reason
|
|
6
|
+
|
|
7
|
+
def initialize(text: "", tool_calls: [], usage: Usage.new, model: nil, finish_reason: nil)
|
|
8
|
+
@text = text.to_s
|
|
9
|
+
@tool_calls = Array(tool_calls)
|
|
10
|
+
@usage = usage || Usage.new
|
|
11
|
+
@model = model
|
|
12
|
+
@finish_reason = finish_reason
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tool_calls?
|
|
16
|
+
tool_calls.any?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class Skill
|
|
5
|
+
attr_reader :key, :name, :description, :content
|
|
6
|
+
|
|
7
|
+
def self.from_file(path, key: nil, name: nil, description: "")
|
|
8
|
+
content = File.read(path)
|
|
9
|
+
base = File.basename(path, File.extname(path))
|
|
10
|
+
new(key: key || base, name: name || base.tr("_-", " ").split.map(&:capitalize).join(" "), description: description, content: content)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(key:, name:, content:, description: "")
|
|
14
|
+
@key = key.to_s
|
|
15
|
+
@name = name.to_s
|
|
16
|
+
@description = description.to_s
|
|
17
|
+
@content = content.to_s
|
|
18
|
+
raise ArgumentError, "key is required" if @key.empty?
|
|
19
|
+
raise ArgumentError, "name is required" if @name.empty?
|
|
20
|
+
raise ArgumentError, "content is required" if @content.empty?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurnKit
|
|
4
|
+
class Store
|
|
5
|
+
def create_conversation(_attributes) = raise(NotImplementedError)
|
|
6
|
+
def load_conversation(_id) = raise(NotImplementedError)
|
|
7
|
+
|
|
8
|
+
def next_message_sequence(_conversation_id) = raise(NotImplementedError)
|
|
9
|
+
def append_message(_attributes) = raise(NotImplementedError)
|
|
10
|
+
def list_messages(_conversation_id, through_sequence: nil, turn_id: nil) = raise(NotImplementedError)
|
|
11
|
+
|
|
12
|
+
def create_turn(_attributes) = raise(NotImplementedError)
|
|
13
|
+
def load_turn(_id) = raise(NotImplementedError)
|
|
14
|
+
def update_turn(_id, _attributes) = raise(NotImplementedError)
|
|
15
|
+
def list_turns(root_turn_id: nil, conversation_id: nil) = raise(NotImplementedError)
|
|
16
|
+
|
|
17
|
+
def create_tool_execution(_attributes) = raise(NotImplementedError)
|
|
18
|
+
def load_tool_execution(_id) = raise(NotImplementedError)
|
|
19
|
+
def update_tool_execution(_id, _attributes) = raise(NotImplementedError)
|
|
20
|
+
def list_tool_executions(turn_id:) = raise(NotImplementedError)
|
|
21
|
+
|
|
22
|
+
def find_stale_turns(before:) = []
|
|
23
|
+
end
|
|
24
|
+
end
|