Skip to content

AI Outline System — Admin Guide

This guide covers setup, configuration, prompt management, cache tuning, and cost monitoring for the TripMaker AI Outline feature.


1. Setup

Prerequisites

  • TQPro server running with TLINQ_HOME pointing to the config/ directory
  • Database migrations applied (specifically 0015-tripmaker-phase0.sql which creates the ai_outline_cache, ai_usage_log, and ai_outline_feedback tables)
  • An Anthropic API key with access to the Claude Messages API

Configuration

All AI settings live in config/tourlinq.properties:

ai.provider=anthropic
ai.api.key=sk-ant-api03-...          # Replace with your Anthropic API key
ai.api.endpoint=https://api.anthropic.com/v1/messages
ai.api.version=2023-06-01
ai.model=claude-sonnet-4-5-20250929  # Model identifier (see "Changing Models" below)
ai.cache.ttl.hours=720               # Cache lifetime in hours (720 = 30 days)
ai.prompt.file=ai-outline-prompt.txt  # System prompt filename (relative to TLINQ_HOME)
ai.max.tokens=4096                    # Max tokens in Claude response
ai.timeout.seconds=30                 # HTTP timeout for API calls

API Key Security: In production, set the API key via AWS Systems Manager Parameter Store using the ##ai.api.key placeholder syntax. The ## prefix tells the config loader to resolve the value from SSM at startup. Never commit a real API key to the properties file.

Verify Setup

After configuring the API key, verify the integration by calling:

POST /ai/outline/generate
{
  "session": "<valid-session>",
  "destination": "London, UK",
  "startDate": "2026-06-01",
  "endDate": "2026-06-08",
  "tripType": "Leisure",
  "budgetTier": "mid-range",
  "adults": 2,
  "childAges": [],
  "nationalities": ["GB"],
  "residences": ["AE"],
  "agentId": 1,
  "tripId": 1
}

A successful response contains apiData.outlineJson with the generated JSON brief. If the API key is missing or invalid, the response will contain an error message indicating the configuration problem.


2. How the Prompt System Works

The AI outline generation follows this flow:

Agent requests outline
        |
  Build cache key (SHA-256 of normalized inputs)
        |
  Check DB cache ──── hit + not expired ──→ return cached outline
        |
     cache miss
        |
  Load system prompt from file
        |
  Build user message from request params
        |
  POST to Claude API (system + user message)
        |
  Parse JSON response
        |
  Save to cache with TTL expiry
        |
  Log usage (tokens, cost, agent, trip)
        |
  Return outline to agent

Cache Key Composition

The cache key is a SHA-256 hash of these normalized (lowercase, trimmed) fields joined by |:

destination | departureCity | travelMonth | tripType | paxProfile | budgetTier | natProfile

This means the same destination/month/travelers/budget/nationality combination returns a cached result regardless of the exact travel dates within that month. Changing any input field produces a different cache key and triggers a fresh generation.

Pax profile format: {adults}a[_{children}c_{age1}_{age2}...] — for example, 2a_2c_8_12 means 2 adults with 2 children aged 8 and 12.

Nationality profile format: nat:{sorted codes}|res:{sorted codes} — for example, nat:DE,GB|res:AE means travelers hold German and British passports, residing in the UAE. This ensures that travelers with different nationalities get visa-specific advice.

Two-Part Prompt Structure

The Claude API receives two parts:

  1. System prompt (ai-outline-prompt.txt) — defines the AI's role, rules, and expected JSON output schema. This is shared across all requests and cached in memory after first load.

  2. User message — dynamically built per request with the specific destination, dates, traveler profile, and budget tier. Example:

Destination: London, UK
Departure city: Dubai
Travel dates: 1 June 2026 to 8 June 2026
Travel month: June 2026
Trip type: Leisure
Budget tier: mid-range
Travelers: 2 adult(s), 2 child(ren) ages 8, 12
Traveler nationalities: GB
Traveler country of residence: AE

The nationality and residence lines are included when the traveler details have this information set. This allows the AI to provide specific visa requirements per nationality rather than generic advice.


3. Editing the System Prompt

