mirror of
https://github.com/katanemo/plano.git
synced 2026-05-10 16:22:42 +02:00
Update docs to Plano (#639)
This commit is contained in:
parent
15fbb6c3af
commit
e224cba3e3
139 changed files with 4407 additions and 24735 deletions
21
demos/use_cases/travel_agents/Dockerfile
Normal file
21
demos/use_cases/travel_agents/Dockerfile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install uv for faster dependency management
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
# Copy dependency files
|
||||
COPY pyproject.toml README.md ./
|
||||
|
||||
# Install dependencies (without lock file to resolve fresh)
|
||||
RUN uv sync --no-dev
|
||||
|
||||
# Copy application code
|
||||
COPY src/ ./src/
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Default command (will be overridden in docker-compose)
|
||||
CMD ["uv", "run", "python", "src/travel_agents/weather_agent.py"]
|
||||
|
|
@ -1,82 +1,78 @@
|
|||
# Travel Booking Agent Demo
|
||||
|
||||
A production-ready multi-agent travel booking system demonstrating Plano's intelligent agent routing. This demo showcases three specialized agents working together to help users plan trips with weather information, flight searches, and currency exchange rates.
|
||||
A production-ready multi-agent travel booking system demonstrating Plano's intelligent agent routing. This demo showcases two specialized agents working together to help users plan trips with weather information and flight searches.
|
||||
|
||||
## Overview
|
||||
|
||||
This demo consists of three intelligent agents that work together seamlessly:
|
||||
This demo consists of two intelligent agents that work together seamlessly:
|
||||
|
||||
- **Weather Agent** - Real-time weather conditions and forecasts for any city worldwide
|
||||
- **Weather Agent** - Real-time weather conditions and multi-day forecasts for any city worldwide
|
||||
- **Flight Agent** - Live flight information between airports with real-time tracking
|
||||
- **Currency Agent** - Real-time currency exchange rates and conversions
|
||||
|
||||
All agents use Plano's agent router to intelligently route user requests to the appropriate specialized agent based on conversation context and user intent.
|
||||
All agents use Plano's agent router to intelligently route user requests to the appropriate specialized agent based on conversation context and user intent. Both agents run as Docker containers for easy deployment.
|
||||
|
||||
## Features
|
||||
|
||||
- **Intelligent Routing**: Plano automatically routes requests to the right agent
|
||||
- **Conversation Context**: Agents understand follow-up questions and references
|
||||
- **Real-Time Data**: Live weather, flight, and currency data from public APIs
|
||||
- **Real-Time Data**: Live weather and flight data from public APIs
|
||||
- **Multi-Day Forecasts**: Weather agent supports up to 16-day forecasts
|
||||
- **LLM-Powered**: Uses GPT-4o-mini for extraction and GPT-4o for responses
|
||||
- **Streaming Responses**: Real-time streaming for better user experience
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10 or higher
|
||||
- [UV package manager](https://github.com/astral-sh/uv) (recommended) or pip
|
||||
- OpenAI API key
|
||||
- Docker and Docker Compose
|
||||
- [Plano CLI](https://docs.planoai.dev) installed
|
||||
- OpenAI API key
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
# Using UV (recommended)
|
||||
uv sync
|
||||
|
||||
# Or using pip
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 2. Set Environment Variables
|
||||
### 1. Set Environment Variables
|
||||
|
||||
Create a `.env` file or export environment variables:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
export AEROAPI_KEY="your-flightaware-api-key" # Optional, demo key included
|
||||
```
|
||||
|
||||
### 3. Start All Agents
|
||||
### 2. Start All Agents with Docker
|
||||
|
||||
```bash
|
||||
chmod +x start_agents.sh
|
||||
./start_agents.sh
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
This starts:
|
||||
- Weather Agent on port 10510
|
||||
- Flight Agent on port 10520
|
||||
- Currency Agent on port 10530
|
||||
- Open WebUI on port 8080
|
||||
|
||||
### 4. Start Plano Orchestrator
|
||||
### 3. Start Plano Orchestrator
|
||||
|
||||
In a new terminal:
|
||||
|
||||
```bash
|
||||
cd /path/to/travel_booking
|
||||
cd /path/to/travel_agents
|
||||
plano up arch_config.yaml
|
||||
```
|
||||
|
||||
The gateway will start on port 8001 and route requests to the appropriate agents.
|
||||
|
||||
### 5. Test the System
|
||||
### 4. Test the System
|
||||
|
||||
Send requests to Plano Orchestrator:
|
||||
**Option 1**: Use Open WebUI at http://localhost:8080
|
||||
|
||||
**Option 2**: Send requests directly to Plano Orchestrator:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/v1/chat/completions \
|
||||
curl http://localhost:8001/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "gpt-4o",
|
||||
|
|
@ -100,20 +96,11 @@ User: What flights go from London to Seattle?
|
|||
Assistant: [Flight Agent shows available flights with schedules and status]
|
||||
```
|
||||
|
||||
### Currency Exchange
|
||||
```
|
||||
User: What's the exchange rate for Turkish Lira to USD?
|
||||
Assistant: [Currency Agent provides current exchange rate]
|
||||
```
|
||||
|
||||
### Multi-Agent Conversation
|
||||
```
|
||||
User: What's the weather in Istanbul?
|
||||
Assistant: [Weather information]
|
||||
|
||||
User: What's their exchange rate?
|
||||
Assistant: [Currency rate for Turkey]
|
||||
|
||||
User: Do they fly out from Seattle?
|
||||
Assistant: [Flight information from Istanbul to Seattle]
|
||||
```
|
||||
|
|
@ -142,75 +129,79 @@ The orchestrator can select multiple agents simultaneously for queries containin
|
|||
- **API**: FlightAware AeroAPI
|
||||
- **Capabilities**: Real-time flight status, schedules, delays, gates, terminals, live tracking
|
||||
|
||||
### Currency Agent
|
||||
- **Port**: 10530
|
||||
- **API**: Frankfurter (free, no API key)
|
||||
- **Capabilities**: Exchange rates, currency conversions, historical rates
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User Request → Plano Gateway (port 8001)
|
||||
↓
|
||||
Agent Router (LLM-based)
|
||||
↓
|
||||
┌───────────┼───────────┐
|
||||
↓ ↓ ↓
|
||||
Weather Flight Currency
|
||||
Agent Agent Agent
|
||||
(10510) (10520) (10530)
|
||||
User Request
|
||||
↓
|
||||
Plano (8001)
|
||||
[Orchestrator]
|
||||
|
|
||||
┌────┴────┐
|
||||
↓ ↓
|
||||
Weather Flight
|
||||
Agent Agent
|
||||
(10510) (10520)
|
||||
[Docker] [Docker]
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
Each agent:
|
||||
1. Extracts intent using GPT-4o-mini
|
||||
1. Extracts intent using GPT-4o-mini (with OpenTelemetry tracing)
|
||||
2. Fetches real-time data from APIs
|
||||
3. Generates response using GPT-4o
|
||||
4. Streams response back to user
|
||||
|
||||
## Configuration
|
||||
|
||||
### plano_config.yaml
|
||||
|
||||
Defines the three agents, their descriptions, and routing configuration. The agent router uses these descriptions to intelligently route requests.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `OPENAI_API_KEY` - Required for LLM operations
|
||||
- `AEROAPI_KEY` - Optional, FlightAware API key (demo key included)
|
||||
- `LLM_GATEWAY_ENDPOINT` - Plano LLM gateway URL (default: http://localhost:12000/v1)
|
||||
Both agents run as Docker containers and communicate with Plano via `host.docker.internal`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
travel_booking/
|
||||
travel_agents/
|
||||
├── arch_config.yaml # Plano configuration
|
||||
├── start_agents.sh # Start all agents script
|
||||
├── docker-compose.yaml # Docker services orchestration
|
||||
├── Dockerfile # Multi-agent container image
|
||||
├── start_agents.sh # Quick start script
|
||||
├── pyproject.toml # Python dependencies
|
||||
└── src/
|
||||
└── travel_agents/
|
||||
├── __init__.py # CLI entry point
|
||||
├── api.py # Shared API models
|
||||
├── weather_agent.py # Weather forecast agent
|
||||
├── flight_agent.py # Flight information agent
|
||||
└── currency_agent.py # Currency exchange agent
|
||||
├── weather_agent.py # Weather forecast agent (multi-day support)
|
||||
└── flight_agent.py # Flight information agent
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### arch_config.yaml
|
||||
|
||||
Defines the two agents, their descriptions, and routing configuration. The agent router uses these descriptions to intelligently route requests.
|
||||
|
||||
### docker-compose.yaml
|
||||
|
||||
Orchestrates the deployment of:
|
||||
- Weather Agent (builds from Dockerfile)
|
||||
- Flight Agent (builds from Dockerfile)
|
||||
- Open WebUI (for testing)
|
||||
- Jaeger (for distributed tracing)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Agents won't start**
|
||||
- Ensure Python 3.10+ is installed
|
||||
- Check that UV is installed: `pip install uv`
|
||||
- Verify ports 10510, 10520, 10530 are available
|
||||
**Docker containers won't start**
|
||||
- Verify Docker and Docker Compose are installed
|
||||
- Check that ports 10510, 10520, 8080 are available
|
||||
- Review container logs: `docker compose logs weather-agent` or `docker compose logs flight-agent`
|
||||
|
||||
**Plano won't start**
|
||||
- Verify Plano is installed: `plano --version`
|
||||
- Check that `OPENAI_API_KEY` is set
|
||||
- Ensure you're in the travel_booking directory
|
||||
- Ensure you're in the travel_agents directory
|
||||
- Check arch_config.yaml is valid
|
||||
|
||||
**No response from agents**
|
||||
- Verify all agents are running (check start_agents.sh output)
|
||||
- Verify all containers are running: `docker compose ps`
|
||||
- Check that Plano is running on port 8001
|
||||
- Review agent logs for errors
|
||||
- Review agent logs: `docker compose logs -f`
|
||||
- Verify `host.docker.internal` resolves correctly (should point to host machine)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
version: v0.3.0
|
||||
|
||||
agents:
|
||||
- id: weather_agent
|
||||
url: http://host.docker.internal:10510
|
||||
- id: flight_agent
|
||||
url: http://host.docker.internal:10520
|
||||
- id: currency_agent
|
||||
url: http://host.docker.internal:10530
|
||||
|
||||
model_providers:
|
||||
- model: openai/gpt-4o
|
||||
access_key: $OPENAI_API_KEY
|
||||
- model: openai/gpt-4o-mini
|
||||
access_key: $OPENAI_API_KEY
|
||||
|
||||
system_prompt: |
|
||||
You are a professional travel planner assistant. Your role is to provide accurate, clear, and helpful information about weather and flights based on the structured data provided to you.\n\nCRITICAL INSTRUCTIONS:\n\n1. DATA STRUCTURE:\n \n WEATHER DATA:\n - You will receive weather data as JSON in a system message\n - The data contains a \"location\" field (string) and a \"forecast\" array\n - Each forecast entry has: date, day_name, temperature_c, temperature_f, temperature_max_c, temperature_min_c, condition, sunrise, sunset\n - Some fields may be null/None - handle these gracefully\n \n FLIGHT DATA:\n - You will receive flight information in a system message\n - Flight data includes: airline, flight number, departure time, arrival time, origin airport, destination airport, aircraft type, status, gate, terminal\n - Information may include both scheduled and estimated times\n - Some fields may be unavailable - handle these gracefully\n\n2. WEATHER HANDLING:\n - For single-day queries: Use temperature_c/temperature_f (current/primary temperature)\n - For multi-day forecasts: Use temperature_max_c and temperature_min_c when available\n - Always provide temperatures in both Celsius and Fahrenheit when available\n - If temperature is null, say \"temperature data unavailable\" rather than making up numbers\n - Use exact condition descriptions provided (e.g., \"Clear sky\", \"Rainy\", \"Partly Cloudy\")\n - Add helpful context when appropriate (e.g., \"perfect for outdoor activities\" for clear skies)\n\n3. FLIGHT HANDLING:\n - Present flight information clearly with airline name and flight number\n - Include departure and arrival times with time zones when provided\n - Mention origin and destination airports with their codes\n - Include gate and terminal information when available\n - Note aircraft type if relevant to the query\n - Highlight any status updates (delays, early arrivals, etc.)\n - For multiple flights, list them in chronological order by departure time\n - If specific details are missing, acknowledge this rather than inventing information\n\n4. MULTI-PART QUERIES:\n - Users may ask about both weather and flights in one message\n - Answer ALL parts of the query that you have data for\n - Organize your response logically - typically weather first, then flights, or vice versa based on the query\n - Provide complete information for each topic without mentioning other agents\n - If you receive data for only one topic but the user asked about multiple, answer what you can with the provided data\n\n5. ERROR HANDLING:\n - If weather forecast contains an \"error\" field, acknowledge the issue politely\n - If temperature or condition is null/None, mention that specific data is unavailable\n - If flight details are incomplete, state which information is unavailable\n - Never invent or guess weather or flight data - only use what's provided\n - If location couldn't be determined, acknowledge this but still provide available data\n\n6. RESPONSE FORMAT:\n \n For Weather:\n - Single-day queries: Provide current conditions, temperature, and condition\n - Multi-day forecasts: List each day with date, day name, high/low temps, and condition\n - Include sunrise/sunset times when available and relevant\n \n For Flights:\n - List flights with clear numbering or bullet points\n - Include key details: airline, flight number, departure/arrival times, airports\n - Add gate, terminal, and status information when available\n - For multiple flights, organize chronologically\n \n General:\n - Use natural, conversational language\n - Be concise but complete\n - Format dates and times clearly\n - Use bullet points or numbered lists for clarity\n\n7. LOCATION HANDLING:\n - Always mention location names from the data\n - For flights, clearly state origin and destination cities/airports\n - If locations differ from what the user asked, acknowledge this politely\n\n8. RESPONSE STYLE:\n - Be friendly and professional\n - Use natural language, not technical jargon\n - Provide information in a logical, easy-to-read format\n - When answering multi-part queries, create a cohesive response that addresses all aspects\n\nRemember: Only use the data provided. Never fabricate weather or flight information. If data is missing, clearly state what's unavailable. Answer all parts of the user's query that you have data for.
|
||||
|
||||
listeners:
|
||||
- type: agent
|
||||
name: travel_booking_service
|
||||
port: 8001
|
||||
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. 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. 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).
|
||||
|
||||
tracing:
|
||||
random_sampling: 100
|
||||
57
demos/use_cases/travel_agents/config.yaml
Normal file
57
demos/use_cases/travel_agents/config.yaml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
version: v0.3.0
|
||||
|
||||
agents:
|
||||
- id: weather_agent
|
||||
url: http://host.docker.internal:10510
|
||||
- id: flight_agent
|
||||
url: http://host.docker.internal:10520
|
||||
|
||||
model_providers:
|
||||
- model: openai/gpt-4o
|
||||
access_key: $OPENAI_API_KEY
|
||||
default: true
|
||||
- model: openai/gpt-4o-mini
|
||||
access_key: $OPENAI_API_KEY # smaller, faster, cheaper model for extracting entities like location
|
||||
|
||||
listeners:
|
||||
- type: agent
|
||||
name: travel_booking_service
|
||||
port: 8001
|
||||
router: plano_orchestrator_v1
|
||||
agents:
|
||||
- id: weather_agent
|
||||
description: |
|
||||
|
||||
WeatherAgent is a specialized AI assistant for real-time weather information and forecasts. It provides accurate weather data for any city worldwide using the Open-Meteo API, helping travelers plan their trips with up-to-date weather conditions.
|
||||
|
||||
Capabilities:
|
||||
* 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
|
||||
* Provides multi-day forecasts
|
||||
* Provides weather conditions
|
||||
* Provides sunrise/sunset times
|
||||
* Provides 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: |
|
||||
|
||||
FlightAgent is an AI-powered tool specialized in providing live flight information between airports. It leverages the FlightAware AeroAPI to deliver real-time flight status, gate information, and delay updates.
|
||||
|
||||
Capabilities:
|
||||
* Get live flight information between airports using FlightAware AeroAPI
|
||||
* Shows real-time flight status
|
||||
* Shows scheduled/estimated/actual departure and arrival times
|
||||
* Shows gate and terminal information
|
||||
* Shows delays
|
||||
* Shows aircraft type
|
||||
* Shows 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
|
||||
|
||||
tracing:
|
||||
random_sampling: 100
|
||||
|
|
@ -1,11 +1,44 @@
|
|||
|
||||
services:
|
||||
jaeger:
|
||||
build:
|
||||
context: ../../shared/jaeger
|
||||
container_name: jaeger
|
||||
restart: always
|
||||
ports:
|
||||
- "16686:16686"
|
||||
- "4317:4317"
|
||||
- "4318:4318"
|
||||
- "16686:16686" # Jaeger UI
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
|
||||
weather-agent:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: weather-agent
|
||||
restart: always
|
||||
ports:
|
||||
- "10510:10510"
|
||||
environment:
|
||||
- LLM_GATEWAY_ENDPOINT=http://host.docker.internal:12000/v1
|
||||
command: ["uv", "run", "python", "src/travel_agents/weather_agent.py"]
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
flight-agent:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: flight-agent
|
||||
restart: always
|
||||
ports:
|
||||
- "10520:10520"
|
||||
environment:
|
||||
- LLM_GATEWAY_ENDPOINT=http://host.docker.internal:12000/v1
|
||||
- AEROAPI_KEY=${AEROAPI_KEY:-ESVFX7TJLxB7OTuayUv0zTQBryA3tOPr}
|
||||
command: ["uv", "run", "python", "src/travel_agents/flight_agent.py"]
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
open-web-ui:
|
||||
image: dyrnq/open-webui:main
|
||||
restart: always
|
||||
|
|
@ -15,3 +48,6 @@ services:
|
|||
- DEFAULT_MODEL=gpt-4o-mini
|
||||
- ENABLE_OPENAI_API=true
|
||||
- OPENAI_API_BASE_URL=http://host.docker.internal:8001/v1
|
||||
depends_on:
|
||||
- weather-agent
|
||||
- flight-agent
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ requires-python = ">=3.10"
|
|||
dependencies = [
|
||||
"click>=8.2.1",
|
||||
"pydantic>=2.11.7",
|
||||
"fastapi>=0.104.1",
|
||||
"uvicorn>=0.24.0",
|
||||
"openai>=2.13.0",
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn>=0.30.0",
|
||||
"openai>=1.0.0",
|
||||
"httpx>=0.24.0",
|
||||
"opentelemetry-api>=1.20.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
@ -19,3 +20,6 @@ travel_agents = "travel_agents:main"
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/travel_agents"]
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import click
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--host", "host", default="localhost", help="Host to bind server to")
|
||||
@click.option("--port", "port", type=int, default=8000, help="Port for server")
|
||||
@click.option(
|
||||
"--agent",
|
||||
"agent",
|
||||
required=True,
|
||||
help="Agent name: weather, flight, or currency",
|
||||
)
|
||||
def main(host, port, agent):
|
||||
"""Start a travel agent REST server."""
|
||||
agent_map = {
|
||||
"weather": ("travel_agents.weather_agent", 10510),
|
||||
"flight": ("travel_agents.flight_agent", 10520),
|
||||
"currency": ("travel_agents.currency_agent", 10530),
|
||||
}
|
||||
|
||||
if agent not in agent_map:
|
||||
print(f"Error: Unknown agent '{agent}'")
|
||||
print(f"Available agents: {', '.join(agent_map.keys())}")
|
||||
return
|
||||
|
||||
module_name, default_port = agent_map[agent]
|
||||
|
||||
if port == 8000:
|
||||
port = default_port
|
||||
|
||||
print(f"Starting {agent} agent REST server on {host}:{port}")
|
||||
|
||||
if agent == "weather":
|
||||
from travel_agents.weather_agent import start_server
|
||||
|
||||
start_server(host=host, port=port)
|
||||
elif agent == "flight":
|
||||
from travel_agents.flight_agent import start_server
|
||||
|
||||
start_server(host=host, port=port)
|
||||
elif agent == "currency":
|
||||
from travel_agents.currency_agent import start_server
|
||||
|
||||
start_server(host=host, port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from . import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class ChatCompletionRequest(BaseModel):
|
||||
model: str
|
||||
messages: List[ChatMessage]
|
||||
temperature: Optional[float] = 1.0
|
||||
max_tokens: Optional[int] = None
|
||||
top_p: Optional[float] = 1.0
|
||||
frequency_penalty: Optional[float] = 0.0
|
||||
presence_penalty: Optional[float] = 0.0
|
||||
stream: Optional[bool] = False
|
||||
stop: Optional[List[str]] = None
|
||||
|
||||
|
||||
class ChatCompletionResponse(BaseModel):
|
||||
id: str
|
||||
object: str = "chat.completion"
|
||||
created: int
|
||||
model: str
|
||||
choices: List[Dict[str, Any]]
|
||||
usage: Dict[str, int]
|
||||
|
||||
|
||||
class ChatCompletionStreamResponse(BaseModel):
|
||||
id: str
|
||||
object: str = "chat.completion.chunk"
|
||||
created: int
|
||||
model: str
|
||||
choices: List[Dict[str, Any]]
|
||||
|
|
@ -1,584 +0,0 @@
|
|||
import json
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from openai import AsyncOpenAI
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
import uvicorn
|
||||
import httpx
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
from .api import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionStreamResponse,
|
||||
)
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - [CURRENCY_AGENT] - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration for archgw LLM gateway
|
||||
LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1")
|
||||
CURRENCY_MODEL = "openai/gpt-4o"
|
||||
CURRENCY_EXTRACTION_MODEL = "openai/gpt-4o-mini"
|
||||
|
||||
# HTTP client for API calls
|
||||
http_client = httpx.AsyncClient(timeout=10.0)
|
||||
|
||||
# Initialize OpenAI client for archgw
|
||||
archgw_client = AsyncOpenAI(
|
||||
base_url=LLM_GATEWAY_ENDPOINT,
|
||||
api_key="EMPTY",
|
||||
)
|
||||
|
||||
# System prompt for currency agent
|
||||
SYSTEM_PROMPT = """You are a professional travel planner assistant. Your role is to provide accurate, clear, and helpful information about weather, flights, and currency exchange based on the structured data provided to you.
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
|
||||
1. DATA STRUCTURE:
|
||||
|
||||
WEATHER DATA:
|
||||
- You will receive weather data as JSON in a system message
|
||||
- The data contains a "location" field (string) and a "forecast" array
|
||||
- Each forecast entry has: date, day_name, temperature_c, temperature_f, temperature_max_c, temperature_min_c, condition, sunrise, sunset
|
||||
- Some fields may be null/None - handle these gracefully
|
||||
|
||||
FLIGHT DATA:
|
||||
- You will receive flight information in a system message
|
||||
- Flight data includes: airline, flight number, departure time, arrival time, origin airport, destination airport, aircraft type, status, gate, terminal
|
||||
- Information may include both scheduled and estimated times
|
||||
- Some fields may be unavailable - handle these gracefully
|
||||
|
||||
CURRENCY DATA:
|
||||
- You will receive currency exchange data as JSON in a system message
|
||||
- The data contains: from_currency, to_currency, rate, date, and optionally original_amount and converted_amount
|
||||
- Some fields may be null/None - handle these gracefully
|
||||
|
||||
2. WEATHER HANDLING:
|
||||
- For single-day queries: Use temperature_c/temperature_f (current/primary temperature)
|
||||
- For multi-day forecasts: Use temperature_max_c and temperature_min_c when available
|
||||
- Always provide temperatures in both Celsius and Fahrenheit when available
|
||||
- If temperature is null, say "temperature data unavailable" rather than making up numbers
|
||||
- Use exact condition descriptions provided (e.g., "Clear sky", "Rainy", "Partly Cloudy")
|
||||
- Add helpful context when appropriate (e.g., "perfect for outdoor activities" for clear skies)
|
||||
|
||||
3. FLIGHT HANDLING:
|
||||
- Present flight information clearly with airline name and flight number
|
||||
- Include departure and arrival times with time zones when provided
|
||||
- Mention origin and destination airports with their codes
|
||||
- Include gate and terminal information when available
|
||||
- Note aircraft type if relevant to the query
|
||||
- Highlight any status updates (delays, early arrivals, etc.)
|
||||
- For multiple flights, list them in chronological order by departure time
|
||||
- If specific details are missing, acknowledge this rather than inventing information
|
||||
|
||||
4. CURRENCY HANDLING:
|
||||
- Present exchange rates clearly with both currency codes and names when helpful
|
||||
- Include the date of the exchange rate
|
||||
- If an amount was provided, show both the original and converted amounts
|
||||
- Use clear formatting (e.g., "100 USD = 92.50 EUR" or "1 USD = 0.925 EUR")
|
||||
- If rate data is unavailable, acknowledge this politely
|
||||
|
||||
5. MULTI-PART QUERIES:
|
||||
- Users may ask about weather, flights, and currency in one message
|
||||
- Answer ALL parts of the query that you have data for
|
||||
- Organize your response logically - typically weather first, then flights, then currency, or based on the query order
|
||||
- Provide complete information for each topic without mentioning other agents
|
||||
- If you receive data for only one topic but the user asked about multiple, answer what you can with the provided data
|
||||
|
||||
6. ERROR HANDLING:
|
||||
- If weather forecast contains an "error" field, acknowledge the issue politely
|
||||
- If temperature or condition is null/None, mention that specific data is unavailable
|
||||
- If flight details are incomplete, state which information is unavailable
|
||||
- If currency rate is unavailable, mention that specific data is unavailable
|
||||
- Never invent or guess weather, flight, or currency data - only use what's provided
|
||||
- If location couldn't be determined, acknowledge this but still provide available data
|
||||
|
||||
7. RESPONSE FORMAT:
|
||||
|
||||
For Weather:
|
||||
- Single-day queries: Provide current conditions, temperature, and condition
|
||||
- Multi-day forecasts: List each day with date, day name, high/low temps, and condition
|
||||
- Include sunrise/sunset times when available and relevant
|
||||
|
||||
For Flights:
|
||||
- List flights with clear numbering or bullet points
|
||||
- Include key details: airline, flight number, departure/arrival times, airports
|
||||
- Add gate, terminal, and status information when available
|
||||
- For multiple flights, organize chronologically
|
||||
|
||||
For Currency:
|
||||
- Show exchange rate clearly: "1 [FROM] = [RATE] [TO]"
|
||||
- If amount provided: "[AMOUNT] [FROM] = [CONVERTED] [TO]"
|
||||
- Include the date of the exchange rate
|
||||
|
||||
General:
|
||||
- Use natural, conversational language
|
||||
- Be concise but complete
|
||||
- Format dates and times clearly
|
||||
- Use bullet points or numbered lists for clarity
|
||||
|
||||
8. LOCATION HANDLING:
|
||||
- Always mention location names from the data
|
||||
- For flights, clearly state origin and destination cities/airports
|
||||
- For currency, use country/city context to resolve currency references
|
||||
- If locations differ from what the user asked, acknowledge this politely
|
||||
|
||||
9. RESPONSE STYLE:
|
||||
- Be friendly and professional
|
||||
- Use natural language, not technical jargon
|
||||
- Provide information in a logical, easy-to-read format
|
||||
- When answering multi-part queries, create a cohesive response that addresses all aspects
|
||||
|
||||
Remember: Only use the data provided. Never fabricate weather, flight, or currency information. If data is missing, clearly state what's unavailable. Answer all parts of the user's query that you have data for."""
|
||||
|
||||
|
||||
CURRENCY_EXTRACTION_PROMPT = """You are a currency information extraction assistant. Your ONLY job is to extract currency-related information from user messages and convert it to standard 3-letter ISO currency codes.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Extract currency codes (3-letter ISO codes like USD, EUR, GBP, JPY, PKR, etc.) from the message AND conversation context
|
||||
2. Extract any mentioned amounts or numbers that might be currency amounts
|
||||
3. PAY ATTENTION TO CONVERSATION CONTEXT:
|
||||
- If previous messages mention a country/city, use that context to resolve pronouns like "their", "that country", "there", etc.
|
||||
- Example: If previous message was "What's the weather in Lahore, Pakistan?" and current message is "What is their currency exchange rate with USD?", then "their" = Pakistan = PKR
|
||||
- Look for country names in the conversation history to infer currencies
|
||||
4. If country names or regions are mentioned (in current message OR conversation context), convert them to their standard currency codes:
|
||||
- United States/USA/US → USD
|
||||
- Europe/Eurozone/France/Germany/Italy/Spain/etc. → EUR
|
||||
- United Kingdom/UK/Britain → GBP
|
||||
- Japan → JPY
|
||||
- China → CNY
|
||||
- India → INR
|
||||
- Pakistan → PKR
|
||||
- Australia → AUD
|
||||
- Canada → CAD
|
||||
- Switzerland → CHF
|
||||
- South Korea → KRW
|
||||
- Singapore → SGD
|
||||
- Hong Kong → HKD
|
||||
- Brazil → BRL
|
||||
- Mexico → MXN
|
||||
- And any other countries you know the currency for
|
||||
5. Determine the FROM currency (source) and TO currency (target) based on context:
|
||||
- "from X to Y" → from_currency=X, to_currency=Y
|
||||
- "X to Y" → from_currency=X, to_currency=Y
|
||||
- "convert X to Y" → from_currency=X, to_currency=Y
|
||||
- "X in Y" → from_currency=X, to_currency=Y
|
||||
- "rate for X" or "X rate" → to_currency=X (assume USD as base)
|
||||
- "their currency with USD" or "their currency to USD" → from_currency=country_from_context, to_currency=USD
|
||||
- "X dollars/euros/pounds/etc." → from_currency=X
|
||||
6. If only one currency is mentioned, determine if it's the source or target based on context
|
||||
7. ALWAYS return currency codes, never country names in the currency fields
|
||||
8. Return your response as a JSON object with the following structure:
|
||||
{
|
||||
"from_currency": "USD" or null,
|
||||
"to_currency": "EUR" or null,
|
||||
"amount": 100.0 or null
|
||||
}
|
||||
|
||||
9. If you cannot determine a currency, use null for that field
|
||||
10. Use standard 3-letter ISO currency codes ONLY
|
||||
11. Ignore error messages, HTML tags, and assistant responses
|
||||
12. Extract from the most recent user message BUT use conversation context to resolve references
|
||||
13. Default behavior: If only one currency is mentioned without context, assume it's the target currency and use USD as the source
|
||||
|
||||
Examples with context:
|
||||
- Conversation: "What's the weather in Lahore, Pakistan?" → Current: "What is their currency exchange rate with USD?" → {"from_currency": "PKR", "to_currency": "USD", "amount": null}
|
||||
- Conversation: "Tell me about Tokyo" → Current: "What's their currency rate?" → {"from_currency": "JPY", "to_currency": "USD", "amount": null}
|
||||
- "What's the exchange rate from USD to EUR?" → {"from_currency": "USD", "to_currency": "EUR", "amount": null}
|
||||
- "Convert 100 dollars to euros" → {"from_currency": "USD", "to_currency": "EUR", "amount": 100.0}
|
||||
- "How much is 50 GBP in Japanese yen?" → {"from_currency": "GBP", "to_currency": "JPY", "amount": 50.0}
|
||||
- "What's the rate for euros?" → {"from_currency": "USD", "to_currency": "EUR", "amount": null}
|
||||
- "Convert money from United States to France" → {"from_currency": "USD", "to_currency": "EUR", "amount": null}
|
||||
- "100 pounds to dollars" → {"from_currency": "GBP", "to_currency": "USD", "amount": 100.0}
|
||||
|
||||
Now extract the currency information from this message, considering the conversation context:"""
|
||||
|
||||
|
||||
async def extract_currency_info_from_messages(messages):
|
||||
"""Extract currency information from user messages using LLM, considering conversation context."""
|
||||
# Get all messages for context (both user and assistant)
|
||||
conversation_context = []
|
||||
for msg in messages:
|
||||
# Skip error messages and HTML tags
|
||||
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})
|
||||
|
||||
# Get the most recent user message
|
||||
user_messages = [msg for msg in messages if msg.role == "user"]
|
||||
|
||||
if not user_messages:
|
||||
logger.warning("No user messages found")
|
||||
return {"from_currency": "USD", "to_currency": "EUR", "amount": None}
|
||||
|
||||
# Get the most recent user message (skip error messages and HTML tags)
|
||||
user_content = None
|
||||
for msg in reversed(user_messages):
|
||||
content = msg.content.strip()
|
||||
# Skip messages with error patterns or HTML tags
|
||||
content_lower = content.lower()
|
||||
if any(
|
||||
pattern in content_lower
|
||||
for pattern in [
|
||||
"<",
|
||||
">",
|
||||
"assistant:",
|
||||
"error:",
|
||||
"i apologize",
|
||||
"i'm having trouble",
|
||||
]
|
||||
):
|
||||
continue
|
||||
user_content = content
|
||||
break
|
||||
|
||||
if not user_content:
|
||||
logger.warning("No valid user message found")
|
||||
return {"from_currency": "USD", "to_currency": "EUR", "amount": None}
|
||||
|
||||
try:
|
||||
logger.info(f"Extracting currency info from user message: {user_content[:200]}")
|
||||
logger.info(
|
||||
f"Using conversation context with {len(conversation_context)} messages"
|
||||
)
|
||||
|
||||
llm_messages = [{"role": "system", "content": CURRENCY_EXTRACTION_PROMPT}]
|
||||
|
||||
context_messages = (
|
||||
conversation_context[-10:]
|
||||
if len(conversation_context) > 10
|
||||
else conversation_context
|
||||
)
|
||||
for msg in context_messages:
|
||||
llm_messages.append({"role": msg["role"], "content": msg["content"]})
|
||||
|
||||
response = await archgw_client.chat.completions.create(
|
||||
model=CURRENCY_EXTRACTION_MODEL,
|
||||
messages=llm_messages,
|
||||
temperature=0.1,
|
||||
max_tokens=200,
|
||||
)
|
||||
|
||||
extracted_text = response.choices[0].message.content.strip()
|
||||
|
||||
try:
|
||||
if "```json" in extracted_text:
|
||||
extracted_text = (
|
||||
extracted_text.split("```json")[1].split("```")[0].strip()
|
||||
)
|
||||
elif "```" in extracted_text:
|
||||
extracted_text = extracted_text.split("```")[1].split("```")[0].strip()
|
||||
|
||||
currency_info = json.loads(extracted_text)
|
||||
|
||||
from_currency = currency_info.get("from_currency")
|
||||
to_currency = currency_info.get("to_currency")
|
||||
amount = currency_info.get("amount")
|
||||
|
||||
if not from_currency:
|
||||
from_currency = "USD"
|
||||
if not to_currency:
|
||||
to_currency = "EUR"
|
||||
|
||||
result = {
|
||||
"from_currency": from_currency,
|
||||
"to_currency": to_currency,
|
||||
"amount": amount,
|
||||
}
|
||||
|
||||
logger.info(f"LLM extracted currency info: {result}")
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(
|
||||
f"Failed to parse JSON from LLM response: {extracted_text}, error: {e}"
|
||||
)
|
||||
return {"from_currency": "USD", "to_currency": "EUR", "amount": None}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting currency info with LLM: {e}, using defaults")
|
||||
return {"from_currency": "USD", "to_currency": "EUR", "amount": None}
|
||||
|
||||
|
||||
async def get_currency_exchange_rate(
|
||||
from_currency: str, to_currency: str
|
||||
) -> Optional[dict]:
|
||||
"""Get currency exchange rate between two currencies using Frankfurter API.
|
||||
|
||||
Uses the Frankfurter API (api.frankfurter.dev) which provides free, open-source
|
||||
currency data tracking reference exchange rates published by institutional sources.
|
||||
No API keys required.
|
||||
|
||||
Args:
|
||||
from_currency: Base currency code (e.g., "USD", "EUR")
|
||||
to_currency: Target currency code (e.g., "EUR", "GBP")
|
||||
|
||||
Returns:
|
||||
Dictionary with exchange rate data or None if error occurs
|
||||
"""
|
||||
try:
|
||||
url = f"https://api.frankfurter.dev/v1/latest?base={from_currency}&symbols={to_currency}"
|
||||
response = await http_client.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
f"Currency API returned status {response.status_code} for {from_currency} to {to_currency}"
|
||||
)
|
||||
return None
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "rates" not in data:
|
||||
logger.warning(f"Invalid API response structure: missing 'rates' field")
|
||||
return None
|
||||
|
||||
if to_currency not in data["rates"]:
|
||||
logger.warning(
|
||||
f"Currency {to_currency} not found in API response for base {from_currency}"
|
||||
)
|
||||
return None
|
||||
|
||||
return {
|
||||
"from_currency": from_currency,
|
||||
"to_currency": to_currency,
|
||||
"rate": data["rates"][to_currency],
|
||||
"date": data.get("date"),
|
||||
"base": data.get("base"),
|
||||
}
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(
|
||||
f"HTTP error fetching currency rate from {from_currency} to {to_currency}: {e}"
|
||||
)
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse JSON response from currency API: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error fetching currency rate: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# FastAPI app for REST server
|
||||
app = FastAPI(title="Currency Exchange Agent", version="1.0.0")
|
||||
|
||||
|
||||
async def prepare_currency_messages(request_body: ChatCompletionRequest):
|
||||
"""Prepare messages with currency exchange data."""
|
||||
# Extract currency information from conversation using LLM
|
||||
currency_info = await extract_currency_info_from_messages(request_body.messages)
|
||||
|
||||
from_currency = currency_info["from_currency"]
|
||||
to_currency = currency_info["to_currency"]
|
||||
amount = currency_info.get("amount")
|
||||
|
||||
# Get currency exchange rate
|
||||
rate_data = await get_currency_exchange_rate(from_currency, to_currency)
|
||||
|
||||
if rate_data:
|
||||
currency_data = {
|
||||
"from_currency": rate_data["from_currency"],
|
||||
"to_currency": rate_data["to_currency"],
|
||||
"rate": rate_data["rate"],
|
||||
"date": rate_data.get("date"),
|
||||
}
|
||||
|
||||
# If an amount was mentioned, calculate the conversion
|
||||
if amount is not None:
|
||||
converted_amount = amount * rate_data["rate"]
|
||||
currency_data["original_amount"] = amount
|
||||
currency_data["converted_amount"] = round(converted_amount, 2)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Could not fetch currency rate for {from_currency} to {to_currency}"
|
||||
)
|
||||
currency_data = {
|
||||
"from_currency": from_currency,
|
||||
"to_currency": to_currency,
|
||||
"rate": None,
|
||||
"error": "Could not retrieve exchange rate",
|
||||
}
|
||||
|
||||
# Create system message with currency data
|
||||
currency_context = f"""
|
||||
Current currency exchange data:
|
||||
|
||||
{json.dumps(currency_data, indent=2)}
|
||||
|
||||
Use this data to answer the user's currency exchange query.
|
||||
"""
|
||||
|
||||
response_messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "assistant", "content": currency_context},
|
||||
]
|
||||
|
||||
# Add conversation history
|
||||
for msg in request_body.messages:
|
||||
response_messages.append({"role": msg.role, "content": msg.content})
|
||||
|
||||
return response_messages
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completion_http(request: Request, request_body: ChatCompletionRequest):
|
||||
"""HTTP endpoint for chat completions with streaming support."""
|
||||
logger.info(f"Received currency request with {len(request_body.messages)} messages")
|
||||
|
||||
traceparent_header = request.headers.get("traceparent")
|
||||
|
||||
if traceparent_header:
|
||||
logger.info(f"Received traceparent header: {traceparent_header}")
|
||||
|
||||
return StreamingResponse(
|
||||
stream_chat_completions(request_body, traceparent_header),
|
||||
media_type="text/plain",
|
||||
headers={
|
||||
"content-type": "text/event-stream",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def stream_chat_completions(
|
||||
request_body: ChatCompletionRequest, traceparent_header: str = None
|
||||
):
|
||||
"""Generate streaming chat completions."""
|
||||
# Prepare messages with currency exchange data
|
||||
response_messages = await prepare_currency_messages(request_body)
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate currency response"
|
||||
)
|
||||
|
||||
# Prepare extra headers
|
||||
extra_headers = {"x-envoy-max-retries": "3"}
|
||||
if traceparent_header:
|
||||
extra_headers["traceparent"] = traceparent_header
|
||||
|
||||
response_stream = await archgw_client.chat.completions.create(
|
||||
model=CURRENCY_MODEL,
|
||||
messages=response_messages,
|
||||
temperature=request_body.temperature or 0.7,
|
||||
max_tokens=request_body.max_tokens or 1000,
|
||||
stream=True,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
|
||||
created_time = int(time.time())
|
||||
collected_content = []
|
||||
|
||||
async for chunk in response_stream:
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
collected_content.append(content)
|
||||
|
||||
stream_chunk = ChatCompletionStreamResponse(
|
||||
id=completion_id,
|
||||
created=created_time,
|
||||
model=request_body.model,
|
||||
choices=[
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {"content": content},
|
||||
"finish_reason": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
yield f"data: {stream_chunk.model_dump_json()}\n\n"
|
||||
|
||||
full_response = "".join(collected_content)
|
||||
updated_history = [{"role": "assistant", "content": full_response}]
|
||||
|
||||
final_chunk = ChatCompletionStreamResponse(
|
||||
id=completion_id,
|
||||
created=created_time,
|
||||
model=request_body.model,
|
||||
choices=[
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": json.dumps(updated_history),
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
yield f"data: {final_chunk.model_dump_json()}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating currency response: {e}")
|
||||
|
||||
error_chunk = ChatCompletionStreamResponse(
|
||||
id=f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
||||
created=int(time.time()),
|
||||
model=request_body.model,
|
||||
choices=[
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"content": "I apologize, but I'm having trouble generating a currency exchange response right now. Please try again."
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy", "agent": "currency_exchange"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=10530)
|
||||
|
||||
|
||||
def start_server(host: str = "localhost", port: int = 10530):
|
||||
"""Start the currency agent server."""
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_config={
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "%(asctime)s - [CURRENCY_AGENT] - %(levelname)s - %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["default"],
|
||||
},
|
||||
},
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -12,12 +12,7 @@ from datetime import datetime, timedelta
|
|||
import httpx
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
from .api import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
ChatCompletionStreamResponse,
|
||||
)
|
||||
from opentelemetry.propagate import extract, inject
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
|
|
@ -26,458 +21,16 @@ logging.basicConfig(
|
|||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration for archgw LLM gateway
|
||||
LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1")
|
||||
|
||||
# Configuration for plano LLM gateway
|
||||
LLM_GATEWAY_ENDPOINT = os.getenv(
|
||||
"LLM_GATEWAY_ENDPOINT", "http://host.docker.internal:12001/v1"
|
||||
)
|
||||
WEATHER_MODEL = "openai/gpt-4o"
|
||||
LOCATION_MODEL = "openai/gpt-4o-mini"
|
||||
|
||||
# HTTP client for API calls
|
||||
http_client = httpx.AsyncClient(timeout=10.0)
|
||||
|
||||
# System prompt for weather agent
|
||||
SYSTEM_PROMPT = """You are a professional travel planner assistant. Your role is to provide accurate, clear, and helpful information about weather and flights based on the structured data provided to you.
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
|
||||
1. DATA STRUCTURE:
|
||||
|
||||
WEATHER DATA:
|
||||
- You will receive weather data as JSON in a system message
|
||||
- The data contains a "location" field (string) and a "forecast" array
|
||||
- Each forecast entry has: date, day_name, temperature_c, temperature_f, temperature_max_c, temperature_min_c, condition, sunrise, sunset
|
||||
- Some fields may be null/None - handle these gracefully
|
||||
|
||||
FLIGHT DATA:
|
||||
- You will receive flight information in a system message
|
||||
- Flight data includes: airline, flight number, departure time, arrival time, origin airport, destination airport, aircraft type, status, gate, terminal
|
||||
- Information may include both scheduled and estimated times
|
||||
- Some fields may be unavailable - handle these gracefully
|
||||
|
||||
2. WEATHER HANDLING:
|
||||
- For single-day queries: Use temperature_c/temperature_f (current/primary temperature)
|
||||
- For multi-day forecasts: Use temperature_max_c and temperature_min_c when available
|
||||
- Always provide temperatures in both Celsius and Fahrenheit when available
|
||||
- If temperature is null, say "temperature data unavailable" rather than making up numbers
|
||||
- Use exact condition descriptions provided (e.g., "Clear sky", "Rainy", "Partly Cloudy")
|
||||
- Add helpful context when appropriate (e.g., "perfect for outdoor activities" for clear skies)
|
||||
|
||||
3. FLIGHT HANDLING:
|
||||
- Present flight information clearly with airline name and flight number
|
||||
- Include departure and arrival times with time zones when provided
|
||||
- Mention origin and destination airports with their codes
|
||||
- Include gate and terminal information when available
|
||||
- Note aircraft type if relevant to the query
|
||||
- Highlight any status updates (delays, early arrivals, etc.)
|
||||
- For multiple flights, list them in chronological order by departure time
|
||||
- If specific details are missing, acknowledge this rather than inventing information
|
||||
|
||||
4. MULTI-PART QUERIES:
|
||||
- Users may ask about both weather and flights in one message
|
||||
- Answer ALL parts of the query that you have data for
|
||||
- Organize your response logically - typically weather first, then flights, or vice versa based on the query
|
||||
- Provide complete information for each topic without mentioning other agents
|
||||
- If you receive data for only one topic but the user asked about multiple, answer what you can with the provided data
|
||||
|
||||
5. ERROR HANDLING:
|
||||
- If weather forecast contains an "error" field, acknowledge the issue politely
|
||||
- If temperature or condition is null/None, mention that specific data is unavailable
|
||||
- If flight details are incomplete, state which information is unavailable
|
||||
- Never invent or guess weather or flight data - only use what's provided
|
||||
- If location couldn't be determined, acknowledge this but still provide available data
|
||||
|
||||
6. RESPONSE FORMAT:
|
||||
|
||||
For Weather:
|
||||
- Single-day queries: Provide current conditions, temperature, and condition
|
||||
- Multi-day forecasts: List each day with date, day name, high/low temps, and condition
|
||||
- Include sunrise/sunset times when available and relevant
|
||||
|
||||
For Flights:
|
||||
- List flights with clear numbering or bullet points
|
||||
- Include key details: airline, flight number, departure/arrival times, airports
|
||||
- Add gate, terminal, and status information when available
|
||||
- For multiple flights, organize chronologically
|
||||
|
||||
General:
|
||||
- Use natural, conversational language
|
||||
- Be concise but complete
|
||||
- Format dates and times clearly
|
||||
- Use bullet points or numbered lists for clarity
|
||||
|
||||
7. LOCATION HANDLING:
|
||||
- Always mention location names from the data
|
||||
- For flights, clearly state origin and destination cities/airports
|
||||
- If locations differ from what the user asked, acknowledge this politely
|
||||
|
||||
8. RESPONSE STYLE:
|
||||
- Be friendly and professional
|
||||
- Use natural language, not technical jargon
|
||||
- Provide information in a logical, easy-to-read format
|
||||
- When answering multi-part queries, create a cohesive response that addresses all aspects
|
||||
|
||||
Remember: Only use the data provided. Never fabricate weather or flight information. If data is missing, clearly state what's unavailable. Answer all parts of the user's query that you have data for."""
|
||||
|
||||
|
||||
async def geocode_city(city: str) -> Optional[dict]:
|
||||
"""Geocode a city name to latitude and longitude using Open-Meteo API."""
|
||||
try:
|
||||
url = f"https://geocoding-api.open-meteo.com/v1/search?name={quote(city)}&count=1&language=en&format=json"
|
||||
response = await http_client.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
f"Geocoding API returned status {response.status_code} for city: {city}"
|
||||
)
|
||||
return None
|
||||
|
||||
data = response.json()
|
||||
|
||||
if not data.get("results") or len(data["results"]) == 0:
|
||||
logger.warning(f"No geocoding results found for city: {city}")
|
||||
return None
|
||||
|
||||
result = data["results"][0]
|
||||
return {
|
||||
"latitude": result["latitude"],
|
||||
"longitude": result["longitude"],
|
||||
"name": result.get("name", city),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error geocoding city {city}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_live_weather(
|
||||
latitude: float, longitude: float, days: int = 1
|
||||
) -> Optional[dict]:
|
||||
"""Get live weather data from Open-Meteo API."""
|
||||
try:
|
||||
forecast_days = min(days, 16)
|
||||
|
||||
url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={latitude}&"
|
||||
f"longitude={longitude}&"
|
||||
f"current=temperature_2m&"
|
||||
f"hourly=temperature_2m&"
|
||||
f"daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code&"
|
||||
f"forecast_days={forecast_days}&"
|
||||
f"timezone=auto"
|
||||
)
|
||||
|
||||
response = await http_client.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Weather API returned status {response.status_code}")
|
||||
return None
|
||||
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching weather data: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def weather_code_to_condition(weather_code: int) -> str:
|
||||
"""Convert WMO weather code to human-readable condition."""
|
||||
# WMO Weather interpretation codes (WW)
|
||||
if weather_code == 0:
|
||||
return "Clear sky"
|
||||
elif weather_code in [1, 2, 3]:
|
||||
return "Partly Cloudy"
|
||||
elif weather_code in [45, 48]:
|
||||
return "Foggy"
|
||||
elif weather_code in [51, 53, 55, 56, 57]:
|
||||
return "Drizzle"
|
||||
elif weather_code in [61, 63, 65, 66, 67]:
|
||||
return "Rainy"
|
||||
elif weather_code in [71, 73, 75, 77]:
|
||||
return "Snowy"
|
||||
elif weather_code in [80, 81, 82]:
|
||||
return "Rainy"
|
||||
elif weather_code in [85, 86]:
|
||||
return "Snowy"
|
||||
elif weather_code in [95, 96, 99]:
|
||||
return "Stormy"
|
||||
else:
|
||||
return "Cloudy"
|
||||
|
||||
|
||||
async def get_weather_data(location: str, days: int = 1):
|
||||
"""Get live weather data for a location using Open-Meteo API."""
|
||||
geocode_result = await geocode_city(location)
|
||||
|
||||
if not geocode_result:
|
||||
logger.warning(f"Could not geocode location: {location}, using fallback")
|
||||
geocode_result = await geocode_city("New York")
|
||||
if not geocode_result:
|
||||
return {
|
||||
"location": location,
|
||||
"forecast": [
|
||||
{
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"day_name": datetime.now().strftime("%A"),
|
||||
"temperature_c": None,
|
||||
"temperature_f": None,
|
||||
"condition": "Unknown",
|
||||
"error": "Could not retrieve weather data",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
location_name = geocode_result["name"]
|
||||
latitude = geocode_result["latitude"]
|
||||
longitude = geocode_result["longitude"]
|
||||
|
||||
weather_data = await get_live_weather(latitude, longitude, days)
|
||||
|
||||
if not weather_data:
|
||||
logger.warning("Could not fetch weather data for requested location")
|
||||
return {
|
||||
"location": location_name,
|
||||
"forecast": [
|
||||
{
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"day_name": datetime.now().strftime("%A"),
|
||||
"temperature_c": None,
|
||||
"temperature_f": None,
|
||||
"condition": "Unknown",
|
||||
"error": "Could not retrieve weather data",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
current_temp = weather_data.get("current", {}).get("temperature_2m")
|
||||
daily_data = weather_data.get("daily", {})
|
||||
|
||||
forecast = []
|
||||
for i in range(min(days, len(daily_data.get("time", [])))):
|
||||
date_str = daily_data["time"][i]
|
||||
date_obj = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
|
||||
temp_max = (
|
||||
daily_data.get("temperature_2m_max", [None])[i]
|
||||
if i < len(daily_data.get("temperature_2m_max", []))
|
||||
else None
|
||||
)
|
||||
temp_min = (
|
||||
daily_data.get("temperature_2m_min", [None])[i]
|
||||
if i < len(daily_data.get("temperature_2m_min", []))
|
||||
else None
|
||||
)
|
||||
weather_code = (
|
||||
daily_data.get("weather_code", [0])[i]
|
||||
if i < len(daily_data.get("weather_code", []))
|
||||
else 0
|
||||
)
|
||||
sunrise = (
|
||||
daily_data.get("sunrise", [None])[i]
|
||||
if i < len(daily_data.get("sunrise", []))
|
||||
else None
|
||||
)
|
||||
sunset = (
|
||||
daily_data.get("sunset", [None])[i]
|
||||
if i < len(daily_data.get("sunset", []))
|
||||
else None
|
||||
)
|
||||
|
||||
temp_c = (
|
||||
temp_max if temp_max is not None else (current_temp if i == 0 else temp_min)
|
||||
)
|
||||
|
||||
day_info = {
|
||||
"date": date_str.split("T")[0],
|
||||
"day_name": date_obj.strftime("%A"),
|
||||
"temperature_c": round(temp_c, 1) if temp_c is not None else None,
|
||||
"temperature_f": (
|
||||
round(temp_c * 9 / 5 + 32, 1) if temp_c is not None else None
|
||||
),
|
||||
"temperature_max_c": round(temp_max, 1) if temp_max is not None else None,
|
||||
"temperature_min_c": round(temp_min, 1) if temp_min is not None else None,
|
||||
"condition": weather_code_to_condition(weather_code),
|
||||
"sunrise": sunrise.split("T")[1] if sunrise else None,
|
||||
"sunset": sunset.split("T")[1] if sunset else None,
|
||||
}
|
||||
forecast.append(day_info)
|
||||
|
||||
return {"location": location_name, "forecast": forecast}
|
||||
|
||||
|
||||
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 associated with WEATHER questions - nothing else
|
||||
2. Return just the location name in plain text (e.g., "London", "New York", "Paris, France")
|
||||
3. **MULTI-PART QUERY HANDLING**: If the user mentions multiple locations in a multi-part query, extract ONLY the location mentioned in the WEATHER part
|
||||
- Look for patterns like "weather in [location]", "forecast for [location]", "weather [location]"
|
||||
- The location that appears WITH "weather" keywords is the weather location
|
||||
- Example: "What's the weather in Seattle, and what is one flight that goes direct to Atlanta?" → Extract "Seattle" (appears with "weather in")
|
||||
- Example: "What is the weather in Atlanta and what flight goes from Detroit to Atlanta?" → Extract "Atlanta" (appears with "weather in", even though Atlanta also appears in flight part)
|
||||
- Example: "Weather in London and flights to Paris" → Extract "London" (weather location)
|
||||
- Example: "What flight goes from Detroit to Atlanta and what's the weather in Atlanta?" → Extract "Atlanta" (appears with "weather in")
|
||||
4. Look for patterns like "weather in [location]", "forecast for [location]", "weather [location]", "temperature in [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"
|
||||
- "What's the weather in Seattle, and what is one flight that goes direct to Atlanta?" → "Seattle" (appears with "weather in")
|
||||
- "What is the weather in Atlanta and what flight goes from Detroit to Atlanta?" → "Atlanta" (appears with "weather in")
|
||||
- "Weather in Istanbul and flights to Seattle" → "Istanbul" (weather location)
|
||||
- "What flight goes from Detroit to Atlanta and what's the weather in Atlanta?" → "Atlanta" (appears with "weather in")
|
||||
- "I'm going to Seattle" → "Seattle" (if context suggests weather query)
|
||||
- "What's happening?" → "NOT_FOUND"
|
||||
|
||||
Now extract the WEATHER location from this message:"""
|
||||
|
||||
|
||||
async def extract_location_from_messages(messages):
|
||||
"""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 [
|
||||
'"role": "assistant"',
|
||||
'"role":"assistant"',
|
||||
"error:",
|
||||
]
|
||||
):
|
||||
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 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=llm_messages,
|
||||
temperature=0.1,
|
||||
max_tokens=50,
|
||||
)
|
||||
|
||||
location = response.choices[0].message.content.strip()
|
||||
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 weather location: {location}")
|
||||
return location
|
||||
|
||||
except Exception as e:
|
||||
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"
|
||||
|
||||
|
||||
# Initialize OpenAI client for archgw
|
||||
archgw_client = AsyncOpenAI(
|
||||
# Initialize OpenAI client for plano
|
||||
openai_client_via_plano = AsyncOpenAI(
|
||||
base_url=LLM_GATEWAY_ENDPOINT,
|
||||
api_key="EMPTY",
|
||||
)
|
||||
|
|
@ -485,60 +38,241 @@ archgw_client = AsyncOpenAI(
|
|||
# FastAPI app for REST server
|
||||
app = FastAPI(title="Weather Forecast Agent", version="1.0.0")
|
||||
|
||||
# HTTP client for API calls
|
||||
http_client = httpx.AsyncClient(timeout=10.0)
|
||||
|
||||
async def prepare_weather_messages(request_body: ChatCompletionRequest):
|
||||
"""Prepare messages with weather data."""
|
||||
# Extract location from conversation using LLM
|
||||
location = await extract_location_from_messages(request_body.messages)
|
||||
|
||||
# Determine if they want forecast (multi-day)
|
||||
last_user_msg = ""
|
||||
for msg in reversed(request_body.messages):
|
||||
if msg.role == "user":
|
||||
last_user_msg = msg.content.lower()
|
||||
break
|
||||
# Utility functions
|
||||
def celsius_to_fahrenheit(temp_c: Optional[float]) -> Optional[float]:
|
||||
"""Convert Celsius to Fahrenheit."""
|
||||
return round(temp_c * 9 / 5 + 32, 1) if temp_c is not None else None
|
||||
|
||||
days = 5 if "forecast" in last_user_msg or "week" in last_user_msg else 1
|
||||
|
||||
# Get live weather data
|
||||
weather_data = await get_weather_data(location, days)
|
||||
def get_user_messages(messages: list) -> list:
|
||||
"""Extract user messages from message list."""
|
||||
return [msg for msg in messages if msg.get("role") == "user"]
|
||||
|
||||
# Create system message with weather data
|
||||
weather_context = f"""
|
||||
Current weather data for {weather_data['location']}:
|
||||
|
||||
{json.dumps(weather_data, indent=2)}
|
||||
def get_last_user_content(messages: list) -> str:
|
||||
"""Get the content of the most recent user message."""
|
||||
for msg in reversed(messages):
|
||||
if msg.get("role") == "user":
|
||||
return msg.get("content", "").lower()
|
||||
return ""
|
||||
|
||||
Use this data to answer the user's weather query.
|
||||
"""
|
||||
|
||||
response_messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "assistant", "content": weather_context},
|
||||
]
|
||||
async def get_weather_data(request: Request, messages: list, days: int = 1):
|
||||
"""Extract location from user's conversation and fetch weather data from Open-Meteo API.
|
||||
|
||||
# Add conversation history
|
||||
for msg in request_body.messages:
|
||||
response_messages.append({"role": msg.role, "content": msg.content})
|
||||
This function does two things:
|
||||
1. Uses an LLM to extract the location from the user's message
|
||||
2. Fetches weather data for that location from Open-Meteo
|
||||
|
||||
return response_messages
|
||||
Currently returns only current day weather. Want to add multi-day forecasts?
|
||||
"""
|
||||
|
||||
instructions = """Extract the location for WEATHER queries. Return just the city name.
|
||||
|
||||
Rules:
|
||||
1. For multi-part queries, extract ONLY the location mentioned with weather keywords ("weather in [location]")
|
||||
2. If user says "there" or "that city", it typically refers to the DESTINATION city in travel contexts (not the origin)
|
||||
3. For flight queries with weather, "there" means the destination city where they're traveling TO
|
||||
4. Return plain text (e.g., "London", "New York", "Paris, France")
|
||||
5. If no weather location found, return "NOT_FOUND"
|
||||
|
||||
Examples:
|
||||
- "What's the weather in London?" → "London"
|
||||
- "Flights from Seattle to Atlanta, and show me the weather there" → "Atlanta"
|
||||
- "Can you get me flights from Seattle to Atlanta tomorrow, and also please show me the weather there" → "Atlanta"
|
||||
- "What's the weather in Seattle, and what is one flight that goes direct to Atlanta?" → "Seattle"
|
||||
- User asked about flights to Atlanta, then "what's the weather like there?" → "Atlanta"
|
||||
- "I'm going to Seattle" → "Seattle"
|
||||
- "What's happening?" → "NOT_FOUND"
|
||||
|
||||
Extract location:"""
|
||||
|
||||
try:
|
||||
user_messages = [
|
||||
msg.get("content") for msg in messages if msg.get("role") == "user"
|
||||
]
|
||||
|
||||
if not user_messages:
|
||||
location = "New York"
|
||||
else:
|
||||
ctx = extract(request.headers)
|
||||
extra_headers = {}
|
||||
inject(extra_headers, context=ctx)
|
||||
|
||||
# For location extraction, pass full conversation for context (e.g., "there" = previous destination)
|
||||
response = await openai_client_via_plano.chat.completions.create(
|
||||
model=LOCATION_MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": instructions},
|
||||
*[
|
||||
{"role": msg.get("role"), "content": msg.get("content")}
|
||||
for msg in messages
|
||||
],
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=50,
|
||||
extra_headers=extra_headers if extra_headers else None,
|
||||
)
|
||||
|
||||
location = response.choices[0].message.content.strip().strip("\"'`.,!?")
|
||||
logger.info(f"Location extraction result: '{location}'")
|
||||
|
||||
if not location or location.upper() == "NOT_FOUND":
|
||||
location = "New York"
|
||||
logger.info(f"Location not found, defaulting to: {location}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting location: {e}")
|
||||
location = "New York"
|
||||
|
||||
logger.info(f"Fetching weather for location: '{location}' (days: {days})")
|
||||
|
||||
# Step 2: Fetch weather data for the extracted location
|
||||
try:
|
||||
# Geocode city to get coordinates
|
||||
geocode_url = f"https://geocoding-api.open-meteo.com/v1/search?name={quote(location)}&count=1&language=en&format=json"
|
||||
geocode_response = await http_client.get(geocode_url)
|
||||
|
||||
if geocode_response.status_code != 200 or not geocode_response.json().get(
|
||||
"results"
|
||||
):
|
||||
logger.warning(f"Could not geocode {location}, using New York")
|
||||
location = "New York"
|
||||
geocode_url = f"https://geocoding-api.open-meteo.com/v1/search?name={quote(location)}&count=1&language=en&format=json"
|
||||
geocode_response = await http_client.get(geocode_url)
|
||||
|
||||
geocode_data = geocode_response.json()
|
||||
if not geocode_data.get("results"):
|
||||
return {
|
||||
"location": location,
|
||||
"weather": {
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"day_name": datetime.now().strftime("%A"),
|
||||
"temperature_c": None,
|
||||
"temperature_f": None,
|
||||
"weather_code": None,
|
||||
"error": "Could not retrieve weather data",
|
||||
},
|
||||
}
|
||||
|
||||
result = geocode_data["results"][0]
|
||||
location_name = result.get("name", location)
|
||||
latitude = result["latitude"]
|
||||
longitude = result["longitude"]
|
||||
|
||||
logger.info(
|
||||
f"Geocoded '{location}' to {location_name} ({latitude}, {longitude})"
|
||||
)
|
||||
|
||||
# Get weather forecast
|
||||
weather_url = (
|
||||
f"https://api.open-meteo.com/v1/forecast?"
|
||||
f"latitude={latitude}&longitude={longitude}&"
|
||||
f"current=temperature_2m&"
|
||||
f"daily=sunrise,sunset,temperature_2m_max,temperature_2m_min,weather_code&"
|
||||
f"forecast_days={days}&timezone=auto"
|
||||
)
|
||||
|
||||
weather_response = await http_client.get(weather_url)
|
||||
if weather_response.status_code != 200:
|
||||
return {
|
||||
"location": location_name,
|
||||
"weather": {
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"day_name": datetime.now().strftime("%A"),
|
||||
"temperature_c": None,
|
||||
"temperature_f": None,
|
||||
"weather_code": None,
|
||||
"error": "Could not retrieve weather data",
|
||||
},
|
||||
}
|
||||
|
||||
weather_data = weather_response.json()
|
||||
current_temp = weather_data.get("current", {}).get("temperature_2m")
|
||||
daily = weather_data.get("daily", {})
|
||||
|
||||
# Build forecast for requested number of days
|
||||
forecast = []
|
||||
for i in range(days):
|
||||
date_str = daily["time"][i]
|
||||
date_obj = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
|
||||
temp_max = (
|
||||
daily.get("temperature_2m_max", [])[i]
|
||||
if daily.get("temperature_2m_max")
|
||||
else None
|
||||
)
|
||||
temp_min = (
|
||||
daily.get("temperature_2m_min", [])[i]
|
||||
if daily.get("temperature_2m_min")
|
||||
else None
|
||||
)
|
||||
weather_code = (
|
||||
daily.get("weather_code", [0])[i] if daily.get("weather_code") else 0
|
||||
)
|
||||
sunrise = daily.get("sunrise", [])[i] if daily.get("sunrise") else None
|
||||
sunset = daily.get("sunset", [])[i] if daily.get("sunset") else None
|
||||
|
||||
# Use current temp for today, otherwise use max temp
|
||||
temp_c = (
|
||||
temp_max
|
||||
if temp_max is not None
|
||||
else (current_temp if i == 0 and current_temp else temp_min)
|
||||
)
|
||||
|
||||
forecast.append(
|
||||
{
|
||||
"date": date_str.split("T")[0],
|
||||
"day_name": date_obj.strftime("%A"),
|
||||
"temperature_c": round(temp_c, 1) if temp_c is not None else None,
|
||||
"temperature_f": celsius_to_fahrenheit(temp_c),
|
||||
"temperature_max_c": round(temp_max, 1)
|
||||
if temp_max is not None
|
||||
else None,
|
||||
"temperature_min_c": round(temp_min, 1)
|
||||
if temp_min is not None
|
||||
else None,
|
||||
"weather_code": weather_code,
|
||||
"sunrise": sunrise.split("T")[1] if sunrise else None,
|
||||
"sunset": sunset.split("T")[1] if sunset else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {"location": location_name, "forecast": forecast}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting weather data: {e}")
|
||||
return {
|
||||
"location": location,
|
||||
"weather": {
|
||||
"date": datetime.now().strftime("%Y-%m-%d"),
|
||||
"day_name": datetime.now().strftime("%A"),
|
||||
"temperature_c": None,
|
||||
"temperature_f": None,
|
||||
"weather_code": None,
|
||||
"error": "Could not retrieve weather data",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def chat_completion_http(request: Request, request_body: ChatCompletionRequest):
|
||||
async def handle_request(request: Request):
|
||||
"""HTTP endpoint for chat completions with streaming support."""
|
||||
logger.info(f"Received weather request with {len(request_body.messages)} messages")
|
||||
|
||||
request_body = await request.json()
|
||||
messages = request_body.get("messages", [])
|
||||
logger.info(
|
||||
f"messages detail json dumps: {json.dumps([msg.model_dump() for msg in request_body.messages], indent=2)}"
|
||||
"messages detail json dumps: %s",
|
||||
json.dumps(messages, indent=2),
|
||||
)
|
||||
|
||||
traceparent_header = request.headers.get("traceparent")
|
||||
|
||||
if traceparent_header:
|
||||
logger.info(f"Received traceparent header: {traceparent_header}")
|
||||
|
||||
return StreamingResponse(
|
||||
stream_chat_completions(request_body, traceparent_header),
|
||||
invoke_weather_agent(request, request_body, traceparent_header),
|
||||
media_type="text/plain",
|
||||
headers={
|
||||
"content-type": "text/event-stream",
|
||||
|
|
@ -546,85 +280,100 @@ async def chat_completion_http(request: Request, request_body: ChatCompletionReq
|
|||
)
|
||||
|
||||
|
||||
async def stream_chat_completions(
|
||||
request_body: ChatCompletionRequest, traceparent_header: str = None
|
||||
async def invoke_weather_agent(
|
||||
request: Request, request_body: dict, traceparent_header: str = None
|
||||
):
|
||||
"""Generate streaming chat completions."""
|
||||
response_messages = await prepare_weather_messages(request_body)
|
||||
messages = request_body.get("messages", [])
|
||||
|
||||
# Detect if user wants multi-day forecast
|
||||
last_user_msg = get_last_user_content(messages)
|
||||
days = 1
|
||||
|
||||
if "forecast" in last_user_msg or "week" in last_user_msg:
|
||||
days = 7
|
||||
elif "tomorrow" in last_user_msg:
|
||||
days = 2
|
||||
|
||||
# Extract specific number of days if mentioned (e.g., "5 day forecast")
|
||||
import re
|
||||
|
||||
day_match = re.search(r"(\d{1,2})\s+day", last_user_msg)
|
||||
if day_match:
|
||||
requested_days = int(day_match.group(1))
|
||||
days = min(requested_days, 16) # API supports max 16 days
|
||||
|
||||
# Get live weather data (location extraction happens inside this function)
|
||||
weather_data = await get_weather_data(request, messages, days)
|
||||
|
||||
# Create weather context to append to user message
|
||||
forecast_type = "forecast" if days > 1 else "current weather"
|
||||
weather_context = f"""
|
||||
|
||||
Weather data for {weather_data['location']} ({forecast_type}):
|
||||
{json.dumps(weather_data, indent=2)}"""
|
||||
|
||||
# System prompt for weather agent
|
||||
instructions = """You are a weather assistant in a multi-agent system. You will receive weather data in JSON format with these fields:
|
||||
|
||||
- "location": City name
|
||||
- "forecast": Array of weather objects, each with date, day_name, temperature_c, temperature_f, temperature_max_c, temperature_min_c, weather_code, sunrise, sunset
|
||||
- weather_code: WMO code (0=clear, 1-3=partly cloudy, 45-48=fog, 51-67=rain, 71-86=snow, 95-99=thunderstorm)
|
||||
|
||||
Your task:
|
||||
1. Present the weather/forecast clearly for the location
|
||||
2. For single day: show current conditions
|
||||
3. For multi-day: show each day with date and conditions
|
||||
4. Include temperature in both Celsius and Fahrenheit
|
||||
5. Describe conditions naturally based on weather_code
|
||||
6. Use conversational language
|
||||
|
||||
Important: If the conversation includes information from other agents (like flight details), acknowledge and build upon that context naturally. Your primary focus is weather, but maintain awareness of the full conversation.
|
||||
|
||||
Remember: Only use the provided data. If fields are null, mention data is unavailable."""
|
||||
|
||||
# Build message history with weather data appended to the last user message
|
||||
response_messages = [{"role": "system", "content": instructions}]
|
||||
|
||||
for i, msg in enumerate(messages):
|
||||
# Append weather data to the last user message
|
||||
if i == len(messages) - 1 and msg.get("role") == "user":
|
||||
response_messages.append(
|
||||
{"role": "user", "content": msg.get("content") + weather_context}
|
||||
)
|
||||
else:
|
||||
response_messages.append(
|
||||
{"role": msg.get("role"), "content": msg.get("content")}
|
||||
)
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate weather response"
|
||||
)
|
||||
|
||||
ctx = extract(request.headers)
|
||||
extra_headers = {"x-envoy-max-retries": "3"}
|
||||
if traceparent_header:
|
||||
extra_headers["traceparent"] = traceparent_header
|
||||
inject(extra_headers, context=ctx)
|
||||
|
||||
response_stream = await archgw_client.chat.completions.create(
|
||||
stream = await openai_client_via_plano.chat.completions.create(
|
||||
model=WEATHER_MODEL,
|
||||
messages=response_messages,
|
||||
temperature=request_body.temperature or 0.7,
|
||||
max_tokens=request_body.max_tokens or 1000,
|
||||
temperature=request_body.get("temperature", 0.7),
|
||||
max_tokens=request_body.get("max_tokens", 1000),
|
||||
stream=True,
|
||||
extra_headers=extra_headers,
|
||||
)
|
||||
|
||||
completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
|
||||
created_time = int(time.time())
|
||||
collected_content = []
|
||||
async for chunk in stream:
|
||||
if chunk.choices:
|
||||
yield f"data: {chunk.model_dump_json()}\n\n"
|
||||
|
||||
async for chunk in response_stream:
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
content = chunk.choices[0].delta.content
|
||||
collected_content.append(content)
|
||||
|
||||
stream_chunk = ChatCompletionStreamResponse(
|
||||
id=completion_id,
|
||||
created=created_time,
|
||||
model=request_body.model,
|
||||
choices=[
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {"content": content},
|
||||
"finish_reason": None,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
yield f"data: {stream_chunk.model_dump_json()}\n\n"
|
||||
|
||||
full_response = "".join(collected_content)
|
||||
updated_history = [{"role": "assistant", "content": full_response}]
|
||||
|
||||
final_chunk = ChatCompletionStreamResponse(
|
||||
id=completion_id,
|
||||
created=created_time,
|
||||
model=request_body.model,
|
||||
choices=[
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": json.dumps(updated_history),
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
yield f"data: {final_chunk.model_dump_json()}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating weather response: {e}")
|
||||
|
||||
error_chunk = ChatCompletionStreamResponse(
|
||||
id=f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
||||
created=int(time.time()),
|
||||
model=request_body.model,
|
||||
choices=[
|
||||
error_chunk = {
|
||||
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(time.time()),
|
||||
"model": request_body.get("model", WEATHER_MODEL),
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
|
|
@ -633,9 +382,8 @@ async def stream_chat_completions(
|
|||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
||||
}
|
||||
yield f"data: {json.dumps(error_chunk)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
|
||||
|
|
@ -672,3 +420,7 @@ def start_server(host: str = "localhost", port: int = 10510):
|
|||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_server(host="0.0.0.0", port=10510)
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
WAIT_FOR_PIDS=()
|
||||
|
||||
log() {
|
||||
timestamp=$(python3 -c 'from datetime import datetime; print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:23])')
|
||||
message="$*"
|
||||
echo "$timestamp - $message"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
log "Caught signal, terminating all agent processes ..."
|
||||
for PID in "${WAIT_FOR_PIDS[@]}"; do
|
||||
if kill $PID 2> /dev/null; then
|
||||
log "killed process: $PID"
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
log "Starting weather agent on port 10510..."
|
||||
uv run python -m travel_agents --host 0.0.0.0 --port 10510 --agent weather &
|
||||
WAIT_FOR_PIDS+=($!)
|
||||
|
||||
log "Starting flight agent on port 10520..."
|
||||
uv run python -m travel_agents --host 0.0.0.0 --port 10520 --agent flight &
|
||||
WAIT_FOR_PIDS+=($!)
|
||||
|
||||
log "Starting currency agent on port 10530..."
|
||||
uv run python -m travel_agents --host 0.0.0.0 --port 10530 --agent currency &
|
||||
WAIT_FOR_PIDS+=($!)
|
||||
|
||||
log "All agents started successfully!"
|
||||
log " - Weather Agent: http://localhost:10510"
|
||||
log " - Flight Agent: http://localhost:10520"
|
||||
log " - Currency Agent: http://localhost:10530"
|
||||
log ""
|
||||
log "Waiting for agents to run..."
|
||||
|
||||
for PID in "${WAIT_FOR_PIDS[@]}"; do
|
||||
wait "$PID"
|
||||
done
|
||||
|
|
@ -7,18 +7,6 @@ Content-Type: application/json
|
|||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a professional travel planner assistant. Your role is to provide accurate, clear, and helpful information about weather and flights based on the structured data provided to you.\n\nCRITICAL INSTRUCTIONS:\n\n1. DATA STRUCTURE:\n \n WEATHER DATA:\n - You will receive weather data as JSON in a system message\n - The data contains a \"location\" field (string) and a \"forecast\" array\n - Each forecast entry has: date, day_name, temperature_c, temperature_f, temperature_max_c, temperature_min_c, condition, sunrise, sunset\n - Some fields may be null/None - handle these gracefully\n \n FLIGHT DATA:\n - You will receive flight information in a system message\n - Flight data includes: airline, flight number, departure time, arrival time, origin airport, destination airport, aircraft type, status, gate, terminal\n - Information may include both scheduled and estimated times\n - Some fields may be unavailable - handle these gracefully\n\n2. WEATHER HANDLING:\n - For single-day queries: Use temperature_c/temperature_f (current/primary temperature)\n - For multi-day forecasts: Use temperature_max_c and temperature_min_c when available\n - Always provide temperatures in both Celsius and Fahrenheit when available\n - If temperature is null, say \"temperature data unavailable\" rather than making up numbers\n - Use exact condition descriptions provided (e.g., \"Clear sky\", \"Rainy\", \"Partly Cloudy\")\n - Add helpful context when appropriate (e.g., \"perfect for outdoor activities\" for clear skies)\n\n3. FLIGHT HANDLING:\n - Present flight information clearly with airline name and flight number\n - Include departure and arrival times with time zones when provided\n - Mention origin and destination airports with their codes\n - Include gate and terminal information when available\n - Note aircraft type if relevant to the query\n - Highlight any status updates (delays, early arrivals, etc.)\n - For multiple flights, list them in chronological order by departure time\n - If specific details are missing, acknowledge this rather than inventing information\n\n4. MULTI-PART QUERIES:\n - Users may ask about both weather and flights in one message\n - Answer ALL parts of the query that you have data for\n - Organize your response logically - typically weather first, then flights, or vice versa based on the query\n - Provide complete information for each topic without mentioning other agents\n - If you receive data for only one topic but the user asked about multiple, answer what you can with the provided data\n\n5. ERROR HANDLING:\n - If weather forecast contains an \"error\" field, acknowledge the issue politely\n - If temperature or condition is null/None, mention that specific data is unavailable\n - If flight details are incomplete, state which information is unavailable\n - Never invent or guess weather or flight data - only use what's provided\n - If location couldn't be determined, acknowledge this but still provide available data\n\n6. RESPONSE FORMAT:\n \n For Weather:\n - Single-day queries: Provide current conditions, temperature, and condition\n - Multi-day forecasts: List each day with date, day name, high/low temps, and condition\n - Include sunrise/sunset times when available and relevant\n \n For Flights:\n - List flights with clear numbering or bullet points\n - Include key details: airline, flight number, departure/arrival times, airports\n - Add gate, terminal, and status information when available\n - For multiple flights, organize chronologically\n \n General:\n - Use natural, conversational language\n - Be concise but complete\n - Format dates and times clearly\n - Use bullet points or numbered lists for clarity\n\n7. LOCATION HANDLING:\n - Always mention location names from the data\n - For flights, clearly state origin and destination cities/airports\n - If locations differ from what the user asked, acknowledge this politely\n\n8. RESPONSE STYLE:\n - Be friendly and professional\n - Use natural language, not technical jargon\n - Provide information in a logical, easy-to-read format\n - When answering multi-part queries, create a cohesive response that addresses all aspects\n\nRemember: Only use the data provided. Never fabricate weather or flight information. If data is missing, clearly state what's unavailable. Answer all parts of the user's query that you have data for."
|
||||
},
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Current weather data for Seattle:\n\n{\n \"location\": \"Seattle\",\n \"forecast\": [\n {\n \"date\": \"2025-12-22\",\n \"day_name\": \"Monday\",\n \"temperature_c\": 8.3,\n \"temperature_f\": 46.9,\n \"temperature_max_c\": 8.3,\n \"temperature_min_c\": 2.8,\n \"condition\": \"Rainy\",\n \"sunrise\": \"07:55\",\n \"sunset\": \"16:20\"\n }\n ]\n}\n\nUse this data to answer the user's weather query."
|
||||
},
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Here are some direct flights from Seattle to Atlanta on December 23, 2025:\n\n1. **Delta Airlines Flight DL552**\n - **Departure:** Scheduled at 3:47 PM (Seattle Time), from Seattle-Tacoma Intl (SEA)\n - **Arrival:** Scheduled at 8:31 PM (Atlanta Time), at Hartsfield-Jackson Intl (ATL)\n - **Aircraft:** Boeing 737-900 (B739)\n - **Status:** Scheduled\n - **Terminal at Atlanta:** S\n - **Estimated arrival slightly early**: 8:26 PM\n\n2. **Delta Airlines Flight DL542**\n - **Departure:** Scheduled at 12:00 PM (Seattle Time), Gate A4, from Seattle-Tacoma Intl (SEA)\n - **Arrival:** Scheduled at 4:49 PM (Atlanta Time), at Hartsfield-Jackson Intl (ATL)\n - **Aircraft:** Boeing 737-900 (B739)\n - **Status:** Scheduled\n - **Gate at Atlanta:** E10, Terminal: S\n - **Estimated early arrival**: 4:44 PM\n\n3. **Delta Airlines Flight DL554**\n - **Departure:** Scheduled at 10:15 AM (Seattle Time), Gate A10, from Seattle-Tacoma Intl (SEA)\n - **Arrival:** Scheduled at 4:05 PM (Atlanta Time), at Hartsfield-Jackson Intl (ATL)\n - **Aircraft:** Boeing 737-900 (B739)\n - **Status:** Scheduled\n - **Gate at Atlanta:** B19, Terminal: S\n - **Estimated late arrival**: 4:06 PM\n\n4. **Alaska Airlines Flight AS334**\n - **Departure:** Scheduled at 9:16 AM (Seattle Time), Gate C20, from Seattle-Tacoma Intl (SEA)\n - **Arrival:** Scheduled at 5:08 PM (Atlanta Time), at Hartsfield-Jackson Intl (ATL)\n - **Aircraft:** Boeing 737-900 (B739)\n - **Status:** Scheduled\n - **Gate at Atlanta:** C5, Terminal: N\n\nThese are just a few of the direct flights available. Please let me know if you need more details on any other specific flight."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What's the weather in Seattle?"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue