Skip to content

Commit d4d5cd2

Browse files
authored
Merge pull request #1770 from oracle-devrel/lsa5-agent
improved mock_api, changed README
2 parents 26d7850 + 02b233a commit d4d5cd2

18 files changed

+328
-69
lines changed
Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,54 @@
11
# Travel Agent
2-
This repository contains all the code for a demo of a Travel Agent.
2+
This repository contains all the code for a demo of a **Travel Agent**.
33
The AI Agent enables a customer to get information about available destinations and to organize a trip, book flight, hotel...
44

5-
The agent has been developed using OCI Generative AI and LangGraph.
5+
The agent has been developed using **OCI Generative AI** and **LangGraph**.
66

7-
## List of packages
7+
## Configuration
8+
You only need to create a file, named config_private.py, with the value for **COMPARTMENT_OCID**.
9+
The compartment must be a compartment where you have setup the right policies to access OCI Generative AI.
10+
11+
In config.py AUTH_TYPE is set to API_KEY, therefore you need to have in $HOME/.oci the key pair to access OCI.
12+
13+
## List of libraries used
814
* oci
915
* langchain-community
1016
* langgraph
1117
* streamlit
1218
* fastapi
1319
* black
20+
* pydeck
1421
* uvicorn
1522

23+
see also: requirements.txt
24+
25+
## Demo data
26+
Demo data are contained in mock_data.py
27+
28+
If you want some realistic results, you should ask to plan a trip from **Rome** to one
29+
of the following cities:
30+
* Amsterdam
31+
* Barcelona
32+
* Florence
33+
* Madrid
34+
* Valencia
35+
36+
or, simply add other records to the JSON in mock_data.py.
37+
38+
If you want to diplsay the positio of the Hotel in a map, you need to provide in the file
39+
correct values for latitude and longitude.
40+
41+
## Supported languages
42+
As of now, the demo supports:
43+
* English
44+
* Italian
45+
46+
to add other languages, you need to add the translations in translations.py and change, accordingly, some
47+
code in streamlit_app.py.
48+
49+
50+
51+
52+
53+
1654

ai/gen-ai-agents/travel_agent/base_node.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,17 @@ def log_error(self, message: str):
6161
message (str): The error message to log.
6262
"""
6363
self.logger.error("[%s] %s", self.name, message)
64+
65+
def invoke(self, state: dict, config=None, **kwargs) -> dict:
66+
"""
67+
Abstract method to be implemented by subclasses.
68+
69+
Args:
70+
state (dict): The current state of the workflow.
71+
config (optional): Configuration options for the node.
72+
**kwargs: Additional keyword arguments.
73+
74+
Returns:
75+
dict: Updated state after processing.
76+
"""
77+
raise NotImplementedError("Subclasses must implement this method.")

ai/gen-ai-agents/travel_agent/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
]
2020

2121
# OCI GenAI services configuration
22+
23+
# can be also INSTANCE_PRINCIPAL
24+
AUTH_TYPE = "API_KEY"
25+
2226
REGION = "eu-frankfurt-1"
2327
SERVICE_ENDPOINT = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com"
2428

25-
# seems to work with both models
29+
# seems to work fine with both models
2630
MODEL_ID = "meta.llama-3.3-70b-instruct"
2731
# MODEL_ID = "cohere.command-a-03-2025"
2832

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""
2+
Config private template
3+
4+
use it to create your config_private.py file
5+
This file contains sensitive information such as compartment OCIDs, and other
6+
"""
7+
8+
COMPARTMENT_OCID = "ocid1.compartment.xxxx"

ai/gen-ai-agents/travel_agent/mock_api.py