The system prompt file is at config/ai-outline-prompt.txt. Changes take effect on the next server restart (the prompt is cached in memory after first load).

Prompt Structure

The current prompt has these sections:

Section Purpose
Role definition Sets the AI as a Dubai-based travel consultant
Rules (numbered 1-6) Behavioral constraints: be specific, respect budget tier, consider travelers, account for departure city, assess timing honestly, use USD
Response format The exact JSON schema the AI must follow
Quantity guidance "2-3 areas, 5-7 sights, 5-7 activities"

Editing Guidelines

Do:

  • Keep the Respond ONLY with valid JSON instruction — the facade parses the response as raw JSON
  • Keep all field names in the JSON schema unchanged — the frontend expects this exact structure
  • Adjust quantity guidance (e.g., change "5-7 sights" to "3-5 sights" to reduce token usage)
  • Add domain-specific rules (e.g., "Always mention UAE resident visa waiver countries")
  • Refine the role description to better match agency positioning
  • Add or remove fields in the practical section as business needs evolve

Don't:

  • Remove the JSON-only instruction — any narrative wrapper will break parsing
  • Rename top-level JSON fields (headline, narrative, timing_score, areas, sights, activities, practical) — the frontend renders these by name
  • Add instructions that would cause the model to refuse reasonable requests
  • Make the prompt excessively long — longer prompts increase input token costs on every non-cached call

Testing Prompt Changes

  1. Edit the prompt file
  2. Restart the server (or restart the specific worker if running multiple)
  3. Make a test request to /ai/outline/generate with a destination that is not already cached
  4. Inspect the outlineJson field in the response
  5. If the JSON structure is malformed, check the prompt for conflicting instructions

Tip: To test without polluting the production cache, use an unusual destination + budget tier combination that no agent would have queried.


4. Enriching the Prompt

Adding New Output Fields

To add a new field to the AI response (e.g., dining recommendations):

  1. Add the new field to the JSON schema in ai-outline-prompt.txt:
    "dining": [
      {
        "name": "Restaurant name",
        "cuisine": "Type of cuisine",
        "price_range": "$$",
        "description": "Why recommended"
      }
    ]
    
  2. Add a corresponding instruction in the rules section (e.g., "Include 3-5 dining recommendations matching the budget tier")
  3. Restart the server
  4. The outlineJson field will now include the new data — the frontend can parse it from the raw JSON without backend changes

Adjusting for Different Departure Cities

The prompt currently assumes Dubai as the primary departure city. If the agency expands to other base cities, add conditional guidance:

When the departure city is Dubai or UAE-based, emphasize:
- UAE resident visa advantages
- Emirates/flydubai direct route availability
- Gulf-specific cultural tips

When the departure city is European, emphasize:
- EU transit rules
- Budget airline options
- Schengen visa considerations

Seasonal Fine-Tuning

Add seasonal rules to improve timing accuracy:

For Gulf-based travelers (Dubai, Abu Dhabi, Doha departure):
- Peak outbound months: June-August (summer escape)
- Secondary peak: December-January (winter holiday)
- Budget opportunity: shoulder seasons (April-May, September-October)

Budget Tier Calibration

Adjust per-tier expectations to match agency pricing:

Budget tier reference ranges (per person per night, accommodation):
- budget: $50-100 USD
- mid-range: $100-250 USD
- luxury: $250-500 USD
- ultra-luxury: $500+ USD

5. Cache Management

How Caching Works

Each unique combination of destination + departure city + travel month + trip type + pax profile + budget tier generates one cache entry. The entry is stored in the nts.ai_outline_cache table with an absolute expiry timestamp.

Cached entries are served until they expire, then the next request triggers a fresh generation.

Cache TTL Tuning

The ai.cache.ttl.hours property controls how long cached outlines remain valid.

Value Duration Use Case
24 1 day During prompt development/testing
168 1 week High-freshness requirement
720 30 days Default — good balance of freshness and cost
2160 90 days Stable destinations, cost-sensitive

Invalidating Cache

To force regeneration for a specific destination (e.g., after a prompt change):

-- Delete specific cached outline
DELETE FROM nts.ai_outline_cache WHERE destination ILIKE '%london%';

