removing langchain agent, we will simplify to just the multi-agent stuff for this demo

This commit is contained in:
Salman Paracha 2026-01-17 00:13:27 -08:00
parent 74c978cc17
commit ddcbe56ad8
13 changed files with 2 additions and 2777 deletions

View file

@ -2,14 +2,14 @@
nodaemon=true
[program:brightstaff]
command=sh -c "envsubst < /app/arch_config_rendered.yaml > /app/arch_config_rendered.env_sub.yaml && RUST_LOG=debug ARCH_CONFIG_PATH_RENDERED=/app/arch_config_rendered.env_sub.yaml /app/brightstaff 2>&1 | tee /var/log/brightstaff.log | while IFS= read -r line; do echo '[brightstaff]' \"$line\"; done"
command=sh -c "envsubst < /app/arch_config_rendered.yaml > /app/arch_config_rendered.env_sub.yaml && RUST_LOG=info ARCH_CONFIG_PATH_RENDERED=/app/arch_config_rendered.env_sub.yaml /app/brightstaff 2>&1 | tee /var/log/brightstaff.log | while IFS= read -r line; do echo '[brightstaff]' \"$line\"; done"
stdout_logfile=/dev/stdout
redirect_stderr=true
stdout_logfile_maxbytes=0
stderr_logfile_maxbytes=0
[program:envoy]
command=/bin/sh -c "uv run python -m planoai.config_generator && envsubst < /etc/envoy/envoy.yaml > /etc/envoy.env_sub.yaml && envoy -c /etc/envoy.env_sub.yaml --component-log-level wasm:debug --log-format '[%%Y-%%m-%%d %%T.%%e][%%l] %%v' 2>&1 | tee /var/log/envoy.log | while IFS= read -r line; do echo '[archgw_logs]' \"$line\"; done"
command=/bin/sh -c "uv run python -m planoai.config_generator && envsubst < /etc/envoy/envoy.yaml > /etc/envoy.env_sub.yaml && envoy -c /etc/envoy.env_sub.yaml --component-log-level wasm:info --log-format '[%%Y-%%m-%%d %%T.%%e][%%l] %%v' 2>&1 | tee /var/log/envoy.log | while IFS= read -r line; do echo '[archgw_logs]' \"$line\"; done"
stdout_logfile=/dev/stdout
redirect_stderr=true
stdout_logfile_maxbytes=0

View file

@ -1,22 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Install bash and uv
RUN apt-get update && apt-get install -y bash && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir 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"]

View file

