app.bettersight.io/backend/industries/adventure_travel/prompt.py

117 lines
5.4 KiB
Python

# Prompt template and injection logic for the adventure travel
# vertical — ported unchanged from the legacy app.py. Per CLAUDE.md
# §10, pivoting to a new industry means adding a new folder under
# industries/ with these same three files; core/ is never touched.
def build_prompt(competitor_name, page_data, product_name,
destination, duration, travel_style, is_ngs=False):
"""
Builds the full extraction prompt for one competitor page, embedding
the scraped page content directly into the returned template.
Data flow:
competitor_name + page_data (scraped text/tables/url) + client
product context (name/destination/duration/style) + is_ngs flag →
NGS-specific field block appended if is_ngs →
full prompt string returned, ready to send to core.extractor
"""
ngs_fields = '''
"exclusiveAccess": "exclusive access or special unique inclusions",
"groupLeader": "group leader and/or local expert description",
"sustainability": "sustainability inclusions or null",''' if is_ngs else ''
content = page_data['text'] if page_data else '[PAGE CONTENT WILL BE INSERTED HERE]'
if page_data and page_data.get('tables'):
content += f'\n\nTABLE DATA:\n{page_data["tables"]}'
url = page_data['url'] if page_data else '[COMPETITOR URL]'
return f'''You are a travel industry analyst helping G Adventures research competitors.
Be concise — use short phrases not full sentences for all fields except comments.
Read the ENTIRE page content carefully before extracting any fields.
Pay particular attention to itinerary sections, departure tables, and pricing grids
which may appear later in the content.
The content below was scraped from the {competitor_name} tour page at: {url}
This is the specific page selected as most comparable to:
- G Adventures Product: {product_name}
- Destination: {destination}
- Duration: ~{duration} days
- G Adventures Travel Style: {travel_style}
PAGE CONTENT:
---
{content}
---
CRITICAL PRICING RULES:
- majorityPrice: Standard/regular adult price for the most common departure.
IGNORE sale, promotional, early bird, or "was/now" pricing. Use non-discounted rack rate only.
- Prices may appear as "from $X", "per person from $X", or inside a departures table or pricing grid.
Extract the most commonly listed standard adult price regardless of how it is labelled on the page.
- priceLow: Lowest standard price across all departures shown (not sale price).
- priceHigh: Highest standard price across all departures shown (not sale price).
- If a site requires clicking "see more departures" to show all dates, note this in seasonality
and use only prices visible on the main listing page.
- Multi-currency: only fill if the price is explicitly listed in that currency on the page.
Do not convert. If the page shows GBP only, fill gbpPrice and leave USD fields null.
MEALS RULE:
- meals: Return the TOTAL number of included meals as a single integer only. No text description.
- Meals may be listed individually per day in the itinerary section
(e.g. "Day 1: Breakfast", "Day 3: Breakfast and Dinner").
Count every individual meal mention across all itinerary days.
Do not rely on a summary box — read through the full itinerary to count.
- Count each meal type separately: 1 breakfast + 1 lunch + 1 dinner = 3 meals.
Return ONLY a valid JSON object. No markdown, no code fences, no explanation.
{{
"company": "{competitor_name}",
"link": "{url}",
"tripName": "exact trip name from the page",
"tripCode": "trip code if shown, otherwise null",
"serviceLevel": "accommodation service level",
"groupSize": max group size as integer,
"duration": number of days as integer,
"meals": total included meals as integer,{ngs_fields}
"startLocation": "departure city",
"endLocation": "end city",
"departures": approximate departures per year as integer or null,
"seasonality": "price bands by season. Note if more departures are hidden behind a button.",
"startDays": "departure days of week",
"targetAudience": "target traveller",
"hotels": "hotel types or properties",
"activities": "key included activities",
"relevancy": relevancy score 1-5 as integer,
"majorityPrice": standard adult USD price as number or null,
"priceLow": lowest standard USD price as number or null,
"priceHigh": highest standard USD price as number or null,
"comments": "analysis: key differences from G Adventures, strengths, weaknesses, pricing position",
"dateUsed": "specific departure date used for majority price",
"audPrice": standard AUD price as number or null,
"cadPrice": standard CAD price as number or null,
"eurPrice": standard EUR price as number or null,
"gbpPrice": standard GBP price as number or null
}}'''
def inject_page_content(prompt_template, competitor_name, page_data):
"""
Injects real scraped content into a user-edited prompt template
(used when the PM has customised the prompt via the dashboard's
prompt preview/edit flow instead of using build_prompt()'s default).
"""
content = page_data['text']
if page_data.get('tables'):
content += f'\n\nTABLE DATA:\n{page_data["tables"]}'
result = prompt_template.replace('[PAGE CONTENT WILL BE INSERTED HERE]', content)
result = result.replace('[COMPETITOR URL]', page_data['url'])
result = result.replace(
competitor_name + ' tour page at: [COMPETITOR URL]',
competitor_name + f' tour page at: {page_data["url"]}'
)
return result