Spaces:
Sleeping
Sleeping
File size: 11,898 Bytes
bf7fdad a1c3ab4 bf7fdad a1c3ab4 bf7fdad da8ff01 bf7fdad a1c3ab4 bf7fdad 6cdc2bc bf7fdad a1c3ab4 bf7fdad 69ad1f5 a1c3ab4 bf7fdad a1c3ab4 b185311 bf7fdad a1c3ab4 bf7fdad 69ad1f5 a1c3ab4 | 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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | # βββββββββββββββββββββββββββββββββββββββββ
# NovaDXB β agent.py
# LangGraph ReAct Agent + 7 MCP Tools
# βββββββββββββββββββββββββββββββββββββββββ
import os
from dotenv import load_dotenv
# LangChain 1.3.4 correct imports
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import SystemMessage
# RAG engine
from rag_engine import query_rag
import json
import warnings
warnings.filterwarnings("ignore")
# Load environment variables
load_dotenv()
# βββββββββββββββββββββββββββββββββββββββββ
# CONFIGURATION
# βββββββββββββββββββββββββββββββββββββββββ
LLM_MODEL = "gpt-4o-mini"
TEMPERATURE = 0.7
MAX_TOKENS = 1000
# βββββββββββββββββββββββββββββββββββββββββ
# GLOBAL
# βββββββββββββββββββββββββββββββββββββββββ
agent_executor = None
# βββββββββββββββββββββββββββββββββββββββββ
# MCP TOOLS β @tool decorator (modern way)
# βββββββββββββββββββββββββββββββββββββββββ
@tool
def dubai_knowledge(query: str) -> str:
"""Search NovaDXB knowledge base for Dubai
tourism information β areas, attractions,
practical tips, culture, transport, visa."""
return query_rag(query)
@tool
def itinerary_builder(details: str) -> str:
"""Build a complete day-by-day Dubai itinerary.
Use when user wants a trip plan.
Input: number of days, budget, interests, group type."""
prompt = (
f"Build a detailed Dubai itinerary for: {details}. "
"Format as Day 1, Day 2 etc with morning afternoon "
"and evening activities, real place names, and "
"estimated AED costs per day."
)
return query_rag(prompt)
@tool
def budget_estimator(trip_details: str) -> str:
"""Estimate realistic AED budget for a Dubai trip.
Use when user asks about costs or money needed.
Input: trip duration, accommodation tier, travel style."""
prompt = (
f"What is a realistic daily and total AED budget for: "
f"{trip_details}. Include accommodation, food, "
f"transport and activities breakdown."
)
return query_rag(prompt)
@tool
def area_recommender(preferences: str) -> str:
"""Recommend the best Dubai area to stay in.
Use when user asks where to stay in Dubai.
Input: budget, group type, interests, travel style."""
prompt = (
f"Which Dubai area or neighbourhood should I stay in "
f"if: {preferences}. Give 2-3 specific area names "
f"with reasons and price ranges."
)
return query_rag(prompt)
@tool
def dining_recommender(requirements: str) -> str:
"""Recommend Dubai restaurants and dining experiences.
Use when user asks about food or restaurants.
Input: cuisine type, budget per person, area, occasion."""
prompt = (
f"Recommend specific Dubai restaurants for: "
f"{requirements}. Include restaurant names, "
f"cuisine, price range in AED and location."
)
return query_rag(prompt)
@tool
def currency_converter(query: str) -> str:
"""Convert an amount between AED (UAE Dirham) and major
tourist currencies, or explain Dubai money matters.
Use when user asks about currency conversion, exchange rates,
or 'how much is X AED in my currency'.
Input: amount and currency, e.g. '500 AED to USD' or '200 USD to AED'."""
# Fixed approximate rates relative to 1 AED (AED is USD-pegged, very stable)
rates_per_aed = {
"USD": 0.272, "EUR": 0.250, "GBP": 0.214, "INR": 22.85,
"PKR": 75.80, "PHP": 15.40, "CNY": 1.97, "SAR": 1.02,
"AED": 1.0,
}
import re as _re
match = _re.search(
r"(\d+(?:\.\d+)?)\s*([A-Za-z]{3})\s*(?:to|in)?\s*([A-Za-z]{3})?",
query, _re.IGNORECASE
)
if not match:
return (
"I can convert between AED and major currencies (USD, EUR, GBP, "
"INR, PKR, PHP, CNY, SAR). Try asking like '500 AED to USD'."
)
amount = float(match.group(1))
from_cur = match.group(2).upper()
to_cur = (match.group(3) or "AED").upper()
if from_cur not in rates_per_aed or to_cur not in rates_per_aed:
return (
f"I support AED conversions with USD, EUR, GBP, INR, PKR, PHP, "
f"CNY and SAR. I don't have a fixed rate for {from_cur} or {to_cur} β "
f"please check a live exchange rate for that currency."
)
# Convert from_cur -> AED -> to_cur
amount_in_aed = amount / rates_per_aed[from_cur] if from_cur != "AED" else amount
result = amount_in_aed * rates_per_aed[to_cur]
return (
f"{amount:.2f} {from_cur} is approximately {result:.2f} {to_cur} "
f"(AED is pegged to USD at a fixed rate, so this stays very stable). "
f"Note: exchange houses in Dubai typically offer 3-5% better rates "
f"than airport counters."
)
@tool
def weather_advisor(query: str) -> str:
"""Give weather expectations and best-time-to-visit advice for Dubai.
Use when user asks about weather, climate, temperature, what to pack,
or the best month/season to visit.
Input: a month, season, or general weather question."""
prompt = (
f"Based on Dubai's seasonal weather patterns, answer this: {query}. "
"Include expected temperature range, humidity, and what to pack "
"if relevant. Mention if it falls in peak, shoulder or low season."
)
return query_rag(prompt)
# βββββββββββββββββββββββββββββββββββββββββ
# SYSTEM PROMPT
# βββββββββββββββββββββββββββββββββββββββββ
SYSTEM_PROMPT = """You are NovaDXB, a premium AI concierge
for Dubai tourism. You help tourists plan their perfect
Dubai experience with personalized recommendations.
Your personality:
- Warm, knowledgeable and professional
- Always mention real names β areas, restaurants, attractions
- Always include AED prices when relevant
- Think like a local expert at a 5-star Dubai hotel
- Be specific and actionable, never vague
Always use your tools to get accurate Dubai information.
Never answer from general knowledge alone.
When a request spans multiple sub-topics (e.g. several areas,
cuisines, or days), call the relevant tool once with all of those
sub-topics combined into a single input, rather than calling it
separately for each one. Synthesize variety from one tool response
instead of making repeated calls to the same tool in one turn."""
# βββββββββββββββββββββββββββββββββββββββββ
# INITIALIZE AGENT
# βββββββββββββββββββββββββββββββββββββββββ
def initialize_agent():
"""Build NovaDXB agent. Called once on startup."""
global agent_executor
print("π€ Initializing NovaDXB Agent...")
# LLM
llm = ChatOpenAI(
model=LLM_MODEL,
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
openai_api_key=os.environ.get("OPENAI_API_KEY")
)
# Tools list
tools = [
dubai_knowledge,
itinerary_builder,
budget_estimator,
area_recommender,
dining_recommender,
currency_converter,
weather_advisor,
]
# Create ReAct agent
try:
agent_executor = create_react_agent(
model=llm,
tools=tools,
prompt=SystemMessage(content=SYSTEM_PROMPT)
)
except TypeError:
agent_executor = create_react_agent(
model=llm,
tools=tools,
state_modifier=SystemMessage(content=SYSTEM_PROMPT)
)
print("β
NovaDXB Agent ready")
return agent_executor
# βββββββββββββββββββββββββββββββββββββββββ
# QUERY AGENT β called by app.py
# βββββββββββββββββββββββββββββββββββββββββ
def query_agent(user_message: str) -> str:
"""Main function called by app.py /chat endpoint."""
global agent_executor
if agent_executor is None:
return "Agent not initialized yet. Please wait."
try:
result = agent_executor.invoke({
"messages": [("human", user_message)]
})
# Extract last AI message from LangGraph response
messages = result.get("messages", [])
if messages:
return messages[-1].content
return "Sorry, I could not process that."
except Exception as e:
return "Sorry, something went wrong. Please try again."
# βββββββββββββββββββββββββββββββββββββββββ
# ITINERARY EXTRACTION β for side panel display
# Lightweight follow-up call, only runs when relevant
# βββββββββββββββββββββββββββββββββββββββββ
_extraction_llm = None
def _get_extraction_llm():
"""Lazy-init a cheap, fast LLM instance just for JSON extraction."""
global _extraction_llm
if _extraction_llm is None:
_extraction_llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
max_tokens=600,
openai_api_key=os.environ.get("OPENAI_API_KEY")
)
return _extraction_llm
def extract_itinerary_json(agent_response: str):
"""
Given the agent's chat response, try to extract a structured
day-by-day itinerary as JSON for the UI side panel.
Returns None if the response doesn't contain itinerary content
(e.g. it was a currency or weather question).
"""
# Quick heuristic β skip the extra API call entirely if response
# clearly isn't an itinerary (saves cost and latency)
lowered = agent_response.lower()
if "day 1" not in lowered and "day1" not in lowered:
return None
llm = _get_extraction_llm()
extraction_prompt = f"""Extract a structured itinerary from this text.
Return ONLY valid JSON, no other text, no markdown code fences.
If the text contains a day-by-day Dubai itinerary, return this exact shape:
{{
"has_itinerary": true,
"days": [
{{
"day_number": 1,
"title": "short theme for the day",
"activities": ["short activity 1", "short activity 2", "short activity 3"],
"estimated_cost": "AED XXX"
}}
],
"total_cost": "AED XXX"
}}
If there is no clear day-by-day itinerary in the text, return:
{{"has_itinerary": false}}
Text to extract from:
{agent_response}
"""
try:
result = llm.invoke(extraction_prompt)
raw = result.content.strip()
# Strip markdown code fences if the model added them anyway
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
raw = raw.strip()
data = json.loads(raw)
if not data.get("has_itinerary"):
return None
return data
except Exception as e:
return None |