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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Manages agent records — listing, creating, viewing, editing, and permanent deletion.
|
|
5
|
+
#
|
|
6
|
+
# An agent is the identity behind an inbox; the controller surfaces the full CRUD the dashboard
|
|
7
|
+
# needs. +destroy+ is the hard, irreversible removal (cascading all the agent's data); the
|
|
8
|
+
# reversible, history-preserving alternative is archiving — see +ArchivesController+.
|
|
9
|
+
class AgentsController < ApplicationController
|
|
10
|
+
before_action :set_agent, only: %i[show edit update destroy]
|
|
11
|
+
|
|
12
|
+
# List every agent. Serves +GET /agents+.
|
|
13
|
+
#
|
|
14
|
+
# @return [void]
|
|
15
|
+
def index
|
|
16
|
+
@agents = Agent.all
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Show a single agent. Serves +GET /agents/:id+.
|
|
20
|
+
#
|
|
21
|
+
# @return [void]
|
|
22
|
+
def show; end
|
|
23
|
+
|
|
24
|
+
# Render the form for creating an agent. Serves +GET /agents/new+.
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
27
|
+
def new
|
|
28
|
+
@agent = Agent.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Render the form for editing an agent. Serves +GET /agents/:id/edit+.
|
|
32
|
+
#
|
|
33
|
+
# @return [void]
|
|
34
|
+
def edit; end
|
|
35
|
+
|
|
36
|
+
# Create an agent from the form's split email fields. Serves +POST /agents+.
|
|
37
|
+
#
|
|
38
|
+
# On failure, re-render the form with validation errors.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def create
|
|
42
|
+
@agent = Agent.new(create_params)
|
|
43
|
+
|
|
44
|
+
if @agent.save
|
|
45
|
+
redirect_to agent_path(@agent), notice: 'Agent created.'
|
|
46
|
+
else
|
|
47
|
+
render :new, status: :unprocessable_entity
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Update an existing agent. Serves +PATCH /agents/:id+.
|
|
52
|
+
#
|
|
53
|
+
# On failure, re-render the edit form with validation errors.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
def update
|
|
57
|
+
if @agent.update(update_params)
|
|
58
|
+
redirect_to agent_path(@agent), notice: 'Agent updated.'
|
|
59
|
+
else
|
|
60
|
+
render :edit, status: :unprocessable_entity
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Permanently delete an agent and everything it owns. Serves +DELETE /agents/:id+.
|
|
65
|
+
#
|
|
66
|
+
# This is the hard, irreversible removal (the reversible path is Archive — see +ArchivesController+):
|
|
67
|
+
# the model cascades the agent's threads, messages, responsibilities, access rules, and tool-use
|
|
68
|
+
# history. +BroadcastableAgent+ removes its sidebar row.
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
def destroy
|
|
72
|
+
@agent.destroy!
|
|
73
|
+
|
|
74
|
+
redirect_to agents_path, notice: 'Agent deleted.'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Load the +Agent+ named by +params[:id]+ into +@agent+.
|
|
80
|
+
#
|
|
81
|
+
# @return [Protege::Agent] the loaded agent
|
|
82
|
+
def set_agent
|
|
83
|
+
@agent = Agent.find(params[:id])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Permit creation attributes and fold the split email fields into a single address.
|
|
87
|
+
#
|
|
88
|
+
# @return [ActionController::Parameters] the permitted attributes plus +:email_address+
|
|
89
|
+
def create_params
|
|
90
|
+
params.require(:agent)
|
|
91
|
+
.permit(:type, :name, :instructions)
|
|
92
|
+
.merge(email_address: compose_email_address)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Permit the editable attributes and recompose the address from the split email fields. Changing the
|
|
96
|
+
# address is allowed — only mail to the *old* address stops routing here; all history keys off
|
|
97
|
+
# +agent_id+, not the address. Tools are not edited here: they come from the toolkits attached to the
|
|
98
|
+
# agent (managed on its show page), not from a per-agent field.
|
|
99
|
+
#
|
|
100
|
+
# @return [ActionController::Parameters] the permitted attributes plus +:email_address+
|
|
101
|
+
def update_params
|
|
102
|
+
params.require(:agent)
|
|
103
|
+
.permit(:name, :instructions)
|
|
104
|
+
.merge(email_address: compose_email_address)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Combine the split prefix + domain fields from the form into a single
|
|
108
|
+
# email address.
|
|
109
|
+
#
|
|
110
|
+
# @return [String] the +prefix@domain+ address with surrounding whitespace stripped
|
|
111
|
+
def compose_email_address
|
|
112
|
+
params[:agent].values_at(:email_prefix, :email_domain)
|
|
113
|
+
.map(&:to_s)
|
|
114
|
+
.map(&:strip)
|
|
115
|
+
.join('@')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Protege
|
|
4
|
-
# Archives and unarchives
|
|
4
|
+
# Archives and unarchives an agent — the reversible, data-preserving alternative to permanent deletion.
|
|
5
5
|
#
|
|
6
|
-
# Archiving is modeled as an +archive+ resource on the
|
|
7
|
-
# +archived_at+), +destroy+ unarchives it (clears it). An archived
|
|
6
|
+
# Archiving is modeled as an +archive+ resource on the agent: +create+ archives it (stamps
|
|
7
|
+
# +archived_at+), +destroy+ unarchives it (clears it). An archived agent stops receiving mail and
|
|
8
8
|
# running scheduled responsibilities, but all its history is kept and it can be restored at any time.
|
|
9
9
|
#
|
|
10
|
-
# Both actions re-render the
|
|
11
|
-
# separately via +
|
|
10
|
+
# Both actions re-render the agent's action bar in place over Turbo (the sidebar row updates
|
|
11
|
+
# separately via +BroadcastableAgent+), falling back to a full redirect for non-Turbo requests.
|
|
12
12
|
class ArchivesController < ApplicationController
|
|
13
|
-
include
|
|
13
|
+
include AgentScoped
|
|
14
14
|
|
|
15
|
-
# Archive the
|
|
15
|
+
# Archive the agent. Serves +POST /agents/:agent_id/archive+.
|
|
16
16
|
#
|
|
17
17
|
# @return [void]
|
|
18
18
|
def create
|
|
19
|
-
@
|
|
19
|
+
@agent.archive!
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
respond_with_agent(notice: 'Agent archived — it no longer receives mail or runs scheduled tasks.')
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Unarchive the
|
|
24
|
+
# Unarchive the agent. Serves +DELETE /agents/:agent_id/archive+.
|
|
25
25
|
#
|
|
26
26
|
# @return [void]
|
|
27
27
|
def destroy
|
|
28
|
-
@
|
|
28
|
+
@agent.unarchive!
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
respond_with_agent(notice: 'Agent restored.')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
-
# Replace the
|
|
35
|
+
# Replace the agent's action bar over Turbo, or redirect back to the agent for a full request.
|
|
36
36
|
#
|
|
37
37
|
# @param notice [String] the flash message for the redirect fallback
|
|
38
38
|
# @return [void]
|
|
39
|
-
def
|
|
39
|
+
def respond_with_agent(notice:)
|
|
40
40
|
respond_to do |format|
|
|
41
41
|
format.turbo_stream do
|
|
42
42
|
render turbo_stream: turbo_stream.replace(
|
|
43
|
-
helpers.dom_id(@
|
|
44
|
-
partial: 'protege/
|
|
45
|
-
locals: {
|
|
43
|
+
helpers.dom_id(@agent, :actions),
|
|
44
|
+
partial: 'protege/agents/actions',
|
|
45
|
+
locals: { agent: @agent }
|
|
46
46
|
)
|
|
47
47
|
end
|
|
48
|
-
format.html { redirect_to
|
|
48
|
+
format.html { redirect_to agent_path(@agent), notice: }
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -4,7 +4,7 @@ module Protege
|
|
|
4
4
|
# Serves the dashboard landing page mounted at the engine root.
|
|
5
5
|
#
|
|
6
6
|
# The page is static — it renders the +show+ view as the entry point into the
|
|
7
|
-
# Protege dashboard, from which operators navigate to
|
|
7
|
+
# Protege dashboard, from which operators navigate to agents, threads, and
|
|
8
8
|
# email domains.
|
|
9
9
|
class HomeController < ApplicationController
|
|
10
10
|
# Render the dashboard landing page. Serves +GET /+.
|
|
@@ -4,7 +4,7 @@ module Protege
|
|
|
4
4
|
# Messages — the console email search and per-message delete, both keyed off the globally-unique
|
|
5
5
|
# message, so neither is thread-scoped.
|
|
6
6
|
#
|
|
7
|
-
# +index+ (+GET /messages+) is the cross-
|
|
7
|
+
# +index+ (+GET /messages+) is the cross-agent search view: query params filter the messages via
|
|
8
8
|
# +MessageSearch+ and the matches render in the sidebar, each linking to its thread. +destroy+
|
|
9
9
|
# (+DELETE /messages/:id+) removes a single message by id.
|
|
10
10
|
class MessagesController < ApplicationController
|
|
@@ -11,7 +11,7 @@ module Protege
|
|
|
11
11
|
# Serves +POST /threads/:thread_id/reply+.
|
|
12
12
|
#
|
|
13
13
|
# The console acts as the sender; the reply is addressed to the thread's
|
|
14
|
-
#
|
|
14
|
+
# agent so the LOGI loop processes it as if it arrived over SMTP.
|
|
15
15
|
#
|
|
16
16
|
# The new message inserts itself into the thread over Turbo (its +after_create_commit+ broadcast,
|
|
17
17
|
# see +BroadcastableMessage+), so a Turbo request needs no body — it just returns +201 Created+, which
|
|
@@ -22,7 +22,7 @@ module Protege
|
|
|
22
22
|
def create
|
|
23
23
|
Gateway.accept_console_message(
|
|
24
24
|
mail: build_reply_mail,
|
|
25
|
-
|
|
25
|
+
agent: @thread.agent,
|
|
26
26
|
attachments: reply_params[:files] || []
|
|
27
27
|
)
|
|
28
28
|
|
|
@@ -53,7 +53,7 @@ module Protege
|
|
|
53
53
|
|
|
54
54
|
Gateway.build_outbound(
|
|
55
55
|
from: Protege.configuration.console_address,
|
|
56
|
-
to: @thread.
|
|
56
|
+
to: @thread.agent.email_address,
|
|
57
57
|
subject: @thread.subject,
|
|
58
58
|
body: reply_params[:body],
|
|
59
59
|
in_reply_to: messages.last&.message_id,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Protege
|
|
4
4
|
# Manages scheduled responsibilities — the Loop layer's dashboard CRUD. A top-level section (like
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Agents or Traces): responsibilities are listed across all agents, and each one names its owning
|
|
6
|
+
# agent in the form rather than carrying it in the URL.
|
|
7
7
|
class ResponsibilitiesController < ApplicationController
|
|
8
8
|
before_action :set_responsibility, only: %i[show edit update destroy]
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ module Protege
|
|
|
11
11
|
#
|
|
12
12
|
# @return [void]
|
|
13
13
|
def index
|
|
14
|
-
@responsibilities = Responsibility.includes(:
|
|
14
|
+
@responsibilities = Responsibility.includes(:agent)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Show a single responsibility and its recent runs.
|
|
@@ -31,7 +31,7 @@ module Protege
|
|
|
31
31
|
# @return [void]
|
|
32
32
|
def edit; end
|
|
33
33
|
|
|
34
|
-
# Create a responsibility (its
|
|
34
|
+
# Create a responsibility (its agent is chosen in the form). On failure, re-render with errors.
|
|
35
35
|
#
|
|
36
36
|
# @return [void]
|
|
37
37
|
def create
|
|
@@ -65,18 +65,18 @@ module Protege
|
|
|
65
65
|
|
|
66
66
|
private
|
|
67
67
|
|
|
68
|
-
# Load the responsibility named by the route into +@responsibility+ (its
|
|
68
|
+
# Load the responsibility named by the route into +@responsibility+ (its agent via the association).
|
|
69
69
|
#
|
|
70
70
|
# @return [Protege::Responsibility] the loaded responsibility
|
|
71
71
|
def set_responsibility
|
|
72
72
|
@responsibility = Responsibility.find(params[:id])
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# Permit the editable responsibility attributes, including the +:
|
|
75
|
+
# Permit the editable responsibility attributes, including the +:agent_id+ the form now supplies.
|
|
76
76
|
#
|
|
77
77
|
# @return [ActionController::Parameters] the permitted attributes
|
|
78
78
|
def responsibility_params
|
|
79
|
-
params.require(:responsibility).permit(:
|
|
79
|
+
params.require(:responsibility).permit(:agent_id, :name, :instructions, :schedule, :active)
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
end
|
|
@@ -35,10 +35,10 @@ module Protege
|
|
|
35
35
|
#
|
|
36
36
|
# @return [void]
|
|
37
37
|
def create
|
|
38
|
-
|
|
38
|
+
agent = Agent.find(compose_params[:agent_id])
|
|
39
39
|
|
|
40
40
|
message = Gateway.message(
|
|
41
|
-
to:
|
|
41
|
+
to: agent.email_address,
|
|
42
42
|
subject: compose_params[:subject],
|
|
43
43
|
body: compose_params[:body],
|
|
44
44
|
attachments: compose_params[:files] || []
|
|
@@ -67,9 +67,9 @@ module Protege
|
|
|
67
67
|
|
|
68
68
|
# Permit the compose-form attributes for a new conversation.
|
|
69
69
|
#
|
|
70
|
-
# @return [ActionController::Parameters] the permitted +:
|
|
70
|
+
# @return [ActionController::Parameters] the permitted +:agent_id+, +:subject+, +:body+, +:files+
|
|
71
71
|
def compose_params
|
|
72
|
-
params.require(:thread).permit(:
|
|
72
|
+
params.require(:thread).permit(:agent_id, :subject, :body, files: [])
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Manages toolkits — the dashboard CRUD for the tool-bundle binding. A top-level section (like
|
|
5
|
+
# Responsibilities): toolkits are global, reusable tool bundles, attached to agents from the agent
|
|
6
|
+
# page. A toolkit is *just* its tools — permission (the sender gate + proactive flag) lives on each grant
|
|
7
|
+
# (+AgentToolkit+), so this controller only edits the name/description/tool set.
|
|
8
|
+
class ToolkitsController < ApplicationController
|
|
9
|
+
before_action :set_toolkit, only: %i[show edit update destroy]
|
|
10
|
+
|
|
11
|
+
# List every toolkit.
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
14
|
+
def index
|
|
15
|
+
@toolkits = Toolkit.order(:name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Show a single toolkit and its gate rules.
|
|
19
|
+
#
|
|
20
|
+
# @return [void]
|
|
21
|
+
def show; end
|
|
22
|
+
|
|
23
|
+
# Render the form for creating a toolkit.
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
26
|
+
def new
|
|
27
|
+
@toolkit = Toolkit.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Render the form for editing a toolkit.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
def edit; end
|
|
34
|
+
|
|
35
|
+
# Create a toolkit. On failure, re-render the form with validation errors.
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
38
|
+
def create
|
|
39
|
+
@toolkit = Toolkit.new(toolkit_params)
|
|
40
|
+
|
|
41
|
+
if @toolkit.save
|
|
42
|
+
redirect_to toolkit_path(@toolkit), notice: 'Toolkit created.'
|
|
43
|
+
else
|
|
44
|
+
render :new, status: :unprocessable_entity
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Update a toolkit. On failure, re-render the edit form with validation errors.
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
def update
|
|
52
|
+
if @toolkit.update(toolkit_params)
|
|
53
|
+
redirect_to toolkit_path(@toolkit), notice: 'Toolkit updated.'
|
|
54
|
+
else
|
|
55
|
+
render :edit, status: :unprocessable_entity
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Remove a toolkit (detaching it from every agent via the join's dependent destroy).
|
|
60
|
+
#
|
|
61
|
+
# @return [void]
|
|
62
|
+
def destroy
|
|
63
|
+
@toolkit.destroy
|
|
64
|
+
redirect_to toolkits_path, notice: 'Toolkit deleted.'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Load the toolkit named by the route.
|
|
70
|
+
#
|
|
71
|
+
# @return [Protege::Toolkit] the loaded toolkit
|
|
72
|
+
def set_toolkit
|
|
73
|
+
@toolkit = Toolkit.find(params[:id])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Permit the editable attributes. The member tools arrive as an array from the checklist; the gate
|
|
77
|
+
# rules are edited separately as child rows, so they are not permitted here.
|
|
78
|
+
#
|
|
79
|
+
# @return [ActionController::Parameters] the permitted attributes
|
|
80
|
+
def toolkit_params
|
|
81
|
+
params.require(:toolkit).permit(:name, :description, member_tool_ids: [])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# View helpers for agent pages — selectors, labels, status, and resolver display.
|
|
5
|
+
module AgentsHelper
|
|
6
|
+
# List the loaded +Agent+ subclasses for the new-agent type selector.
|
|
7
|
+
#
|
|
8
|
+
# @return [Array<Class>] the +Agent+ descendant classes
|
|
9
|
+
def available_agent_types
|
|
10
|
+
Agent.descendants
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# List active email domains for the agent form's domain selector.
|
|
14
|
+
#
|
|
15
|
+
# @return [ActiveRecord::Relation<Protege::EmailDomain>] domains eligible for selection
|
|
16
|
+
def available_email_domains
|
|
17
|
+
EmailDomain.all
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Fetch every +Agent+ ordered by name for sidebar listing.
|
|
21
|
+
#
|
|
22
|
+
# @return [ActiveRecord::Relation<Protege::Agent>] agents ordered alphabetically
|
|
23
|
+
def all_agents
|
|
24
|
+
Agent.order(:name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Format an agent's name together with its type for display.
|
|
28
|
+
#
|
|
29
|
+
# agent_label(agent) # => "Bob <Executive Agent>"
|
|
30
|
+
#
|
|
31
|
+
# @param agent [Protege::Agent] the agent to label
|
|
32
|
+
# @return [String] the +"name <Type>"+ label
|
|
33
|
+
def agent_label(agent)
|
|
34
|
+
"#{agent.name} <#{agent.class.display_name}>"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Render a status dot reflecting whether an agent is available.
|
|
38
|
+
#
|
|
39
|
+
# @param agent [Protege::Agent] the agent whose availability to show
|
|
40
|
+
# @return [ActiveSupport::SafeBuffer] a green dot when active, red otherwise
|
|
41
|
+
def agent_status(agent)
|
|
42
|
+
if agent.active?
|
|
43
|
+
status_dot('Available', :green)
|
|
44
|
+
else
|
|
45
|
+
status_dot('Unavailable', :red)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Format an agent's resolver chain as a hash suitable for +info_card+ (rendered full-width, since
|
|
50
|
+
# the resolver class names are long). Each entry maps the resolver's short class name to a summary
|
|
51
|
+
# of its positional args, keyword args, and whether it was given a block.
|
|
52
|
+
#
|
|
53
|
+
# @param agent [Protege::Agent] the agent whose reply (+message_resolvers+) chain to format
|
|
54
|
+
# @return [Hash{String => String}] resolver-name => configuration summary
|
|
55
|
+
def resolver_pairs(agent)
|
|
56
|
+
pairs = agent.message_resolvers.entries.each_with_object({}) do |(klass, args, kwargs, block), accumulator|
|
|
57
|
+
accumulator[klass.name.demodulize] = resolver_config(args, kwargs, block)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
pairs.any? ? pairs : { 'Resolvers' => 'None configured' }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# Summarize a resolver entry's positional args, keyword args, and whether it was given a block.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] a comma-joined summary, or "" when the resolver takes no configuration
|
|
68
|
+
def resolver_config(args, kwargs, block)
|
|
69
|
+
parts = args.map(&:inspect)
|
|
70
|
+
parts += kwargs.map { |key, value| "#{key}: #{value.inspect}" }
|
|
71
|
+
parts << '{ … }' if block
|
|
72
|
+
parts.join(', ')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -8,16 +8,17 @@ module Protege
|
|
|
8
8
|
# +Protege::Components::+ — pulled in as a whole via its own aggregator, +Components::ComponentsHelper+,
|
|
9
9
|
# which includes the rest of the group (typography, tables, badges, buttons, cards, dialogs, menus,
|
|
10
10
|
# forms, layout, sidebar, pagination, the introspection panel) — and the model-named helpers at the
|
|
11
|
-
# top level (threads,
|
|
11
|
+
# top level (threads, agents, email
|
|
12
12
|
# domains, messages, responsibilities, traces). Any cross-helper call resolves through the view, which
|
|
13
13
|
# has them all via this module.
|
|
14
14
|
module ApplicationHelper
|
|
15
15
|
include Components::ComponentsHelper
|
|
16
16
|
include EmailDomainsHelper
|
|
17
17
|
include MessagesHelper
|
|
18
|
-
include
|
|
18
|
+
include AgentsHelper
|
|
19
19
|
include ResponsibilitiesHelper
|
|
20
20
|
include ThreadsHelper
|
|
21
|
+
include ToolkitsHelper
|
|
21
22
|
include TraceHelper
|
|
22
23
|
|
|
23
24
|
# List of JS files to be added to the console layout
|
|
@@ -10,13 +10,22 @@ module Protege
|
|
|
10
10
|
#
|
|
11
11
|
# @param text [String] the badge label
|
|
12
12
|
# @param color [Symbol] the semantic color (see +badge_colors+)
|
|
13
|
+
# @param size [Symbol] +:md+ (default) or +:sm+ for a tinier pill (see +badge_size+)
|
|
13
14
|
# @return [ActiveSupport::SafeBuffer] the badge markup
|
|
14
|
-
def badge(text, color = :gray)
|
|
15
|
+
def badge(text, color = :gray, size: :md)
|
|
15
16
|
_, fg = badge_colors(color)
|
|
16
|
-
tag.span(text, class: "inline-flex items-center rounded
|
|
17
|
+
tag.span(text, class: "inline-flex items-center rounded font-medium #{badge_size(size)} #{fg}",
|
|
17
18
|
style: 'background: var(--surface-subtle); border: 1px solid var(--border)')
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# Map a badge size to its padding + text-size classes.
|
|
22
|
+
#
|
|
23
|
+
# @param size [Symbol] +:sm+ or +:md+
|
|
24
|
+
# @return [String] the sizing classes
|
|
25
|
+
def badge_size(size)
|
|
26
|
+
size == :sm ? 'px-1 py-px text-[10px]' : 'px-1.5 py-0.5 text-xs'
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
# Map a semantic badge color to its +[background, foreground]+ classes.
|
|
21
30
|
#
|
|
22
31
|
# @param color [Symbol] the semantic color
|
|
@@ -75,13 +75,13 @@ module Protege
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# Render the split prefix/domain email-address control for
|
|
78
|
+
# Render the split prefix/domain email-address control for an agent.
|
|
79
79
|
#
|
|
80
80
|
# When domains exist, render a prefix text box, an +@+ separator, and a domain
|
|
81
81
|
# select preselected to the record's current domain; otherwise prompt the
|
|
82
82
|
# operator to add a domain first.
|
|
83
83
|
#
|
|
84
|
-
# @param record [Protege::
|
|
84
|
+
# @param record [Protege::Agent] the agent whose email address to edit
|
|
85
85
|
# @param domains [Enumerable<Protege::EmailDomain>] the selectable domains
|
|
86
86
|
# @return [ActiveSupport::SafeBuffer] the email-address control markup
|
|
87
87
|
def form_email_address(record, domains)
|
|
@@ -138,16 +138,16 @@ module Protege
|
|
|
138
138
|
# @param hint [String, nil] optional helper text shown beneath the label
|
|
139
139
|
# @param value [Symbol, Proc] maps an item to its submitted value (default: +:to_s+)
|
|
140
140
|
# @param text [Symbol, Proc] maps an item to its checkbox label (default: +:to_s+)
|
|
141
|
+
# @param description [Symbol, nil] reader for an item's secondary description; when given (and the
|
|
142
|
+
# item answers it), each row stacks the label over a muted description line for scannability
|
|
141
143
|
# @return [ActiveSupport::SafeBuffer] the checklist markup
|
|
142
|
-
def form_checklist(form, field, collection, label: nil, hint: nil, value: :to_s, text: :to_s)
|
|
144
|
+
def form_checklist(form, field, collection, label: nil, hint: nil, value: :to_s, text: :to_s, description: nil)
|
|
143
145
|
tag.div do
|
|
144
146
|
(label ? tag.label(label, class: FORM_LABEL) : ''.html_safe) +
|
|
145
147
|
form_hint(hint) +
|
|
146
|
-
tag.div(class: 'space-y-2
|
|
148
|
+
tag.div(class: 'space-y-2 mt-3') do
|
|
147
149
|
form.collection_check_boxes(field, collection, value, text) do |b|
|
|
148
|
-
b
|
|
149
|
-
b.check_box(class: FORM_CHECKBOX, style: 'border: 1px solid var(--border)') + b.text
|
|
150
|
-
end
|
|
150
|
+
checklist_option(b, description)
|
|
151
151
|
end
|
|
152
152
|
end
|
|
153
153
|
end
|
|
@@ -155,6 +155,41 @@ module Protege
|
|
|
155
155
|
|
|
156
156
|
private
|
|
157
157
|
|
|
158
|
+
# Render a single checklist row: the checkbox and its label. With a +description+ reader, the label
|
|
159
|
+
# stacks the option name over a muted description line and top-aligns the box so multi-line rows read
|
|
160
|
+
# cleanly; without one, it falls back to a compact centered single line.
|
|
161
|
+
#
|
|
162
|
+
# @param builder [ActionView::Helpers::Tags::CollectionHelpers::Builder] the per-item builder
|
|
163
|
+
# @param description [Symbol, nil] reader for the option's description text
|
|
164
|
+
# @return [ActiveSupport::SafeBuffer] the labelled checkbox row
|
|
165
|
+
def checklist_option(builder, description)
|
|
166
|
+
desc = description && builder.object.public_send(description).presence
|
|
167
|
+
# With a description the row is multi-line, so top-align it and nudge the box down to sit on the
|
|
168
|
+
# first text line; without one, a compact centered single line.
|
|
169
|
+
row_align = desc ? 'items-start' : 'items-center'
|
|
170
|
+
box_class = desc ? "#{FORM_CHECKBOX} mt-0.5" : FORM_CHECKBOX
|
|
171
|
+
|
|
172
|
+
builder.label(class: "flex #{row_align} gap-3 text-sm") do
|
|
173
|
+
builder.check_box(class: box_class, style: 'border: 1px solid var(--border)') +
|
|
174
|
+
checklist_option_body(builder, desc)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Build the text beside a checklist checkbox: a plain label, or — when a description is present — the
|
|
179
|
+
# option name stacked over its muted description.
|
|
180
|
+
#
|
|
181
|
+
# @param builder [ActionView::Helpers::Tags::CollectionHelpers::Builder] the per-item builder
|
|
182
|
+
# @param desc [String, nil] the resolved description text, or nil for a plain label
|
|
183
|
+
# @return [ActiveSupport::SafeBuffer] the label body
|
|
184
|
+
def checklist_option_body(builder, desc)
|
|
185
|
+
return builder.text unless desc
|
|
186
|
+
|
|
187
|
+
tag.span(class: 'flex flex-col gap-0.5') do
|
|
188
|
+
tag.span(builder.text, class: 'font-medium') +
|
|
189
|
+
tag.span(desc, class: 'text-xs leading-snug', style: 'color: var(--text-faint)')
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
158
193
|
# Render the muted hint paragraph shown beneath a field, or nothing.
|
|
159
194
|
#
|
|
160
195
|
# @param hint [String, nil] the hint text
|
|
@@ -167,7 +202,7 @@ module Protege
|
|
|
167
202
|
|
|
168
203
|
# Render the prefix input, +@+ separator, and domain select for an address.
|
|
169
204
|
#
|
|
170
|
-
# @param record [Protege::
|
|
205
|
+
# @param record [Protege::Agent] the agent whose address to edit
|
|
171
206
|
# @param domains [Enumerable<Protege::EmailDomain>] the selectable domains
|
|
172
207
|
# @return [ActiveSupport::SafeBuffer] the prefix/separator/select markup
|
|
173
208
|
def email_address_fields(record, domains)
|
|
@@ -180,13 +215,13 @@ module Protege
|
|
|
180
215
|
|
|
181
216
|
# Render the local-part text input, seeded with the record's current prefix.
|
|
182
217
|
#
|
|
183
|
-
# @param record [Protege::
|
|
218
|
+
# @param record [Protege::Agent] the agent whose prefix to seed
|
|
184
219
|
# @param input_base [String] the shared input classes without the +w-full+ utility
|
|
185
220
|
# @return [ActiveSupport::SafeBuffer] the prefix input markup
|
|
186
221
|
def email_address_prefix(record, input_base)
|
|
187
222
|
tag.input(
|
|
188
223
|
type: 'text',
|
|
189
|
-
name: '
|
|
224
|
+
name: 'agent[email_prefix]',
|
|
190
225
|
value: record.email_address&.split('@')&.first,
|
|
191
226
|
placeholder: 'alice',
|
|
192
227
|
class: "block w-40 #{input_base}",
|
|
@@ -196,7 +231,7 @@ module Protege
|
|
|
196
231
|
|
|
197
232
|
# Render the domain select, preselecting the record's current domain.
|
|
198
233
|
#
|
|
199
|
-
# @param record [Protege::
|
|
234
|
+
# @param record [Protege::Agent] the agent whose domain to preselect
|
|
200
235
|
# @param domains [Enumerable<Protege::EmailDomain>] the selectable domains
|
|
201
236
|
# @param input_base [String] the shared input classes without the +w-full+ utility
|
|
202
237
|
# @return [ActiveSupport::SafeBuffer] the domain select markup
|
|
@@ -206,7 +241,7 @@ module Protege
|
|
|
206
241
|
selected: record.email_address&.end_with?("@#{domain.domain}"))
|
|
207
242
|
end)
|
|
208
243
|
|
|
209
|
-
tag.select(options, name: '
|
|
244
|
+
tag.select(options, name: 'agent[email_domain]', class: "block flex-1 #{input_base}",
|
|
210
245
|
style: form_input_style)
|
|
211
246
|
end
|
|
212
247
|
|
|
@@ -39,7 +39,7 @@ module Protege
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Render a block of verbatim body text — whitespace preserved, monospace — for content shown
|
|
42
|
-
# as-is, such as an email body or
|
|
42
|
+
# as-is, such as an email body or an agent's instructions. Escapes its input.
|
|
43
43
|
#
|
|
44
44
|
# @param text [String] the text to render
|
|
45
45
|
# @return [ActiveSupport::SafeBuffer] the preformatted text block
|
|
@@ -21,14 +21,14 @@ module Protege
|
|
|
21
21
|
message.sent_at&.strftime('%-m/%-d/%y')
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Render the sender line —
|
|
24
|
+
# Render the sender line — agent name for outbound mail, local part of the
|
|
25
25
|
# address for inbound mail — followed by the full address.
|
|
26
26
|
#
|
|
27
27
|
# @param message [Protege::Message] the message to render the sender for
|
|
28
28
|
# @return [ActiveSupport::SafeBuffer] the bold +"name <address>"+ span
|
|
29
29
|
def message_sender(message)
|
|
30
30
|
name = if message.outbound?
|
|
31
|
-
message.
|
|
31
|
+
message.agent.name
|
|
32
32
|
else
|
|
33
33
|
message.from_address.to_s.split('@').first
|
|
34
34
|
end
|