diff --git a/internal/driver/parser/client.go b/internal/driver/parser/client.go index 073c632..f0ae013 100644 --- a/internal/driver/parser/client.go +++ b/internal/driver/parser/client.go @@ -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 diff --git a/internal/driver/parser/parser_test.go b/internal/driver/parser/parser_test.go new file mode 100644 index 0000000..a4467c8 --- /dev/null +++ b/internal/driver/parser/parser_test.go @@ -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 où 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") + } +} diff --git a/main.go b/main.go index 9f088c7..b84bf00 100644 --- a/main.go +++ b/main.go @@ -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 {