Lines changed: 27 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
A simplified mock FastAPI server with two endpoints:
55
- /search/transport
66
- /search/hotels
7+
8+
mock data in mock_data.py
79
"""
810

911
from fastapi import FastAPI, Query
1012
from fastapi.responses import JSONResponse
13+
from mock_data import hotels_by_city, transport_data
1114

1215
app = FastAPI()
1316

@@ -19,24 +22,33 @@ def search_transport(
1922
transport_type: str = Query(...),
2023
):
2124
"""
22-
Mock endpoint to simulate transport search.
25+
Mock endpoint to simulate transport search from Rome.
2326
Args:
2427
destination (str): Destination city.
2528
start_date (str): Start date of the trip in 'YYYY-MM-DD' format.
26-
transport_type (str): Type of transport (e.g., "airplane", "train").
29+
transport_type (str): Type of transport ("airplane" or "train").
2730
Returns:
2831
JSONResponse: Mocked transport options.
2932
"""
33+
key = destination.strip().lower()
34+
option = transport_data.get(key, {}).get(transport_type.lower())
35+
36+
if not option:
37+
return JSONResponse(content={"options": []}, status_code=404)
38+
39+
departure_time = f"{start_date}T08:00"
40+
duration = option["duration_hours"]
41+
arrival_hour = 8 + int(duration)
42+
arrival_time = f"{start_date}T{arrival_hour:02}:00"
43+
3044
return JSONResponse(
3145
content={
3246
"options": [
3347
{
34-
"provider": (
35-
"TrainItalia" if transport_type == "train" else "Ryanair"
36-
),
37-
"price": 45.50,
38-
"departure": f"{start_date}T09:00",
39-
"arrival": f"{start_date}T13:00",
48+
"provider": option["provider"],
49+
"price": option["price"],
50+
"departure": departure_time,
51+
"arrival": arrival_time,
4052
"type": transport_type,
4153
}
4254
]
@@ -45,7 +57,12 @@ def search_transport(
4557

4658

4759
@app.get("/search/hotels")
48-
def search_hotels(destination: str = Query(...), stars: int = Query(3)):
60+
def search_hotels(
61+
destination: str = Query(...),
62+
start_date: str = Query(...),
63+
num_days: int = Query(1),
64+
stars: int = Query(3),
65+
):
4966
"""
5067
Mock endpoint to simulate hotel search.
5168
Args:
@@ -54,58 +71,11 @@ def search_hotels(destination: str = Query(...), stars: int = Query(3)):
5471
Returns:
5572
JSONResponse: Mocked hotel options.
5673
"""
57-
hotels_by_city = {
58-
"valencia": {
59-
"name": "Hotel Vincci Lys",
60-
"price": 135.0,
61-
"stars": stars,
62-
"location": "Central district",
63-
"amenities": ["WiFi", "Breakfast"],
64-
"latitude": 39.4702,
65-
"longitude": -0.3750,
66-
},
67-
"barcelona": {
68-
"name": "Hotel Jazz",
69-
"price": 160.0,
70-
"stars": stars,
71-
"location": "Eixample",
72-
"amenities": ["WiFi", "Rooftop pool"],
73-
"latitude": 41.3849,
74-
"longitude": 2.1675,
75-
},
76-
"madrid": {
77-
"name": "Only YOU Hotel Atocha",
78-
"price": 170.0,
79-
"stars": stars,
80-
"location": "Retiro",
81-
"amenities": ["WiFi", "Gym", "Restaurant"],
82-
"latitude": 40.4093,
83-
"longitude": -3.6828,
84-
},
85-
"florence": {
86-
"name": "Hotel L'Orologio Firenze",
87-
"price": 185.0,
88-
"stars": stars,
89-
"location": "Santa Maria Novella",
90-
"amenities": ["WiFi", "Spa", "Bar"],
91-
"latitude": 43.7760,
92-
"longitude": 11.2486,
93-
},
94-
"amsterdam": {
95-
"name": "INK Hotel Amsterdam",
96-
"price": 190.0,
97-
"stars": stars,
98-
"location": "City Center",
99-
"amenities": ["WiFi", "Breakfast", "Bar"],
100-
"latitude": 52.3745,
101-
"longitude": 4.8901,
102-
},
103-
}
104-
10574
hotel_key = destination.strip().lower()
10675
hotel = hotels_by_city.get(hotel_key)
10776

10877
if not hotel:
10978
return JSONResponse(content={"hotels": []}, status_code=404)
11079

80+
hotel["stars"] = stars
11181
return JSONResponse(content={"hotels": [hotel]})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
Mock data for API
3+
"""
4+
5+
# Hotel data by city
6+
hotels_by_city = {
7+
"valencia": {
8+
"name": "Hotel Vincci Lys",
9+
"price": 135.0,
10+
"stars": None, # placeholder, updated dynamically
11+
"location": "Central district",
12+
"amenities": ["WiFi", "Breakfast"],
13+
"latitude": 39.4702,
14+
"longitude": -0.3750,
15+
},
16+
"barcelona": {
17+
"name": "Hotel Jazz",
18+
"price": 160.0,
19+
"stars": None,
20+
"location": "Eixample",
21+
"amenities": ["WiFi", "Rooftop pool"],
22+
"latitude": 41.3849,
23+
"longitude": 2.1675,
24+
},
25+
"madrid": {
26+
"name": "Only YOU Hotel Atocha",
27+
"price": 170.0,
28+
"stars": None,
29+
"location": "Retiro",
30+
"amenities": ["WiFi", "Gym", "Restaurant"],
31+
"latitude": 40.4093,
32+
"longitude": -3.6828,
33+
},
34+
"florence": {
35+
"name": "Hotel L'Orologio Firenze",
36+
"price": 185.0,
37+
"stars": None,
38+
"location": "Santa Maria Novella",
39+
"amenities": ["WiFi", "Spa", "Bar"],
40+
"latitude": 43.7760,
41+
"longitude": 11.2486,
42+
},
43+
"amsterdam": {
44+
"name": "INK Hotel Amsterdam",
45+
"price": 190.0,
46+
"stars": None,
47+
"location": "City Center",
48+
"amenities": ["WiFi", "Breakfast", "Bar"],
49+
"latitude": 52.3745,
50+
"longitude": 4.8901,
51+
},
52+
}
53+
54+
# Transport data from Rome
55+
transport_data = {
56+
"valencia": {
57+
"train": {"provider": "TrainItalia", "duration_hours": 15, "price": 120.0},
58+
"airplane": {"provider": "Ryanair", "duration_hours": 2.5, "price": 160.0},
59+
},
60+
"barcelona": {
61+
"train": {"provider": "TrainItalia", "duration_hours": 13, "price": 110.0},
62+
"airplane": {"provider": "Vueling", "duration_hours": 2.0, "price": 155.0},
63+
},
64+
"madrid": {
65+
"train": {"provider": "TrainItalia", "duration_hours": 17, "price": 130.0},
66+
"airplane": {"provider": "Iberia", "duration_hours": 2.5, "price": 165.0},
67+
},
68+
"amsterdam": {
69+
"train": {"provider": "Thalys", "duration_hours": 20, "price": 150.0},
70+
"airplane": {"provider": "KLM", "duration_hours": 2.5, "price": 175.0},
71+
},
72+
"florence": {
73+
"train": {"provider": "Frecciarossa", "duration_hours": 1.5, "price": 30.0},
74+
"airplane": {"provider": "ITA Airways", "duration_hours": 1.0, "price": 190.0},
75+
},
76+
}

