File size: 4,635 Bytes
6380b21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import json
from typing import List, Dict, Optional, Tuple
import google.generativeai as genai
from .config import get_gemini_key
from .utils import is_iata_code

# Initialize once
_gen_inited = False

def _ensure_init():
    global _gen_inited
    if not _gen_inited:
        genai.configure(api_key=get_gemini_key())
        _gen_inited = True


def _model():
    _ensure_init()
    return genai.GenerativeModel(
        model_name="gemini-2.0-flash-lite",
        generation_config={
            "temperature": 0.2,
            "top_p": 0.9,
            "top_k": 40,
            "max_output_tokens": 600,
        },
    )


def resolve_city_to_iata_ai(city: str, country_hint: Optional[str] = None) -> Tuple[str, str, str]:
    """Use the LLM to resolve a human-readable city to an IATA code.
    Returns (code, canonical_name, kind) where kind is 'CITY' or 'AIRPORT'.
    Raises RuntimeError on failure.
    """
    m = _model()
    prompt = f"""
You convert a city name into a 3-letter IATA code for flight search.
Rules:
- If the metro area has multiple major airports with an aggregate IATA city code, return the CITY code (e.g., New York→NYC, London→LON, Paris→PAR, Tokyo→TYO).
- If the city typically uses a single primary commercial airport, return that AIRPORT code (e.g., Dubai→DXB, Doha→DOH, Lahore→LHE, Karachi→KHI, Istanbul→IST).
- Return empty code "" if no suitable airport exists.
- The code MUST be exactly 3 uppercase letters A–Z.
- Respond with ONLY a compact JSON object, no additional text, using this schema:
  {{"code":"XXX","name":"Canonical City or Airport Name","kind":"CITY|AIRPORT","alternates":["AAA","BBB"]}}

Input:
- city: {city}
- country_hint: {country_hint or 'unknown'}
"""
    resp = m.generate_content(prompt)
    text = (resp.text or "").strip()

    # Extract JSON block
    mjson = None
    if "{" in text:
        try:
            start = text.find("{")
            end = text.rfind("}") + 1
            mjson = json.loads(text[start:end])
        except Exception:
            pass
    if not mjson or not isinstance(mjson, dict):
        raise RuntimeError("AI could not produce a valid JSON mapping for the city.")

    code = str(mjson.get("code", "")).strip().upper()
    name = str(mjson.get("name", city)).strip()
    kind = str(mjson.get("kind", "")).strip().upper()

    if not is_iata_code(code):
        raise RuntimeError(f"AI returned an invalid IATA code: {code}")
    if kind not in ("CITY", "AIRPORT"):
        kind = "AIRPORT"
    return code, name, kind


def rank_accommodations(accommodations: List[Dict], prefs: str = "") -> List[Dict]:
    """Add an 'llm_score' to each accommodation and sort by it."""
    if not accommodations:
        return []
    m = _model()
    lines = "\n".join(
        [
            f"- {a.get('name','(no name)')} | rate:{a.get('rate')} | dist:{int(a.get('dist',0))}m | kinds:{a.get('kinds','')}"
            for a in accommodations[:50]
        ]
    )
    prompt = f"""Rank the following accommodations for a tourist trip. Prefer central, well-rated options.
User preferences: {prefs or 'not specified'}.
Return a JSON array of objects with 'name' and an integer 'score' 1-100.

Items:
{lines}
"""
    resp = m.generate_content(prompt)
    try:
        text = resp.text or ""
        if "```json" in text:
            data = json.loads(text.split("```json")[-1].split("```")[0])
        else:
            data = json.loads(text)
    except Exception:
        data = []
    score_map = {d.get("name", ""): int(d.get("score", 50)) for d in data if isinstance(d, dict)}
    for a in accommodations:
        a["llm_score"] = score_map.get(a.get("name", ""), 50)
    return sorted(accommodations, key=lambda x: x.get("llm_score", 50), reverse=True)


def generate_itinerary(
    city: str,
    start_date: str,
    days: int,
    selected_attractions: List[Dict],
    selected_stay: Dict | None,
    weather_summary: str,
) -> str:
    m = _model()
    attractions_text = "\n".join([f"- {a.get('name')} ({a.get('kinds','')})" for a in selected_attractions])
    hotel_text = selected_stay.get("name") if selected_stay else "TBD"
    prompt = f"""
Create a practical, day-by-day itinerary for a trip.

City: {city}
Start Date: {start_date}
Days: {days}
Hotel: {hotel_text}
Weather (summary): {weather_summary}

Attractions to consider:
{attractions_text}

Constraints: group nearby sights, account for weather, add meal/time suggestions, include commute notes.
Return Markdown with sections Day 1, Day 2, ..., and a brief daily plan (morning/afternoon/evening).
"""
    resp = m.generate_content(prompt)
    return resp.text