Skip to content

Commit

Permalink
Merge pull request #6 from mohdjami/jami/multi-agent
Browse files Browse the repository at this point in the history
[FEAT] - Multi Agent AI Travel Planner
  • Loading branch information
mohdjami authored Feb 5, 2025
2 parents 64caaeb + aaacab3 commit 27050b6
Show file tree
Hide file tree
Showing 22 changed files with 670 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,21 @@ yarn-error.log*

# local env files
.env*.local
multi-agent/.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# Python
__pycache__/
*.py[cod]
*$py.class
.Python
*.so
.env
venv/
ENV/
104 changes: 104 additions & 0 deletions multi-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Multi-Agent Travel Planner

This is a backend application for generating personalized travel itineraries using a multi-agent system and language models (LLMs). It takes user preferences as input and outputs a complete itinerary including routes, activities, and cost breakdown.

## Architecture

The system is built using Python and the FastAPI framework. It follows a multi-agent architecture with the following key components:

```mermaid
graph TD
User((User)) --> |Preferences| Server[FastAPI Server]
Server --> |Workflow| TravelService
TravelService --> |Plan Route| RouteAgent
TravelService --> |Plan Activities| ActivitiesAgent
RouteAgent --> LLMService
ActivitiesAgent --> LLMService
LLMService --> |Groq/Gemini| LLMs((Language Models))
```

1. **TravelService**: Orchestrates the itinerary generation process using a StateGraph workflow. Initializes and coordinates the agents.

2. **RouteAgent**: Plans the optimal travel route between the start and end locations. Interacts with an LLM to generate human-friendly route descriptions.

3. **ActivitiesAgent**: Suggests activities for each location based on user preferences and creates a daily schedule. Also uses an LLM for generating activity descriptions.

4. **LLMService**: Provides access to the language models (Groq or Google Gemini) used by the agents for generating text.

5. **Schemas**: Defines the data models for user preferences, route segments, activities, daily itineraries, and the complete itinerary using Pydantic.

6. **Config**: Handles configuration settings using Pydantic Settings and a .env file.

## Interaction Flow

```mermaid
sequenceDiagram
participant User
participant Server
participant TravelService
participant RouteAgent
participant ActivitiesAgent
participant LLMService
User->>Server: POST /generate
Server->>TravelService: generate_itinerary(preferences)
TravelService->>RouteAgent: plan_route(start, end)
RouteAgent->>LLMService: get_llm("route")
LLMService->>RouteAgent: LLM response
RouteAgent->>TravelService: route_segments, cost_breakdown
TravelService->>ActivitiesAgent: recommend_activities(location, interests)
ActivitiesAgent->>LLMService: get_llm("activities")
LLMService->>ActivitiesAgent: LLM response
ActivitiesAgent->>TravelService: daily_itineraries
TravelService->>Server: complete_itinerary
Server->>User: Itinerary Response
```

1. User sends a POST request to the `/generate` endpoint with their travel preferences.

2. The `TravelService` creates a StateGraph workflow with nodes for route planning, activity planning, and finalization.

3. The `RouteAgent` plans the overall route:
- Sends a prompt to the LLM (via `LLMService`) to describe the route
- Parses the LLM response to extract structured route segments
- Calculates cost breakdown for transportation and other categories

4. The `ActivitiesAgent` suggests activities for each location:
- Queries the LLM for activity ideas based on user preferences
- Parses activity details from the LLM response
- Creates a daily schedule with selected activities

5. The `TravelService` combines the route and activity results into a `CompleteItinerary`

6. The itinerary is returned to the user as the response to their POST request

## StateGraph Workflow

```mermaid
stateDiagram-v2
[*] --> RouteAgent
RouteAgent --> ActivitiesAgent
ActivitiesAgent --> TravelService
TravelService --> [*]
```

The `TravelService` uses a `StateGraph` to define the workflow between the `RouteAgent` and `ActivitiesAgent`. This allows for a clear separation of concerns and a modular architecture.

## Potential Improvements

- More robust error handling and retry logic in case of LLM failures
- Caching of frequently requested routes and activities to improve response time
- Integration with real-world booking APIs for flights, hotels, etc.
- Enhanced personalization based on user preferences and past behavior
- Interactive itinerary refinement allowing users to customize their trips
- Improved natural language understanding for more flexible user inputs

## Getting Started