-- Delete all expired entries (cleanup)
DELETE FROM nts.ai_outline_cache WHERE expires_at < NOW();

-- Nuclear option: clear entire cache (after major prompt rewrite)
TRUNCATE nts.ai_outline_cache;

After deleting cache entries, the next agent request for that destination will trigger a fresh API call.


6. Changing Models

To switch Claude model versions, update ai.model in tourlinq.properties:

# Sonnet (default — balanced cost/quality)
ai.model=claude-sonnet-4-5-20250929

# Haiku (faster, cheaper, less detailed)
ai.model=claude-haiku-4-5-20251001

# Opus (most capable, higher cost)
ai.model=claude-opus-4-6

After changing the model, clear the cache to regenerate outlines with the new model's output quality:

TRUNCATE nts.ai_outline_cache;

Cost impact (approximate per outline, ~1500 input tokens + ~2000 output tokens):

Model Input Cost Output Cost Total per Outline
Haiku ~$0.001 ~$0.005 ~$0.006
Sonnet ~$0.005 ~$0.030 ~$0.035
Opus ~$0.023 ~$0.150 ~$0.173

With 30-day caching, even at 100 unique destinations/month, Sonnet costs approximately $3.50/month.


7. Monitoring

Usage & Cost Tracking

Query the usage log to monitor API spend:

-- Daily cost summary (last 30 days)
SELECT DATE(created_at) AS day,
       COUNT(*) AS total_requests,
       SUM(CASE WHEN cached THEN 1 ELSE 0 END) AS cache_hits,
       SUM(CASE WHEN NOT cached THEN 1 ELSE 0 END) AS api_calls,
       ROUND(SUM(cost_usd)::numeric, 4) AS total_cost_usd
FROM nts.ai_usage_log
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at)
ORDER BY day DESC;

-- Monthly cost summary
SELECT TO_CHAR(created_at, 'YYYY-MM') AS month,
       COUNT(*) AS requests,
       ROUND(SUM(cost_usd)::numeric, 4) AS cost_usd,
       ROUND(AVG(input_tokens)::numeric, 0) AS avg_input_tokens,
       ROUND(AVG(output_tokens)::numeric, 0) AS avg_output_tokens
FROM nts.ai_usage_log
WHERE NOT cached
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month DESC;

-- Cache hit rate
SELECT ROUND(100.0 * SUM(CASE WHEN cached THEN 1 ELSE 0 END) / COUNT(*), 1) AS cache_hit_pct
FROM nts.ai_usage_log
WHERE created_at > NOW() - INTERVAL '7 days';

Feedback Monitoring

Track agent satisfaction with generated outlines:

-- Feedback summary
SELECT COUNT(*) AS total_feedback,
       SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) AS thumbs_up,
       SUM(CASE WHEN rating = -1 THEN 1 ELSE 0 END) AS thumbs_down,
       ROUND(100.0 * SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0), 1) AS approval_pct
FROM nts.ai_outline_feedback;

-- Recent negative feedback with comments (for prompt improvement)
SELECT f.created_at, f.comment, c.destination, c.budget_tier, c.pax_profile
FROM nts.ai_outline_feedback f
JOIN nts.ai_outline_cache c ON c.cache_id = f.cache_id
WHERE f.rating = -1 AND f.comment IS NOT NULL
ORDER BY f.created_at DESC
LIMIT 20;

Use negative feedback patterns to identify prompt improvements — if agents consistently dislike outlines for a certain destination type or budget tier, refine the prompt's guidance for that scenario.

Health Checks

If agents report "AI outline generation failed" errors:

  1. Check API key: Verify ai.api.key in tourlinq.properties is set and not the ## placeholder
  2. Check connectivity: curl -s https://api.anthropic.com/v1/messages -H "x-api-key: YOUR_KEY" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"claude-sonnet-4-5-20250929","max_tokens":10,"messages":[{"role":"user","content":"hello"}]}'
  3. Check prompt file: Ensure config/ai-outline-prompt.txt exists and is readable
  4. Check logs: Look for AiOutlineFacade entries in the application log at WARNING or SEVERE level
  5. Check model availability: If an older model ID has been deprecated, update ai.model to a current version