Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

servlet: Amadeus flight api #27

Merged
merged 5 commits into from
Dec 23, 2024
Merged
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
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
go 1.23

use (
./servlets/amadeus-flight-api
./servlets/crypto-hash
./servlets/github
./servlets/historical-flight-api
Expand Down
1 change: 1 addition & 0 deletions servlets/amadeus-flight-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
76 changes: 76 additions & 0 deletions servlets/amadeus-flight-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Amadeus Flight API

Amadeus is a multinational tech company that provides software for the global travel and tourism industry.

The company's core business involves connecting travel providers (like airlines, hotels, and car rental companies) with travel sellers (such as travel agencies and websites). Their systems help manage reservations, inventory, and departure control for airlines, while also providing booking capabilities for travel agents and online travel platforms.

One of Amadeus's key strengths is its flight booking system, which processes hundreds of millions of bookings annually. They also provide important technological infrastructure that powers many airline websites and reservation systems worldwide.

This servlet makes available the [Flight Offer Search endpoint](https://developers.amadeus.com/self-service/category/flights/api-doc/flight-offers-search) as a tool.

NOTE: Depending on the credentials and host you will use, you might get real-time or simulated data.

You can get free development access to the test endpoints by creating an account at https://developers.amadeus.com/register

## Config

- `base-url`: the base URL of the API. For the test system use `https://test.api.amadeus.com`
- `api-key`: your self-service or production API key
- `api-secret`: your self-service or production API Secret

## Hosts

- You should allow access to `*.amadeus.com` or at least `test.api.amadeus.com`

## Example:

— hi Claude I'd like to spend my holidays in Paris, can you find me a flight from Milan Malpensa on Xmas eve, and return on January new year's day?


<blockquote>
I'll help you search for flights from Milan Malpensa (MXP) to Paris for the holiday season.

I've found several flight options for your Christmas holiday trip to Paris. All flights are operated by Lufthansa and include a stopover in Munich. Here are the best options:

1. **Afternoon Departure (13:15)**
- Outbound: Dec 24, MXP 13:15 → CDG 17:25 (4h 10m total)
- Return: Jan 1, CDG 09:25 → MXP 12:35 (3h 10m total)
- Price: €241.32

2. **Early Morning Departure (06:00)**
- Outbound: Dec 24, MXP 06:00 → CDG 11:30 (5h 30m total)
- Return: Jan 1, CDG 09:25 → MXP 12:35 (3h 10m total)
- Price: €241.32

All flights include:
- 1 checked bag
- Standard seat reservation
- Option to purchase meals
- Possibility to change flights (with fee)

Would you like to know more details about any of these options or would you like me to search for different times?
</blockquote>

— is there any non-stop option?

<blockquote>
Let me search for direct flights between Milan Malpensa and Paris.

Yes, I found several direct flight options! You have choices between Air France (flying to Charles de Gaulle - CDG) and Vueling (flying to Orly - ORY). Here are the best options:

1. **Air France to CDG**
- Outbound: Dec 24, MXP 18:40 → CDG 20:15 (1h 35m)
- Return: Jan 1, CDG 09:05 → MXP 10:35 (1h 30m)
- Price: €247.16
- Includes: 1 checked bag, snack, beverage, and standard seat selection

2. **Vueling to ORY**
- Outbound: Dec 24, MXP 20:45 → ORY 22:15 (1h 30m)
- Return: Jan 1, ORY 06:40 → MXP 08:10 (1h 30m)
- Price: €250.62
- Includes: 25kg checked baggage allowance

These direct flights are only slightly more expensive than the connecting flights (around €5-10 more) but save you 2-4 hours of travel time each way. Would you like me to provide more details about any of these options?

Note that the Air France flight arrives at Charles de Gaulle (CDG), which is connected to central Paris by RER B train, while the Vueling flight uses Orly (ORY), which is typically closer to southern Paris and connected by the Orlyval shuttle to RER B.
</blockquote>
150 changes: 150 additions & 0 deletions servlets/amadeus-flight-api/flights.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package main

import (
"fmt"
"net/url"
"strconv"
"strings"

pdk "github.com/extism/go-pdk"
)

var (
FlightOfferSearchTool = ToolDescription{
Name: "am-flights-offer-search",
Description: "Search for flight offers. For each itinerary, the API provides a list of flight offers with prices, " +
"fare details, airline names, baggage allowances and departure terminals. " +
"The results can be displayed as a table. Users should be offered to display such a table",
InputSchema: schema{
"type": "object",
"properties": props{
"originLocationCode": prop("string", "city/airport IATA code from which the traveler will depart, e.g. BOS for Boston"),
"destinationLocationCode": prop("string", "city/airport IATA code to which the traveler is going, e.g. PAR for Paris"),
"departureDate": prop("string", "the date on which the traveler will depart from the origin to go to the destination. Dates are specified in the ISO 8601 YYYY-MM-DD format, e.g. 2017-12-25"),
"returnDate": prop("string", "the date on which the traveler will depart from the destination to return to the origin. "+
"If this parameter is not specified, only one-way itineraries are found. "+
"If this parameter is specified, only round-trip itineraries are found. Dates are specified in the ISO 8601 YYYY-MM-DD format, e.g. 2018-02-28"),
"adults": prop("integer", "the number of adult travelers (age 12 or older on date of departure)."+
"The total number of seated travelers (adult and children) cannot exceed 9."),
"children": prop("integer", "(optional) The number of children"),
"infants": prop("integer", "(optional) The number of infants"),
"travelClass": prop("string", "most of the flight time should be spent in a cabin of this quality or higher. "+
"The accepted travel class is economy, premium economy, business or first class. If no travel class is specified, "+
"the search considers any travel class. Available values : ECONOMY, PREMIUM_ECONOMY, BUSINESS, FIRST"),
"nonStop": prop("boolean", "If set to true, the search will find only flights going from the origin to the destination with no stop in between"),
"currencyCode": prop("string", "The preferred currency for the flight offers. Currency is specified in the ISO 4217 format, e.g. EUR for Euro"),
"maxPrice": prop("integer", "The maximum price of the flight offers. If specified, the value should be a positive number with no decimals"),
"max": prop("integer", "The maximum number of offers to return (default: 10)"),
},
"required": []string{"originLocationCode", "destinationLocationCode", "departureDate", "adults", "travelClass", "currencyCode"},
},
}

FlightsTools = []ToolDescription{
FlightOfferSearchTool,
}
)

// https://developers.amadeus.com/self-service/category/flights/api-doc/flight-offers-search
func flightOfferSearch(args map[string]any) CallToolResult {
const endpoint = "/v2/shopping/flight-offers"

q := url.Values{}
q.Set("originLocationCode", args["originLocationCode"].(string))
q.Set("destinationLocationCode", args["destinationLocationCode"].(string))
q.Set("departureDate", args["departureDate"].(string))

if returnDate, hasReturnDate := getString(args, "returnDate"); hasReturnDate {
q.Set("returnDate", returnDate)
}

if adults, hasAdults := getNumber(args, "adults"); hasAdults {
q.Set("adults", fmt.Sprint(adults))
}

children, _ := getNumber(args, "children")
q.Set("children", fmt.Sprint(children))

infants, _ := getNumber(args, "infants")
q.Set("infants", fmt.Sprint(infants))

if travelClass, hasTravelClass := getString(args, "travelClass"); hasTravelClass {
q.Set("travelClass", strings.ToUpper(travelClass))
} else {
q.Set("travelClass", "ECONOMY")
}

nonStop, _ := getBool(args, "nonStop")

q.Set("nonStop", fmt.Sprint(nonStop))

if currencyCode, hasCurrencyCode := getString(args, "currencyCode"); hasCurrencyCode {
q.Set("currencyCode", currencyCode)
} else {
q.Set("currencyCode", "USD")
}

if maxPrice, hasMaxPrice := getString(args, "maxPrice"); hasMaxPrice {
q.Set("maxPrice", fmt.Sprint(maxPrice))
}
if max, hasMax := getNumber(args, "max"); hasMax {
q.Set("max", fmt.Sprint(max))
} else {
q.Set("max", fmt.Sprint(10))
}

req := pdk.NewHTTPRequest(pdk.MethodGet, config.baseUrl+endpoint+"?"+q.Encode())
req.SetHeader("Authorization", "Bearer "+config.token)
req.SetHeader("Accept", "application/json")
resp := req.Send()

if resp.Status() != 200 {
return callToolError("error while searching for flight offers " + string(resp.Body()))
}

return callToolSuccess(string(resp.Body()))
}

func getNumber(args map[string]any, key string) (float64, bool) {
if n, ok := args[key]; ok && n != nil {
if i, ok := n.(float64); ok {
return i, true
}
if i, ok := n.(int); ok {
return float64(i), true
}
if i, ok := n.(string); ok {
if f, err := strconv.ParseFloat(i, 64); err == nil {
return f, true
}
}
}
return 0, false
}

func getBool(args map[string]any, key string) (bool, bool) {
if n, ok := args[key]; ok && n != nil {
if i, ok := n.(bool); ok {
return i, true
}
if i, ok := n.(string); ok {
if f, err := strconv.ParseBool(i); err == nil {
return f, true
}
}
}
return false, false
}

func getString(args map[string]any, key string) (string, bool) {
if s, ok := args[key]; ok && s != nil {
if ss, ok := s.(string); ok {
ss = strings.TrimSpace(ss)
if ss == "" {
return "", false
}
return ss, ok
}
}
return "", false
}
5 changes: 5 additions & 0 deletions servlets/amadeus-flight-api/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module amadeus-flight-api

go 1.23

require github.com/extism/go-pdk v1.0.5
2 changes: 2 additions & 0 deletions servlets/amadeus-flight-api/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/extism/go-pdk v1.0.5 h1:5d5yYkWBweBP84Z+H3DP5DsD0fwvf2anWXyypCXpSW8=
github.com/extism/go-pdk v1.0.5/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
119 changes: 119 additions & 0 deletions servlets/amadeus-flight-api/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Note: run `go doc -all` in this package to see all of the types and functions available.
// ./pdk.gen.go contains the domain types from the host where your plugin will run.
package main

import (
"encoding/json"
"errors"
"fmt"
"time"

"github.com/extism/go-pdk"
)

var (
config = Config{}
)

// Called when the tool is invoked.
// If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
// It takes CallToolRequest as input (The incoming tool request from the LLM)
// And returns CallToolResult (The servlet's response to the given tool call)
func Call(input CallToolRequest) (CallToolResult, error) {
if err := loadConfig(); err != nil {
return callToolError(err.Error()), nil
}
refreshToken()
args := input.Params.Arguments.(map[string]any)
switch input.Params.Name {
case FlightOfferSearchTool.Name:
return flightOfferSearch(args), nil
default:
return CallToolResult{}, fmt.Errorf("unknown tool name %q", input.Params.Name)
}
}

// Called by mcpx to understand how and why to use this tool.
// Note: Your servlet configs will not be set when this function is called,
// so do not rely on config in this function
// And returns ListToolsResult (The tools' descriptions, supporting multiple tools from a single servlet.)
func Describe() (ListToolsResult, error) {
return ListToolsResult{
Tools: FlightsTools,
}, nil
}

func loadConfig() error {
if config.apiKey != "" && config.apiSecret != "" {
return nil
}

var okU, okK, okS bool
config.baseUrl, okU = pdk.GetConfig("base-url")
config.apiKey, okK = pdk.GetConfig("api-key")
config.apiSecret, okS = pdk.GetConfig("api-secret")
if !okU || !okK || !okS {
return errors.New("missing required configuration")
}

return nil
}

func refreshToken() {
if time.Now().UTC().Unix() > config.expiration {
req := pdk.NewHTTPRequest(pdk.MethodPost, config.baseUrl+"/v1/security/oauth2/token")
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
form := "grant_type=client_credentials&client_id=" + config.apiKey + "&client_secret=" + config.apiSecret
pdk.Log(pdk.LogDebug, form)
req.SetBody([]byte(form))
resp := req.Send()
if resp.Status() == 200 {
var res map[string]any
json.Unmarshal(resp.Body(), &res)
config.token = res["access_token"].(string)
config.expiration = time.Now().UTC().Unix() + int64(res["expires_in"].(float64))
}
}
}

func some[T any](t T) *T {
return &t
}

type SchemaProperty struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
Items *schema `json:"items,omitempty"`
}

func prop(tpe, description string) SchemaProperty {
return SchemaProperty{Type: tpe, Description: description}
}

func arrprop(tpe, description, itemstpe string) SchemaProperty {
items := schema{"type": itemstpe}
return SchemaProperty{Type: tpe, Description: description, Items: &items}
}

type schema = map[string]any
type props = map[string]SchemaProperty

func callToolSuccess(msg string) (res CallToolResult) {
res.Content = []Content{{Type: ContentTypeText, Text: some(msg)}}
return
}

func callToolError(msg string) (res CallToolResult) {
res.IsError = some(true)
res.Content = []Content{{Type: ContentTypeText, Text: some(msg)}}
return
}

type Config struct {
// grant_type=client_credentials&client_id={client_id}&client_secret={client_secret}
baseUrl string
apiKey string
apiSecret string
token string
expiration int64
}
Loading
Loading