1. Clone the repository
2. Install dependencies: `pip install -r requirements.txt`
3. Set up your `.env` file with API keys for the LLMs
4. Start the server: `uvicorn app.main:app --reload`
5. Send a POST request to `http://localhost:8000/generate` with your travel preferences

Feel free to explore the code and adapt it to your needs. Happy travels!
File renamed without changes.
Empty file.
Binary file not shown.
Binary file not shown.
Binary file not shown.
114 changes: 114 additions & 0 deletions multi-agent/app/agents/activities_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from datetime import date, timedelta
from langchain.agents import Tool, AgentExecutor
from langchain.prompts import ChatPromptTemplate
from app.models.schemas import Activity, DailyItinerary, RouteSegment
from typing import List, Dict
import json
import logging

logger = logging.getLogger(__name__)
class ActivitiesAgent:
def __init__(self, llm_service):
self.llm_service = llm_service
self.tools = self._create_tools()

def _recommend_activities(self, location: str, interests: List[str], budget: float) -> List[Activity]:
llm = self.llm_service.get_llm("activities")

system_message = """You are a local tour guide API. Return only a JSON array of activities.
Each activity must be in this exact format:
[
{
"name": "Activity Name",
"location": "Specific Place in City",
"duration": "02:00:00",
"cost": 50.00,
"description": "Brief description of the activity",
"category": "culture",
"booking_required": true,
"recommended_time": "morning",
"weather_dependent": false
}
]"""

human_message = f"Suggest activities in {location} matching these interests: {', '.join(interests)}. Daily budget: ${budget}"

messages = [
("system", system_message),
("human", human_message)
]

logger.info(f"Requesting activities for {location}")
response = llm.invoke(messages)
logger.info(f"Raw activities response: {response.content}")

try:
content = response.content.strip()
start_idx = content.find('[')
end_idx = content.rfind(']') + 1
json_str = content[start_idx:end_idx]

activities_data = json.loads(json_str)
return [Activity(**activity) for activity in activities_data]
except Exception as e:
logger.error(f"Failed to parse activities response: {str(e)}")
logger.error(f"Response content: {response.content}")
raise ValueError(f"Failed to parse activities: {str(e)}")
def execute(self, state: dict) -> dict:
daily_itineraries = []
current_date = state["user_preferences"]["start_date"]

logger.info("Starting to generate daily itineraries")
while current_date <= state["user_preferences"]["end_date"]:
logger.info(f"Processing date: {current_date}")
location = self._get_location_for_date(current_date, state["route_segments"])
logger.info(f"Location for {current_date}: {location}")
activities = self._recommend_activities(
location,
state["user_preferences"]["interests"],
state["budget_breakdown"].activities / len(state["route_segments"])
)
logger.info(f"Recommended activities for {current_date}: {activities}")

daily_schedule = self._create_daily_schedule(activities, current_date, location)
logger.info(f"Daily schedule for {current_date}: {daily_schedule}")
daily_itineraries.append(daily_schedule)
current_date += timedelta(days=1)

logger.info("Finished generating daily itineraries")
return {"daily_itineraries": daily_itineraries}
def _create_tools(self):
return [
Tool(
name="recommend_activities",
func=self._recommend_activities,
description="Recommend activities based on location and interests"
),
Tool(
name="create_daily_schedule",
func=self._create_daily_schedule,
description="Create detailed daily schedule with activities"
)
]
def _create_daily_schedule(self, activities: List[Activity], date: date, location: str) -> DailyItinerary:
# Simple v1 implementation
daily_cost = sum(activity.cost for activity in activities)

return DailyItinerary(
date=date,
location=location,
activities=activities[:3], # Limit to 3 activities per day for v1
total_cost=daily_cost,
free_time=timedelta(hours=2), # Default free time
accommodation={"type": "hotel", "cost_per_night": "100.00"} # Basic accommodation info
)

def _get_location_for_date(self, target_date: date, route_segments: List[RouteSegment]) -> str:
"""Determine the location for a given date based on route segments"""
logger.info(f"Getting location for date: {target_date}")

# For v1, simple implementation: if it's the first day, use start location
# otherwise use end location of the first segment
if target_date == route_segments[0].start_date:
return route_segments[0].from_location
return route_segments[0].to_location
150 changes: 150 additions & 0 deletions multi-agent/app/agents/route_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from langchain.agents import Tool, AgentExecutor
from langchain.prompts import ChatPromptTemplate
from app.models.schemas import RouteSegment, BudgetBreakdown
from typing import List, Dict
import json
import logging

logger = logging.getLogger(__name__)

from datetime import date
import json
from typing import List, Dict
from langchain.prompts import ChatPromptTemplate
from app.models.schemas import RouteSegment, BudgetBreakdown
import logging
from datetime import date, timedelta
logger = logging.getLogger(__name__)

class RouteAgent:
def __init__(self, llm_service):
self.llm_service = llm_service
self.tools = self._create_tools()

def _serialize_preferences(self, preferences: dict) -> dict:
"""Convert date objects to ISO format strings"""
serialized = preferences.copy()
if 'start_date' in serialized and isinstance(serialized['start_date'], date):
serialized['start_date'] = serialized['start_date'].isoformat()
if 'end_date' in serialized and isinstance(serialized['end_date'], date):
serialized['end_date'] = serialized['end_date'].isoformat()
logger.info(f"Serialized preferences: {serialized}") # Log serialized preferences
return serialized

def _plan_route(self, start: str, end: str, preferences: dict) -> List[RouteSegment]:
try:
llm = self.llm_service.get_llm("route")
logger.info(f"Starting route planning from {start} to {end}")

# Specify exact transport_mode values in the system message
system_message = """You are a route planning API. Return only a JSON array containing route segments.
Each segment must be in this exact format:
[
{
"from_location": "StartCity",
"to_location": "EndCity",
"transport_mode": "train", # MUST be one of: flight, train, bus, car
"departure_time": "09:00",
"arrival_time": "12:00",
"cost": 100.00,
"duration": "03:00:00",
"details": {"service": "Example"}
}
]"""

human_message = f"Create a route from {start} to {end} using train as transport mode."

messages = [
("system", system_message),
("human", human_message)
]

logger.info("Sending prompt to LLM")
response = llm.invoke(messages)
logger.info(f"Raw LLM response received: {response.content}")

# Rest of the implementation remains same...
# Clean and parse response
content = response.content.strip()

# Find JSON array
start_idx = content.find('[')
end_idx = content.rfind(']')

if start_idx == -1 or end_idx == -1:
logger.error("No valid JSON array found in response")
raise ValueError("Invalid response format: no JSON array found")

json_str = content[start_idx:end_idx + 1]
logger.info(f"Extracted JSON string: {json_str}")

try:
route_data = json.loads(json_str)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON: {e}")
raise ValueError(f"Invalid JSON format: {e}")

segments = []
for segment in route_data:
# Convert duration string to timedelta
duration_str = segment['duration']
h, m, s = map(int, duration_str.split(':'))
segment['duration'] = timedelta(hours=h, minutes=m, seconds=s)
segments.append(RouteSegment(**segment))

return segments

except Exception as e:
logger.error(f"Route planning failed: {str(e)}", exc_info=True)
raise ValueError(f"Route planning failed: {str(e)}")


def execute(self, state: dict) -> dict:
try:
logger.info("Starting route agent execution")
logger.info(f"Input state: {state}")

route_segments = self._plan_route(
state["user_preferences"]["start_location"],
state["user_preferences"]["end_location"],
state["user_preferences"]
)

logger.info(f"Route planning completed, calculating costs")
budget = self._calculate_costs(route_segments, state["user_preferences"])

result = {
"route_segments": route_segments,
"budget_breakdown": budget
}
logger.info(f"Execution completed successfully: {result}")
return result

except Exception as e:
logger.error(f"Execution failed: {str(e)}", exc_info=True)
raise

def _calculate_costs(self, route_segments: List[RouteSegment],
preferences: dict) -> BudgetBreakdown:
total_transport = sum(segment.cost for segment in route_segments)

return BudgetBreakdown(
transport=total_transport,
accommodation=preferences["budget"] * 0.4,
activities=preferences["budget"] * 0.3,
food=preferences["budget"] * 0.2,
miscellaneous=preferences["budget"] * 0.1
)
def _create_tools(self):
return [
Tool(
name="plan_route",
func=self._plan_route,
description="Plan optimal route between locations"
),
Tool(
name="calculate_costs",
func=self._calculate_costs,
description="Calculate travel costs and budget allocation"
)
]
Empty file.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 27050b6

Please sign in to comment.