rename folder

This commit is contained in:
Musa 2025-12-22 11:20:41 -08:00
parent bd0d6000f3
commit ca4769ae90
12 changed files with 251 additions and 50 deletions

View file

@ -120,6 +120,16 @@ Assistant: [Flight information from Istanbul to Seattle]
The system understands context and pronouns, automatically routing to the right agent.
### Multi-Intent Queries
```
User: What's the weather in Seattle, and do any flights go direct to New York?
Assistant: [Both weather_agent and flight_agent respond simultaneously]
- Weather Agent: [Weather information for Seattle]
- Flight Agent: [Flight information from Seattle to New York]
```
The orchestrator can select multiple agents simultaneously for queries containing multiple intents.
## Agent Details
### Weather Agent

View file

@ -14,6 +14,9 @@ model_providers:
- model: openai/gpt-4o-mini
access_key: $OPENAI_API_KEY
system_prompt: |
You are a helpful travel assistant. You are able to answer questions about weather, flights, and currency. You are able to answer questions about the weather in a city, the flights between two cities, and the currency exchange rate between two currencies.
listeners:
- type: agent
name: travel_booking_service
@ -21,9 +24,9 @@ listeners:
router: plano_orchestrator_v1
agents:
- id: weather_agent
description: Get real-time weather conditions and multi-day forecasts for any city worldwide using Open-Meteo API (free, no API key needed). Provides current temperature, multi-day forecasts, weather conditions, sunrise/sunset times, and detailed weather information. Understands conversation context to resolve location references from previous messages.
description: Get real-time weather conditions and multi-day forecasts for any city worldwide using Open-Meteo API (free, no API key needed). Provides current temperature, multi-day forecasts, weather conditions, sunrise/sunset times, and detailed weather information. Understands conversation context to resolve location references from previous messages. Handles weather-related questions including "What's the weather in [city]?", "What's the forecast for [city]?", "How's the weather in [city]?". When queries include both weather and other travel questions (e.g., flights, currency), this agent answers ONLY the weather part.
- id: flight_agent
description: Get live flight information between airports using FlightAware AeroAPI. Shows real-time flight status, scheduled/estimated/actual departure and arrival times, gate and terminal information, delays, aircraft type, and flight status. Automatically resolves city names to airport codes (IATA/ICAO). Understands conversation context to infer origin/destination from follow-up questions. Supports queries like "What flights go from London to Seattle?" or "Do they fly out from Seattle?" (using context from previous messages).
description: Get live flight information between airports using FlightAware AeroAPI. Shows real-time flight status, scheduled/estimated/actual departure and arrival times, gate and terminal information, delays, aircraft type, and flight status. Automatically resolves city names to airport codes (IATA/ICAO). Understands conversation context to infer origin/destination from follow-up questions. Handles flight-related questions including "What flights go from [city] to [city]?", "Do flights go to [city]?", "Are there direct flights from [city]?". When queries include both flight and other travel questions (e.g., weather, currency), this agent answers ONLY the flight part.
- id: currency_agent
description: Get real-time currency exchange rates and perform currency conversions using Frankfurter API (free, no API key needed). Provides latest exchange rates, currency conversions with amount calculations, and supports any currency pair. Automatically extracts currency codes from country names and conversation context. Understands pronouns like "their currency" when referring to previously mentioned countries. Uses standard 3-letter ISO currency codes (e.g., USD, EUR, GBP, JPY, PKR).

View file

