Class: Assistant::CommunicationToolBuilder

Inherits:
Object
  • Object
show all
Defined in:
app/services/assistant/communication_tool_builder.rb

Overview

RubyLLM tools for CRM Communication records — the "send this template to
these people" side of email, as opposed to EmailToolBuilder's templates
(content) and campaign emails (scheduled marketing sends).

Julia asked Sunny to "create a new communication using this template, put
it in draft state, add all active employees except Scott as recipients"
(2026-07-28) and Sunny had no tool for it — it could author the template
but not address it to anyone, and couldn't read employee contact points.

Drafts only, by construction. create_communication_draft always
passes keep_as_draft: true, so the record stays in Communication's
initial draft state and no release worker is ever enqueued. There is
deliberately no tool here that queues, releases, or sends — a human opens
the draft in the CRM, reviews the recipient list, and presses send. That
matters more here than for templates: a template is inert until someone
uses it, whereas a communication already carries its audience.

Usage (via ChatToolBuilder's email-management service):
tools = Assistant::CommunicationToolBuilder.tools(audit_context: { user_id: 42 })

Constant Summary collapse

CRM_COMMUNICATION_URL =

CRM communication URL.

"#{CRM_URL}/communications".freeze
MAX_RECIPIENTS =

Refuse absurd recipient lists. A draft can't send itself, but a runaway
list is a sign the model misread the request, and it's cheaper to stop
than to have someone eyeball 10k rows in the CRM.

500

Class Method Summary collapse

Class Method Details

.employee_email_rows(exclude_names: []) ⇒ Array<Hash>

Active employees with an email contact point, minus any whose name
matches exclude_names (substring, case-insensitive — the user says
"everyone except Scott", not an employee id).

Deduplicated by address: shared mailboxes mean one address can hang off
two employee records (epasek@ is on both "Elodie Pasek" and "Warehouse
Canada2"). Communication's own unique index would collapse those
anyway; doing it here keeps the count we report to the user honest.

Parameters:

  • exclude_names (Array<String>) (defaults to: [])

Returns:

  • (Array<Hash>)


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'app/services/assistant/communication_tool_builder.rb', line 55

def employee_email_rows(exclude_names: [])
  excluded = Array(exclude_names).filter_map { |n| n.to_s.strip.downcase.presence }

  ContactPoint.emails.joins(:party)
              .merge(Employee.active_employees)
              .includes(:party)
              .order(Party[:full_name])
              .filter_map do |cp|
    name = cp.party&.full_name.to_s
    downcased_name = name.downcase
    # rubocop:disable Style/ArrayIntersect -- substring match ("Scott" must
    # hit "Scott Rosenbaum"); intersect? compares whole elements.
    next if excluded.any? { |term| downcased_name.include?(term) }
    # rubocop:enable Style/ArrayIntersect
    next if cp.detail.blank?

    { employee_id: cp.party_id, name: name, email: cp.detail, contact_point_id: cp.id }
  end.uniq { |row| row[:email].downcase }
end

.tools(audit_context: {}) ⇒ Array<RubyLLM::Tool>

Build the communication tools.

Parameters:

  • audit_context (Hash) (defaults to: {})

    expects :user_id (Employee id, used as the
    draft's sender when the template doesn't name one).

Returns:

  • (Array<RubyLLM::Tool>)


37
38
39
40
41
42
# File 'app/services/assistant/communication_tool_builder.rb', line 37

def tools(audit_context: {})
  [
    build_list_employee_emails_tool,
    build_create_communication_draft_tool(audit_context)
  ]
end