Skip to content

Commit 0d33804

Browse files
authored
Merge pull request #1768 from oracle-devrel/lsa-branch4
first commit, initial code
2 parents ec747ba + 5b5adc0 commit 0d33804

28 files changed

+1383
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Travel Agent
2+
This repository contains all the code for a demo of a Travel Agent.
3+
The AI Agent enables a customer to get information about available destinations and to organize a trip, book flight, hotel...
4+
5+
The agent has been developed using OCI Generative AI and LangGraph.
6+
7+
## List of packages
8+
* oci
9+
* langchain-community
10+
* langgraph
11+
* streamlit
12+
* fastapi
13+
* black
14+
* uvicorn
15+
16+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Base Node class for LangGraph nodes.
3+
4+
This module defines a base class `BaseNode` for all LangGraph nodes,
5+
providing a standard logging interface via `log_info` and `log_error` methods.
6+
Each subclass should implement the `invoke(input, config=None)` method.
7+
"""
8+
9+
import logging
10+
from langchain_core.runnables import Runnable
11+
12+
13+
class BaseNode(Runnable):
14+
"""
15+
Abstract base class for LangGraph nodes.
16+
17+
All node classes in the graph should inherit from this base class.
18+
It provides convenient logging utilities and stores a unique node name
19+
for identification in logs and debugging.
20+
21+
Attributes:
22+
name (str): Identifier for the node, used in logging.
23+
logger (logging.Logger): Configured logger instance for the node.
24+
"""
25+
26+
def __init__(self, name: str):
27+
"""
28+
Initialize the base node with a logger.
29+
30+
Args:
31+
name (str): Unique name of the node for logging purposes.
32+
"""
33+
self.name = name
34+
self.logger = logging.getLogger(name)
35+
self.logger.setLevel(logging.INFO)
36+
37+
# Attach a default console handler if no handlers are present
38+
if not self.logger.handlers:
39+
handler = logging.StreamHandler()
40+
handler.setLevel(logging.INFO)
41+
formatter = logging.Formatter(
42+
"[%(asctime)s] %(levelname)s in %(name)s: %(message)s"
43+
)
44+
handler.setFormatter(formatter)
45+
self.logger.addHandler(handler)
46+
47+
def log_info(self, message: str):
48+
"""
49+
Log an informational message.
50+
51+
Args:
52+
message (str): The message to log.
53+
"""
54+
self.logger.info("[%s] %s", self.name, message)
55+
56+
def log_error(self, message: str):
57+
"""
58+
Log an error message.
59+
60+
Args:
61+
message (str): The error message to log.
62+
"""
63+
self.logger.error("[%s] %s", self.name, message)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
General configuration options
3+
"""
4+
5+
#
6+
# application configs
7+
#
8+
DEBUG = False
9+
10+
# this is the list of the mandatory fields in user input
11+
# if any of these fields is missing, the agent will ask for clarification
12+
REQUIRED_FIELDS = [
13+
"place_of_departure",
14+
"destination",
15+
"start_date",
16+
"end_date",
17+
"num_persons",
18+
"transport_type",
19+
]
20+
21+
# OCI GenAI services configuration
22+
REGION = "eu-frankfurt-1"
23+
SERVICE_ENDPOINT = f"https://inference.generativeai.{REGION}.oci.oraclecloud.com"
24+
25+
# seems to work with both models
26+
MODEL_ID = "meta.llama-3.3-70b-instruct"
27+
# MODEL_ID = "cohere.command-a-03-2025"
28+
29+
MAX_TOKENS = 2048
30+
31+
# Mock API configuration
32+
HOTEL_API_URL = "http://localhost:8000/search/hotels"
33+
TRANSPORT_API_URL = "http://localhost:8000/search/transport"
34+
35+
# Hotel Map
36+
MAP_STYLE = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
mock_api.py
3+
4+
A simplified mock FastAPI server with two endpoints:
5+
- /search/transport
6+
- /search/hotels
7+
"""
8+
9+
from fastapi import FastAPI, Query
10+
from fastapi.responses import JSONResponse
11+
12+
app = FastAPI()
13+
14+
15+
@app.get("/search/transport")
16+
def search_transport(
17+
destination: str = Query(...),
18+
start_date: str = Query(...),
19+
transport_type: str = Query(...),
20+
):
21+
"""
22+
Mock endpoint to simulate transport search.
23+
Args:
24+
destination (str): Destination city.
25+
start_date (str): Start date of the trip in 'YYYY-MM-DD' format.
26+
transport_type (str): Type of transport (e.g., "airplane", "train").
27+
Returns:
28+
JSONResponse: Mocked transport options.
29+
"""
30+
return JSONResponse(
31+
content={
32+
"options": [
33+
{
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",
40+
"type": transport_type,
41+
}
42+
]
43+
}
44+
)
45+
46+
47+
@app.get("/search/hotels")
48+
def search_hotels(destination: str = Query(...), stars: int = Query(3)):
49+
"""
50+
Mock endpoint to simulate hotel search.
51+
Args:
52+
destination (str): Destination city.
53+
stars (int): Number of stars for hotel preference.
54+
Returns:
55+
JSONResponse: Mocked hotel options.
56+
"""
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+
105+
hotel_key = destination.strip().lower()
106+
hotel = hotels_by_city.get(hotel_key)
107+
108+
if not hotel:
109+
return JSONResponse(content={"hotels": []}, status_code=404)
110+
111+
return JSONResponse(content={"hotels": [hotel]})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Factory for Chat models
3+
"""
4+
5+
from langchain_community.chat_models import ChatOCIGenAI
6+
7+
from config import MODEL_ID, SERVICE_ENDPOINT
8+
from config_private import COMPARTMENT_OCID
9+
10+
11+
def get_chat_model(
12+
model_id: str = MODEL_ID,
13+
service_endpoint: str = SERVICE_ENDPOINT,
14+
temperature=0,
15+
max_tokens=2048,
16+
) -> ChatOCIGenAI:
17+
"""
18+
Factory function to create and return a ChatOCIGenAI model instance.
19+
20+
Returns:
21+
ChatOCIGenAI: Configured chat model instance.
22+
"""
23+
# Create and return the chat model
24+
return ChatOCIGenAI(
25+
model_id=model_id,
26+
service_endpoint=service_endpoint,
27+
model_kwargs={"temperature": temperature, "max_tokens": max_tokens},
28+
compartment_id=COMPARTMENT_OCID,
29+
)
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)