@ -1,252 +0,0 @@
# Travel Booking Agent Demo (LangChain-first)
A lightweight **LangChain-powered** multi-agent travel booking system that runs two agents behind Plano's router: a weather agent and a flight agent. Each agent is implemented with LangChain's tool-calling capabilities for a clean, modular design.
## Overview
This demo showcases how to integrate LangChain agents with Plano:
- **Weather Agent** - Uses `@tool` decorator to fetch real-time weather from Open-Meteo API
- **Flight Agent** - Uses `@tool` decorator to search flights via FlightAware API
Both agents use LangChain's `create_tool_calling_agent` and `AgentExecutor` for:
- Automatic tool selection and execution
- Streaming responses via `astream_events`
- OpenAI-compatible API endpoints
## Architecture
```
User Request
Plano Gateway (8001)
[Orchestrator]
|
┌────┴────┐
↓ ↓
Weather Flight
Agent Agent
(10510) (10520)
│ │
└──────────┴─── LangChain Tools ───→ External APIs
```
Each agent:
1. Receives OpenAI-compatible chat requests
2. Uses LangChain's agent executor with tools
3. Tools fetch data from external APIs (Open-Meteo, FlightAware)
4. Streams responses back in OpenAI format
## LangChain Implementation Details
### Weather Agent Tools
```python
@tool
async def get_weather(city: str, days: int = 1) -> str:
"""Get weather information for a city."""
# Geocode city → fetch weather from Open-Meteo
...
```
### Flight Agent Tools
```python
@tool
async def resolve_airport_code(city: str) -> str:
"""Convert city name to IATA airport code."""
...
@tool
async def search_flights(origin_code: str, destination_code: str, travel_date: str = None) -> str:
"""Search flights between two airports."""
# Query FlightAware AeroAPI
...
```
### Agent Setup
```python
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="openai/gpt-4o",
base_url=LLM_GATEWAY_ENDPOINT, # Plano gateway
api_key="EMPTY",
streaming=True,
)
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
```
## Prerequisites
- Docker and Docker Compose
- [Plano CLI](https://docs.planoai.dev) installed
- OpenAI API key
- (Optional) FlightAware AeroAPI key for live flight data
## Quick Start
### 1. Set Environment Variables
```bash
export OPENAI_API_KEY="your-openai-api-key"
export AEROAPI_KEY="your-flightaware-api-key" # Optional for flight agent
```
### 2. Start All Services with Docker Compose
```bash
docker compose up --build
```
This starts:
- Plano Gateway on port 8001 (and 12000 for LLM proxy)
- Weather Agent on port 10510
- Flight Agent on port 10520
- Open WebUI on port 8080
- Jaeger tracing on port 16686
### 3. Test the System
**Option 1**: Use Open WebUI at http://localhost:8080
**Option 2**: Send requests directly:
```bash
# Weather query
curl http://localhost:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "What is the weather like in Paris?"}]
}'
# Flight query
curl http://localhost:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Find flights from Seattle to New York"}]
}'
```
## Example Conversations
### Weather Query
```
User: What's the 5-day forecast for Tokyo?
Assistant: [Weather Agent uses get_weather tool → presents forecast]
```
### Flight Search
```
User: What flights go from London to Seattle tomorrow?
Assistant: [Flight Agent uses resolve_airport_code → search_flights → presents results]
```
### Multi-Agent (via Plano routing)
```
User: What's the weather in Seattle, and any flights to New York?
Assistant: [Plano routes to both agents → combined response]
```
## Local Development
### Run agents locally (without Docker)
```bash
# Install dependencies
cd demos/use_cases/langchain
uv sync
# Start weather agent
uv run python src/travel_agents/weather_agent.py
# In another terminal, start flight agent
uv run python src/travel_agents/flight_agent.py
```
### Using the CLI
```bash
# Start weather agent
uv run travel_agents weather --port 10510
# Start flight agent
uv run travel_agents flight --port 10520
```
## Project Structure
```
langchain/
├── config.yaml # Plano gateway configuration
├── docker-compose.yaml # Docker services orchestration
├── Dockerfile # Container image
├── pyproject.toml # Python dependencies (LangChain, FastAPI, etc.)
├── README.md # This file
└── src/
└── travel_agents/
├── __init__.py # CLI entry points
├── weather_agent.py # Weather agent with get_weather tool
└── flight_agent.py # Flight agent with search_flights tool
```
## Configuration
### config.yaml
Defines agent descriptions for Plano's intelligent routing:
```yaml
agents:
- id: weather_agent
url: http://host.docker.internal:10510
- id: flight_agent
url: http://host.docker.internal:10520
listeners:
- type: agent
name: travel_booking_service
port: 8001
router: plano_orchestrator_v1
agents:
- id: weather_agent
description: |
WeatherAgent provides real-time weather and forecasts...
- id: flight_agent
description: |
FlightAgent provides live flight information...
```
## Troubleshooting
**Agents not responding**
- Check container logs: `docker compose logs weather-agent`
- Verify Plano is running: `curl http://localhost:8001/health`
**LangChain agent errors**
- Check that `LLM_GATEWAY_ENDPOINT` is correctly set
- Verify OpenAI API key is valid
**Flight API returning mock data**
- Set `AEROAPI_KEY` for live FlightAware data
- Without the key, the agent returns sample flight data
## API Endpoints
All agents expose OpenAI-compatible endpoints:
- `POST /v1/chat/completions` - Chat completion (streaming)
- `GET /health` - Health check
## Key Dependencies
- `langchain>=0.3.13` - Agent framework
- `langchain-openai>=0.2.14` - OpenAI integration via Plano
- `fastapi>=0.115.0` - Web framework
- `httpx>=0.24.0` - Async HTTP client for API calls

View file

@ -1,57 +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
model_providers:
- model: openai/gpt-4o
access_key: $OPENAI_API_KEY # smaller, faster, cheaper model for extracting entities like location
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

View file

@ -1,63 +0,0 @@
services:
plano:
build:
context: ../../../
dockerfile: Dockerfile
ports:
- "12000:12000"
- "8001:8001"
environment:
- ARCH_CONFIG_PATH=/config/config.yaml
- OPENAI_API_KEY=${OPENAI_API_KEY:?OPENAI_API_KEY environment variable is required but not set}
volumes:
- ./config.yaml:/app/arch_config.yaml
- /etc/ssl/cert.pem:/etc/ssl/cert.pem
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:-} # Optional: mock data returned if not set
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
ports:
- "8080:8080"
environment:
- 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
jaeger:
build:
context: ../../shared/jaeger
container_name: jaeger
restart: always
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver

View file

@ -1,35 +0,0 @@
### Weather via Plano (8001)
POST http://localhost:8001/v1/chat/completions
Content-Type: application/json
{
"model": "openai/gpt-4o-mini",
"stream": true,
"messages": [
{ "role": "user", "content": "What's the weather in Lahore?" }
]
}
### Flight via Plano (8001)
POST http://localhost:8001/v1/chat/completions
Content-Type: application/json
{
"model": "openai/gpt-4o-mini",
"stream": false,
"messages": [
{ "role": "user", "content": "Find flights from Seattle to Atlanta." }
]
}
### Multi-intent via Plano (8001)
POST http://localhost:8001/v1/chat/completions
Content-Type: application/json
{
"model": "openai/gpt-4o-mini",
"stream": false,
"messages": [
{ "role": "user", "content": "What's the weather in Tokyo this weekend, and any direct flights to Sydney?" }
]
}

View file

@ -1,34 +0,0 @@
[project]
name = "travel-agents"
version = "0.1.0"
description = "Travel Booking Agents (LangChain-first) - Weather and Flight agents powered by LangChain"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
# Web framework
"fastapi>=0.115.0",
"uvicorn>=0.30.0",
# HTTP client
"httpx>=0.24.0",
# CLI
"click>=8.2.1",
"pydantic>=2.11.7",
# LangChain ecosystem (v1.0+)
"langchain>=1.0.0",
"langchain-openai>=0.3.0",
"langchain-core>=1.0.0",
# OpenAI (for direct API calls if needed)
"openai>=1.0.0",
# Telemetry
"opentelemetry-api>=1.20.0",
]
[project.scripts]
travel_agents = "travel_agents:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/travel_agents"]

View file

@ -1,60 +0,0 @@
"""
Travel Agents - LangChain Demo
LangChain-powered travel agents that integrate with Plano gateway.
Each agent uses LangChain's tool-calling capabilities for a clean, modular design.
Agents:
- Weather Agent (port 10510): Real-time weather and forecasts
- Flight Agent (port 10520): Flight search and information
Usage:
# Start weather agent
python -m travel_agents.weather_agent
# Start flight agent
python -m travel_agents.flight_agent
# Or use the CLI
travel_agents weather --port 10510
travel_agents flight --port 10520
"""
import click
@click.group()
def cli():
"""Travel Agents - LangChain demo for Plano integration."""
pass
@cli.command()
@click.option("--host", default="0.0.0.0", help="Host to bind to")
@click.option("--port", default=10510, help="Port to listen on")
def weather(host: str, port: int):
"""Start the Weather Agent (LangChain)."""
from travel_agents.weather_agent import start_server
click.echo(f"🌤️ Starting Weather Agent on {host}:{port}")
start_server(host=host, port=port)
@cli.command()
@click.option("--host", default="0.0.0.0", help="Host to bind to")
@click.option("--port", default=10520, help="Port to listen on")
def flight(host: str, port: int):
"""Start the Flight Agent (LangChain)."""
from travel_agents.flight_agent import start_server
click.echo(f"✈️ Starting Flight Agent on {host}:{port}")
start_server(host=host, port=port)
def main():
"""Entry point for the CLI."""
cli()
if __name__ == "__main__":
main()