@ -1,4 +1,5 @@
import json
import re
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
@ -59,26 +60,37 @@ CRITICAL INSTRUCTIONS:
- Indicate flight status (scheduled, enroute, arrived, cancelled, etc.)
- Show aircraft type when available
3. RESPONSE FORMAT:
3. MULTI-PART QUERIES AND MULTI-AGENT COLLABORATION:
- If the user asks multiple questions in one message (e.g., "What's the weather in Seattle, and what flights go to New York?"), focus ONLY on answering the flight-related part
- When queries contain multiple intents (weather + flights, flights + currency), you are part of a coordinated response where each agent handles their domain
- Provide complete flight information directly without mentioning other agents or deferring to them
- Example: "Here's a flight from Seattle to New York: [provide complete flight info]." (Do NOT say "other agents may handle weather")
- Do NOT attempt to answer questions outside your flight expertise (e.g., weather, currency, hotels)
- Simply provide your flight response - the system coordinates responses from multiple agents automatically
4. RESPONSE FORMAT:
- For flight searches: List available flights with clear details
- Include departure and arrival times (scheduled, estimated, actual)
- Mention any delays or status changes
- Use natural, conversational language
- Be concise but complete
4. HANDLING INCOMPLETE QUERIES:
- If the user asks a follow-up question like "Do they fly out from X?" or "Do they have flights to Y?", use conversation context to understand what they're asking
- If origin or destination is missing from the data, check if it can be inferred from the conversation context
- If you cannot determine the complete route, politely ask for clarification: "I can help you find flights! Could you please specify both the origin and destination cities? For example, 'flights from Istanbul to Seattle' or 'flights from Seattle to Istanbul'."
- When answering follow-up questions, acknowledge what you understand from context: "Based on our conversation about Istanbul, I can help you find flights from Istanbul to Seattle..."
5. HANDLING INCOMPLETE QUERIES AND CONVERSATION CONTEXT:
- ALWAYS check conversation history for context when origin or destination is missing
- If the user asks "What flights go direct from Seattle?" after asking about weather in Istanbul, infer: origin=Seattle, destination=Istanbul (from previous context)
- If the user asks "Do they fly out from X?" look for previously mentioned cities in the conversation
- When a destination is missing but a city was mentioned earlier (e.g., "What's the weather in Istanbul?"), use that city as the destination
- When an origin is missing but a city was mentioned earlier, use that city as the origin
- If you cannot determine the complete route from context, politely ask for clarification: "I can help you find flights! Could you please specify both the origin and destination cities? For example, 'flights from Istanbul to Seattle' or 'flights from Seattle to Istanbul'."
- When answering follow-up questions, acknowledge what you understand from context: "Based on our conversation about Istanbul, I can help you find flights from Seattle to Istanbul..."
5. ERROR HANDLING:
6. ERROR HANDLING:
- If flight data is missing or null, acknowledge this politely
- Never invent or guess flight information - only use what's provided
- If airports or flights cannot be found, mention this clearly
- If the route doesn't exist or no flights are available, suggest alternatives or ask if they meant a different route
Remember: Only use the data provided. Never fabricate flight information. If data is missing, clearly state what's unavailable. Use conversation context to understand follow-up questions."""
Remember: Only use the data provided. Never fabricate flight information. If data is missing, clearly state what's unavailable. Use conversation context to understand follow-up questions. Focus ONLY on flight-related questions. Provide complete flight responses without mentioning other agents."""
FLIGHT_EXTRACTION_PROMPT = """You are a flight information extraction assistant. Your ONLY job is to extract flight-related information from user messages and convert it to structured data.
@ -106,10 +118,12 @@ CRITICAL RULES:
6. Determine the origin and destination based on context:
- "from X to Y" origin=X, destination=Y
- "X to Y" origin=X, destination=Y
- "flights from X" origin=X, destination=null (unless context provides destination)
- "flights to Y" origin=null (unless context provides origin), destination=Y
- "Do they fly out from X?" origin=X (or from context), destination=from context or null
- "Do they have flights to Y?" origin=from context, destination=Y
- "flights from X" origin=X, destination=null (UNLESS conversation context provides a previously mentioned city - use that as destination)
- "flights to Y" origin=null (UNLESS conversation context provides a previously mentioned city - use that as origin), destination=Y
- "What flights go direct from X?" origin=X, destination=from conversation context (if a city was mentioned earlier)
- "Do they fly out from X?" origin=X (or from context), destination=from context (check ALL previous messages for mentioned cities)
- "Do they have flights to Y?" origin=from context (check ALL previous messages), destination=Y
- CRITICAL: When only one part (origin OR destination) is provided, ALWAYS check conversation history for the missing part
7. Return your response as a JSON object with the following structure:
{
"origin": "London" or null,
@ -128,6 +142,7 @@ CRITICAL RULES:
Examples with context:
- Conversation: "What's the weather in Istanbul?" Current: "Do they fly out from Seattle?" {"origin": "Istanbul", "destination": "Seattle", "date": null, "origin_airport_code": null, "destination_airport_code": null}
- Conversation: "What's the weather in Istanbul?" Current: "What flights go direct from Seattle?" {"origin": "Seattle", "destination": "Istanbul", "date": null, "origin_airport_code": null, "destination_airport_code": null} (Istanbul from previous context)
- Conversation: "What's the weather in London?" Current: "What flights go from there to Seattle?" {"origin": "London", "destination": "Seattle", "date": null, "origin_airport_code": null, "destination_airport_code": null}
- Conversation: "Tell me about Istanbul" Current: "Do they have flights to Seattle?" {"origin": "Istanbul", "destination": "Seattle", "date": null, "origin_airport_code": null, "destination_airport_code": null}
- "What flights go from London to Seattle?" {"origin": "London", "destination": "Seattle", "date": null, "origin_airport_code": null, "destination_airport_code": null}
@ -162,25 +177,45 @@ async def extract_flight_info_from_messages(messages):
"destination_airport_code": None,
}
# CRITICAL: Always preserve the FIRST user message (original query) for multi-agent scenarios
# When Plano processes multiple agents, it may add assistant responses that get filtered out,
# but we need to always use the original user query
original_user_message = user_messages[0].content.strip() if user_messages else None
# Try to find a valid recent user message first (for follow-up queries)
user_content = None
for msg in reversed(user_messages):
content = msg.content.strip()
content_lower = content.lower()
# Skip messages that are clearly JSON-encoded assistant responses or errors
# But be less aggressive - only skip if it's clearly not a user query
if content.startswith("[{") or content.startswith("[{"):
# Likely JSON-encoded assistant response
continue
if any(
pattern in content_lower
for pattern in [
"<",
">",
"assistant:",
'"role": "assistant"',
'"role":"assistant"',
"error:",
"i apologize",
"i'm having trouble",
]
):
continue
# Don't skip messages that just happen to contain these words naturally
user_content = content
break
# Fallback to original user message if no valid recent message found
if not user_content and original_user_message:
# Check if original message is valid (not JSON-encoded)
if not (
original_user_message.startswith("[{")
or original_user_message.startswith("[{")
):
user_content = original_user_message
logger.info(f"Using original user message: {user_content[:200]}")
if not user_content:
logger.warning("No valid user message found")
return {
@ -244,6 +279,39 @@ async def extract_flight_info_from_messages(messages):
"destination_airport_code": flight_info.get("destination_airport_code"),
}
# Fallback: If destination is missing but we have origin, try to infer from conversation context
if result["origin"] and not result["destination"]:
# Look for cities mentioned in previous messages
for msg in reversed(conversation_context):
if msg["role"] == "user":
content = msg["content"]
# Look for weather queries mentioning cities
if (
"weather" in content.lower()
or "forecast" in content.lower()
):
# Common patterns: "weather in [city]", "forecast for [city]", "weather [city]"
patterns = [
r"(?:weather|forecast).*?(?:in|for)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
r"weather\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
]
for pattern in patterns:
city_match = re.search(pattern, content, re.IGNORECASE)
if city_match:
potential_city = city_match.group(1).strip()
# Don't use the same city as origin
if (
potential_city.lower()
!= result["origin"].lower()
):
logger.info(
f"Inferring destination from conversation context: {potential_city}"
)
result["destination"] = potential_city
break
if result["destination"]:
break
logger.info(f"LLM extracted flight info: {result}")
return result
@ -472,6 +540,34 @@ async def prepare_flight_messages(request_body: ChatCompletionRequest):
origin_code = flight_info.get("origin_airport_code")
dest_code = flight_info.get("destination_airport_code")
# Enhanced context extraction: If destination is missing, try to infer from conversation
if origin and not destination:
# Look through conversation history for mentioned cities
mentioned_cities = set()
for msg in request_body.messages:
if msg.role == "user":
content = msg.content
# Extract cities from weather queries: "weather in [city]", "forecast for [city]"
weather_patterns = [
r"(?:weather|forecast).*?(?:in|for)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
r"weather\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
]
for pattern in weather_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
for match in matches:
city = match.strip()
# Don't use same city as origin, and validate it's a real city name
if city.lower() != origin.lower() and len(city.split()) <= 3:
mentioned_cities.add(city)
# If we found cities in context, use the first one as destination
if mentioned_cities:
destination = list(mentioned_cities)[0]
logger.info(
f"Inferred destination from conversation context: {destination}"
)
flight_info["destination"] = destination
if origin and not origin_code:
origin_code = await resolve_airport_code(origin)
if destination and not dest_code:

View file

@ -1,4 +1,5 @@
import json
import re
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
@ -50,34 +51,42 @@ CRITICAL INSTRUCTIONS:
- Always provide temperatures in both Celsius and Fahrenheit when available
- If temperature is null, say "temperature data unavailable" rather than making up numbers
3. ERROR HANDLING:
3. MULTI-PART QUERIES AND MULTI-AGENT COLLABORATION:
- If the user asks multiple questions in one message (e.g., "What's the weather in Seattle, and what flights go to New York?"), focus ONLY on answering the weather-related part
- When queries contain multiple intents (weather + flights, weather + currency), you are part of a coordinated response where each agent handles their domain
- Provide complete weather information directly without mentioning other agents or deferring to them
- Example: "Here's the weather in Seattle: [provide complete weather info]." (Do NOT say "other agents may handle flights")
- Do NOT attempt to answer questions outside your weather expertise
- Simply provide your weather response - the system coordinates responses from multiple agents automatically
4. ERROR HANDLING:
- If the forecast array contains an "error" field, acknowledge the issue politely
- If temperature or condition is null/None, mention that specific data is unavailable
- Never invent or guess weather data - only use what's provided
- If location couldn't be determined, acknowledge this but still provide available data
4. RESPONSE FORMAT:
5. RESPONSE FORMAT:
- For single-day queries: Provide current conditions, temperature, and condition
- For multi-day forecasts: List each day with date, day name, high/low temps, and condition
- Include sunrise/sunset times when available and relevant
- Use natural, conversational language
- Be concise but complete
5. CONDITION DESCRIPTIONS:
6. CONDITION DESCRIPTIONS:
- Use the exact condition provided (e.g., "Clear sky", "Rainy", "Partly Cloudy")
- Add context when helpful (e.g., "perfect for outdoor activities" for clear skies)
6. LOCATION HANDLING:
7. LOCATION HANDLING:
- Always mention the location name from the data
- If the location differs from what the user asked, acknowledge this politely
7. RESPONSE STYLE:
8. RESPONSE STYLE:
- Be friendly and professional
- Use natural language, not technical jargon
- Format dates and times clearly
- For forecasts, use bullet points or numbered lists for clarity
Remember: Only use the data provided. Never fabricate weather information. If data is missing, clearly state what's unavailable."""
Remember: Only use the data provided. Never fabricate weather information. If data is missing, clearly state what's unavailable. Focus ONLY on weather-related questions. Provide complete weather responses without mentioning other agents."""
async def geocode_city(city: str) -> Optional[dict]:
@ -264,68 +273,114 @@ async def get_weather_data(location: str, days: int = 1):
return {"location": location_name, "forecast": forecast}
LOCATION_EXTRACTION_PROMPT = """You are a location extraction assistant. Your ONLY job is to extract the geographic location (city, state, country, etc.) from user messages.
LOCATION_EXTRACTION_PROMPT = """You are a location extraction assistant for WEATHER queries. Your ONLY job is to extract the geographic location (city, state, country, etc.) that the user is asking about for WEATHER information.
CRITICAL RULES:
1. Extract ONLY the location name - nothing else
1. Extract ONLY the location name associated with WEATHER questions - nothing else
2. Return just the location name in plain text (e.g., "London", "New York", "Paris, France")
3. If the user mentions multiple locations, extract the PRIMARY location they're asking about
4. Ignore error messages, HTML tags, and assistant responses
5. If no clear location is found, return exactly: "NOT_FOUND"
6. Clean the location name - remove words like "about", "for", "in", "the weather in", etc.
7. Return the location in a format suitable for geocoding (city name, or "City, State", or "City, Country")
3. If the user mentions multiple locations in a multi-part query, extract ONLY the location mentioned in the WEATHER part
- Example: "What's the weather in Seattle, and what is one flight that goes direct to Atlanta?" Extract "Seattle" (the weather location, NOT Atlanta which is for flights)
- Example: "Weather in London and flights to Paris" Extract "London" (weather location)
4. Look for patterns like "weather in [location]", "forecast for [location]", "weather [location]"
5. Ignore error messages, HTML tags, and assistant responses
6. If no clear weather-related location is found, return exactly: "NOT_FOUND"
7. Clean the location name - remove words like "about", "for", "in", "the weather in", etc.
8. Return the location in a format suitable for geocoding (city name, or "City, State", or "City, Country")
Examples:
- "What's the weather in London?" "London"
- "Tell me about the weather for New York" "New York"
- "Weather forecast for Paris, France" "Paris, France"
- "I'm going to Seattle" "Seattle"
- "About London" "London"
- "What's the weather in Seattle, and what is one flight that goes direct to Atlanta?" "Seattle" (NOT Atlanta - that's for flights)
- "Weather in Istanbul and flights to Seattle" "Istanbul" (weather location)
- "I'm going to Seattle" "Seattle" (if context suggests weather query)
- "What's happening?" "NOT_FOUND"
Now extract the location from this message:"""
Now extract the WEATHER location from this message:"""
async def extract_location_from_messages(messages):
"""Extract location from user messages using LLM."""
"""Extract location from user messages using LLM, focusing on weather-related locations."""
user_messages = [msg for msg in messages if msg.role == "user"]
if not user_messages:
logger.warning("No user messages found, using default: New York")
return "New York"
# CRITICAL: Always preserve the FIRST user message (original query) for multi-agent scenarios
# When Plano processes multiple agents, it may add assistant responses that get filtered out,
# but we need to always use the original user query
original_user_message = user_messages[0].content.strip() if user_messages else None
# Try to find a valid recent user message first (for follow-up queries)
user_content = None
for msg in reversed(user_messages):
content = msg.content.strip()
content_lower = content.lower()
# Skip messages that are clearly JSON-encoded assistant responses or errors
# But be less aggressive - only skip if it's clearly not a user query
if content.startswith("[{") or content.startswith("[{"):
# Likely JSON-encoded assistant response
continue
if any(
pattern in content_lower
for pattern in [
"<",
">",
"assistant:",
'"role": "assistant"',
'"role":"assistant"',
"error:",
"i apologize",
"i'm having trouble",
]
):
continue
# Don't skip messages that just happen to contain these words naturally
user_content = content
break
# Fallback to original user message if no valid recent message found
if not user_content and original_user_message:
# Check if original message is valid (not JSON-encoded)
if not (
original_user_message.startswith("[{")
or original_user_message.startswith("[{")
):
user_content = original_user_message
logger.info(f"Using original user message: {user_content[:200]}")
if not user_content:
logger.warning("No valid user message found, using default: New York")
return "New York"
try:
logger.info(f"Extracting location from user message: {user_content[:200]}")
logger.info(
f"Extracting weather location from user message: {user_content[:200]}"
)
# Build context from conversation history
conversation_context = []
for msg in messages:
content = msg.content.strip()
content_lower = content.lower()
if any(
pattern in content_lower
for pattern in ["<", ">", "error:", "i apologize", "i'm having trouble"]
):
continue
conversation_context.append({"role": msg.role, "content": content})
# Use last 5 messages for context
context_messages = (
conversation_context[-5:]
if len(conversation_context) > 5
else conversation_context
)
llm_messages = [{"role": "system", "content": LOCATION_EXTRACTION_PROMPT}]
for msg in context_messages:
llm_messages.append({"role": msg["role"], "content": msg["content"]})
response = await archgw_client.chat.completions.create(
model=LOCATION_MODEL,
messages=[
{"role": "system", "content": LOCATION_EXTRACTION_PROMPT},
{"role": "user", "content": user_content},
],
messages=llm_messages,
temperature=0.1,
max_tokens=50,
)
@ -334,18 +389,55 @@ async def extract_location_from_messages(messages):
location = location.strip("\"'`.,!?")
if not location or location.upper() == "NOT_FOUND":
# Fallback: Try regex extraction for weather patterns
weather_patterns = [
r"weather\s+(?:in|for)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
r"forecast\s+(?:in|for)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
r"weather\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
]
for msg in reversed(context_messages):
if msg["role"] == "user":
content = msg["content"]
for pattern in weather_patterns:
match = re.search(pattern, content, re.IGNORECASE)
if match:
potential_location = match.group(1).strip()
logger.info(
f"Fallback regex extracted weather location: {potential_location}"
)
return potential_location
logger.warning(
f"LLM could not extract location from message, using default: New York"
)
return "New York"
logger.info(f"LLM extracted location: {location}")
logger.info(f"LLM extracted weather location: {location}")
return location
except Exception as e:
logger.error(
f"Error extracting location with LLM: {e}, using default: New York"
)
logger.error(f"Error extracting location with LLM: {e}, trying fallback regex")
# Fallback regex extraction
try:
for msg in reversed(messages):
if msg.role == "user":
content = msg.content
weather_patterns = [
r"weather\s+(?:in|for)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
r"forecast\s+(?:in|for)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)",
]
for pattern in weather_patterns:
match = re.search(pattern, content, re.IGNORECASE)
if match:
potential_location = match.group(1).strip()
logger.info(
f"Fallback regex extracted weather location: {potential_location}"
)
return potential_location
except:
pass
logger.error("All extraction methods failed, using default: New York")
return "New York"