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
-
.all ⇒ Array<Profile>
All profiles, in display order.
-
.available_for(permitted_service_keys) ⇒ Array<Profile>
Profiles the user is permitted to use, given their permitted service keys.
-
.directive_for(profile_key) ⇒ String?
The system-prompt directive for a conversation's active profile, or nil.
-
.find(key) ⇒ Profile?
Look up a profile by key.
Class Method Details
.all ⇒ Array<Profile>
All profiles, in display order.
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.
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.
185 186 187 |
# File 'app/services/assistant/profiles.rb', line 185 def directive_for(profile_key) find(profile_key)&.system_directive.presence end |