View file

@ -1,389 +0,0 @@
import json
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
from openai import AsyncOpenAI
import os
import logging
import uvicorn
from datetime import datetime
import httpx
from typing import Optional
import uuid
import time
from opentelemetry.propagate import extract, inject
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.agents import create_agent
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - [FLIGHT_AGENT] - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
LLM_GATEWAY_ENDPOINT = os.getenv(
"LLM_GATEWAY_ENDPOINT", "http://host.docker.internal:12000/v1"
)
FLIGHT_MODEL = "openai/gpt-4o"
EXTRACTION_MODEL = "openai/gpt-4o-mini"
AEROAPI_BASE_URL = "https://aeroapi.flightaware.com/aeroapi"
AEROAPI_KEY = os.getenv("AEROAPI_KEY")
http_client = httpx.AsyncClient(timeout=30.0)
openai_client = AsyncOpenAI(base_url=LLM_GATEWAY_ENDPOINT, api_key="EMPTY")
class FlightSearchInput(BaseModel):
origin_city: str = Field(..., description="Origin city for the flight search")
destination_city: str = Field(
..., description="Destination city for the flight search"
)
travel_date: Optional[str] = Field(
None,
description="Optional travel date in YYYY-MM-DD format. If not provided, defaults to today.",
)
SYSTEM_PROMPT = """You are a travel planning assistant specializing in flight information. You support both direct flights AND multi-leg connecting flights.
Flight data fields:
- airline: Full airline name (e.g., "Delta Air Lines")
- flight_number: Flight identifier (e.g., "DL123")
- departure_time/arrival_time: ISO 8601 timestamps
- origin/destination: Airport IATA codes
- aircraft_type: Aircraft model code (e.g., "B739")
- status: Flight status (e.g., "Scheduled", "Delayed")
- terminal_origin/gate_origin: Departure terminal and gate (may be null)
Your task:
1. Present flights clearly with airline, flight number, readable times, airports, and aircraft
2. Organize chronologically by departure time
3. Convert ISO timestamps to readable format (e.g., "11:00 AM")
4. Include terminal/gate info when available
5. For multi-leg flights: present each leg separately with connection timing
Multi-agent context: If the conversation includes information from other sources, incorporate it naturally into your response."""
def build_flight_agent(
request: Request,
request_body: dict,
streaming: bool,
):
ctx = extract(request.headers)
extra_headers = {"x-envoy-max-retries": "3"}
request_id = request.headers.get("x-request-id")
if request_id:
extra_headers["x-request-id"] = request_id
inject(extra_headers, context=ctx)
@tool("search_flights", args_schema=FlightSearchInput)
async def search_flights(
origin_city: str, destination_city: str, travel_date: Optional[str] = None
):
"""Search for flights between two cities. Supports optional travel date."""
origin_code = await resolve_airport_code(origin_city, request)
dest_code = await resolve_airport_code(destination_city, request)
if not origin_code or not dest_code:
return {
"error": "Could not resolve airport codes for provided cities.",
"origin_city": origin_city,
"destination_city": destination_city,
}
flight_data = await fetch_flights(origin_code, dest_code, travel_date)
return {
"origin_city": origin_city,
"destination_city": destination_city,
"origin_code": origin_code,
"destination_code": dest_code,
"travel_date": travel_date or datetime.now().strftime("%Y-%m-%d"),
"flights": flight_data.get("flights", []),
"count": flight_data.get("count", 0),
"error": flight_data.get("error"),
}
llm = ChatOpenAI(
model=FLIGHT_MODEL,
api_key="EMPTY",
base_url=LLM_GATEWAY_ENDPOINT,
temperature=request_body.get("temperature", 0.7),
max_tokens=request_body.get("max_tokens", 1000),
streaming=streaming,
default_headers=extra_headers,
)
return create_agent(
model=llm,
tools=[search_flights],
system_prompt=SYSTEM_PROMPT,
)
async def resolve_airport_code(city_name: str, request: Request) -> Optional[str]:
if not city_name:
return None
try:
ctx = extract(request.headers)
extra_headers = {}
inject(extra_headers, context=ctx)
response = await openai_client.chat.completions.create(
model=EXTRACTION_MODEL,
messages=[
{
"role": "system",
"content": "Convert city names to primary airport IATA codes. Return only the 3-letter code. Examples: Seattle→SEA, Atlanta→ATL, New York→JFK, Dubai→DXB, Lahore→LHE",
},
{"role": "user", "content": city_name},
],
temperature=0.1,
max_tokens=10,
extra_headers=extra_headers or None,
)
code = response.choices[0].message.content.strip().upper()
code = code.strip("\"'`.,!? \n\t")
return code if len(code) == 3 else None
except Exception as e:
logger.error(f"Error resolving airport code for {city_name}: {e}")
return None
async def fetch_flights(
origin_code: str, dest_code: str, travel_date: Optional[str] = None
) -> dict:
"""Fetch flights between two airports. Note: FlightAware limits to 2 days ahead."""
search_date = travel_date or datetime.now().strftime("%Y-%m-%d")
search_date_obj = datetime.strptime(search_date, "%Y-%m-%d")
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
days_ahead = (search_date_obj - today).days
if days_ahead > 2:
logger.warning(
f"Date {search_date} is {days_ahead} days ahead, exceeds FlightAware limit"
)
return {
"origin_code": origin_code,
"destination_code": dest_code,
"flights": [],
"count": 0,
"error": f"FlightAware API only provides data up to 2 days ahead. Requested date ({search_date}) is {days_ahead} days away.",
}
try:
url = f"{AEROAPI_BASE_URL}/airports/{origin_code}/flights/to/{dest_code}"
headers = {"x-apikey": AEROAPI_KEY}
params = {
"start": f"{search_date}T00:00:00Z",
"end": f"{search_date}T23:59:59Z",
"connection": "nonstop",
"max_pages": 1,
}
response = await http_client.get(url, headers=headers, params=params)
if response.status_code != 200:
logger.error(
f"FlightAware API error {response.status_code}: {response.text}"
)
return {
"origin_code": origin_code,
"destination_code": dest_code,
"flights": [],
"count": 0,
}
data = response.json()
flights = []
for flight_group in data.get("flights", [])[:5]:
segments = flight_group.get("segments", [])
if not segments:
continue
flight = segments[0]
flights.append(
{
"airline": flight.get("operator"),
"flight_number": flight.get("ident_iata") or flight.get("ident"),
"departure_time": flight.get("scheduled_out"),
"arrival_time": flight.get("scheduled_in"),
"origin": flight["origin"].get("code_iata")
if isinstance(flight.get("origin"), dict)
else None,
"destination": flight["destination"].get("code_iata")
if isinstance(flight.get("destination"), dict)
else None,
"aircraft_type": flight.get("aircraft_type"),
"status": flight.get("status"),
"terminal_origin": flight.get("terminal_origin"),
"gate_origin": flight.get("gate_origin"),
}
)
logger.info(f"Found {len(flights)} flights from {origin_code} to {dest_code}")
return {
"origin_code": origin_code,
"destination_code": dest_code,
"flights": flights,
"count": len(flights),
}
except Exception as e:
logger.error(f"Error fetching flights: {e}")
return {
"origin_code": origin_code,
"destination_code": dest_code,
"flights": [],
"count": 0,
}
app = FastAPI(title="Flight Information Agent", version="1.0.0")
@app.post("/v1/chat/completions")
async def handle_request(request: Request):
request_body = await request.json()
is_streaming = request_body.get("stream", True)
model = request_body.get("model", FLIGHT_MODEL)
if is_streaming:
return StreamingResponse(
invoke_flight_agent_stream(request, request_body, model),
media_type="text/event-stream",
headers={"content-type": "text/event-stream"},
)
content = await invoke_flight_agent(request, request_body)
return JSONResponse(
{
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
}
)
async def invoke_flight_agent(request: Request, request_body: dict):
"""Generate flight information using a LangChain agent with the modern create_agent API."""
messages = request_body.get("messages", [])
agent = build_flight_agent(request, request_body, streaming=False)
try:
# Invoke agent with messages
result = await agent.ainvoke({"messages": messages})
# Extract final response from messages
final_message = result["messages"][-1]
content = (
final_message.content
if hasattr(final_message, "content")
else str(final_message)
)
return content
except Exception as e:
logger.error(f"Error generating response: {e}")
return "I'm having trouble retrieving flight information right now. Please try again."
def build_openai_chunk(model: str, content: str, finish_reason: Optional[str] = None):
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"delta": {"content": content} if content else {},
"finish_reason": finish_reason,
}
],
}
async def invoke_flight_agent_stream(
request: Request,
request_body: dict,
model: str,
):
messages = request_body.get("messages", [])
agent = build_flight_agent(request, request_body, streaming=True)
try:
async for event in agent.astream_events(
{"messages": messages},
version="v2",
):
if event.get("event") != "on_chat_model_stream":
continue
chunk = event.get("data", {}).get("chunk")
content = getattr(chunk, "content", None)
if not content:
continue
if isinstance(content, list):
content = "".join(
piece for piece in content if isinstance(piece, str)
).strip()
if not content:
continue
yield f"data: {json.dumps(build_openai_chunk(model, content))}\n\n"
yield f"data: {json.dumps(build_openai_chunk(model, '', 'stop'))}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
logger.error(f"Error streaming response: {e}")
error_message = "I'm having trouble retrieving flight information right now. Please try again."
yield f"data: {json.dumps(build_openai_chunk(model, error_message, 'stop'))}\n\n"
yield "data: [DONE]\n\n"
@app.get("/health")
async def health_check():
return {"status": "healthy", "agent": "flight_information"}
def start_server(host: str = "0.0.0.0", port: int = 10520):
uvicorn.run(
app,
host=host,
port=port,
log_config={
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - [FLIGHT_AGENT] - %(levelname)s - %(message)s"
}
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
}
},
"root": {"level": "INFO", "handlers": ["default"]},
},
)
if __name__ == "__main__":
start_server()

