Content Management
AI-powered content management system for rephrasing hotel, activity, and transfer descriptions to match the Volāre brand voice. Supports single-record editing with review and bulk operations via queued jobs.
When to Use
Section titled “When to Use”- Brand consistency – ensure all supplier content follows the same tone, vocabulary, and style guidelines
- Single-record rephrase – click “Rephrase Content” on any hotel, activity, or transfer Edit page to get AI suggestions with a before/after review modal
- Bulk rephrase – select multiple records on a list page, queue them for AI rephrasing, then review drafts in the Content Rephrase Dashboard
- Content rules – define per-field constraints (max length, forbidden words, required topics) that the AI validates against during generation
Configuration
Section titled “Configuration”Required env vars: OPENROUTER_API_KEY, OPENROUTER_BASE_URL, OPENROUTER_MODEL
Config file: config/ai.php – model resolution via UsesConfiguredModel concern
Rate limiter: content-rephrase – 20 requests/minute, defined in AppServiceProvider
Brand Voice Profiles
Section titled “Brand Voice Profiles”TCAI Profiles (tcai_profiles table) store brand voice configuration. Each profile has a service_type discriminator:
null(Global) – brand-wide tone, vocabulary, and personality applied to all content- Service-specific (
hotel,activity,transfer,cms_home,cms_about_us,cms_legal) – guidelines tailored to a content type
The admin form at /admin/tcai-profiles has four tabs:
| Tab | Fields |
|---|---|
| Profile | Name, service type selector, description, default toggle |
| Brand Voice | Tone, target audience, vocabulary preferences, forbidden words (tags), personality traits (tags), communication standards, formatting rules |
| Examples | Three separate repeaters: Examples to Emulate (positive_examples, label + text), Examples to Avoid (negative_examples, label + text), and Rewrite Patterns (rewrite_examples, label + before/after) |
| Custom Instructions | Free-text instructions appended to AI prompts |
TcaiProfile::getBrandVoicePrompt() compiles all structured fields into a single prompt string used by agents. The examples render as three labelled blocks — EXAMPLES TO EMULATE, EXAMPLES TO AVOID (anti-patterns; do NOT imitate …), and REWRITE PATTERNS (BEFORE → AFTER). For backward compatibility the legacy examples key (before/after pairs from profiles seeded before the split) is still rendered under REWRITE PATTERNS.
Lookup logic: GetBrandVoiceTool resolves a single editorial layer by substitution, not merging: the service-type-specific default replaces the global brand voice when one exists (precedence: explicit profile override → per-service default → global override → global default). Only one profile reaches the prompt, keeping it lean.
Source: backend/app/Models/TcaiProfile.php, backend/app/Filament/Resources/TcaiProfiles/Schemas/TcaiProfileForm.php
Content Rules
Section titled “Content Rules”Per-field validation rules stored in the content_rules table and managed via the ContentRuleResource Filament resource (nav group: “AI Configuration”).
Each rule targets a service_type + field_name combination and contains a JSON rules object with constraints such as:
min_length/max_length– character limitsmin_words/max_words– word count limitsforbidden_words– array of words that must not appear
Rules are fetched by ContentRule::getFor($serviceType, $field) and exposed to the agent via GetContentRulesTool. Deterministic PHP validation against these rules runs outside the agent, in ContentValidator (see ContentRephraseAgent).
Source: backend/app/Models/ContentRule.php, backend/app/Filament/Resources/ContentRules/ContentRuleResource.php
Content Field Map
Section titled “Content Field Map”ContentFieldMap defines which fields are rephrased for each service type:
| Service Type | Fields |
|---|---|
hotel |
hotel_name, address, description |
activity |
name, description, inclusions, warnings, notes |
transfer |
name, description |
Source: backend/app/Ai/ContentFieldMap.php
ContentRephraseAgent
Section titled “ContentRephraseAgent”The agent implements Agent, HasStructuredOutput, and HasTools from laravel/ai. Configuration: 8000 max tokens, temperature 0.7, 10 max steps, 120s timeout.
It exposes 5 content tools (ContentRephraseAgent::tools()): GetBrandVoiceTool, GetContentRulesTool, GetEntityContextTool, SearchSimilarContentTool, GetExampleContentTool.
Prescribed Workflow
Section titled “Prescribed Workflow”The agent’s instructions define a strict tool-calling order:
get_brand_voice– fetch global + service-specific brand voiceget_content_rules– fetch per-field constraintsget_entity_context– load the actual record with relationships (hotel amenities, activity inclusions, etc.)- (Optional)
search_similar_content– find similar entities in the same city to avoid duplicate phrasing - (Optional)
get_example_content– fetch high-quality examples as style reference - Generate rephrased content as structured JSON output
The structured output schema is dynamically built from the field names for the given service type.
Validation is not an agent step. Rule validation was previously an in-agent validate_content tool, but the model (gpt-4o-mini) looped validate → re-fetch rules → validate without ever emitting the structured output. It was removed. ContentRephraseService now runs ContentValidator::validate() on the parsed output once, in PHP, after the agent returns — a single, predictable place that logs any rule violations as warnings for follow-up (it does not re-prompt the agent to self-correct).
Source: backend/app/Ai/Agents/ContentRephraseAgent.php, backend/app/Ai/Tools/Content/, backend/app/Services/Ai/ContentValidator.php, backend/app/Services/ContentRephraseService.php
Single-Record Rephrasing
Section titled “Single-Record Rephrasing”Edit pages for hotels, activities, and transfers use the HandlesContentRephrase trait to add a “Rephrase Content” header action.
Flow:
- Admin clicks the sparkles button on the Edit page
ContentRephraseService::rephrase()runs theContentRephraseAgentwith the record’s current content- A review modal opens showing before/after for each changed field
- Each field has an editable “After” textarea and an accept/reject toggle
- Admin clicks “Apply Accepted” – only toggled-on fields update the form (not yet saved to DB)
- Admin reviews the form and saves normally
The modal also includes a “Discard All” button to reject everything.
Source: backend/app/Filament/Concerns/HandlesContentRephrase.php, backend/app/Services/ContentRephraseService.php
Bulk Rephrasing
Section titled “Bulk Rephrasing”Dispatching
Section titled “Dispatching”On list pages (hotels, activities, transfers), the RephraseContentBulkAction adds a bulk action. Selecting records and clicking “Rephrase Content” opens a modal with:
- Brand voice profile selector (global profiles)
- Content guideline selector (service-type-specific profiles)
- Additional instructions textarea
Submitting dispatches a ContentRephraseBatch and queues individual RephraseRecordJob instances via Bus::batch().
Job Processing
Section titled “Job Processing”RephraseRecordJob is rate-limited to 20/minute, retries twice with 30s/60s backoff, and has a 120s timeout. Each job:
- Loads the draft and its polymorphic record
- Calls
ContentRephraseService::rephrase() - Stores rephrased content on the
ContentRephraseDraft - Increments batch counters atomically
- Checks batch completion in a locked transaction
Review Dashboard
Section titled “Review Dashboard”The ContentRephraseDashboard Filament page (nav group: “AI Configuration”) is admin-only – supplier-managers and other non-admin roles cannot see it in the navigation nor reach it via direct URL. It shows two views:
Batches table – lists all batches with status, record counts, and actions:
- “Review Drafts” – drill into individual drafts
- “Apply Approved” – bulk-apply all approved drafts in the batch
Drafts table – per-draft actions:
- Approve – mark as ready to apply (from Pending status)
- Reject – discard the draft
- Apply – write the rephrased content to the actual record
Draft Lifecycle
Section titled “Draft Lifecycle”Drafts follow this status flow: Pending -> Approved -> Applied (or Rejected / Failed at any point). A draft is marked Skipped instead of Pending when the rephrased field(s) were empty, so there was nothing to rephrase — distinct from Failed (a real error). Skipped drafts count as processed (not failed) for batch completion.
Batch status transitions: Processing -> Completed (all succeeded) or Failed (any failures).
Source: backend/app/Filament/Actions/RephraseContentBulkAction.php, backend/app/Services/BulkRephraseService.php, backend/app/Jobs/RephraseRecordJob.php, backend/app/Filament/Pages/ContentRephraseDashboard.php
Data Model
Section titled “Data Model”| Model | Table | Purpose |
|---|---|---|
TcaiProfile |
tcai_profiles |
Brand voice and style configuration |
ContentRule |
content_rules |
Per-field validation rules |
ContentRephraseBatch |
content_rephrase_batches |
Tracks a bulk rephrase operation |
ContentRephraseDraft |
content_rephrase_drafts |
Individual record draft (polymorphic via rephrasable) |
Source: backend/app/Models/ContentRule.php, backend/app/Models/ContentRephraseBatch.php, backend/app/Models/ContentRephraseDraft.php
Related
Section titled “Related”- AI System – parent AI architecture, other agents, and TCAI chat
- Product Templates – AI content generation for tour templates
- Source:
backend/app/Ai/Agents/ContentRephraseAgent.php - Source:
backend/app/Ai/Tools/Content/– all 5 content tools - Source:
backend/app/Services/ContentRephraseService.php - Source:
backend/app/Services/Ai/ContentValidator.php - Source:
backend/app/Services/BulkRephraseService.php - Source:
backend/app/Filament/Concerns/HandlesContentRephrase.php - Source:
backend/app/Filament/Pages/ContentRephraseDashboard.php