-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from mohdjami/jami/multi-agent
[FEAT] - Multi Agent AI Travel Planner
- Loading branch information
Showing
22 changed files
with
670 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.