View file

@ -1,470 +0,0 @@
import json
import logging
import os
import time
import uuid
from datetime import datetime
from typing import Optional
from urllib.parse import quote
import httpx
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from openai import AsyncOpenAI
from opentelemetry.propagate import extract, inject
from pydantic import BaseModel, Field
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - [WEATHER_AGENT] - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
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"
openai_client_via_plano = AsyncOpenAI(
base_url=LLM_GATEWAY_ENDPOINT,
api_key="EMPTY",
)
app = FastAPI(title="Weather Forecast Agent", version="1.0.0")
http_client = httpx.AsyncClient(timeout=10.0)
def celsius_to_fahrenheit(temp_c: Optional[float]) -> Optional[float]:
return round(temp_c * 9 / 5 + 32, 1) if temp_c is not None else None
async def get_weather_data(
request: Request,
messages: list,
days: int = 1,
request_id: str = None,
city_override: Optional[str] = None,
):
instructions = """You are a city name extractor. Look at the FINAL user message ONLY and extract the city name.
The FINAL user message will be the LAST message with role "user" in the conversation.
IMPORTANT: Ignore all previous messages. Focus ONLY on the FINAL user message.
Examples of what to extract from the FINAL user message:
- "What's the weather in Seattle?" -> Seattle
- "What's the weather in San Francisco?" -> San Francisco
- "What about Dubai?" -> Dubai
- "How's the weather in Tokyo today?" -> Tokyo
- "Tell me about Lahore" -> Lahore
- "What about there?" -> Look at conversation for the last mentioned city
Output ONLY the city name. Nothing else. One word or city name only.
If no city can be found, output: NOT_FOUND"""
location = city_override
if not 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 = {}
if request_id:
extra_headers["x-request-id"] = request_id
inject(extra_headers, context=ctx)
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=10,
extra_headers=extra_headers if extra_headers else None,
)
location = response.choices[0].message.content.strip().strip("\"'`.,!?")
logger.info("Location extraction result: '%s'", location)
if not location or location.upper() == "NOT_FOUND":
location = "New York"
logger.info("Location not found, defaulting to: %s", location)
except Exception as e:
logger.error("Error extracting location: %s", e)
location = "New York"
logger.info("Fetching weather for location: '%s' (days: %s)", location, days)
try:
geocode_url = (
"https://geocoding-api.open-meteo.com/v1/search?"
f"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("Could not geocode %s, using New York", location)
location = "New York"
geocode_url = (
"https://geocoding-api.open-meteo.com/v1/search?"
f"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(
"Geocoded '%s' to %s (%s, %s)", location, location_name, latitude, longitude
)
weather_url = (
"https://api.open-meteo.com/v1/forecast?"
f"latitude={latitude}&longitude={longitude}&"
"current=temperature_2m&"
"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", {})
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
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("Error getting weather data: %s", 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",
},
}
class WeatherToolInput(BaseModel):
city: str = Field(..., description="City name to look up weather for")
days: int = Field(
1,
ge=1,
le=16,
description="Number of forecast days (1-16). Defaults to 1 (current).",
)
WEATHER_SYSTEM_PROMPT = """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
Multi-agent context: You are part of a larger system. If the conversation includes additional context or information from other sources, acknowledge and incorporate it naturally into your response. Your primary focus is weather, but be aware of the full conversation context.
Remember: Only use the provided data. If fields are null, mention data is unavailable."""
def build_openai_chunk(model: str, content: str, finish_reason: Optional[str] = None):
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"delta": {"content": content} if content else {},
"finish_reason": finish_reason,
}
],
}
def build_weather_agent(
request: Request,
request_body: dict,
streaming: bool,
):
messages = request_body.get("messages", [])
ctx = extract(request.headers)
extra_headers = {"x-envoy-max-retries": "3"}
request_id = request.headers.get("x-request-id")
if request_id:
extra_headers["x-request-id"] = request_id
inject(extra_headers, context=ctx)
@tool("get_weather_forecast", args_schema=WeatherToolInput)
async def get_weather_forecast(city: str, days: int = 1):
"""Fetch a structured weather forecast for a city."""
return await get_weather_data(
request,
messages,
days,
request_id=request_id,
city_override=city,
)
llm = ChatOpenAI(
model=WEATHER_MODEL,
api_key="EMPTY",
base_url=LLM_GATEWAY_ENDPOINT,
temperature=request_body.get("temperature", 0.7),
max_tokens=request_body.get("max_tokens", 1000),
streaming=streaming,
default_headers=extra_headers,
)
return create_agent(
model=llm,
tools=[get_weather_forecast],
system_prompt=WEATHER_SYSTEM_PROMPT,
)
@app.post("/v1/chat/completions")
async def handle_request(request: Request):
request_body = await request.json()
is_streaming = request_body.get("stream", True)
try:
model = request_body.get("model", WEATHER_MODEL)
if is_streaming:
return StreamingResponse(
invoke_weather_agent_stream(request, request_body, model),
media_type="text/event-stream",
headers={"content-type": "text/event-stream"},
)
content = await invoke_weather_agent(request, request_body)
return JSONResponse(
{
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
}
)
except Exception as e:
logger.error("Error generating weather response: %s", e)
if is_streaming:
return StreamingResponse(
invoke_weather_agent_error_stream(
request_body,
"I'm having trouble retrieving weather information right now. Please try again.",
),
media_type="text/event-stream",
headers={"content-type": "text/event-stream"},
)
return JSONResponse(
{
"error": {
"message": "I'm having trouble retrieving weather information right now. Please try again.",
"type": "server_error",
}
},
status_code=500,
)
async def invoke_weather_agent(
request: Request,
request_body: dict,
):
messages = request_body.get("messages", [])
agent = build_weather_agent(request, request_body, streaming=False)
result = await agent.ainvoke({"messages": messages})
final_message = result["messages"][-1]
return (
final_message.content
if hasattr(final_message, "content")
else str(final_message)
)
async def invoke_weather_agent_stream(
request: Request,
request_body: dict,
model: str,
):
messages = request_body.get("messages", [])
agent = build_weather_agent(request, request_body, streaming=True)
try:
async for event in agent.astream_events(
{"messages": messages},
version="v2",
):
if event.get("event") != "on_chat_model_stream":
continue
chunk = event.get("data", {}).get("chunk")
content = getattr(chunk, "content", None)
if not content:
continue
if isinstance(content, list):
content = "".join(
piece for piece in content if isinstance(piece, str)
).strip()
if not content:
continue
yield f"data: {json.dumps(build_openai_chunk(model, content))}\n\n"
yield f"data: {json.dumps(build_openai_chunk(model, '', 'stop'))}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
logger.error("Error streaming weather response: %s", e)
error_message = "I'm having trouble retrieving weather information right now. Please try again."
yield f"data: {json.dumps(build_openai_chunk(model, error_message, 'stop'))}\n\n"
yield "data: [DONE]\n\n"
async def invoke_weather_agent_error_stream(request_body: dict, error_message: str):
model = request_body.get("model", WEATHER_MODEL)
yield f"data: {json.dumps(build_openai_chunk(model, error_message, 'stop'))}\n\n"
yield "data: [DONE]\n\n"
@app.get("/health")
async def health_check():
return {"status": "healthy", "agent": "weather_forecast"}
def start_server(host: str = "localhost", port: int = 10510):
uvicorn.run(
app,
host=host,
port=port,
log_config={
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - [WEATHER_AGENT] - %(levelname)s - %(message)s",
}
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
}
},
"root": {"level": "INFO", "handlers": ["default"]},
},
)
if __name__ == "__main__":
start_server(host="0.0.0.0", port=10510)

