protege 0.1.0.alpha.2 → 0.1.0.alpha.8
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/dev_setup.rb +110 -0
- data/app/services/protege/message_search.rb +2 -2
- data/app/services/protege/system_toolkits.rb +80 -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 +19 -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 +40 -0
- metadata +67 -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,29 @@
|
|
|
1
|
+
- content_for :sidebar do
|
|
2
|
+
= render "sidebar", current: @agent
|
|
3
|
+
|
|
4
|
+
= page_container do
|
|
5
|
+
= page_header(@agent.name, @agent.class.display_name) do
|
|
6
|
+
= render("actions", agent: @agent)
|
|
7
|
+
|
|
8
|
+
.space-y-4
|
|
9
|
+
= info_card("Email address", { "Address" => @agent.email_address })
|
|
10
|
+
|
|
11
|
+
= card do
|
|
12
|
+
= heading_2("Instructions")
|
|
13
|
+
- if @agent.instructions.present?
|
|
14
|
+
.mt-3
|
|
15
|
+
= body_text(@agent.instructions)
|
|
16
|
+
- else
|
|
17
|
+
.mt-3
|
|
18
|
+
= hint("No instructions set yet — add them via Edit. They're used as the system prompt only when a resolver (e.g. LoadText) includes them in the agent's context.")
|
|
19
|
+
|
|
20
|
+
= render("access_rules", agent: @agent)
|
|
21
|
+
|
|
22
|
+
= render("toolkits", agent: @agent)
|
|
23
|
+
|
|
24
|
+
= render("protege/shared/confirm_dialog",
|
|
25
|
+
id: "delete-agent",
|
|
26
|
+
title: "Delete #{@agent.name}?",
|
|
27
|
+
message: "This permanently removes the agent and ALL of its data — every thread, message, attachment, responsibility, access rule, and tool-use record. This cannot be undone. To keep the history but stop the agent instead, archive it.",
|
|
28
|
+
action_url: agent_path(@agent),
|
|
29
|
+
action_text: "Delete agent")
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
= render("protege/shared/confirm_dialog",
|
|
32
32
|
id: "remove-domain",
|
|
33
33
|
title: "Remove #{@email_domain.domain}?",
|
|
34
|
-
message: "Removing this domain only deletes it from Protege. It does not affect your DNS records. If this domain is in use by
|
|
34
|
+
message: "Removing this domain only deletes it from Protege. It does not affect your DNS records. If this domain is in use by an agent, you will need to de-register the MX, SPF, DKIM, and DMARC entries with your DNS provider separately.",
|
|
35
35
|
action_url: email_domain_path(@email_domain),
|
|
36
36
|
action_text: "Remove domain")
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
= page_header('About Protege')
|
|
3
3
|
|
|
4
4
|
div class="space-y-4 text-sm leading-relaxed mb-10"
|
|
5
|
-
p Protege is an email-native AI agent toolkit. Your
|
|
5
|
+
p Protege is an email-native AI agent toolkit. Your agents receive email, process it through an inference pipeline, and respond autonomously using the tools you configure. Every conversation — whether from an external email client or this console — flows through the same harness.
|
|
6
6
|
|
|
7
|
-
p To get started, configure at least one email domain with valid DNS records, create
|
|
7
|
+
p To get started, configure at least one email domain with valid DNS records, create an agent with a routable email address, and set up your provider credentials. Once an agent is active and your MTA is running, it will process inbound email automatically.
|
|
8
8
|
|
|
9
9
|
p Protege is a toolkit for developers, not an out-of-the-box solution. Every part of the pipeline — providers, resolvers, tools, hooks — is an extension surface you implement. The #{ui_link("documentation", "https://docs.protege.bot", target: "_blank")} covers each surface in detail with examples and guides.
|
|
10
10
|
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
= heading_2("Extension surfaces")
|
|
14
14
|
|
|
15
15
|
= grid_container(cols: 2) do
|
|
16
|
-
= grid_card("
|
|
17
|
-
|
|
18
|
-
= grid_card("Providers", "LLM provider integration — OpenRouter, Anthropic, or any OpenAI-compatible endpoint.", href: "https://docs.protege.bot/providers")
|
|
19
|
-
|
|
20
|
-
= grid_card("Resolvers", "Build inference context before each generation — system prompts, conversation history, or custom data.", href: "https://docs.protege.bot/resolvers")
|
|
21
|
-
|
|
22
|
-
= grid_card("Tools", "Capabilities exposed to the LLM — send email, query APIs, or anything you implement.", href: "https://docs.protege.bot/tools")
|
|
23
|
-
|
|
24
|
-
= grid_card("Hooks", "Subscribe to lifecycle events — inference, tool calls, failures — for logging, metrics, or side effects.", href: "https://docs.protege.bot/hooks")
|
|
25
|
-
|
|
26
|
-
= grid_card("Gateway", "Inbound and outbound email transport — SMTP, console messages, threading, and persistence.", href: "https://docs.protege.bot/gateway")
|
|
16
|
+
= grid_card("Agents", "The addressable identity that receives and responds to email. Each agent has its own address, resolver chain, and display name.", href: "https://docs.protege.bot/reference/agents")
|
|
17
|
+
|
|
18
|
+
= grid_card("Providers", "LLM provider integration — OpenRouter, Anthropic, or any OpenAI-compatible endpoint.", href: "https://docs.protege.bot/extensions/providers")
|
|
19
|
+
|
|
20
|
+
= grid_card("Resolvers", "Build inference context before each generation — system prompts, conversation history, or custom data.", href: "https://docs.protege.bot/extensions/resolvers")
|
|
21
|
+
|
|
22
|
+
= grid_card("Tools", "Capabilities exposed to the LLM — send email, query APIs, or anything you implement.", href: "https://docs.protege.bot/extensions/tools")
|
|
23
|
+
|
|
24
|
+
= grid_card("Hooks", "Subscribe to lifecycle events — inference, tool calls, failures — for logging, metrics, or side effects.", href: "https://docs.protege.bot/extensions/hooks-and-events")
|
|
25
|
+
|
|
26
|
+
= grid_card("Gateway", "Inbound and outbound email transport — SMTP, console messages, threading, and persistence.", href: "https://docs.protege.bot/architecture/gateway")
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
= form_with url:, method:, scope: :responsibility, class: "space-y-5" do |f|
|
|
5
5
|
= form_errors(responsibility)
|
|
6
6
|
|
|
7
|
-
= form_select(f, :
|
|
7
|
+
= form_select(f, :agent_id, all_agents.map { |p| [p.name, p.id] }, label: "Agent", prompt: "Choose an agent")
|
|
8
8
|
|
|
9
9
|
= form_field(f, :name, placeholder: "e.g. Morning digest")
|
|
10
10
|
|
|
11
11
|
= form_field(f, :schedule, label: "Schedule", placeholder: "0 9 * * 1-5", hint: "A 5-field cron expression (minute hour day-of-month month day-of-week) in the app timezone — e.g. \"0 9 * * 1-5\" is 9am on weekdays, \"*/15 * * * *\" every 15 minutes.")
|
|
12
12
|
|
|
13
|
-
= form_text_area(f, :instructions, label: "Instructions", rows: 8, hint: "The task the agent carries out on each run, delivered as the opening message. The
|
|
13
|
+
= form_text_area(f, :instructions, label: "Instructions", rows: 8, hint: "The task the agent carries out on each run, delivered as the opening message. The agent's responsibility_resolvers chain supplies the system context around it.")
|
|
14
14
|
|
|
15
15
|
- unless responsibility.new_record?
|
|
16
16
|
= form_checkbox(f, :active, label: "Active")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
= sidebar_item(responsibility.name, responsibility_path(responsibility),
|
|
2
2
|
detail: responsibility_status(responsibility),
|
|
3
|
-
subtitle: "#{responsibility.
|
|
3
|
+
subtitle: "#{responsibility.agent.name} · #{responsibility.schedule}",
|
|
4
4
|
active: local_assigns[:active],
|
|
5
5
|
id: dom_id(responsibility))
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
= render "sidebar"
|
|
3
3
|
|
|
4
4
|
= page_container do
|
|
5
|
-
= page_header("New responsibility", "A scheduled duty
|
|
5
|
+
= page_header("New responsibility", "A scheduled duty an agent carries out on a cron schedule.")
|
|
6
6
|
|
|
7
7
|
= card { render "form", responsibility: @responsibility }
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
= render "sidebar", current: @responsibility
|
|
3
3
|
|
|
4
4
|
= page_container do
|
|
5
|
-
= page_header(@responsibility.name, "#{@responsibility.
|
|
5
|
+
= page_header(@responsibility.name, "#{@responsibility.agent.name} · #{@responsibility.schedule}") do
|
|
6
6
|
div
|
|
7
7
|
= ui_link("Edit", edit_responsibility_path(@responsibility), class: 'px-3')
|
|
8
8
|
= inline_button("Run now", responsibility_runs_path, params: { responsibility_run: { responsibility_id: @responsibility.id } })
|
|
9
9
|
|
|
10
10
|
.space-y-4
|
|
11
|
-
= info_card("Schedule", { "Cron" => @responsibility.schedule, "
|
|
11
|
+
= info_card("Schedule", { "Cron" => @responsibility.schedule, "Agent" => @responsibility.agent.name, "Active" => (@responsibility.active? ? "Yes" : "No") })
|
|
12
12
|
|
|
13
13
|
= card do
|
|
14
14
|
= heading_2("Instructions")
|
|
@@ -4,8 +4,9 @@ header class="flex h-14 flex-shrink-0 items-center justify-between px-5" style="
|
|
|
4
4
|
|
|
5
5
|
nav class="flex items-center gap-1"
|
|
6
6
|
= nav_item("Inbox", protege.threads_path, request.path)
|
|
7
|
+
= nav_item("Agents", protege.agents_path, request.path)
|
|
8
|
+
= nav_item("Toolkits", protege.toolkits_path, request.path)
|
|
7
9
|
= nav_item("Responsibilities", protege.responsibilities_path, request.path)
|
|
8
|
-
= nav_item("Personas", protege.personas_path, request.path)
|
|
9
10
|
= nav_item("Domains", protege.email_domains_path, request.path)
|
|
10
11
|
= nav_item("Traces", protege.traces_path, request.path)
|
|
11
12
|
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
/
|
|
4
4
|
/ Registry — the single source of truth for app-wide combos. Each is a hidden link keys.js activates on
|
|
5
5
|
/ its combo; it navigates via the same route helper the visible UI uses, so a global command works from
|
|
6
|
-
/ anywhere, independent of what's on screen. Nav combos are numbered by position (1..
|
|
6
|
+
/ anywhere, independent of what's on screen. Nav combos are numbered by position (1..6, left to right)
|
|
7
7
|
/ rather than by initial — the "go to the Nth section" convention (⌘1..9 in browsers/editors). Rendered
|
|
8
8
|
/ after the page body, so a page-local hotkey reusing a combo is matched first and shadows the global one.
|
|
9
9
|
div hidden=true aria-hidden="true"
|
|
10
10
|
= link_to "Documentation", 'https://docs.protege.bot', data: { hotkey: "g 0" }, target: '_blank'
|
|
11
11
|
= link_to "Inbox", protege.threads_path, data: { hotkey: "g 1" }
|
|
12
|
-
= link_to "
|
|
13
|
-
= link_to "
|
|
14
|
-
= link_to "
|
|
15
|
-
= link_to "
|
|
12
|
+
= link_to "Agents", protege.agents_path, data: { hotkey: "g 2" }
|
|
13
|
+
= link_to "Toolkits", protege.toolkits_path, data: { hotkey: "g 3" }
|
|
14
|
+
= link_to "Responsibilities", protege.responsibilities_path, data: { hotkey: "g 4" }
|
|
15
|
+
= link_to "Domains", protege.email_domains_path, data: { hotkey: "g 5" }
|
|
16
|
+
= link_to "Traces", protege.traces_path, data: { hotkey: "g 6" }
|
|
16
17
|
= link_to "New Thread", protege.new_thread_path, data: { hotkey: "g c" }
|
|
18
|
+
= link_to "New Agent", protege.new_agent_path, data: { hotkey: "g a" }
|
|
19
|
+
= link_to "New Toolkit", protege.new_toolkit_path, data: { hotkey: "g k" }
|
|
17
20
|
= link_to "New Responsibility", protege.new_responsibility_path, data: { hotkey: "g r" }
|
|
18
|
-
= link_to "New Persona", protege.new_persona_path, data: { hotkey: "g p" }
|
|
19
21
|
= link_to "New Email Domain", protege.new_email_domain_path, data: { hotkey: "g d" }
|
|
20
22
|
|
|
21
23
|
/ Help overlay, opened with "?" or the header's "?" button (keys.js just toggles it open/closed). This
|
|
@@ -26,16 +28,18 @@ div hidden=true aria-hidden="true"
|
|
|
26
28
|
.mt-3.space-y-1
|
|
27
29
|
= hotkey_section("Go to")
|
|
28
30
|
= hotkey_row("Inbox", "g", "1")
|
|
29
|
-
= hotkey_row("
|
|
30
|
-
= hotkey_row("
|
|
31
|
-
= hotkey_row("
|
|
32
|
-
= hotkey_row("
|
|
31
|
+
= hotkey_row("Agents", "g", "2")
|
|
32
|
+
= hotkey_row("Toolkits", "g", "3")
|
|
33
|
+
= hotkey_row("Responsibilities", "g", "4")
|
|
34
|
+
= hotkey_row("Domains", "g", "5")
|
|
35
|
+
= hotkey_row("Traces", "g", "6")
|
|
33
36
|
= hotkey_row("Documentation", "g", "0")
|
|
34
37
|
|
|
35
38
|
= hotkey_section("Create")
|
|
36
39
|
= hotkey_row("New thread", "g", "c")
|
|
40
|
+
= hotkey_row("New agent", "g", "a")
|
|
41
|
+
= hotkey_row("New toolkit", "g", "k")
|
|
37
42
|
= hotkey_row("New responsibility", "g", "r")
|
|
38
|
-
= hotkey_row("New persona", "g", "p")
|
|
39
43
|
= hotkey_row("New email domain", "g", "d")
|
|
40
44
|
|
|
41
45
|
= hotkey_section("General")
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
= render "protege/threads/sidebar", threads: inbox_threads
|
|
3
3
|
|
|
4
4
|
= page_container do
|
|
5
|
-
= page_header("New message", "Send a message to
|
|
5
|
+
= page_header("New message", "Send a message to an agent to start a thread.")
|
|
6
6
|
|
|
7
7
|
= card do
|
|
8
8
|
= form_with url: threads_path, scope: :thread, class: "space-y-5", multipart: true do |f|
|
|
9
|
-
= form_select(f, :
|
|
9
|
+
= form_select(f, :agent_id, available_agent_options, label: "To", prompt: "Select agent...")
|
|
10
10
|
|
|
11
11
|
= form_field(f, :subject, label: "Subject", placeholder: "Subject")
|
|
12
12
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
= form_with model: toolkit, class: "space-y-5" do |f|
|
|
2
|
+
= form_errors(toolkit)
|
|
3
|
+
|
|
4
|
+
= form_field(f, :name, placeholder: "e.g. Finance")
|
|
5
|
+
|
|
6
|
+
= form_text_area(f, :description, label: "Description", rows: 3, hint: "What this toolkit is for — shown only in the dashboard.")
|
|
7
|
+
|
|
8
|
+
- tool_id = ->(entry) { entry.id.to_s }
|
|
9
|
+
= form_checklist(f, :member_tool_ids, toolkit_tool_options, label: "Tools", value: tool_id, text: :display_name, description: :summary, hint: "The tools this toolkit bundles. Who may use them is set per agent when you attach it.")
|
|
10
|
+
|
|
11
|
+
.flex.items-center.gap-3.pt-2
|
|
12
|
+
= button("Save toolkit", type: :submit)
|
|
13
|
+
= ui_link("Cancel", toolkits_path)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
- pagy, toolkits = paginate(all_toolkits)
|
|
2
|
+
|
|
3
|
+
= sidebar_content do
|
|
4
|
+
= sidebar_header("Toolkits (#{pagy.count})", new_path: new_toolkit_path)
|
|
5
|
+
|
|
6
|
+
= sidebar_stream("protege:toolkits", pagy: pagy)
|
|
7
|
+
|
|
8
|
+
= sidebar_body(toolkits, empty: "No toolkits yet", id: "toolkits_list") do |toolkit|
|
|
9
|
+
= render "toolkit_sidebar_item", toolkit: toolkit, active: local_assigns[:current] == toolkit
|
|
10
|
+
|
|
11
|
+
= sidebar_pagination(pagy)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
- content_for :sidebar do
|
|
2
|
+
= render "sidebar", current: @toolkit
|
|
3
|
+
|
|
4
|
+
= page_container do
|
|
5
|
+
= page_header(@toolkit.name, @toolkit.description) do
|
|
6
|
+
div
|
|
7
|
+
= ui_link("Edit", edit_toolkit_path(@toolkit), class: "px-3")
|
|
8
|
+
- unless @toolkit.system?
|
|
9
|
+
= destructive_link("Delete", toolkit_path(@toolkit), confirm: "Delete this toolkit?", method: :delete)
|
|
10
|
+
|
|
11
|
+
.space-y-4
|
|
12
|
+
= info_card("Toolkit", toolkit_overview(@toolkit))
|
|
13
|
+
= hint("This toolkit is a reusable set of tools. Who may use them is decided per agent — attach it on an agent's page to set its sender gate.")
|
data/config/routes.rb
CHANGED
|
@@ -18,13 +18,24 @@ Protege::Engine.routes.draw do
|
|
|
18
18
|
resources :responsibilities
|
|
19
19
|
resources :responsibility_runs
|
|
20
20
|
|
|
21
|
+
resources :toolkits
|
|
22
|
+
|
|
23
|
+
# A grant (agent↔toolkit) is split across two controllers so nesting never goes two deep. The
|
|
24
|
+
# *nested* Agents::AgentToolkitsController lists an agent's grants and attaches/detaches them (in
|
|
25
|
+
# the agent's scope); the *top-level* AgentToolkitsController manages a created grant on its own
|
|
26
|
+
# page, where its sender-gate rules nest one level deep (like access_rules under an agent).
|
|
27
|
+
resources :agent_toolkits do
|
|
28
|
+
resources :agent_toolkit_rules
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
resources :threads do
|
|
22
32
|
resource :reply
|
|
23
33
|
end
|
|
24
34
|
|
|
25
|
-
resources :
|
|
35
|
+
resources :agents do
|
|
26
36
|
resource :archive
|
|
27
37
|
resources :access_rules
|
|
38
|
+
resources :agent_toolkits, module: :agents
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
resources :email_domains do
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Replace the persona +active+ boolean with an +archived_at+ timestamp — the idiomatic soft-delete shape,
|
|
4
4
|
# which also records *when* a persona was archived. The model keeps +active?+/+scope :active+ as derived
|
|
5
5
|
# readers over +archived_at+, so routing/scheduler/status code is unchanged.
|
|
6
|
-
class ReplacePersonaActiveWithArchivedAt < ActiveRecord::Migration[
|
|
6
|
+
class ReplacePersonaActiveWithArchivedAt < ActiveRecord::Migration[7.2]
|
|
7
7
|
# Add archived_at, mark already-deactivated personas as archived-now, then drop active.
|
|
8
8
|
#
|
|
9
9
|
# @return [void]
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# has switched *off* within that grant, so scoping narrows and never widens. Stored as a JSON array in a
|
|
6
6
|
# text column (SQLite has no native JSON type), matching +Message+'s recipient-list convention, and
|
|
7
7
|
# defaults to an empty array so an unconfigured persona keeps its full grant.
|
|
8
|
-
class AddDisabledToolIdsToPersonas < ActiveRecord::Migration[
|
|
8
|
+
class AddDisabledToolIdsToPersonas < ActiveRecord::Migration[7.2]
|
|
9
9
|
# Add the JSON-array text column, non-null with an empty-array default.
|
|
10
10
|
#
|
|
11
11
|
# @return [void]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Scopes inbound-mail uniqueness per persona, so one email addressed to several proteges can be handled
|
|
4
|
+
# by each of them independently. Previously +thread_id+ and +message_id+ were globally unique, which
|
|
5
|
+
# assumed exactly one persona per inbound message: the second persona's thread/message insert collided on
|
|
6
|
+
# the global index. Making both unique per +persona_id+ lets every addressed persona own its own thread
|
|
7
|
+
# and message for the same RFC Message-ID, while still keeping a single persona's retries idempotent.
|
|
8
|
+
class ScopeMessageAndThreadUniquenessPerPersona < ActiveRecord::Migration[7.2]
|
|
9
|
+
def change
|
|
10
|
+
remove_index :protege_email_threads, column: :thread_id, unique: true
|
|
11
|
+
add_index :protege_email_threads, %i[persona_id thread_id], unique: true
|
|
12
|
+
|
|
13
|
+
remove_index :protege_messages, column: :message_id, unique: true
|
|
14
|
+
add_index :protege_messages, %i[persona_id message_id], unique: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Creates the toolkit binding tables — the first "Binding" of the extension framework. A toolkit is a
|
|
4
|
+
# global, dashboard-managed, reusable bundle of tools (referenced by their manifest ids, stored as a JSON
|
|
5
|
+
# id list like +disabled_tool_ids+) — nothing more. Permission lives on the *attachment*: a
|
|
6
|
+
# +protege_persona_toolkits+ row gives a toolkit to one persona and carries that grant's sender gate
|
|
7
|
+
# (child +persona_toolkit_rules+ rows, kind + pattern, mirroring +AccessRule+) plus +allow_proactive+ (may
|
|
8
|
+
# these tools run on that persona's scheduled, senderless runs?). So the same toolkit attached to two
|
|
9
|
+
# personas can be gated differently, with no tool-list duplication.
|
|
10
|
+
class CreateProtegeToolkits < ActiveRecord::Migration[7.2]
|
|
11
|
+
def change
|
|
12
|
+
create_table :protege_toolkits do |t|
|
|
13
|
+
t.string :name, null: false, index: { unique: true }
|
|
14
|
+
t.text :description
|
|
15
|
+
t.text :member_tool_ids, null: false, default: '[]'
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
create_table :protege_persona_toolkits do |t|
|
|
21
|
+
t.references :persona, null: false, foreign_key: { to_table: :protege_personas }
|
|
22
|
+
t.references :toolkit, null: false, foreign_key: { to_table: :protege_toolkits }
|
|
23
|
+
t.boolean :allow_proactive, null: false, default: false
|
|
24
|
+
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
add_index :protege_persona_toolkits, %i[persona_id toolkit_id], unique: true
|
|
28
|
+
|
|
29
|
+
create_table :protege_persona_toolkit_rules do |t|
|
|
30
|
+
t.references :persona_toolkit, null: false, foreign_key: { to_table: :protege_persona_toolkits }
|
|
31
|
+
t.string :kind, null: false
|
|
32
|
+
t.string :pattern, null: false
|
|
33
|
+
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Remove +disabled_tool_ids+ from personas. Tool access no longer flows through a per-persona ceiling (a
|
|
4
|
+
# code-declared +tools+ grant minus this disable list); a persona's tools now come entirely from the
|
|
5
|
+
# toolkits attached to it, gated per attachment (see +Protege::Toolable+ / +PersonaToolkit+). The
|
|
6
|
+
# subtractive override this column backed no longer exists, so the column is dropped.
|
|
7
|
+
class RemoveDisabledToolIdsFromPersonas < ActiveRecord::Migration[7.2]
|
|
8
|
+
# Drop the column.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
11
|
+
def up
|
|
12
|
+
remove_column :protege_personas, :disabled_tool_ids
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Restore the JSON-array text column, non-null with an empty-array default.
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
18
|
+
def down
|
|
19
|
+
add_column :protege_personas, :disabled_tool_ids, :text, null: false, default: '[]'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Add a stable +key+ to toolkits so the engine can identify its *system* toolkits — the auto-maintained
|
|
4
|
+
# "All Tools" (+all_tools+) and the auto-attached "Default Tools" (+default_tools+) — independently of
|
|
5
|
+
# their display name (which operators may rename). User-created toolkits leave +key+ null; a unique index
|
|
6
|
+
# (SQLite allows multiple NULLs) keeps at most one toolkit per system key.
|
|
7
|
+
#
|
|
8
|
+
# Installs seeded before system toolkits existed already have an "All Tools" toolkit (created by the old
|
|
9
|
+
# host seed). Adopt it as the keyed system toolkit here, so the sync updates that record in place rather
|
|
10
|
+
# than colliding on its unique name.
|
|
11
|
+
class AddKeyToProtegeToolkits < ActiveRecord::Migration[7.2]
|
|
12
|
+
def up
|
|
13
|
+
add_column :protege_toolkits, :key, :string
|
|
14
|
+
add_index :protege_toolkits, :key, unique: true
|
|
15
|
+
|
|
16
|
+
execute("UPDATE protege_toolkits SET key = 'all_tools' WHERE name = 'All Tools' AND key IS NULL")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def down
|
|
20
|
+
remove_column :protege_toolkits, :key
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rename the Persona concept to Agent across the schema — the industry has settled on "agent" for the
|
|
4
|
+
# addressable actor, and the code now follows suit (+Protege::Agent+, +AgentToolkit+, …). Pure renames,
|
|
5
|
+
# so existing installs transition in place with their data intact: the three persona tables become agent
|
|
6
|
+
# tables, and every +persona_id+ foreign key becomes +agent_id+. Rails renames the conventionally-named
|
|
7
|
+
# indexes along with each table/column, and SQLite keeps foreign-key references pointed at the renamed
|
|
8
|
+
# tables. Earlier migrations keep their historical persona names — on a fresh install they run first and
|
|
9
|
+
# this migration renames their output.
|
|
10
|
+
class RenamePersonasToAgents < ActiveRecord::Migration[7.2]
|
|
11
|
+
def change
|
|
12
|
+
rename_table :protege_personas, :protege_agents
|
|
13
|
+
rename_table :protege_persona_toolkits, :protege_agent_toolkits
|
|
14
|
+
rename_table :protege_persona_toolkit_rules, :protege_agent_toolkit_rules
|
|
15
|
+
|
|
16
|
+
rename_column :protege_email_threads, :persona_id, :agent_id
|
|
17
|
+
rename_column :protege_messages, :persona_id, :agent_id
|
|
18
|
+
rename_column :protege_access_rules, :persona_id, :agent_id
|
|
19
|
+
rename_column :protege_responsibilities, :persona_id, :agent_id
|
|
20
|
+
rename_column :protege_tool_uses, :persona_id, :agent_id
|
|
21
|
+
rename_column :protege_agent_toolkits, :persona_id, :agent_id
|
|
22
|
+
rename_column :protege_agent_toolkit_rules, :persona_toolkit_id, :agent_toolkit_id
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/named_base'
|
|
4
|
+
require_relative '../extension_naming'
|
|
5
|
+
|
|
6
|
+
module Protege
|
|
7
|
+
module Generators
|
|
8
|
+
# Scaffolds an agent — the identity Protege binds an inbox to — into the host app's agents
|
|
9
|
+
# directory (+config.agents_path+, default +app/agents+). An agent is an STI subclass of
|
|
10
|
+
# +Protege::Agent+ (an ActiveRecord model); subclassing registers the subtype, and each agent
|
|
11
|
+
# declares its display name and the resolver chains that assemble context for its reply and
|
|
12
|
+
# scheduled runs.
|
|
13
|
+
#
|
|
14
|
+
# The +Agent+ suffix is applied for you, so all of +support+, +Support+, and +SupportAgent+
|
|
15
|
+
# produce class +SupportAgent+ in +support_agent.rb+.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# bin/rails g protege:agent support
|
|
19
|
+
class AgentGenerator < Rails::Generators::NamedBase
|
|
20
|
+
include ExtensionNaming
|
|
21
|
+
|
|
22
|
+
source_root File.expand_path('templates', __dir__)
|
|
23
|
+
|
|
24
|
+
desc 'Scaffold a Protege::Agent subclass in the configured agents path (default app/agents).'
|
|
25
|
+
|
|
26
|
+
# Render the agent scaffold into +config.agents_path+.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
def create_agent
|
|
30
|
+
template 'agent.rb.tt',
|
|
31
|
+
File.join(Protege.configuration.agents_path, "#{extension_file_name('Agent')}.rb")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# <%= extension_class_name('
|
|
3
|
+
# <%= extension_class_name('Agent') %> — an agent: the identity Protege binds an inbox to.
|
|
4
4
|
#
|
|
5
|
-
# Subclassing +Protege::
|
|
5
|
+
# Subclassing +Protege::Agent+ (an STI ActiveRecord model) registers this subtype. Create the row in
|
|
6
6
|
# the dashboard (or a seed) to give it a +name+, +email_address+, and +instructions+ (its editable
|
|
7
7
|
# system prompt); inbound mail to that address routes here, and outbound replies are sent as it.
|
|
8
8
|
#
|
|
9
9
|
# The two chains below assemble the context sent to the model. Engine built-in resolvers are named by
|
|
10
10
|
# their flat +Protege::+ constant; host-app resolvers (see <%= Protege.configuration.resolvers_path %>/) by their own top-level constant.
|
|
11
|
-
class <%= extension_class_name('
|
|
12
|
-
# Human-friendly label shown for this
|
|
13
|
-
self.display_name = '<%= extension_class_name('
|
|
11
|
+
class <%= extension_class_name('Agent') %> < Protege::Agent
|
|
12
|
+
# Human-friendly label shown for this agent in the dashboard, logs, and tooling.
|
|
13
|
+
self.display_name = '<%= extension_class_name('Agent').titleize %>'
|
|
14
14
|
|
|
15
15
|
# Context assembled before every reply to inbound mail. +Protege::LoadTextResolver+ supplies this
|
|
16
|
-
#
|
|
16
|
+
# agent's dashboard-editable +instructions+ as the system prompt; +Protege::ThreadHistoryResolver+
|
|
17
17
|
# replays the conversation so far. Insert your own resolvers between them (e.g. to inject records,
|
|
18
18
|
# summaries, or retrieved facts).
|
|
19
19
|
message_resolvers do |chain|
|
|
20
|
-
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.
|
|
20
|
+
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
|
|
21
21
|
chain.use Protege::ThreadHistoryResolver
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Context assembled for a scheduled run (the Loop layer). There is no inbound thread, so the chain is
|
|
25
|
-
# just identity: the
|
|
25
|
+
# just identity: the agent's +instructions+ as the system prompt and the responsibility's own
|
|
26
26
|
# prompt as the opening user message. Add task-specific resolvers here if a responsibility needs more.
|
|
27
27
|
responsibility_resolvers do |chain|
|
|
28
|
-
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.
|
|
28
|
+
chain.use(Protege::LoadTextResolver, role: :system) { |ctx| ctx.agent.instructions }
|
|
29
29
|
chain.use(Protege::LoadTextResolver, role: :user) { |ctx| ctx.responsibility.instructions }
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -9,7 +9,7 @@ module Protege
|
|
|
9
9
|
#
|
|
10
10
|
# The suffix is applied whether or not it was supplied, so all three of +send_email+, +SendEmail+,
|
|
11
11
|
# and +SendEmailTool+ resolve to class +SendEmailTool+ / file +send_email_tool.rb+. Pass an empty
|
|
12
|
-
# suffix (as the
|
|
12
|
+
# suffix (as the agent generator does) to use the name verbatim.
|
|
13
13
|
module ExtensionNaming
|
|
14
14
|
private
|
|
15
15
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Subclassing +Protege::Hook+ wires these handlers onto the event bus at boot (auto-discovered, no
|
|
6
6
|
# registration). Each +on+ block receives a typed, frozen +Protege::Event+ and runs isolated — a raised
|
|
7
7
|
# error is swallowed and logged, never breaking the run. Read event fields with nil-safe named readers
|
|
8
|
-
# (+event.
|
|
8
|
+
# (+event.agent+, +event.tool_call+, +event.result+, +event.error+, +event.correlation_id+).
|
|
9
9
|
#
|
|
10
10
|
# Declare a handler with +on(EventClass) { |event| ... }+ (pass several classes to share one block).
|
|
11
11
|
# Available events (all +Protege::+ constants), in fire order:
|
|
@@ -21,7 +21,7 @@ class <%= extension_class_name('Hook') %> < Protege::Hook
|
|
|
21
21
|
|
|
22
22
|
# TODO: replace with the events you care about.
|
|
23
23
|
on InferenceStartedEvent do |event|
|
|
24
|
-
log "inference started for
|
|
24
|
+
log "inference started for agent=#{event.agent&.name} (#{event.correlation_id})"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|