Skip to content

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.

  • 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

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

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

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 limits
  • min_words / max_words – word count limits
  • forbidden_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

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

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.

The agent’s instructions define a strict tool-calling order:

  1. get_brand_voice – fetch global + service-specific brand voice
  2. get_content_rules – fetch per-field constraints
  3. get_entity_context – load the actual record with relationships (hotel amenities, activity inclusions, etc.)
  4. (Optional) search_similar_content – find similar entities in the same city to avoid duplicate phrasing
  5. (Optional) get_example_content – fetch high-quality examples as style reference
  6. 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

Edit pages for hotels, activities, and transfers use the HandlesContentRephrase trait to add a “Rephrase Content” header action.

Flow:

  1. Admin clicks the sparkles button on the Edit page
  2. ContentRephraseService::rephrase() runs the ContentRephraseAgent with the record’s current content
  3. A review modal opens showing before/after for each changed field
  4. Each field has an editable “After” textarea and an accept/reject toggle
  5. Admin clicks “Apply Accepted” – only toggled-on fields update the form (not yet saved to DB)
  6. 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

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().

RephraseRecordJob is rate-limited to 20/minute, retries twice with 30s/60s backoff, and has a 120s timeout. Each job:

  1. Loads the draft and its polymorphic record
  2. Calls ContentRephraseService::rephrase()
  3. Stores rephrased content on the ContentRephraseDraft
  4. Increments batch counters atomically
  5. Checks batch completion in a locked transaction

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

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

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

  • 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