View file

@ -1,43 +0,0 @@
@llm_endpoint = http://localhost:12000
### Travel Agent Chat Completion Request
POST {{llm_endpoint}}/v1/chat/completions HTTP/1.1
Content-Type: application/json
{
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": "What's the weather in Seattle?"
},
{
"role": "assistant",
"content": "The weather in Seattle is sunny with a temperature of 60 degrees Fahrenheit."
},
{
"role": "user",
"content": "What is one Alaska flight that goes direct to Atlanta from Seattle?"
}
],
"max_tokens": 1000,
"stream": false,
"temperature": 1.0
}
### test 8001
### test upstream llm
POST http://localhost:8001/v1/chat/completions HTTP/1.1
Content-Type: application/json
{
"messages": [
{
"role": "system",
"content": "\nCurrent 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."
}
],
"model": "gpt-4o",
}

View file

@ -1,30 +0,0 @@
@llm_endpoint = http://localhost:12000
### Travel Agent Chat Completion - Full Conversation
POST {{llm_endpoint}}/v1/chat/completions HTTP/1.1
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": "assistant",
"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": "assistant",
"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 is the weather in Atlanta and what flight goes from Seattle to Atlanta?"
}
],
"max_tokens": 1000,
"stream": false,
"temperature": 1.0
}

File diff suppressed because it is too large Load diff