Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 51 additions & 41 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,43 @@ on:
push:
branches: [ deploy ]

permissions:
contents: read
packages: write

env:
PYTHON_VERSION: '3.12'
NODE_VERSION: '18'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set lowercase image name
run: echo "IMAGE_NAME=$(echo ghcr.io/${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build Docker image (Nginx + FastAPI)
run: |
docker build -t weather-center-chat:ci .
docker build -t weather-center-chat:ci -t ${{ env.IMAGE_NAME }}:latest .

- name: Push to GHCR
run: |
docker push ${{ env.IMAGE_NAME }}:latest

- name: Save Docker image as artifact
run: |
docker save weather-center-chat:ci -o image.tar

- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -58,24 +79,12 @@ jobs:
done
echo "service not ready"; docker logs app || true; exit 1

- name: Set up Python (for client tests)
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install uv (official installer)
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH

- name: Sync backend deps for requests
run: |
cd backend
uv sync
- name: Install test dependencies
run: pip install requests

- name: Run test_local.py against Nginx
- name: Run integration tests
run: |
BACKEND_BASE_URL=http://localhost:8080 uv run --directory backend python ../test_local.py
BACKEND_BASE_URL=http://localhost:8080 python test_local.py
env:
VISUAL_CROSSING_API_KEY: ${{ secrets.VISUAL_CROSSING_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
Expand All @@ -90,29 +99,30 @@ jobs:
render_deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/deploy'

if: github.ref == 'refs/heads/deploy'
steps:
- uses: actions/checkout@v4

- name: Deploy to Render (Manual)
run: |
echo "Deployment to Render should be configured in Render dashboard"
echo "Environment variables are available in GitHub Secrets:"
echo ""
echo "Backend variables:"
echo "- VISUAL_CROSSING_API_KEY: ${{ secrets.VISUAL_CROSSING_API_KEY != '' }}"
echo "- GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY != '' }}"

- name: Notify deployment status
run: |
echo "✅ Tests passed and deployment is ready"
echo "📋 Next steps:"
echo "1. Go to Render.com dashboard"
echo "2. Create new Web Service from this repository"
echo "3. Use Docker deployment with render.yaml"
echo "4. Set environment variables:"
echo " Backend:"
echo " - VISUAL_CROSSING_API_KEY"
echo " - GOOGLE_API_KEY"
echo "5. Deploy!"
run: echo "✅ Tests passed — Render deploys automatically via dashboard webhook"

azure_deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/deploy'
steps:
- name: Set lowercase image name
run: echo "IMAGE_NAME=$(echo ghcr.io/${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

- name: Log in to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Deploy to Azure Container Apps
uses: azure/container-apps-deploy-action@v1
with:
containerAppName: weather-chat-env
resourceGroup: weather-chat-env
imageToDeploy: ${{ env.IMAGE_NAME }}:latest
registryUrl: ghcr.io
registryUsername: po33ski
registryPassword: ${{ secrets.GHCR_PAT }}
209 changes: 174 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,205 @@
# Weather Center Chat

Weather Center Chat focuses on a single conversational experience: users ask for **weather insights** and **weather-aware travel advice**, and the app orchestrates Google ADK agents to produce concise human responses plus a structured `weather-json` payload that powers the chat UI.
Weather Center Chat is a conversational assistant that combines **weather data**, **weather-aware travel advice**, and **hotel search**. Users chat naturally, and the app orchestrates a Google ADK multi-agent system to produce human-friendly responses paired with structured JSON payloads that drive the chat UI panels.

## Features

- **Current weather** — real-time conditions (temperature, wind, humidity, pressure, sunrise/sunset)
- **Weather forecast** — up to 15-day forecast for any city
- **Historical weather** — data for any past date range
- **Travel advice** — three weather-adapted activity suggestions for a city (outdoor vs indoor based on conditions)
- **Hotel search** — finds up to 3 hotels in any city via live web search (Tavily), returning price, rating, reviews and booking highlights

## AI Chat Flow

- A React (Vite SPA) chat surface (`frontend/src/app/views/ChatPage.tsx`) renders the human-friendly message and consumes the structured `weather-json` block returned by the agent.
- The FastAPI backend exposes `/api/chat`, keeps lightweight per-browser sessions alive, and pipes user prompts into a Google ADK runner.
- The response travels back as one string that combines chat text and the fenced JSON block so the frontend can deterministically split it.
```
User message
FastAPI /api/chat
Google ADK Runner ──► root_agent (weather_assistant)
┌───────┼───────────────┐
▼ ▼ ▼
get_weather travel_advice search_hotels
_agent _agent _agent
│ │
weather tools Tavily search
(Visual Crossing API) (booking.com,
hotels.com, etc.)
```

- The **root agent** maintains a per-session context template (city, date range, language) and routes to the correct child agent.
- Every response returns a human-readable text block **plus** one fenced JSON block (`weather-json` or `hotel-json`) that the frontend parses to render the side panel.
- The FastAPI layer validates the structured payload with Pydantic before sending it to the client.

## Google ADK Agent Graph

```text
backend/agent_system/src/multi_tool_agent/
├── agent.py # root agent stitched into FastAPI
├── prompt.py # global instructions/shared context template
├── agent.py # root agent wired into FastAPI
├── prompt.py # routing logic + shared context template
├── sub_agents/
│ ├── get_weather/
│ │ ├── agent.py # specialist enforcing weather-json output contract
│ │ ├── agent.py # enforces weather-json output contract
│ │ └── prompt.py
│ ├── travel_advice/
│ │ ├── agent.py # suggests activities based on current weather
│ │ └── prompt.py
│ └── travel_advice/
│ ├── agent.py # specialist suggesting places/activities based on weather
│ └── search_hotels/
│ ├── agent.py # calls Tavily, extracts hotel data, returns hotel-json
│ └── prompt.py
└── tools/
├── get_current_weather.py # wraps backend weather service: current conditions
├── get_forecast.py # wraps backend weather service: forecast
├── get_history_weather.py # wraps backend weather service: historical data
└── send_email.py # SMTP helper (currently unused in the agent graph)
├── tools/
│ ├── get_current_weather.py # Visual Crossing API — current conditions
│ ├── get_forecast.py # Visual Crossing API — 15-day forecast
│ ├── get_history_weather.py # Visual Crossing API — historical date range
│ └── search_hotels.py # Tavily web search — hotels in a city
└── templates/
├── json_format.py # all JSON output schemas (current/forecast/history/hotels)
└── context_template.py # shared session context passed to all agents
```

## JSON Output Schemas

All responses include one fenced JSON block. The frontend uses `meta.kind` to select the correct rendering panel.

### `weather-json` — weather and forecast data

**`kind: "current"`**
```json
{
"meta": { "city": "Krakow", "kind": "current", "date": "2025-08-01", "date_range": null, "language": "en" },
"current": {
"temp": 22, "tempmax": 25, "tempmin": 16,
"windspeed": 14, "winddir": 200, "pressure": 1012,
"humidity": 60, "sunrise": "05:10", "sunset": "20:30",
"conditions": "Partly cloudy"
}
}
```

**`kind: "forecast"` / `kind: "history"`**
```json
{
"meta": { "city": "Warsaw", "kind": "forecast", "date": null, "date_range": "2025-08-01..2025-08-15", "language": "pl" },
"days": [
{ "datetime": "2025-08-01", "temp": 24, "tempmax": 27, "tempmin": 18, "windspeed": 10, "winddir": 180,
"conditions": "Sunny", "sunrise": "05:20", "sunset": "20:10", "pressure": 1015, "humidity": 55 }
]
}
```

- The root agent loads into a `google.adk.runners.Runner`, maintains the chat context (city, date range, unit system, language), and decides when to delegate to child agents.
- The weather specialist ensures every weather reply follows the `weather-json` contract and only calls tools when the context is complete.
- The travel advice specialist reads the same context (city + current weather) and returns three short, human‑friendly suggestions (places/activities) tailored to the conditions (sun, rain, heat, cold, wind) without any JSON.
- Tool wrappers read normalized weather data from the backend service, keeping the agent grounded on deterministic values instead of free-form hallucinations.
### `hotel-json` — hotel search results

**`kind: "hotels"`**
```json
{
"meta": { "city": "Paris", "kind": "hotels", "date": null, "date_range": "2025-08-10..2025-08-17", "language": "en" },
"hotels": [
{
"name": "Hotel Le Marais",
"price_per_night": "145",
"currency": "EUR",
"availability": "available",
"rating": 8.7,
"reviews_count": 2340,
"highlights": ["Central location", "Great breakfast", "Friendly staff"],
"url": "https://www.booking.com/..."
}
]
}
```

### Example conversations
## Frontend Components

- **Pure weather:**
- "What is the current weather in Krakow?"
- "What will the weather be in Paris tomorrow?"
- **Weather‑aware travel advice (two‑step flow):**
- Step 1: "What is the current weather in Lisbon?"
- Step 2: "Given this weather, what are three things worth visiting in Lisbon?"
- Under the hood, the root agent first calls `get_weather_agent` and then `travel_advice_agent`, which uses the current weather data for the city.
```text
frontend/src/app/
├── views/
│ └── ChatPage.tsx # two-column layout: Chat (left) + panel (right)
├── components/
│ ├── Chat/Chat.tsx # message input, session management, API calls
│ ├── AiWeatherPanel/ # right panel — switches view based on meta.kind
│ ├── WeatherView/ # renders current weather data
│ ├── DayList/ # renders forecast / history day list
│ └── HotelView/HotelView.tsx # renders hotel cards (price, rating, highlights, booking link)
├── utils/
│ └── parseAiMessage.ts # splits human text from weather-json / hotel-json fence
└── types/
├── aiChat.ts # AiMeta, AiChatData (current | days | hotels)
└── hotelTypes.ts # Hotel, HotelMeta, HotelPayload
```

## Local Development (quick start)
The `AiWeatherPanel` uses `meta.kind` to decide which component to render:

Requirements: Python 3.12 with `uv`, Node.js 18+, and API keys for Google Generative AI + Visual Crossing.
| `meta.kind` | Rendered component |
|----------------------|--------------------|
| `current` | `WeatherView` |
| `forecast`/`history` | `DayList` |
| `hotels` | `HotelView` |
| `travel_advice` | *(text only)* |

## Backend API

| Method | Path | Description |
|--------|--------------|--------------------------------------|
| `POST` | `/api/chat` | Send a message; returns `ChatResponse` |
| `GET` | `/api/health`| Health check + env/service status |

`ChatResponse` shape:
```json
{ "success": true, "data": { "message": "<text + fenced json>", "sender": "ai" }, "session_id": "..." }
```

## Example Conversations

**Weather:**
- "What is the current weather in Krakow?"
- "Show me the forecast for Paris for the next 7 days."
- "What was the weather in London last week?" *(asks for date range)*

**Travel advice** *(two-step)*:
1. "What is the current weather in Lisbon?"
2. "Given this weather, what are three things worth visiting?" → `travel_advice_agent` uses the cached weather context

**Hotel search:**
- "Find hotels in Rome"
- "Search for hotels in Warsaw for August 10–17"
- "Znajdź hotele w Krakowie na przyszły tydzień"

## Environment Variables

| Variable | Required | Description |
|---------------------------|----------|----------------------------------------------|
| `GOOGLE_API_KEY` | ✅ | Google Generative AI key for ADK agents |
| `VISUAL_CROSSING_API_KEY` | ✅ | Visual Crossing Weather API key |
| `TAVILY_API_KEY` | ✅ | Tavily search API key (hotel search) |
| `MODEL` | optional | Gemini model ID (default: `gemini-2.5-flash`)|
| `PUBLIC_WEB_ORIGIN` | optional | Public domain added to CORS allowed origins |
| `ENVIRONMENT` | optional | Set to `production` to enforce required vars |

Get your free Tavily key at [tavily.com](https://tavily.com) — the free tier provides 1000 requests/month.

## Local Development

Requirements: Python 3.12 with `uv`, Node.js 18+.

```bash
# 1. Backend
cd backend
uv sync
source ../env-scratchpad.sh # exports GOOGLE_API_KEY, VISUAL_CROSSING_API_KEY, etc.
uv sync # installs all deps including tavily-python
source ../env-scratchpad.sh # exports GOOGLE_API_KEY, VISUAL_CROSSING_API_KEY, TAVILY_API_KEY
uv run uvicorn api.main:app --reload --port 8000

# 2. Frontend (new terminal)
cd frontend
npm install
# Optional: point the frontend straight at FastAPI without Nginx
export VITE_API_URL=http://localhost:8000
npm run dev

# App will be available at:
# http://localhost:5173
# App: http://localhost:5173
# API: http://localhost:8000/api/health
```

## Docker (single container)
Expand All @@ -72,6 +209,8 @@ source env-scratchpad.sh
./deploy-production.sh
```

The script builds the multi-stage image (backend builder → frontend (Vite) builder → runtime), launches Nginx on port 80, and proxies `/api` requests to FastAPI. Use `curl http://localhost/api/health` to confirm the stack is ready.
Builds a multi-stage image (backend + Vite frontend), starts Nginx on port 80 and proxies `/api/*` to FastAPI.

Visit `http://localhost`, sign in, and start chatting—the assistant will stream Google ADK-backed weather answers into the single-page experience.
```bash
curl http://localhost/api/health
```
Empty file.
Empty file.
Empty file.
Loading
Loading