Module: Assistant::Profiles

Defined in:
app/services/assistant/profiles.rb

Overview

Declarative registry of Sunny "profiles" (a.k.a. personalities / templates).

A profile is a named bundle that pre-sets, from the very first turn, the
three things that otherwise have to be guessed per turn (a regex + a
classifier LLM round-trip): the MODEL, the TOOL SET, and a system-prompt
tone/intent DIRECTIVE — plus a few starter prompts. Picking a profile at
/assistant therefore prevents the model/tool mismatch that produced the
blog-editing outages (convs 3105/3109) before it can start, instead of
detecting and recovering after the fact.

It rides entirely on mechanisms that already exist on AssistantConversation:

  • model_preference → pins the model (skips auto-routing)
  • tool_services → the toolset
  • conversation_type → triggers the controller's "preconfigured" bypass
    (pinned tools, no per-turn classifier)
  • default_tool_handles → renders the tool chips
  • profile_key → drives the per-profile system directive

The heuristic auto-routing (ChatService.auto_select_model) remains the
fallback for un-profiled, free-form chats.

Defined Under Namespace

Classes: Profile

Constant Summary collapse

PROFILES =

Ordered registry. Models reference ChatService constants where they exist so
the tiers stay in sync with the router.

[
  Profile.new(
    key: 'blog_editing',
    label: 'Blog Editing',
    icon: 'pen-to-square',
    tagline: 'Write & edit blog posts with the block-safe editor and brand voice.',
    model: Assistant::ChatService::BLOG_AUTHORING_MODEL, # Opus 4.8, 1M context
    tool_handles: %w[blog-management image-management content-search seo-audit],
    required_services: %w[blog_management],
    system_directive: <<~TXT.strip,
      PROFILE: Blog Editing. Default to the block-addressed editor for every change.
      For a small edit inside one block (fixing a link, URL, or phrase) call
      get_block_html(post_id, block_id) then edit_blog_post with a replace_in_block op —
      never work from a truncated HTML snapshot, and never re-send a whole large block when
      a scoped find/replace will do. Follow the WarmlyYours blog style guide and brand
      voice (company name is "WarmlyYours", one word). Posts are saved as drafts for human
      review — never imply you published anything.
    TXT
    starters: [
      'Audit this blog post for broken internal links and fix them: <paste CRM blog URL>',
      'Update this post to match our brand voice and add an FAQ section: <paste CRM blog URL>',
      'Refresh the meta description and title tag for this post: <paste CRM blog URL>'
    ]
  ),
  Profile.new(
    key: 'email_editing',
    label: 'Email Editing',
    icon: 'envelope',
    tagline: 'Edit email templates safely — surgical block fixes, no full rewrites.',
    model: Assistant::ChatService::WRITING_MODEL_CLAUDE, # Claude Sonnet
    tool_handles: %w[email-management image-management content-search],
    required_services: %w[email_management],
    system_directive: <<~TXT.strip,
      PROFILE: Email Editing. Edit Redactor-4 templates with the block-addressed editor.
      For a small change inside one block (a button URL, link, or phrase) call
      get_email_block_html(email_template_id, block_id) then edit_email_template with a
      replace_in_block op — email blocks are nested tables, so avoid reproducing whole
      blocks. Refuse legacy R3 templates and tell the user to migrate them first.
    TXT
    starters: [
      'Fix the call-to-action link in this email template: <template id or name>',
      'Update the footer copy across this template and keep the unsubscribe link intact',
      'Proofread this template and tighten the subject line'
    ]
  ),
  Profile.new(
    key: 'analytics',
    label: 'Analytics',
    icon: 'chart-line',
    tagline: 'Query sales & web data, build reports, explain trends.',
    model: 'gemini-pro',
    tool_handles: %w[app-db google-analytics search-console],
    required_services: %w[app_db],
    system_directive: <<~TXT.strip,
      PROFILE: Analytics. Favor precise, sourced answers. Call describe_available_data before
      writing SQL against analytics views, prefer the curated views over raw tables, and show
      the numbers behind any claim. Surface the date window and scope you used.
    TXT
    starters: [
      'What were our top-selling product lines last quarter, by revenue?',
      'Show month-over-month organic traffic for the last 6 months',
      'Which sales reps closed the most opportunities this month?'
    ]
  ),
  Profile.new(
    key: 'seo_research',
    label: 'SEO Research',
    icon: 'magnifying-glass-chart',
    tagline: 'Keyword & backlink research, audits — and content edits to act on them.',
    model: Assistant::ChatService::BLOG_AUTHORING_MODEL, # large context: research + content editing
    tool_handles: %w[ahrefs search-console keywords-people-use blog-management web-fetch content-search],
    required_services: %w[ahrefs],
    system_directive: <<~TXT.strip,
      PROFILE: SEO Research. Combine keyword/backlink/SERP research with on-site action. When
      a fix is editorial, edit the blog post directly using the block-addressed editor
      (get_block_html + replace_in_block for scoped changes). Always ground recommendations in
      the data you pulled, and cite the source tool.
    TXT
    starters: [
      'Find keyword gaps vs our top competitor for radiant floor heating',
      'Audit this page for SEO issues and fix what you can: <paste URL>',
      'What questions are people asking about heated floors? Suggest an FAQ set.'
    ]
  ),
  Profile.new(
    key: 'sales_automation',
    label: 'Sales Force Automation',
    icon: 'users-gear',
    tagline: 'Review pipeline, summarize opportunities, draft follow-ups.',
    model: Assistant::ChatService::WRITING_MODEL_CLAUDE, # Claude Sonnet
    tool_handles: %w[sales-management app-db email-management],
    required_services: %w[sales_management],
    system_directive: <<~TXT.strip,
      PROFILE: Sales Force Automation. Help reps and managers work the pipeline: summarize
      opportunities and activity, surface stalled deals, and draft follow-up emails. Be
      concise and action-oriented; never invent customer details — pull them from the tools.
    TXT
    starters: [
      'Summarize my open opportunities and flag any that have gone quiet',
      'Draft a follow-up email for opportunity #<id>',
      'Which deals in my pipeline are closing this month?'
    ]
  )
].freeze

Class Method Summary collapse

Class Method Details

.allArray<Profile>

All profiles, in display order.

Returns:



163
# File 'app/services/assistant/profiles.rb', line 163

def all = PROFILES

.available_for(permitted_service_keys) ⇒ Array<Profile>

Profiles the user is permitted to use, given their permitted service keys.

Parameters:

  • permitted_service_keys (Array<String>)

Returns:



177
178
179
180
# File 'app/services/assistant/profiles.rb', line 177

def available_for(permitted_service_keys)
  keys = Array(permitted_service_keys)
  PROFILES.select { |p| p.available_to?(keys) }
end

.directive_for(profile_key) ⇒ String?

The system-prompt directive for a conversation's active profile, or nil.

Parameters:

  • profile_key (String, nil)

Returns:

  • (String, nil)


185
186
187
# File 'app/services/assistant/profiles.rb', line 185

def directive_for(profile_key)
  find(profile_key)&.system_directive.presence
end

.find(key) ⇒ Profile?

Look up a profile by key.

Parameters:

  • key (String, Symbol, nil)

Returns:



168
169
170
171
172
# File 'app/services/assistant/profiles.rb', line 168

def find(key)
  return nil if key.blank?

  PROFILES.find { |p| p.key == key.to_s }
end