protege 0.1.0.alpha.2 → 0.1.0.alpha.7
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/MIT-LICENSE +21 -0
- data/Rakefile +1 -3
- data/app/assets/builds/protege.css +76 -83
- data/app/controllers/concerns/protege/agent_scoped.rb +22 -0
- data/app/controllers/concerns/protege/agent_toolkit_scoped.rb +22 -0
- data/app/controllers/protege/access_rules_controller.rb +13 -13
- data/app/controllers/protege/agent_toolkit_rules_controller.rb +62 -0
- data/app/controllers/protege/agent_toolkits_controller.rb +44 -0
- data/app/controllers/protege/agents/agent_toolkits_controller.rb +65 -0
- data/app/controllers/protege/agents_controller.rb +118 -0
- data/app/controllers/protege/archives_controller.rb +18 -18
- data/app/controllers/protege/home_controller.rb +1 -1
- data/app/controllers/protege/messages_controller.rb +1 -1
- data/app/controllers/protege/replies_controller.rb +3 -3
- data/app/controllers/protege/responsibilities_controller.rb +7 -7
- data/app/controllers/protege/threads_controller.rb +4 -4
- data/app/controllers/protege/toolkits_controller.rb +84 -0
- data/app/helpers/protege/agents_helper.rb +75 -0
- data/app/helpers/protege/application_helper.rb +3 -2
- data/app/helpers/protege/components/badges_helper.rb +11 -2
- data/app/helpers/protege/components/forms_helper.rb +47 -12
- data/app/helpers/protege/components/typography_helper.rb +1 -1
- data/app/helpers/protege/messages_helper.rb +2 -2
- data/app/helpers/protege/responsibilities_helper.rb +2 -2
- data/app/helpers/protege/threads_helper.rb +11 -11
- data/app/helpers/protege/toolkits_helper.rb +66 -0
- data/app/hooks/protege/event_logger_hook.rb +8 -8
- data/app/jobs/protege/console_inference_job.rb +8 -8
- data/app/jobs/protege/inference_job.rb +11 -11
- data/app/jobs/protege/responsibility_job.rb +10 -10
- data/app/mailboxes/protege/agent_mailbox.rb +54 -29
- data/app/mailers/protege/alert_mailer.rb +41 -0
- data/app/mailers/protege/application_mailer.rb +9 -8
- data/app/models/concerns/protege/broadcastable_agent.rb +50 -0
- data/app/models/concerns/protege/broadcastable_toolkit.rb +49 -0
- data/app/models/concerns/protege/toolable.rb +108 -0
- data/app/models/protege/access_rule.rb +12 -12
- data/app/models/protege/{persona.rb → agent.rb} +45 -45
- data/app/models/protege/agent_toolkit.rb +50 -0
- data/app/models/protege/agent_toolkit_rule.rb +25 -0
- data/app/models/protege/application_record.rb +1 -1
- data/app/models/protege/email_thread.rb +16 -13
- data/app/models/protege/message.rb +36 -20
- data/app/models/protege/responsibility.rb +12 -12
- data/app/models/protege/tool_use.rb +6 -6
- data/app/models/protege/toolkit.rb +115 -0
- data/app/models/protege/trace.rb +1 -1
- data/app/providers/protege/open_router_provider.rb +17 -0
- data/app/resolvers/README.md +4 -4
- data/app/resolvers/protege/load_file_resolver.rb +5 -5
- data/app/resolvers/protege/load_text_resolver.rb +3 -3
- data/app/resolvers/protege/thread_history_resolver.rb +1 -1
- data/app/services/protege/message_search.rb +2 -2
- data/app/services/protege/system_toolkits.rb +67 -0
- data/app/tools/protege/create_file_tool.rb +2 -0
- data/app/tools/protege/read_attachment_tool.rb +3 -1
- data/app/tools/protege/search_emails_tool.rb +15 -13
- data/app/tools/protege/send_email_tool.rb +60 -18
- data/app/tools/protege/web_fetch_tool.rb +2 -0
- data/app/tools/protege/web_search_tool.rb +3 -1
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/protege/access_rules/create.turbo_stream.slim +3 -3
- data/app/views/protege/agent_toolkit_rules/create.turbo_stream.slim +6 -0
- data/app/views/protege/agent_toolkits/_gate_rule.html.slim +7 -0
- data/app/views/protege/agent_toolkits/_gate_rule_form.html.slim +8 -0
- data/app/views/protege/agent_toolkits/_gate_rules.html.slim +19 -0
- data/app/views/protege/agent_toolkits/show.html.slim +17 -0
- data/app/views/protege/{personas → agents}/_access_rule.html.slim +1 -1
- data/app/views/protege/{personas → agents}/_access_rule_form.html.slim +1 -1
- data/app/views/protege/{personas → agents}/_access_rules.html.slim +5 -5
- data/app/views/protege/agents/_actions.html.slim +15 -0
- data/app/views/protege/agents/_agent_sidebar_item.html.slim +5 -0
- data/app/views/protege/agents/_agent_toolkit.html.slim +7 -0
- data/app/views/protege/agents/_agent_toolkit_form.html.slim +7 -0
- data/app/views/protege/agents/_form.html.slim +25 -0
- data/app/views/protege/agents/_sidebar.html.slim +11 -0
- data/app/views/protege/agents/_toolkits.html.slim +19 -0
- data/app/views/protege/agents/agent_toolkits/create.turbo_stream.slim +6 -0
- data/app/views/protege/agents/edit.html.slim +7 -0
- data/app/views/protege/agents/index.html.slim +6 -0
- data/app/views/protege/agents/new.html.slim +7 -0
- data/app/views/protege/agents/show.html.slim +29 -0
- data/app/views/protege/alert_mailer/inference_failed.text.erb +4 -0
- data/app/views/protege/email_domains/show.html.slim +1 -1
- data/app/views/protege/home/show.html.slim +13 -13
- data/app/views/protege/responsibilities/_form.html.slim +2 -2
- data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +1 -1
- data/app/views/protege/responsibilities/new.html.slim +1 -1
- data/app/views/protege/responsibilities/show.html.slim +2 -2
- data/app/views/protege/responsibility_runs/show.html.slim +1 -1
- data/app/views/protege/shared/_header.html.slim +2 -1
- data/app/views/protege/shared/_hotkeys.html.slim +15 -11
- data/app/views/protege/threads/new.html.slim +2 -2
- data/app/views/protege/toolkits/_form.html.slim +13 -0
- data/app/views/protege/toolkits/_sidebar.html.slim +11 -0
- data/app/views/protege/toolkits/_toolkit_sidebar_item.html.slim +5 -0
- data/app/views/protege/toolkits/edit.html.slim +7 -0
- data/app/views/protege/toolkits/index.html.slim +6 -0
- data/app/views/protege/toolkits/new.html.slim +7 -0
- data/app/views/protege/toolkits/show.html.slim +13 -0
- data/config/routes.rb +12 -1
- data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +1 -1
- data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +1 -1
- data/db/migrate/20260728140000_scope_message_and_thread_uniqueness_per_persona.rb +16 -0
- data/db/migrate/20260812000001_create_protege_toolkits.rb +37 -0
- data/db/migrate/20260815000001_remove_disabled_tool_ids_from_personas.rb +21 -0
- data/db/migrate/20260815000002_add_key_to_protege_toolkits.rb +22 -0
- data/db/migrate/20260815100000_rename_personas_to_agents.rb +24 -0
- data/lib/generators/protege/agent/agent_generator.rb +35 -0
- data/lib/generators/protege/{persona/templates/persona.rb.tt → agent/templates/agent.rb.tt} +9 -9
- data/lib/generators/protege/extension_naming.rb +1 -1
- data/lib/generators/protege/hook/templates/hook.rb.tt +2 -2
- data/lib/generators/protege/install/install_generator.rb +16 -11
- data/lib/generators/protege/install/templates/initializer.rb.tt +17 -6
- data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +2 -2
- data/lib/generators/protege/resolver/resolver_generator.rb +2 -2
- data/lib/generators/protege/resolver/templates/resolver.rb.tt +4 -4
- data/lib/generators/protege/tool/templates/tool.rb.tt +7 -5
- data/lib/protege/configuration.rb +43 -8
- data/lib/protege/engine.rb +3 -1
- data/lib/protege/errors/tool_not_available_error.rb +7 -6
- data/lib/protege/events/event.rb +2 -2
- data/lib/protege/events/inference_chunk_event.rb +1 -1
- data/lib/protege/events/inference_completed_event.rb +1 -1
- data/lib/protege/events/inference_failed_event.rb +1 -1
- data/lib/protege/events/inference_generated_event.rb +1 -1
- data/lib/protege/events/inference_max_turns_reached_event.rb +1 -1
- data/lib/protege/events/inference_started_event.rb +1 -1
- data/lib/protege/events/loop_run_completed_event.rb +1 -1
- data/lib/protege/events/loop_run_enqueued_event.rb +1 -1
- data/lib/protege/events/loop_run_failed_event.rb +1 -1
- data/lib/protege/events/loop_run_started_event.rb +1 -1
- data/lib/protege/events/tool_call_completed_event.rb +1 -1
- data/lib/protege/events/tool_call_failed_event.rb +1 -1
- data/lib/protege/events/tool_call_started_event.rb +1 -1
- data/lib/protege/events/tool_calls_received_event.rb +1 -1
- data/lib/protege/extensions/hook.rb +1 -0
- data/lib/protege/extensions/hook_mixin.rb +1 -1
- data/lib/protege/extensions/manifest.rb +67 -0
- data/lib/protege/extensions/manifested.rb +81 -0
- data/lib/protege/extensions/provider.rb +1 -0
- data/lib/protege/extensions/provider_mixin.rb +9 -17
- data/lib/protege/extensions/resolver.rb +2 -1
- data/lib/protege/extensions/resolver_mixin.rb +2 -2
- data/lib/protege/extensions/tool.rb +1 -0
- data/lib/protege/extensions/tool_mixin.rb +16 -40
- data/lib/protege/gateway/access_control.rb +19 -19
- data/lib/protege/gateway/access_policy.rb +2 -2
- data/lib/protege/gateway/mail/outbound.rb +22 -5
- data/lib/protege/gateway.rb +48 -37
- data/lib/protege/loop/scheduler.rb +1 -1
- data/lib/protege/orchestrator/context.rb +8 -8
- data/lib/protege/orchestrator/harness.rb +26 -24
- data/lib/protege/orchestrator/reply_context.rb +3 -3
- data/lib/protege/orchestrator/reply_harness.rb +8 -8
- data/lib/protege/orchestrator/resolver_chain.rb +3 -3
- data/lib/protege/orchestrator/responsibility_context.rb +4 -4
- data/lib/protege/orchestrator/responsibility_harness.rb +8 -8
- data/lib/protege/orchestrator.rb +7 -7
- data/lib/protege/subscribers/alerter.rb +117 -0
- data/lib/protege/subscribers/tracing.rb +1 -1
- data/lib/protege/version.rb +1 -1
- data/lib/protege.rb +2 -2
- data/lib/tasks/protege_tasks.rake +12 -0
- metadata +66 -23
- data/app/controllers/concerns/protege/persona_scoped.rb +0 -22
- data/app/controllers/protege/personas_controller.rb +0 -118
- data/app/helpers/protege/personas_helper.rb +0 -90
- data/app/models/concerns/protege/broadcastable_persona.rb +0 -50
- data/app/models/concerns/protege/tool_scoped.rb +0 -88
- data/app/views/protege/personas/_actions.html.slim +0 -15
- data/app/views/protege/personas/_form.html.slim +0 -29
- data/app/views/protege/personas/_persona_sidebar_item.html.slim +0 -5
- data/app/views/protege/personas/_sidebar.html.slim +0 -11
- data/app/views/protege/personas/edit.html.slim +0 -7
- data/app/views/protege/personas/index.html.slim +0 -6
- data/app/views/protege/personas/new.html.slim +0 -7
- data/app/views/protege/personas/show.html.slim +0 -37
- data/lib/generators/protege/persona/persona_generator.rb +0 -35
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
module Protege
|
|
4
4
|
# View helpers for responsibility pages — status display and run-history formatting.
|
|
5
5
|
module ResponsibilitiesHelper
|
|
6
|
-
# Fetch every +Responsibility+ (
|
|
6
|
+
# Fetch every +Responsibility+ (agent eager-loaded) for the global sidebar listing. The model's
|
|
7
7
|
# default scope already orders by name.
|
|
8
8
|
#
|
|
9
9
|
# @return [ActiveRecord::Relation<Protege::Responsibility>] all responsibilities
|
|
10
10
|
def all_responsibilities
|
|
11
|
-
Responsibility.includes(:
|
|
11
|
+
Responsibility.includes(:agent)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# Render a status dot reflecting whether a responsibility is active.
|
|
@@ -11,18 +11,18 @@ module Protege
|
|
|
11
11
|
EmailThread.order(last_message_at: :desc)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# List the active
|
|
14
|
+
# List the active agents eligible for the compose form.
|
|
15
15
|
#
|
|
16
|
-
# @return [ActiveRecord::Relation<Protege::
|
|
17
|
-
def
|
|
18
|
-
|
|
16
|
+
# @return [ActiveRecord::Relation<Protege::Agent>] agents eligible for selection
|
|
17
|
+
def available_agents
|
|
18
|
+
Agent.active
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Format the available
|
|
21
|
+
# Format the available agents as +[label, id]+ pairs for a select dropdown.
|
|
22
22
|
#
|
|
23
23
|
# @return [Array<Array(String, Integer)>] option label/value pairs
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def available_agent_options
|
|
25
|
+
available_agents.map { |agent| ["#{agent.name} <#{agent.email_address}>", agent.id] }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Format a thread subject with its message count for sidebar display.
|
|
@@ -34,7 +34,7 @@ module Protege
|
|
|
34
34
|
# @return [String] the subject, suffixed with the count when it exceeds one
|
|
35
35
|
def thread_title(thread)
|
|
36
36
|
subject = thread.subject.presence || '(no subject)'
|
|
37
|
-
thread.message_count > 1 ? "#{thread.
|
|
37
|
+
thread.message_count > 1 ? "#{thread.agent.name} - #{subject} (#{thread.message_count})" : subject
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Format the thread's last-activity date for sidebar display.
|
|
@@ -53,7 +53,7 @@ module Protege
|
|
|
53
53
|
thread.latest_message&.text_body.to_s.lines.first.to_s.strip.presence || '—'
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Render the thread detail header — subject,
|
|
56
|
+
# Render the thread detail header — subject, agent, message count, last
|
|
57
57
|
# activity, and thread ID. An optional block renders actions beside the title.
|
|
58
58
|
#
|
|
59
59
|
# @param thread [Protege::EmailThread] the thread whose header to render
|
|
@@ -81,14 +81,14 @@ module Protege
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Render the metadata row —
|
|
84
|
+
# Render the metadata row — agent, message count, and last activity.
|
|
85
85
|
#
|
|
86
86
|
# @param thread [Protege::EmailThread] the thread whose metadata to render
|
|
87
87
|
# @return [ActiveSupport::SafeBuffer] the metadata row markup
|
|
88
88
|
def thread_header_meta(thread)
|
|
89
89
|
tag.div(class: 'mt-1 flex items-center gap-3 text-xs', style: 'color: var(--text-muted)') do
|
|
90
90
|
tag.span do
|
|
91
|
-
'
|
|
91
|
+
'Agent: '.html_safe + tag.span(thread.agent.name, class: 'font-medium')
|
|
92
92
|
end +
|
|
93
93
|
tag.span(pluralize(thread.message_count, 'message')) +
|
|
94
94
|
thread_header_last_activity(thread)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# View helpers for the toolkit dashboard. The member-tool checklist is built from the uniform extension
|
|
5
|
+
# manifest; the allow/deny gate is edited as +ToolkitRule+ child rows (see the toolkit-rules partials).
|
|
6
|
+
# The overview helpers keep computed display values (tool names, proactive/count labels) out of the
|
|
7
|
+
# templates.
|
|
8
|
+
module ToolkitsHelper
|
|
9
|
+
# Every +Toolkit+ for the global sidebar listing, ordered by name.
|
|
10
|
+
#
|
|
11
|
+
# @return [ActiveRecord::Relation<Protege::Toolkit>] all toolkits
|
|
12
|
+
def all_toolkits
|
|
13
|
+
Toolkit.order(:name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The tool manifest entries offered as the member-tool checklist — the toolkit equivalent of
|
|
17
|
+
# +AgentsHelper#agent_tool_options+, built from the uniform extension manifest.
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<Protege::Manifest::Entry>] the selectable tools
|
|
20
|
+
def toolkit_tool_options
|
|
21
|
+
Manifest.registered(:tool)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The labelled summary rows shown in a toolkit's overview card.
|
|
25
|
+
#
|
|
26
|
+
# @param toolkit [Protege::Toolkit] the toolkit
|
|
27
|
+
# @return [Hash{String=>String}] the info-card rows
|
|
28
|
+
def toolkit_overview(toolkit)
|
|
29
|
+
{ 'Tools' => toolkit_member_tool_names(toolkit) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A short "N tools" label for a toolkit listing.
|
|
33
|
+
#
|
|
34
|
+
# @param toolkit [Protege::Toolkit] the toolkit
|
|
35
|
+
# @return [String] the pluralized tool count
|
|
36
|
+
def toolkit_tool_count_label(toolkit)
|
|
37
|
+
pluralize(toolkit.member_tool_ids.size, 'tool')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The badge colour for a gate rule — green for allow, red for deny.
|
|
41
|
+
#
|
|
42
|
+
# @param rule [Protege::AgentToolkitRule] the rule
|
|
43
|
+
# @return [Symbol] +:green+ or +:red+
|
|
44
|
+
def gate_rule_kind_color(rule)
|
|
45
|
+
rule.allow? ? :green : :red
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The toolkits not yet attached to an agent — the options for its attach form.
|
|
49
|
+
#
|
|
50
|
+
# @param agent [Protege::Agent] the agent
|
|
51
|
+
# @return [ActiveRecord::Relation<Protege::Toolkit>] the attachable toolkits, by name
|
|
52
|
+
def attachable_toolkits(agent)
|
|
53
|
+
Toolkit.where.not(id: agent.toolkit_ids).order(:name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# The toolkit's member-tool display names, comma-joined, or "None".
|
|
59
|
+
#
|
|
60
|
+
# @param toolkit [Protege::Toolkit] the toolkit
|
|
61
|
+
# @return [String] the joined tool names
|
|
62
|
+
def toolkit_member_tool_names(toolkit)
|
|
63
|
+
toolkit.member_tools.map(&:display_name).join(', ').presence || 'None'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -9,14 +9,14 @@ module Protege
|
|
|
9
9
|
# at boot; each +on+ block receives a typed +Protege::Event+ and runs isolated, so a logging
|
|
10
10
|
# error can never break inference. As a built-in it is always active.
|
|
11
11
|
class EventLoggerHook < Protege::Hook
|
|
12
|
-
# Log the start of an inference run for
|
|
12
|
+
# Log the start of an inference run for an agent.
|
|
13
13
|
on InferenceStartedEvent do |event|
|
|
14
|
-
log "[#{ts}] #{cid(event)} INF START
|
|
14
|
+
log "[#{ts}] #{cid(event)} INF START agent=#{event.agent&.name}"
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# Log the completion of an inference run for
|
|
17
|
+
# Log the completion of an inference run for an agent.
|
|
18
18
|
on InferenceCompletedEvent do |event|
|
|
19
|
-
log "[#{ts}] #{cid(event)} INF DONE
|
|
19
|
+
log "[#{ts}] #{cid(event)} INF DONE agent=#{event.agent&.name}"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Log the set of tool calls the model requested in a single turn.
|
|
@@ -52,22 +52,22 @@ module Protege
|
|
|
52
52
|
|
|
53
53
|
# Log a scheduled responsibility run being enqueued — pending, awaiting a worker (the Loop layer).
|
|
54
54
|
on LoopRunEnqueuedEvent do |event|
|
|
55
|
-
log "[#{ts}] #{cid(event)} LOOP PENDING #{event.name}
|
|
55
|
+
log "[#{ts}] #{cid(event)} LOOP PENDING #{event.name} agent=#{event.agent_name}"
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Log the start of a scheduled responsibility run (the Loop layer).
|
|
59
59
|
on LoopRunStartedEvent do |event|
|
|
60
|
-
log "[#{ts}] #{cid(event)} LOOP START #{event.name}
|
|
60
|
+
log "[#{ts}] #{cid(event)} LOOP START #{event.name} agent=#{event.agent_name}"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Log the successful completion of a scheduled responsibility run.
|
|
64
64
|
on LoopRunCompletedEvent do |event|
|
|
65
|
-
log "[#{ts}] #{cid(event)} LOOP DONE #{event.name}
|
|
65
|
+
log "[#{ts}] #{cid(event)} LOOP DONE #{event.name} agent=#{event.agent_name}"
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# Log a failed scheduled responsibility run with its error class, message, and short backtrace.
|
|
69
69
|
on LoopRunFailedEvent do |event|
|
|
70
|
-
log "[#{ts}] #{cid(event)} LOOP FAIL #{event.name}
|
|
70
|
+
log "[#{ts}] #{cid(event)} LOOP FAIL #{event.name} agent=#{event.agent_name} " \
|
|
71
71
|
"#{event.error&.class}: #{event.error&.message}\n #{error_backtrace(event.error)}"
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -13,7 +13,7 @@ module Protege
|
|
|
13
13
|
# == Lifecycle
|
|
14
14
|
#
|
|
15
15
|
# 1. Restore the correlation ID for event tracing.
|
|
16
|
-
# 2. Find the +Message+ and +
|
|
16
|
+
# 2. Find the +Message+ and +Agent+ from their IDs.
|
|
17
17
|
# 3. Transition the message to +processing+.
|
|
18
18
|
# 4. Run the inference loop (+Harness+).
|
|
19
19
|
# 5. Mark the message +processed+ on success, +failed+ on error.
|
|
@@ -23,17 +23,17 @@ module Protege
|
|
|
23
23
|
# Process one pre-persisted console message end to end.
|
|
24
24
|
#
|
|
25
25
|
# @param message_id [Integer] primary key of the pre-persisted +Message+ record
|
|
26
|
-
# @param
|
|
26
|
+
# @param agent_id [Integer] primary key of the +Agent+; STI loads the correct subclass
|
|
27
27
|
# @param correlation_id [String] trace token forwarded from the console request
|
|
28
28
|
# @return [void]
|
|
29
29
|
# @raise [StandardError] re-raised after being recorded on the message, to trigger retries
|
|
30
|
-
def perform(message_id:,
|
|
30
|
+
def perform(message_id:, agent_id:, correlation_id:)
|
|
31
31
|
restore_correlation_id(correlation_id)
|
|
32
32
|
|
|
33
33
|
message = Message.find(message_id)
|
|
34
|
-
|
|
34
|
+
agent = Agent.find(agent_id)
|
|
35
35
|
|
|
36
|
-
process(message:,
|
|
36
|
+
process(message:, agent:)
|
|
37
37
|
rescue StandardError => e
|
|
38
38
|
message&.mark_failed!(e)
|
|
39
39
|
raise
|
|
@@ -44,12 +44,12 @@ module Protege
|
|
|
44
44
|
# Run the message through the inference loop and record success.
|
|
45
45
|
#
|
|
46
46
|
# @param message [Protege::Message] the message to process
|
|
47
|
-
# @param
|
|
47
|
+
# @param agent [Protege::Agent] the agent handling it
|
|
48
48
|
# @return [void]
|
|
49
|
-
def process(message:,
|
|
49
|
+
def process(message:, agent:)
|
|
50
50
|
message.start_processing!
|
|
51
51
|
|
|
52
|
-
Orchestrator.reply(
|
|
52
|
+
Orchestrator.reply(agent:, message:)
|
|
53
53
|
|
|
54
54
|
message.mark_processed!
|
|
55
55
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Protege
|
|
4
4
|
# Runs one inbound email through the full Protege inference pipeline (the "I" of LOGI).
|
|
5
5
|
#
|
|
6
|
-
# Enqueued by +AgentMailbox+ once
|
|
7
|
-
#
|
|
6
|
+
# Enqueued by +AgentMailbox+ once agent routing succeeds. The job re-hydrates the raw email and
|
|
7
|
+
# agent from their IDs, ingests a +Message+, runs the Orchestrator's +Harness+, and records the
|
|
8
8
|
# outcome on the message. IDs (not objects) are passed across the async boundary so the queue
|
|
9
9
|
# adapter can serialize and safely retry the job across process restarts.
|
|
10
10
|
#
|
|
@@ -18,25 +18,25 @@ module Protege
|
|
|
18
18
|
# Process one inbound email end to end: ingest, run inference, record the result.
|
|
19
19
|
#
|
|
20
20
|
# @param inbound_email_id [Integer] id of the +ActionMailbox::InboundEmail+ to process
|
|
21
|
-
# @param
|
|
21
|
+
# @param agent_id [Integer] primary key of the +Protege::Agent+; STI loads the subclass
|
|
22
22
|
# @param correlation_id [String] the RFC 2822 Message-ID set by +AgentMailbox+ for tracing
|
|
23
23
|
# @return [void]
|
|
24
24
|
# @raise [StandardError] re-raised after being recorded on the message, to trigger retries
|
|
25
|
-
def perform(inbound_email_id:,
|
|
25
|
+
def perform(inbound_email_id:, agent_id:, correlation_id:)
|
|
26
26
|
restore_correlation_id(correlation_id)
|
|
27
27
|
|
|
28
|
-
# Re-hydrate the raw email and the target
|
|
28
|
+
# Re-hydrate the raw email and the target agent from their IDs.
|
|
29
29
|
# IDs are passed rather than objects so Solid Queue can serialize
|
|
30
30
|
# the job and retry it safely across process restarts.
|
|
31
31
|
inbound_email = ActionMailbox::InboundEmail.find(inbound_email_id)
|
|
32
|
-
|
|
32
|
+
agent = Agent.find(agent_id)
|
|
33
33
|
|
|
34
34
|
# Parse the raw email, find-or-create the EmailThread, and persist
|
|
35
35
|
# a Protege::Message record with processing_status: :processing.
|
|
36
36
|
# Returns the saved message so we can update its lifecycle below.
|
|
37
|
-
message = Message.ingest!(inbound_email:,
|
|
37
|
+
message = Message.ingest!(inbound_email:, agent:)
|
|
38
38
|
|
|
39
|
-
process(message:,
|
|
39
|
+
process(message:, agent:)
|
|
40
40
|
rescue StandardError => e
|
|
41
41
|
# Capture the error on the message record before re-raising so the
|
|
42
42
|
# job can be inspected and manually re-queued without losing context.
|
|
@@ -49,10 +49,10 @@ module Protege
|
|
|
49
49
|
# Run the message through the inference loop and record success.
|
|
50
50
|
#
|
|
51
51
|
# @param message [Protege::Message] the message to process
|
|
52
|
-
# @param
|
|
52
|
+
# @param agent [Protege::Agent] the agent handling it
|
|
53
53
|
# @return [void]
|
|
54
|
-
def process(message:,
|
|
55
|
-
Orchestrator.reply(
|
|
54
|
+
def process(message:, agent:)
|
|
55
|
+
Orchestrator.reply(agent:, message:)
|
|
56
56
|
|
|
57
57
|
message.mark_processed!
|
|
58
58
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Protege
|
|
4
4
|
# Carries out one scheduled responsibility run — the Loop layer's worker (the "L" of LOGI driving the
|
|
5
5
|
# "I"). Enqueued by +Responsibility#dispatch!+ with the id of a +pending+ +ResponsibilityRun+; it
|
|
6
|
-
# advances that run to running, runs the
|
|
6
|
+
# advances that run to running, runs the agent through +ResponsibilityHarness+, and records the
|
|
7
7
|
# outcome on the run. The run id (not the object) is passed across the async boundary so the queue
|
|
8
8
|
# adapter can serialize the job.
|
|
9
9
|
#
|
|
@@ -21,15 +21,15 @@ module Protege
|
|
|
21
21
|
# @param run_id [Integer] id of the pending +ResponsibilityRun+ to carry out
|
|
22
22
|
# @return [void]
|
|
23
23
|
def perform(run_id:)
|
|
24
|
-
responsibility_run = ResponsibilityRun.eager_load(responsibility: :
|
|
24
|
+
responsibility_run = ResponsibilityRun.eager_load(responsibility: :agent).find(run_id)
|
|
25
25
|
# Adopt the trace id minted at dispatch (Responsibility#dispatch!) so the whole run lifecycle
|
|
26
26
|
# shares it; fall back to a fresh one for a run created some other way (e.g. in a test).
|
|
27
27
|
restore_correlation_id(responsibility_run.correlation_id || SecureRandom.uuid)
|
|
28
28
|
|
|
29
29
|
responsibility = responsibility_run.responsibility
|
|
30
|
-
|
|
30
|
+
agent = responsibility.agent
|
|
31
31
|
|
|
32
|
-
process(responsibility_run:, responsibility:,
|
|
32
|
+
process(responsibility_run:, responsibility:, agent:)
|
|
33
33
|
rescue StandardError => e
|
|
34
34
|
# Record the failure without re-raising — the cron schedule, not ActiveJob, drives the retry.
|
|
35
35
|
responsibility_run&.mark_failed!(e)
|
|
@@ -42,16 +42,16 @@ module Protege
|
|
|
42
42
|
#
|
|
43
43
|
# @param responsibility_run [Protege::ResponsibilityRun] the run to carry out
|
|
44
44
|
# @param responsibility [Protege::Responsibility] the responsibility to handle
|
|
45
|
-
# @param
|
|
45
|
+
# @param agent [Protege::Agent] the agent handling the responsibility
|
|
46
46
|
# @return [void]
|
|
47
|
-
def process(responsibility_run:, responsibility:,
|
|
47
|
+
def process(responsibility_run:, responsibility:, agent:)
|
|
48
48
|
responsibility_run.start!
|
|
49
|
-
LoopRunStartedEvent.emit(name: responsibility.name,
|
|
49
|
+
LoopRunStartedEvent.emit(name: responsibility.name, agent_name: agent.name)
|
|
50
50
|
|
|
51
|
-
Orchestrator.run_responsibility(
|
|
51
|
+
Orchestrator.run_responsibility(agent:, responsibility:, responsibility_run:)
|
|
52
52
|
|
|
53
53
|
responsibility_run.mark_completed!
|
|
54
|
-
LoopRunCompletedEvent.emit(name: responsibility.name,
|
|
54
|
+
LoopRunCompletedEvent.emit(name: responsibility.name, agent_name: agent.name)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Emit +loop_run_failed+ for a recorded failure — omitted when the run never loaded (e.g. the
|
|
@@ -64,7 +64,7 @@ module Protege
|
|
|
64
64
|
return unless responsibility_run
|
|
65
65
|
|
|
66
66
|
responsibility = responsibility_run.responsibility
|
|
67
|
-
LoopRunFailedEvent.emit(name: responsibility.name,
|
|
67
|
+
LoopRunFailedEvent.emit(name: responsibility.name, agent_name: responsibility.agent.name, error:)
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -4,52 +4,76 @@ module Protege
|
|
|
4
4
|
# Inbound edge of the Gateway: routes every received email toward the inference pipeline.
|
|
5
5
|
#
|
|
6
6
|
# Action Mailbox delivers parsed mail here, exposing +mail+ and +inbound_email+ as instance
|
|
7
|
-
# methods and calling {#process} as the single entry point. The mailbox resolves
|
|
8
|
-
#
|
|
9
|
-
# +InferenceJob
|
|
10
|
-
#
|
|
7
|
+
# methods and calling {#process} as the single entry point. The mailbox resolves every agent
|
|
8
|
+
# addressed in the +To:+ header, then hands each off to +Gateway.accept_smtp_message+, which enqueues
|
|
9
|
+
# an +InferenceJob+ per agent — so an email to several proteges is handled by each independently. The
|
|
10
|
+
# inbound Message-ID is forwarded as the correlation ID so the trace token survives the async boundary
|
|
11
|
+
# and is restored inside the job.
|
|
11
12
|
#
|
|
12
13
|
# == Recursion guard
|
|
13
14
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# Agent-sent mail carries an +X-Protege-Recursion+ hop-count header (+Gateway::RECURSION_HEADER+):
|
|
16
|
+
# +1+ on fresh mail, the inbound count plus one on a reply. A human mail client never echoes the
|
|
17
|
+
# header back, so conversations with people don't accumulate — only an unbroken Protege-to-Protege
|
|
18
|
+
# reply chain does. Any inbound email whose count has reached the configured limit
|
|
19
|
+
# (+config.recursion_limit+, default 50) is silently dropped, preventing infinite loops when one agent
|
|
20
|
+
# replies to another. Unroutable mail (no matching agent) is bounced with an unrouted notice instead.
|
|
17
21
|
#
|
|
18
22
|
# == Access guardrail
|
|
19
23
|
#
|
|
20
|
-
#
|
|
21
|
-
# (+Gateway::AccessControl+): the committed global ceiling intersected with
|
|
22
|
-
#
|
|
24
|
+
# Each addressed agent is checked against its own inbound access guardrail
|
|
25
|
+
# (+Gateway::AccessControl+): the committed global ceiling intersected with that agent's runtime rules.
|
|
26
|
+
# The email is delivered only to the agents that permit the sender; if none do, it is bounced with an
|
|
27
|
+
# access-denied notice and never reaches inference.
|
|
23
28
|
class AgentMailbox < ActionMailbox::Base
|
|
24
|
-
# Route the inbound email: drop on exhausted recursion budget, bounce when
|
|
25
|
-
#
|
|
29
|
+
# Route the inbound email: drop on exhausted recursion budget, bounce when no addressed agent
|
|
30
|
+
# matches, bounce when the sender may reach none of them, bounce on an attachment breach, else accept
|
|
31
|
+
# it for every addressed agent the sender is permitted to reach (one email to several proteges is
|
|
32
|
+
# handled by each independently).
|
|
26
33
|
#
|
|
27
34
|
# @return [void]
|
|
28
35
|
def process
|
|
29
36
|
return handle_recursion_limit if recursion_limit_reached?
|
|
30
|
-
return bounce_with_unrouted if
|
|
31
|
-
return bounce_with_access_denied
|
|
37
|
+
return bounce_with_unrouted if agents.empty?
|
|
38
|
+
return bounce_with_access_denied if permitted_agents.empty?
|
|
32
39
|
return bounce_with_attachment_error if attachment_violation
|
|
33
40
|
|
|
34
|
-
Gateway.accept_smtp_message(inbound_email:,
|
|
41
|
+
permitted_agents.each { |agent| Gateway.accept_smtp_message(inbound_email:, agent:) }
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
private
|
|
38
45
|
|
|
39
|
-
#
|
|
46
|
+
# Every active agent addressed on the email — the proteges it is for. Scans both +To:+ and +Cc:+
|
|
47
|
+
# (so a protege reached only as a Cc recipient, e.g. on a reply-all, is still handled), preserving
|
|
48
|
+
# order and collapsing duplicates. +Agent.lookup+ strips plus-tag subaddresses before matching, and
|
|
49
|
+
# scanning all recipients (not just the first) is what lets a protege be reached wherever it appears.
|
|
40
50
|
#
|
|
41
|
-
# @return [
|
|
42
|
-
def
|
|
43
|
-
|
|
51
|
+
# @return [Array<Protege::Agent>] the matched agents, possibly empty
|
|
52
|
+
def agents
|
|
53
|
+
@agents ||= recipient_addresses.filter_map { |address| Protege::Agent.lookup(address) }.uniq
|
|
44
54
|
end
|
|
45
55
|
|
|
46
|
-
#
|
|
56
|
+
# Every address the email was sent to, across +To:+ and +Cc:+.
|
|
47
57
|
#
|
|
48
|
-
#
|
|
58
|
+
# @return [Array<String>] the combined recipient addresses
|
|
59
|
+
def recipient_addresses
|
|
60
|
+
Array(mail.to) + Array(mail.cc)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The addressed agents the sender is actually permitted to reach, per each one's access guardrail.
|
|
64
|
+
# An email may be delivered to some proteges and withheld from others on the same +To:+ line.
|
|
49
65
|
#
|
|
50
|
-
# @return [Protege::
|
|
51
|
-
def
|
|
52
|
-
@
|
|
66
|
+
# @return [Array<Protege::Agent>] the permitted subset of {agents}
|
|
67
|
+
def permitted_agents
|
|
68
|
+
@permitted_agents ||= agents.select { |agent| sender_permitted_for?(agent) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Report whether the sender may reach one agent, per its access guardrail.
|
|
72
|
+
#
|
|
73
|
+
# @param agent [Protege::Agent] the addressed agent
|
|
74
|
+
# @return [Boolean] true when every access layer permits the sender
|
|
75
|
+
def sender_permitted_for?(agent)
|
|
76
|
+
Gateway.permits?(agent:, address: mail.from.first)
|
|
53
77
|
end
|
|
54
78
|
|
|
55
79
|
# The attachment-limit breach for this email, or nil when its attachments are within the limits.
|
|
@@ -64,15 +88,16 @@ module Protege
|
|
|
64
88
|
end
|
|
65
89
|
end
|
|
66
90
|
|
|
67
|
-
# Report whether the +X-Protege-Recursion+
|
|
91
|
+
# Report whether the +X-Protege-Recursion+ hop count has reached the configured limit.
|
|
68
92
|
#
|
|
69
|
-
# @return [Boolean] true when the header is present and its
|
|
93
|
+
# @return [Boolean] true when the header is present and its count is at or above
|
|
94
|
+
# +config.recursion_limit+
|
|
70
95
|
def recursion_limit_reached?
|
|
71
|
-
header = mail.header[
|
|
72
|
-
header.present? && header.value.to_i
|
|
96
|
+
header = mail.header[Gateway::RECURSION_HEADER]
|
|
97
|
+
header.present? && header.value.to_i >= Protege.configuration.recursion_limit
|
|
73
98
|
end
|
|
74
99
|
|
|
75
|
-
# Silently drop the message when the recursion
|
|
100
|
+
# Silently drop the message when the recursion hop count is exhausted — no reply, no error.
|
|
76
101
|
#
|
|
77
102
|
# @return [void]
|
|
78
103
|
def handle_recursion_limit; end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Operational alert mail — emailed to the platform admin when an inference run fails and cannot be
|
|
5
|
+
# recovered. A sibling of the Gateway's bounce notices ({ApplicationMailer}): both are operational
|
|
6
|
+
# mail the engine originates itself, not agent conversation (so neither goes through
|
|
7
|
+
# +Gateway.deliver+, which is agent/thread-scoped). Sent by +Protege::Subscribers::Alerter+ off the
|
|
8
|
+
# +InferenceFailedEvent+; the recipients and sender come from +config.failure_alerts+.
|
|
9
|
+
#
|
|
10
|
+
# The alert is deliberately trigger-agnostic — it carries only the error and the run's correlation id,
|
|
11
|
+
# not agent/message specifics, so the same notice serves agent-based and non-agent failures
|
|
12
|
+
# alike (the correlation id is the thread back to the full trace). The body renders from
|
|
13
|
+
# +alert_mailer/inference_failed.text.erb+ through the engine's +mailer+ text layout — the engine's
|
|
14
|
+
# first template-rendered mail (the bounce notices use inline bodies).
|
|
15
|
+
class AlertMailer < ApplicationMailer
|
|
16
|
+
# Alert the configured recipients that an inference run failed.
|
|
17
|
+
#
|
|
18
|
+
# +:to+ and +:from+ are read from +config.failure_alerts+. There is no valid default sender — a
|
|
19
|
+
# deliverable +:from+ must be an address at a registered +EmailDomain+ (so the self-hosted MTA
|
|
20
|
+
# DKIM-signs it), so the +Alerter+ only invokes this mailer once +:from+ is configured; it is passed
|
|
21
|
+
# through verbatim here rather than defaulted.
|
|
22
|
+
#
|
|
23
|
+
# @param error_class [String] the error's class name (also names the subject)
|
|
24
|
+
# @param error_message [String] the error's message
|
|
25
|
+
# @param correlation_id [String, nil] the run's correlation id, for tracing the failure
|
|
26
|
+
# @return [Mail::Message] the alert addressed to the configured recipients
|
|
27
|
+
def inference_failed(error_class:, error_message:, correlation_id:)
|
|
28
|
+
@error_class = error_class
|
|
29
|
+
@error_message = error_message
|
|
30
|
+
@correlation_id = correlation_id
|
|
31
|
+
|
|
32
|
+
alerts = Protege.configuration.failure_alerts
|
|
33
|
+
|
|
34
|
+
mail(
|
|
35
|
+
from: alerts[:from],
|
|
36
|
+
to: alerts[:to],
|
|
37
|
+
subject: "[Protege] Inference failed: #{error_class}"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -4,17 +4,18 @@ module Protege
|
|
|
4
4
|
# Base mailer for the Protege engine and home of the Gateway's bounce notices.
|
|
5
5
|
#
|
|
6
6
|
# Sits at the outbound edge of the Gateway. +AgentMailbox+ returns these notices to Action Mailbox's
|
|
7
|
-
# +bounce_with+ when an inbound email can't be accepted — no matching
|
|
8
|
-
# a sender rejected by the access guardrail ({#access_denied_bounce}). The
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# +bounce_with+ when an inbound email can't be accepted — no matching agent ({#unrouted_bounce}) or
|
|
8
|
+
# a sender rejected by the access guardrail ({#access_denied_bounce}). The sender is the host's
|
|
9
|
+
# app-wide default (+config.action_mailer.default_options+) when one is set, falling back to a
|
|
10
|
+
# deliberate placeholder — set the app-wide default so outbound mail originates from a domain the
|
|
11
|
+
# host actually controls.
|
|
12
12
|
class ApplicationMailer < ActionMailer::Base
|
|
13
|
-
#
|
|
14
|
-
default
|
|
13
|
+
# Resolved lazily per mail so the host's +config.action_mailer.default_options+ wins even though
|
|
14
|
+
# this subclass declares its own default (a literal here would shadow the app-wide setting).
|
|
15
|
+
default from: -> { ActionMailer::Base.default[:from] || 'from@example.com' }
|
|
15
16
|
layout 'mailer'
|
|
16
17
|
|
|
17
|
-
# Bounce an inbound email that matched no
|
|
18
|
+
# Bounce an inbound email that matched no agent back to its sender.
|
|
18
19
|
#
|
|
19
20
|
# Returned to +AgentMailbox#bounce_with_unrouted+, which hands it to Action Mailbox's +bounce_with+
|
|
20
21
|
# for delivery. The body is built inline (no template/layout) so a bounce can never fail to render.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Drives real-time Turbo Stream broadcasting for +Agent+ records — keeping the Agents sidebar
|
|
5
|
+
# live: the row is appended on create, replaced on update, and removed on destroy. First-page viewers
|
|
6
|
+
# subscribe via +turbo_stream_from "protege:agents"+ (see +SidebarHelper#sidebar_stream+).
|
|
7
|
+
module BroadcastableAgent
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# Turbo Stream channel for all +Agent+ records.
|
|
11
|
+
# @return [String]
|
|
12
|
+
STREAM = 'protege:agents'
|
|
13
|
+
|
|
14
|
+
included do
|
|
15
|
+
after_create_commit :broadcast_agent_created
|
|
16
|
+
after_update_commit :broadcast_agent_updated
|
|
17
|
+
after_destroy_commit :broadcast_agent_removed
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Append this agent's row to the Agents sidebar on creation.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def broadcast_agent_created
|
|
26
|
+
broadcast_append_to STREAM,
|
|
27
|
+
target: 'agents_list',
|
|
28
|
+
partial: 'protege/agents/agent_sidebar_item',
|
|
29
|
+
locals: { agent: self }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Replace this agent's sidebar row in place when its attributes change.
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
def broadcast_agent_updated
|
|
36
|
+
broadcast_replace_to STREAM,
|
|
37
|
+
target: ActionView::RecordIdentifier.dom_id(self),
|
|
38
|
+
partial: 'protege/agents/agent_sidebar_item',
|
|
39
|
+
locals: { agent: self }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Remove this agent's sidebar row on destruction.
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def broadcast_agent_removed
|
|
46
|
+
broadcast_remove_to STREAM,
|
|
47
|
+
target: ActionView::RecordIdentifier.dom_id(self)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Drives real-time Turbo Stream broadcasting for +Toolkit+ records — keeping the (global) Toolkits
|
|
5
|
+
# sidebar live: the row is appended on create, replaced on update, and removed on destroy. First-page
|
|
6
|
+
# viewers subscribe via +turbo_stream_from "protege:toolkits"+ (see +SidebarHelper#sidebar_stream+).
|
|
7
|
+
module BroadcastableToolkit
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# Turbo Stream channel for all +Toolkit+ records.
|
|
11
|
+
# @return [String]
|
|
12
|
+
STREAM = 'protege:toolkits'
|
|
13
|
+
|
|
14
|
+
included do
|
|
15
|
+
after_create_commit :broadcast_toolkit_created
|
|
16
|
+
after_update_commit :broadcast_toolkit_updated
|
|
17
|
+
after_destroy_commit :broadcast_toolkit_removed
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Append this toolkit's row to the Toolkits sidebar on creation.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def broadcast_toolkit_created
|
|
26
|
+
broadcast_append_to STREAM,
|
|
27
|
+
target: 'toolkits_list',
|
|
28
|
+
partial: 'protege/toolkits/toolkit_sidebar_item',
|
|
29
|
+
locals: { toolkit: self }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Replace this toolkit's sidebar row in place when its attributes change.
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
def broadcast_toolkit_updated
|
|
36
|
+
broadcast_replace_to STREAM,
|
|
37
|
+
target: ActionView::RecordIdentifier.dom_id(self),
|
|
38
|
+
partial: 'protege/toolkits/toolkit_sidebar_item',
|
|
39
|
+
locals: { toolkit: self }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Remove this toolkit's sidebar row on destroy.
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def broadcast_toolkit_removed
|
|
46
|
+
broadcast_remove_to STREAM, target: ActionView::RecordIdentifier.dom_id(self)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|