ai/gen-ai-agents/travel_agent/model_factory.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
"""
22
Factory for Chat models
3+
4+
This module contains a factory function to create and return a ChatOCIGenAI model instance.
5+
It is designed to be used in the context of an application that interacts with Oracle Cloud
6+
Infrastructure (OCI) Generative AI services.
7+
8+
Author: L. Saetta
9+
Date: 21/05/2025
10+
311
"""
412

513
from langchain_community.chat_models import ChatOCIGenAI
614

7-
from config import MODEL_ID, SERVICE_ENDPOINT
15+
from config import MODEL_ID, SERVICE_ENDPOINT, AUTH_TYPE
816
from config_private import COMPARTMENT_OCID
917

1018

@@ -22,6 +30,7 @@ def get_chat_model(
2230
"""
2331
# Create and return the chat model
2432
return ChatOCIGenAI(
33+
auth_type=AUTH_TYPE,
2534
model_id=model_id,
2635
service_endpoint=service_endpoint,
2736
model_kwargs={"temperature": temperature, "max_tokens": max_tokens},
Binary file not shown.
Binary file not shown.
Binary file not shown.

ai/gen-ai-agents/travel_agent/nodes/generate_itinerary_node.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
"""
1212

1313
from base_node import BaseNode
14-
from langchain_core.runnables import Runnable
15-
from langchain_core.output_parsers import StrOutputParser
1614
from model_factory import get_chat_model
1715
from config import MODEL_ID, SERVICE_ENDPOINT, MAX_TOKENS, DEBUG
1816
from translations import TRANSLATIONS
1917

2018

2119
class GenerateItineraryNode(BaseNode):
20+
"""
21+
Node in the LangGraph workflow responsible for generating a personalized travel itinerary.
22+
"""
2223
def __init__(self):
2324
super().__init__("GenerateItineraryNode")
2425

@@ -47,9 +48,9 @@ def invoke(self, state: dict, config=None, **kwargs) -> dict:
4748

4849
if DEBUG:
4950
self.log_info("Generating itinerary...")
50-
51+
5152
response = self.llm.invoke(itinerary_prompt).content
5253

5354
state["final_plan"] += f"\n\n{t['suggested_itinerary_title']}\n{response}"
54-
55+
5556
return state

ai/gen-ai-agents/travel_agent/nodes/hotel_node.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ def __init__(self):
2626
def invoke(self, state: dict, config=None, **kwargs) -> dict:
2727
self.log_info("Searching for hotels")
2828
try:
29+
num_days = state.get("num_days", 1)
30+
start_date = state.get("start_date")
2931
prefs = state.get("hotel_preferences", {})
3032

3133
# here we call the API
3234
response = requests.get(
3335
HOTEL_API_URL,
3436
params={
3537
"destination": state.get("destination"),
38+
"start_date": start_date,
39+
"num_days": num_days,
3640
# default 3 stars
3741
"stars": prefs.get("stars", 3),
3842
},

ai/gen-ai-agents/travel_agent/nodes/router_node.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from base_node import BaseNode
1616
from model_factory import get_chat_model
1717
from prompt_template import router_prompt
18-
from config import MODEL_ID, SERVICE_ENDPOINT, DEBUG
18+
from config import MODEL_ID, SERVICE_ENDPOINT
1919

2020

2121
class RouterNode(BaseNode):

0 commit comments

Comments
 (0)