legion-data 1.7.3 → 1.8.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 +4 -4
- data/.pre-commit-config.yaml +29 -0
- data/AGENTS.md +66 -13
- data/CHANGELOG.md +29 -0
- data/CLAUDE.md +44 -307
- data/README.md +119 -7
- data/lib/legion/data/connection.rb +3 -1
- data/lib/legion/data/migrations/077_create_llm_conversations.rb +32 -0
- data/lib/legion/data/migrations/078_create_llm_messages.rb +33 -0
- data/lib/legion/data/migrations/079_create_llm_message_inference_requests.rb +47 -0
- data/lib/legion/data/migrations/080_create_llm_message_inference_responses.rb +39 -0
- data/lib/legion/data/migrations/081_add_llm_message_inference_foreign_keys.rb +17 -0
- data/lib/legion/data/migrations/082_create_llm_route_attempts.rb +31 -0
- data/lib/legion/data/migrations/083_create_llm_message_inference_metrics.rb +36 -0
- data/lib/legion/data/migrations/084_create_llm_tool_calls.rb +32 -0
- data/lib/legion/data/migrations/085_add_llm_message_tool_call_foreign_key.rb +15 -0
- data/lib/legion/data/migrations/086_create_llm_tool_call_attempts.rb +30 -0
- data/lib/legion/data/migrations/087_create_llm_conversation_compactions.rb +31 -0
- data/lib/legion/data/migrations/088_create_llm_policy_evaluations.rb +33 -0
- data/lib/legion/data/migrations/089_create_llm_security_events.rb +33 -0
- data/lib/legion/data/migrations/090_create_llm_registry_events.rb +23 -0
- data/lib/legion/data/migrations/091_create_portable_identity_providers.rb +35 -0
- data/lib/legion/data/migrations/092_create_portable_identity_principals.rb +25 -0
- data/lib/legion/data/migrations/093_create_portable_identities.rb +31 -0
- data/lib/legion/data/migrations/094_create_portable_identity_groups.rb +21 -0
- data/lib/legion/data/migrations/095_create_portable_identity_group_memberships.rb +25 -0
- data/lib/legion/data/migrations/096_create_portable_identity_audit_log.rb +26 -0
- data/lib/legion/data/migrations/097_add_llm_dispatch_fields.rb +16 -0
- data/lib/legion/data/model.rb +11 -1
- data/lib/legion/data/models/apollo/access_log.rb +17 -0
- data/lib/legion/data/models/apollo/entries.rb +22 -0
- data/lib/legion/data/models/apollo/expertise.rb +16 -0
- data/lib/legion/data/models/apollo/model_helpers.rb +17 -0
- data/lib/legion/data/models/apollo/operation.rb +16 -0
- data/lib/legion/data/models/apollo/relation.rb +18 -0
- data/lib/legion/data/models/function.rb +1 -0
- data/lib/legion/data/models/identity/audit_log.rb +20 -0
- data/lib/legion/data/models/identity/group.rb +28 -0
- data/lib/legion/data/models/identity/group_memberships.rb +28 -0
- data/lib/legion/data/models/identity/identity.rb +24 -0
- data/lib/legion/data/models/identity/model_helpers.rb +86 -0
- data/lib/legion/data/models/identity/principal.rb +37 -0
- data/lib/legion/data/models/identity/providers.rb +34 -0
- data/lib/legion/data/models/identity.rb +8 -0
- data/lib/legion/data/models/identity_group.rb +13 -0
- data/lib/legion/data/models/identity_provider.rb +8 -0
- data/lib/legion/data/models/llm/conversation.rb +25 -0
- data/lib/legion/data/models/llm/conversation_compaction.rb +22 -0
- data/lib/legion/data/models/llm/message.rb +105 -0
- data/lib/legion/data/models/llm/message_inference_metric.rb +46 -0
- data/lib/legion/data/models/llm/message_inference_request.rb +80 -0
- data/lib/legion/data/models/llm/message_inference_response.rb +23 -0
- data/lib/legion/data/models/llm/model_helpers.rb +18 -0
- data/lib/legion/data/models/llm/policy_evaluation.rb +20 -0
- data/lib/legion/data/models/llm/registry_event.rb +15 -0
- data/lib/legion/data/models/llm/route_attempt.rb +18 -0
- data/lib/legion/data/models/llm/security_event.rb +66 -0
- data/lib/legion/data/models/llm/tool_call.rb +21 -0
- data/lib/legion/data/models/llm/tool_call_attempt.rb +18 -0
- data/lib/legion/data/models/node.rb +2 -1
- data/lib/legion/data/models/principal.rb +13 -0
- data/lib/legion/data/models/rbac/cross_team_grants.rb +25 -0
- data/lib/legion/data/models/rbac/model_helpers.rb +25 -0
- data/lib/legion/data/models/rbac/role_assignments.rb +25 -0
- data/lib/legion/data/models/rbac/runner_grants.rb +23 -0
- data/lib/legion/data/models/relationship.rb +1 -0
- data/lib/legion/data/models/runner.rb +24 -2
- data/lib/legion/data/models/task.rb +4 -0
- data/lib/legion/data/version.rb +1 -1
- data/scripts/pre-commit-rubocop.sh +16 -0
- metadata +54 -1
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'identity/model_helpers'
|
|
4
|
+
|
|
3
5
|
return unless Legion::Data::Connection.adapter == :postgres
|
|
4
6
|
|
|
5
7
|
module Legion
|
|
6
8
|
module Data
|
|
7
9
|
module Model
|
|
8
10
|
class IdentityProvider < Sequel::Model(:identity_providers)
|
|
11
|
+
include Identity::ModelHelpers
|
|
12
|
+
|
|
9
13
|
one_to_many :identities, class: 'Legion::Data::Model::Identity'
|
|
10
14
|
|
|
15
|
+
def self.lookup_columns
|
|
16
|
+
%i[id uuid name]
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
def parsed_capabilities
|
|
12
20
|
Array(capabilities)
|
|
13
21
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class Conversation < Sequel::Model(:llm_conversations)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
one_to_many :messages
|
|
13
|
+
one_to_many :message_inference_requests
|
|
14
|
+
one_to_many :conversation_compactions
|
|
15
|
+
one_to_many :policy_evaluations
|
|
16
|
+
one_to_many :security_events
|
|
17
|
+
|
|
18
|
+
def security_incident_lineage
|
|
19
|
+
SecurityEvent.lineage_for_conversation(self)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class ConversationCompaction < Sequel::Model(:llm_conversation_compactions)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :conversation
|
|
13
|
+
many_to_one :triggered_by_message_inference_request,
|
|
14
|
+
class: 'Legion::Data::Models::LLM::MessageInferenceRequest',
|
|
15
|
+
key: :triggered_by_message_inference_request_id
|
|
16
|
+
many_to_one :replaces_message_from, class: 'Legion::Data::Models::LLM::Message', key: :replaces_message_from_id
|
|
17
|
+
many_to_one :replaces_message_to, class: 'Legion::Data::Models::LLM::Message', key: :replaces_message_to_id
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class Message < Sequel::Model(:llm_messages)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :conversation
|
|
13
|
+
many_to_one :parent_message, class: 'Legion::Data::Models::LLM::Message', key: :parent_message_id
|
|
14
|
+
many_to_one :message_inference_request
|
|
15
|
+
many_to_one :message_inference_response
|
|
16
|
+
many_to_one :tool_call
|
|
17
|
+
|
|
18
|
+
one_to_many :child_messages, class: 'Legion::Data::Models::LLM::Message', key: :parent_message_id
|
|
19
|
+
one_to_many :triggered_message_inference_requests,
|
|
20
|
+
class: 'Legion::Data::Models::LLM::MessageInferenceRequest',
|
|
21
|
+
key: :latest_message_id
|
|
22
|
+
one_to_many :message_inference_responses,
|
|
23
|
+
class: 'Legion::Data::Models::LLM::MessageInferenceResponse',
|
|
24
|
+
key: :response_message_id
|
|
25
|
+
one_to_many :requested_tool_calls, class: 'Legion::Data::Models::LLM::ToolCall',
|
|
26
|
+
key: :requested_by_message_id
|
|
27
|
+
one_to_many :result_tool_calls, class: 'Legion::Data::Models::LLM::ToolCall',
|
|
28
|
+
key: :result_message_id
|
|
29
|
+
one_to_many :compactions_from, class: 'Legion::Data::Models::LLM::ConversationCompaction',
|
|
30
|
+
key: :replaces_message_from_id
|
|
31
|
+
one_to_many :compactions_to, class: 'Legion::Data::Models::LLM::ConversationCompaction',
|
|
32
|
+
key: :replaces_message_to_id
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def incident_flow_from(message_or_id)
|
|
36
|
+
message = message_or_id.is_a?(self) ? message_or_id : self[message_or_id]
|
|
37
|
+
message&.incident_flow
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def incident_flow
|
|
42
|
+
requests = incident_flow_requests
|
|
43
|
+
responses = incident_flow_responses(requests)
|
|
44
|
+
route_attempts = RouteAttempt.where(message_inference_request_id: requests.map(&:id))
|
|
45
|
+
.order(:message_inference_request_id, :attempt_no, :id)
|
|
46
|
+
.all
|
|
47
|
+
tool_calls = incident_flow_tool_calls(responses)
|
|
48
|
+
tool_call_attempts = ToolCallAttempt.where(tool_call_id: tool_calls.map(&:id))
|
|
49
|
+
.order(:tool_call_id, :attempt_no, :id)
|
|
50
|
+
.all
|
|
51
|
+
|
|
52
|
+
{
|
|
53
|
+
message: self,
|
|
54
|
+
conversation: conversation,
|
|
55
|
+
requests: requests,
|
|
56
|
+
route_attempts: route_attempts,
|
|
57
|
+
responses: responses,
|
|
58
|
+
response_messages: responses.filter_map(&:response_message),
|
|
59
|
+
tool_calls: tool_calls,
|
|
60
|
+
tool_call_attempts: tool_call_attempts,
|
|
61
|
+
result_messages: incident_flow_result_messages(responses, tool_calls)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def incident_flow_requests
|
|
68
|
+
request_ids = []
|
|
69
|
+
request_ids << message_inference_request_id if message_inference_request_id
|
|
70
|
+
request_ids.concat(MessageInferenceRequest.where(latest_message_id: id).select_map(:id))
|
|
71
|
+
if message_inference_response_id && (linked_response = MessageInferenceResponse[message_inference_response_id])
|
|
72
|
+
request_ids << linked_response.message_inference_request_id
|
|
73
|
+
end
|
|
74
|
+
if tool_call_id && (linked_tool_call = ToolCall[tool_call_id])
|
|
75
|
+
request_ids << linked_tool_call.message_inference_response.message_inference_request_id
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
MessageInferenceRequest.where(id: request_ids.uniq).order(:id).all
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def incident_flow_responses(requests)
|
|
82
|
+
request_ids = requests.map(&:id)
|
|
83
|
+
response_scope = MessageInferenceResponse.where(message_inference_request_id: request_ids)
|
|
84
|
+
response_scope = response_scope.or(id: message_inference_response_id) if message_inference_response_id
|
|
85
|
+
response_scope.order(:id).all
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def incident_flow_tool_calls(responses)
|
|
89
|
+
response_ids = responses.map(&:id)
|
|
90
|
+
scope = ToolCall.where(message_inference_response_id: response_ids)
|
|
91
|
+
scope = scope.or(requested_by_message_id: id).or(result_message_id: id)
|
|
92
|
+
scope.order(:message_inference_response_id, :tool_call_index, :id).all
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def incident_flow_result_messages(responses, tool_calls)
|
|
96
|
+
message_ids = responses.filter_map(&:response_message_id) + tool_calls.filter_map(&:result_message_id)
|
|
97
|
+
scope = Message.where(id: message_ids.uniq)
|
|
98
|
+
scope = scope.or(tool_call_id: tool_calls.map(&:id)) unless tool_calls.empty?
|
|
99
|
+
scope.order(:seq, :id).all
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class MessageInferenceMetric < Sequel::Model(:llm_message_inference_metrics)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :message_inference_request
|
|
13
|
+
many_to_one :message_inference_response
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def finance_usage_by_cost_center_model_day(cost_center: nil, model_key: nil, from: nil, to: nil)
|
|
17
|
+
usage_day = Sequel.function(:date, :recorded_at)
|
|
18
|
+
scope = dataset
|
|
19
|
+
scope = scope.where(cost_center: cost_center) unless cost_center.nil?
|
|
20
|
+
scope = scope.where(model_key: model_key) unless model_key.nil?
|
|
21
|
+
scope = scope.where { recorded_at >= from } unless from.nil?
|
|
22
|
+
scope = scope.where { recorded_at < to } unless to.nil?
|
|
23
|
+
|
|
24
|
+
scope
|
|
25
|
+
.select(
|
|
26
|
+
:cost_center,
|
|
27
|
+
:model_key,
|
|
28
|
+
usage_day.as(:usage_day),
|
|
29
|
+
Sequel.function(:sum, :input_tokens).as(:input_tokens),
|
|
30
|
+
Sequel.function(:sum, :output_tokens).as(:output_tokens),
|
|
31
|
+
Sequel.function(:sum, :thinking_tokens).as(:thinking_tokens),
|
|
32
|
+
Sequel.function(:sum, :total_tokens).as(:total_tokens),
|
|
33
|
+
Sequel.function(:sum, :cost_usd).as(:cost_usd),
|
|
34
|
+
Sequel.function(:sum, :latency_ms).as(:latency_ms),
|
|
35
|
+
Sequel.function(:sum, :wall_clock_ms).as(:wall_clock_ms)
|
|
36
|
+
)
|
|
37
|
+
.group(:cost_center, :model_key, usage_day)
|
|
38
|
+
.order(:cost_center, :model_key, usage_day)
|
|
39
|
+
.map(&:values)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class MessageInferenceRequest < Sequel::Model(:llm_message_inference_requests)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :conversation
|
|
13
|
+
many_to_one :latest_message, class: 'Legion::Data::Models::LLM::Message', key: :latest_message_id
|
|
14
|
+
one_to_many :message_inference_responses
|
|
15
|
+
one_to_many :route_attempts
|
|
16
|
+
one_to_many :message_inference_metrics
|
|
17
|
+
one_to_many :conversation_compactions, key: :triggered_by_message_inference_request_id
|
|
18
|
+
one_to_many :policy_evaluations
|
|
19
|
+
one_to_many :security_events
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def lookup(reference)
|
|
23
|
+
return reference if reference.is_a?(self)
|
|
24
|
+
|
|
25
|
+
value = reference.to_s
|
|
26
|
+
scope = where(uuid: value).or(request_ref: value)
|
|
27
|
+
scope = scope.or(id: value.to_i) if value.match?(/\A\d+\z/)
|
|
28
|
+
scope.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def audit_lineage_for(reference)
|
|
32
|
+
lookup(reference)&.audit_lineage
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def audit_lineage
|
|
37
|
+
responses = message_inference_responses_dataset.order(:id).all
|
|
38
|
+
response_ids = responses.map(&:id)
|
|
39
|
+
tool_calls = ToolCall.where(message_inference_response_id: response_ids).order(:tool_call_index, :id).all
|
|
40
|
+
tool_call_ids = tool_calls.map(&:id)
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
request: self,
|
|
44
|
+
request_id: id,
|
|
45
|
+
request_ref: request_ref,
|
|
46
|
+
conversation: conversation,
|
|
47
|
+
latest_message: latest_message,
|
|
48
|
+
caller_principal: caller_principal,
|
|
49
|
+
caller_identity: caller_identity,
|
|
50
|
+
route_attempts: route_attempts_dataset.order(:attempt_no, :id).all,
|
|
51
|
+
responses: responses,
|
|
52
|
+
response_messages: responses.filter_map(&:response_message),
|
|
53
|
+
metrics: message_inference_metrics_dataset.order(:recorded_at, :id).all,
|
|
54
|
+
policy_evaluations: policy_evaluations_dataset.order(:evaluated_at, :id).all,
|
|
55
|
+
security_events: security_events_dataset.order(:detected_at, :id).all,
|
|
56
|
+
tool_calls: tool_calls,
|
|
57
|
+
tool_call_attempts: ToolCallAttempt.where(tool_call_id: tool_call_ids).order(:tool_call_id, :attempt_no, :id).all
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def request
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def caller_principal
|
|
66
|
+
return nil unless caller_principal_id && defined?(Legion::Data::Model::Identity::Principal)
|
|
67
|
+
|
|
68
|
+
Legion::Data::Model::Identity::Principal.first(id: caller_principal_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def caller_identity
|
|
72
|
+
return nil unless caller_identity_id && defined?(Legion::Data::Model::Identity::Identity)
|
|
73
|
+
|
|
74
|
+
Legion::Data::Model::Identity::Identity.first(id: caller_identity_id)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class MessageInferenceResponse < Sequel::Model(:llm_message_inference_responses)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :message_inference_request
|
|
13
|
+
many_to_one :response_message, class: 'Legion::Data::Models::LLM::Message', key: :response_message_id
|
|
14
|
+
one_to_many :route_attempts
|
|
15
|
+
one_to_many :message_inference_metrics
|
|
16
|
+
one_to_many :tool_calls
|
|
17
|
+
one_to_many :policy_evaluations
|
|
18
|
+
one_to_many :security_events
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
module ModelHelpers
|
|
10
|
+
def before_create
|
|
11
|
+
self[:uuid] ||= SecureRandom.uuid if columns.include?(:uuid)
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class PolicyEvaluation < Sequel::Model(:llm_policy_evaluations)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :conversation
|
|
13
|
+
many_to_one :message_inference_request
|
|
14
|
+
many_to_one :message_inference_response
|
|
15
|
+
one_to_many :security_events
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class RouteAttempt < Sequel::Model(:llm_route_attempts)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :message_inference_request
|
|
13
|
+
many_to_one :message_inference_response
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class SecurityEvent < Sequel::Model(:llm_security_events)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :conversation
|
|
13
|
+
many_to_one :message_inference_request
|
|
14
|
+
many_to_one :message_inference_response
|
|
15
|
+
many_to_one :tool_call
|
|
16
|
+
many_to_one :tool_call_attempt
|
|
17
|
+
many_to_one :policy_evaluation
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def lineage_for_conversation(conversation_or_id)
|
|
21
|
+
conversation_id = conversation_or_id.respond_to?(:id) ? conversation_or_id.id : conversation_or_id
|
|
22
|
+
requests = MessageInferenceRequest.where(conversation_id: conversation_id).order(:id).all
|
|
23
|
+
request_ids = requests.map(&:id)
|
|
24
|
+
responses = MessageInferenceResponse.where(message_inference_request_id: request_ids).order(:id).all
|
|
25
|
+
response_ids = responses.map(&:id)
|
|
26
|
+
tool_calls = ToolCall.where(message_inference_response_id: response_ids).order(:tool_call_index, :id).all
|
|
27
|
+
tool_call_ids = tool_calls.map(&:id)
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
conversation: Conversation[conversation_id],
|
|
31
|
+
messages: Message.where(conversation_id: conversation_id).order(:seq, :id).all,
|
|
32
|
+
requests: requests,
|
|
33
|
+
route_attempts: RouteAttempt.where(message_inference_request_id: request_ids).order(:message_inference_request_id, :attempt_no,
|
|
34
|
+
:id).all,
|
|
35
|
+
responses: responses,
|
|
36
|
+
request_payload_hashes: requests.filter_map(&:request_content_hash),
|
|
37
|
+
response_payload_hashes: responses.filter_map(&:response_content_hash),
|
|
38
|
+
policy_evaluations: policy_evaluations_for(conversation_id, request_ids, response_ids),
|
|
39
|
+
security_events: security_events_for(conversation_id, request_ids, response_ids, tool_call_ids),
|
|
40
|
+
tool_calls: tool_calls,
|
|
41
|
+
tool_call_attempts: ToolCallAttempt.where(tool_call_id: tool_call_ids).order(:tool_call_id, :attempt_no, :id).all
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def policy_evaluations_for(conversation_id, request_ids, response_ids)
|
|
48
|
+
scope = PolicyEvaluation.where(conversation_id: conversation_id)
|
|
49
|
+
scope = scope.or(message_inference_request_id: request_ids) unless request_ids.empty?
|
|
50
|
+
scope = scope.or(message_inference_response_id: response_ids) unless response_ids.empty?
|
|
51
|
+
scope.order(:evaluated_at, :id).all
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def security_events_for(conversation_id, request_ids, response_ids, tool_call_ids)
|
|
55
|
+
scope = where(conversation_id: conversation_id)
|
|
56
|
+
scope = scope.or(message_inference_request_id: request_ids) unless request_ids.empty?
|
|
57
|
+
scope = scope.or(message_inference_response_id: response_ids) unless response_ids.empty?
|
|
58
|
+
scope = scope.or(tool_call_id: tool_call_ids) unless tool_call_ids.empty?
|
|
59
|
+
scope.order(:detected_at, :id).all
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class ToolCall < Sequel::Model(:llm_tool_calls)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :message_inference_response
|
|
13
|
+
many_to_one :requested_by_message, class: 'Legion::Data::Models::LLM::Message', key: :requested_by_message_id
|
|
14
|
+
many_to_one :result_message, class: 'Legion::Data::Models::LLM::Message', key: :result_message_id
|
|
15
|
+
one_to_many :tool_call_attempts
|
|
16
|
+
one_to_many :security_events
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Models
|
|
8
|
+
module LLM
|
|
9
|
+
class ToolCallAttempt < Sequel::Model(:llm_tool_call_attempts)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
many_to_one :tool_call
|
|
13
|
+
one_to_many :security_events
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -8,7 +8,8 @@ module Legion
|
|
|
8
8
|
class Node < Sequel::Model
|
|
9
9
|
include Legion::Logging::Helper
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
one_to_many :task_log
|
|
12
|
+
one_to_many :task_logs, class: 'Legion::Data::Model::TaskLog'
|
|
12
13
|
many_to_one :principal, class: 'Legion::Data::Model::Principal'
|
|
13
14
|
|
|
14
15
|
def parsed_metrics
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'identity/model_helpers'
|
|
4
|
+
|
|
3
5
|
return unless Legion::Data::Connection.adapter == :postgres
|
|
4
6
|
|
|
5
7
|
module Legion
|
|
6
8
|
module Data
|
|
7
9
|
module Model
|
|
8
10
|
class Principal < Sequel::Model(:principals)
|
|
11
|
+
include Identity::ModelHelpers
|
|
12
|
+
|
|
9
13
|
one_to_many :identities, class: 'Legion::Data::Model::Identity'
|
|
10
14
|
one_to_many :group_memberships, class: 'Legion::Data::Model::IdentityGroupMembership'
|
|
15
|
+
many_to_many :groups,
|
|
16
|
+
class: 'Legion::Data::Model::IdentityGroup',
|
|
17
|
+
join_table: :identity_group_memberships,
|
|
18
|
+
left_key: :principal_id,
|
|
19
|
+
right_key: :group_id
|
|
20
|
+
|
|
21
|
+
def self.lookup_columns
|
|
22
|
+
%i[id uuid canonical_name employee_key employee_id]
|
|
23
|
+
end
|
|
11
24
|
|
|
12
25
|
def active_groups
|
|
13
26
|
group_memberships_dataset
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Model
|
|
8
|
+
module RBAC
|
|
9
|
+
class CrossTeamGrant < Sequel::Model(:rbac_cross_team_grants)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
def validate
|
|
13
|
+
super
|
|
14
|
+
errors.add(:source_team, 'cannot be empty') if source_team.nil? || source_team.empty?
|
|
15
|
+
errors.add(:target_team, 'cannot be empty') if target_team.nil? || target_team.empty?
|
|
16
|
+
errors.add(:source_team, 'cannot equal target_team') if source_team == target_team
|
|
17
|
+
errors.add(:runner_pattern, 'cannot be empty') if runner_pattern.nil? || runner_pattern.empty?
|
|
18
|
+
errors.add(:actions, 'cannot be empty') if actions.nil? || actions.empty?
|
|
19
|
+
errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Data
|
|
5
|
+
module Model
|
|
6
|
+
module RBAC
|
|
7
|
+
module ModelHelpers
|
|
8
|
+
def expired?
|
|
9
|
+
return false if expires_at.nil?
|
|
10
|
+
|
|
11
|
+
expires_at < Time.now
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def active?
|
|
15
|
+
!expired?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def actions_list
|
|
19
|
+
(actions || '').split(',').map(&:strip)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Model
|
|
8
|
+
module RBAC
|
|
9
|
+
class RoleAssignment < Sequel::Model(:rbac_role_assignments)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
VALID_PRINCIPAL_TYPES = %w[worker human].freeze
|
|
13
|
+
|
|
14
|
+
def validate
|
|
15
|
+
super
|
|
16
|
+
errors.add(:principal_type, 'must be worker or human') unless VALID_PRINCIPAL_TYPES.include?(principal_type)
|
|
17
|
+
errors.add(:principal_id, 'cannot be empty') if principal_id.nil? || principal_id.empty?
|
|
18
|
+
errors.add(:role, 'cannot be empty') if role.nil? || role.empty?
|
|
19
|
+
errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'model_helpers'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Data
|
|
7
|
+
module Model
|
|
8
|
+
module RBAC
|
|
9
|
+
class RunnerGrant < Sequel::Model(:rbac_runner_grants)
|
|
10
|
+
include ModelHelpers
|
|
11
|
+
|
|
12
|
+
def validate
|
|
13
|
+
super
|
|
14
|
+
errors.add(:team, 'cannot be empty') if team.nil? || team.empty?
|
|
15
|
+
errors.add(:runner_pattern, 'cannot be empty') if runner_pattern.nil? || runner_pattern.empty?
|
|
16
|
+
errors.add(:actions, 'cannot be empty') if actions.nil? || actions.empty?
|
|
17
|
+
errors.add(:granted_by, 'cannot be empty') if granted_by.nil? || granted_by.empty?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|