mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-06 02:36:49 +00:00
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:
parent
e298efabb4
commit
9e2cef07e1
3 changed files with 137 additions and 29 deletions
|
|
@ -1,58 +1,89 @@
|
||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/openai/openai-go"
|
||||||
|
"github.com/openai/openai-go/responses"
|
||||||
"github.com/rjNemo/rentease/internal/service/booking"
|
"github.com/rjNemo/rentease/internal/service/booking"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BookingAgentParser struct {
|
type BookingAgentParser struct {
|
||||||
baseUrl string
|
systemPrompt string
|
||||||
|
llmClient openai.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBookingAgentParser(baseUrl string) *BookingAgentParser {
|
func NewBookingAgentParser() *BookingAgentParser {
|
||||||
return &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) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error sending request to booking agent parser: %w", err)
|
return nil, fmt.Errorf("error sending request to OpenAI: %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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
var r struct {
|
||||||
Data ResponseData `json:"data"`
|
Data ResponseData `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var r Response
|
// The LLM is instructed to return the JSON for ResponseData directly, so parse it
|
||||||
err = json.NewDecoder(resp.Body).Decode(&r)
|
err = json.Unmarshal([]byte(resp.OutputText()), &r)
|
||||||
if err != nil {
|
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
|
var b booking.Booking
|
||||||
|
|
|
||||||
77
internal/driver/parser/parser_test.go
Normal file
77
internal/driver/parser/parser_test.go
Normal 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 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
2
main.go
2
main.go
|
|
@ -69,7 +69,7 @@ func run(c context.Context) error {
|
||||||
return fmt.Errorf("error starting pdf client %w", err)
|
return fmt.Errorf("error starting pdf client %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
parsingClient := parser.NewBookingAgentParser(appConfig.ParserBaseUrl)
|
parsingClient := parser.NewBookingAgentParser()
|
||||||
|
|
||||||
bookingService, err := booking.NewService(bookingStore, parsingClient, pc)
|
bookingService, err := booking.NewService(bookingStore, parsingClient, pc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue