feat(parser): refactor BookingAgentParser to use OpenAI client and add parsing tests

- Update BookingAgentParser to utilize OpenAI's client for parsing booking data.
- Remove base URL dependency and streamline the initialization process.
- Introduce ResponseData struct to define expected JSON structure from the LLM.
- Add unit tests for BookingAgentParser to validate parsing logic and expected output.
This commit is contained in:
Ruidy 2025-05-04 23:18:42 +02:00
parent e298efabb4
commit 9e2cef07e1
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
3 changed files with 137 additions and 29 deletions

View file

@ -1,58 +1,89 @@
package parser
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"
"github.com/openai/openai-go"
"github.com/openai/openai-go/responses"
"github.com/rjNemo/rentease/internal/service/booking"
)
type BookingAgentParser struct {
baseUrl string
systemPrompt string
llmClient openai.Client
}
func NewBookingAgentParser(baseUrl string) *BookingAgentParser {
func NewBookingAgentParser() *BookingAgentParser {
return &BookingAgentParser{
baseUrl: baseUrl,
llmClient: openai.NewClient(),
systemPrompt: ` Extract the following fields from the booking content and return a JSON object with this exact structure (all fields required, use null or empty string if not available):
{
"data": {
"arrival_date": "YYYY-MM-DD",
"booking_fees": "number as string",
"booking_id": "string",
"departure_date": "YYYY-MM-DD",
"stay_length": int,
"guest_email": "string",
"guest_name": "string",
"guest_number": int,
"guest_phone": "string",
"room_booked": "string",
"standard_rate": "number as string",
"special_requests": "string"
}
}
Respond ONLY with the JSON object above, wrapped in a top-level "data" field as shown, no code fences.
Booking content:`,
}
}
// ResponseData is the expected structure returned by the LLM parser.
type ResponseData struct {
ArrivalDate string `json:"arrival_date"`
BookingFees json.Number `json:"booking_fees"`
BookingID string `json:"booking_id"`
DepartureDate string `json:"departure_date"`
StayLength int `json:"stay_length"`
GuestEmail string `json:"guest_email"`
GuestName string `json:"guest_name"`
GuestNumber int `json:"guest_number"`
GuestPhone string `json:"guest_phone"`
RoomBooked string `json:"room_booked"`
StandardRate json.Number `json:"standard_rate"`
SpecialRequests string `json:"special_requests"`
}
func (p *BookingAgentParser) Parse(rawContent string) (*booking.Booking, error) {
log.Println("sending request to booking agent parser")
log.Println("sending request to OpenAI LLM parser")
resp, err := http.Post(fmt.Sprintf("%s/sync?input=%s", p.baseUrl, url.QueryEscape(rawContent)), "application/json", nil)
ctx := context.Background()
prompt := p.systemPrompt + "\n" + rawContent
resp, err := p.llmClient.Responses.New(ctx, responses.ResponseNewParams{
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(prompt)},
Model: "gpt-4o",
})
if err != nil {
return nil, fmt.Errorf("error sending request to booking agent parser: %w", err)
}
defer resp.Body.Close()
type ResponseData struct {
ArrivalDate string `json:"arrival_date"`
BookingFees json.Number `json:"booking_fees"`
BookingID string `json:"booking_id"`
DepartureDate string `json:"departure_date"`
StayLength int `json:"stay_length"`
GuestEmail string `json:"guest_email"`
GuestName string `json:"guest_name"`
GuestNumber int `json:"guest_number"`
GuestPhone string `json:"guest_phone"`
RoomBooked string `json:"room_booked"`
StandardRate json.Number `json:"standard_rate"`
SpecialRequests string `json:"special_requests"`
return nil, fmt.Errorf("error sending request to OpenAI: %w", err)
}
type Response struct {
var r struct {
Data ResponseData `json:"data"`
}
var r Response
err = json.NewDecoder(resp.Body).Decode(&r)
// The LLM is instructed to return the JSON for ResponseData directly, so parse it
err = json.Unmarshal([]byte(resp.OutputText()), &r)
if err != nil {
return nil, fmt.Errorf("error decoding response from booking agent parser: %w", err)
return nil, fmt.Errorf("error decoding response from OpenAI: %w", err)
}
var b booking.Booking

View file

@ -0,0 +1,77 @@
package parser
import (
"testing"
"time"
)
func TestBookingAgentParser_Parse(t *testing.T) {
input := ` Date d'arrivée jeu. 3 avr. 2025 Date de départ dim. 6 avr. 2025 Durée de séjour : 3 nuits Nombre de personnes :
2
Nombre d'hébergements
1
Montant total 186 Nom du client :
Olga Korovina
ru
okorov.905387@guest.booking.com
Contactez vos clients ! Indiquez-leur l'heure à laquelle vous souhaitez les accueillir ou l'endroit ils récupéreront leurs clés. Un simple appel suffit : Afficher le numéro de téléphone Vous pouvez également leur envoyer un e-mail ou un message. Langue préférée
russe
Canal : Booking.com Code IATA/TIDS :
PC029090
Numéro de réservation :
4453602306
Montant soumis à commission : 177 Reçu jeu. 2 janv. 2025 Commission : 31,86 Bloc-notes (usage interne) Ajoutez une note ici
Maison 1 Chambre T2 - VillaFleurie au bourg du Gosier)
186 jeu. 3 avr. 2025 dim. 6 avr. 2025 Nom du client
Olga Korovina
Occupation maximum 2 adultes, 2 enfants (3 personnes max.) Photo de l'hébergement Date Tarif Tarif par nuit
03 - 04 avril
Standard Rate
59
04 - 05 avril
Standard Rate
59
05 - 06 avril
Standard Rate
59Sous-total 177
Taxe de séjour
1.50 par personne et par nuit 9Tarif total de l'hébergement 186 Le tarif comprend 8.9 % de TVA Conversation avec le client
Aucun message
Les conversations avec vos clients apparaîtront ici.
Booking.com reçoit tous les messages écrits ici et les traite selon sa Charte de confidentialité et informations sur les cookies Conditions `
parser := NewBookingAgentParser()
booking, err := parser.Parse(input)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
if booking.Name != "Olga Korovina" {
t.Errorf("expected Name 'Olga Korovina', got '%s'", booking.Name)
}
if booking.Email != "okorov.905387@guest.booking.com" {
t.Errorf("expected Email 'okorov.905387@guest.booking.com', got '%s'", booking.Email)
}
if booking.Platform != "Booking" {
t.Errorf("expected Platform 'Booking', got '%s'", booking.Platform)
}
if booking.ExternalId == nil || *booking.ExternalId != "4453602306" {
t.Errorf("expected ExternalId '4453602306', got '%v'", booking.ExternalId)
}
if !booking.From.Equal(time.Date(2025, 4, 3, 0, 0, 0, 0, time.UTC)) {
t.Errorf("expected From 2025-04-03, got %v", booking.From)
}
if !booking.To.Equal(time.Date(2025, 4, 6, 0, 0, 0, 0, time.UTC)) {
t.Errorf("expected To 2025-04-06, got %v", booking.To)
}
if len(booking.Items) == 0 {
t.Errorf("expected at least one item, got 0")
}
}

View file

@ -69,7 +69,7 @@ func run(c context.Context) error {
return fmt.Errorf("error starting pdf client %w", err)
}
parsingClient := parser.NewBookingAgentParser(appConfig.ParserBaseUrl)
parsingClient := parser.NewBookingAgentParser()
bookingService, err := booking.NewService(bookingStore, parsingClient, pc)
if err != nil {