feat: add client parser to get response from LLM

This commit is contained in:
Ruidy 2025-01-12 12:52:47 +01:00
parent b4ffff3c67
commit 49c49f4098
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
6 changed files with 114 additions and 160 deletions

View file

@ -0,0 +1,91 @@
package parser
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"
"github.com/rjNemo/rentease/internal/service/booking"
)
type BookingAgentParser struct {
baseUrl string
}
func NewBookingAgentParser(baseUrl string) *BookingAgentParser {
return &BookingAgentParser{
baseUrl: baseUrl,
}
}
func (p *BookingAgentParser) Parse(rawContent string) (*booking.Booking, error) {
log.Println("sending request to booking agent parser")
resp, err := http.Post(fmt.Sprintf("%s/sync?input=%s", p.baseUrl, url.QueryEscape(rawContent)), "application/json", nil)
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"`
}
type Response struct {
Data ResponseData `json:"data"`
}
var r Response
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
return nil, fmt.Errorf("error decoding response from booking agent parser: %w", err)
}
var b booking.Booking
b.From, err = time.Parse("2006-01-02", r.Data.ArrivalDate)
if err != nil {
return nil, fmt.Errorf("error parsing arrival_date: %w", err)
}
b.To, err = time.Parse("2006-01-02", r.Data.DepartureDate)
if err != nil {
return nil, fmt.Errorf("error parsing departure_date: %w", err)
}
b.Name = r.Data.GuestName
b.PhoneNumber = r.Data.GuestPhone
b.Email = r.Data.GuestEmail
b.CustomerNumber = r.Data.GuestNumber
bookingFees, err := r.Data.BookingFees.Float64()
if err != nil {
return nil, fmt.Errorf("error parsing booking fees: %w", err)
}
b.Platform = "Booking"
b.PlatformFees = bookingFees
b.ExternalId = &r.Data.BookingID
price, err := r.Data.StandardRate.Float64()
if err != nil {
return nil, fmt.Errorf("error parsing standard rate: %w", err)
}
b.Items = append(b.Items, booking.Item{
Item: r.Data.RoomBooked,
Quantity: r.Data.StayLength,
Price: price,
PaymentMethod: "Card",
})
return &b, nil
}

View file

@ -18,7 +18,7 @@ type Store interface {
Create(b *Booking) error
Update(b *Booking) error
Cancel(id int) error
// Item methods
CreateItem(i *Item) error
PayItem(id int) (*Item, error)
@ -26,15 +26,21 @@ type Store interface {
UpdateItem(id int, item string, paymentMethod string, paymentStatus string, qty int, price float64) (*Item, error)
}
type parserClient interface {
Parse(rawContent string) (*Booking, error)
}
type Service struct {
store Store
parser parserClient
calendar calendar.Client
pdf pdf.Client
}
func NewService(store Store, calendar calendar.Client, pdf pdf.Client) (*Service, error) {
func NewService(store Store, parser parserClient, calendar calendar.Client, pdf pdf.Client) (*Service, error) {
return &Service{
store: store,
parser: parser,
calendar: calendar,
pdf: pdf,
}, nil

View file

@ -52,7 +52,7 @@ func TestService_All(t *testing.T) {
mockStore.On("All").Return(tt.mockData)
// Create service with mocks
service, err := NewService(mockStore, mockCalendar, mockPDF)
service, err := NewService(mockStore, nil, mockCalendar, mockPDF)
assert.NoError(t, err)
// Call the method
@ -134,7 +134,7 @@ func TestService_Search(t *testing.T) {
mockStore.On("Search", tt.searchQuery).Return(tt.expectedLines)
// Create service with mocks
service, err := NewService(mockStore, mockCalendar, mockPDF)
service, err := NewService(mockStore, nil, mockCalendar, mockPDF)
assert.NoError(t, err)
// Call the method

View file

@ -1,170 +1,21 @@
package booking
import (
"fmt"
"log"
"regexp"
"strconv"
"strings"
"time"
"github.com/rjNemo/rentease/internal/config"
)
func (bs Service) ParseFromApi(rawContent string) (*Booking, error) {
b, err := ParseBooking(rawContent)
log.Printf("parsed booking: %+v", b)
b, err := bs.parser.Parse(rawContent)
if err != nil {
return nil, err
}
b = bs.Create(b.From, b.To, b.Name, b.PhoneNumber, b.Email, b.Platform, b.CustomerNumber, b.PlatformFees, b.ExternalId)
itm := b.Items[0]
b = bs.Create(b.From, b.To, b.Name, b.PhoneNumber, b.Email, b.Platform, b.CustomerNumber, b.PlatformFees, b.ExternalId)
if item, ok := config.NewHost().Items[itm.Item]; ok {
bs.CreateItem(b.Id, item, itm.Quantity, itm.Price, itm.PaymentMethod, b.CustomerNumber, b.Platform)
}
return b, nil
}
func ParseBooking(content string) (*Booking, error) {
content = strings.ReplaceAll(strings.TrimSpace(content), "\u00a0", " ")
arrivalDate, err := extractDate(`Date d'arrivée `, content)
if err != nil {
return nil, err
}
departureDate, err := extractDate(`Date de départ `, content)
if err != nil {
return nil, err
}
stayLength, err := extractInt(`Durée de séjour : (\d+) nuits`, content)
if err != nil {
return nil, err
}
customerName, err := extractString(`Nom du client : \n\s+([A-Za-z\s]+)`, content)
if err != nil {
return nil, err
}
customerName = strings.SplitN(customerName, "\n", 2)[0]
customerEmail, err := extractString(`[\w\.\-]+@[\w\.\-]+\.\w+`, content)
if err != nil {
return nil, err
}
customerNumber, err := extractInt(`Nombre de personnes : \s*\n\s*(\d+)`, content)
if err != nil {
return nil, err
}
commissionAmount, err := extractFloat(`Commission : € (\d+,\d+)`, content)
if err != nil {
return nil, err
}
itemName, err := extractString(`Maison . Chambre(s?) \((T2|T3)`, content)
if err != nil {
return nil, err
}
externalId, err := extractString(`Numéro de réservation : \n\s+(\d+)`, content)
if err != nil {
return nil, err
}
standardRate, err := extractFloat(`Standard Rate\n\s+€ (\d+)`, content)
if err != nil {
return nil, err
}
return &Booking{
From: *formatDate(arrivalDate),
To: *formatDate(departureDate),
Name: customerName,
Email: customerEmail,
Platform: "Booking",
CustomerNumber: customerNumber,
PlatformFees: commissionAmount,
ExternalId: &externalId,
Items: []Item{
{
Item: itemName,
Quantity: stayLength,
Price: standardRate,
PaymentMethod: "Card",
},
},
}, nil
}
func extractDate(pattern, content string) (string, error) {
re := regexp.MustCompile(pattern + `(lun|mar|mer|jeu|ven|sam|dim)\. \d{1,2} (janv|févr|mars|avr|mai|juin|juil|août|sept|oct|nov|déc)\.? \d{4}`)
dateMatch := re.FindString(content)
if dateMatch == "" {
return "", fmt.Errorf("date not found")
}
// Regular expression to remove the prefix
rePrefix := regexp.MustCompile(pattern + `\w+\.\s*`)
dateString := rePrefix.ReplaceAllString(dateMatch, "")
return dateString, nil
}
func extractInt(pattern, content string) (int, error) {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(content)
if len(match) > 1 {
val, err := strconv.Atoi(match[1])
if err != nil {
return 0, err
}
return val, nil
}
return 0.0, fmt.Errorf("no match for %s", pattern)
}
func extractFloat(pattern, content string) (float64, error) {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(content)
if len(match) > 1 {
val, err := strconv.ParseFloat(strings.ReplaceAll(match[1], ",", "."), 64)
if err != nil {
return 0, err
}
return val, nil
}
return 0.0, fmt.Errorf("no match for %s", pattern)
}
func extractString(pattern, content string) (string, error) {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(content)
if len(match) > 1 {
return strings.TrimSpace(match[1]), nil
} else if len(match) > 0 {
return strings.TrimSpace(match[0]), nil
}
return "", fmt.Errorf("pattern %s not found", pattern)
}
func formatDate(date string) *time.Time {
months := map[string]string{
"janv.": "01", "févr.": "02", "mar.": "03", "avr.": "04",
"mai": "05", "juin": "06", "juil.": "07", "août": "08",
"sep.": "09", "oct.": "10", "nov.": "11", "déc.": "12",
}
parts := strings.Split(date, " ")
dateString := fmt.Sprintf("%s-%02s-%02s", parts[2], months[parts[1]], parts[0])
t, err := time.Parse(time.DateOnly, dateString)
if err != nil {
log.Println(err)
return nil
}
return &t
}

View file

@ -9,7 +9,7 @@ import (
"github.com/rjNemo/rentease/internal/service/booking"
)
const content = " 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 : \n 2\n Nombre d'hébergements \n 1\n Montant total € 186 Nom du client : \n Olga Korovina\n \n ru\n \n okorov.905387@guest.booking.com\n 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 \n russe\n Canal : Booking.com Code IATA/TIDS : \n PC029090\n Numéro de réservation : \n 4453602306\n Montant soumis à commission : € 177 Reçu jeu. 2 janv. 2025 Commission : € 31,86 Bloc-notes (usage interne) Ajoutez une note ici \n\n Maison 1 Chambre (T2 - VillaFleurie au bourg du Gosier)\n € 186 jeu. 3 avr. 2025 dim. 6 avr. 2025 Nom du client \n Olga Korovina\n Occupation maximum 2 adultes, 2 enfants (3 personnes max.) Photo de l'hébergement Date Tarif Tarif par nuit \n 03 - 04 avril\n \n Standard Rate\n € 59\n 04 - 05 avril\n \n Standard Rate\n € 59\n 05 - 06 avril\n \n Standard Rate\n € 59Sous-total € 177\n Taxe de séjour\n € 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 \n Aucun message\n \n Les conversations avec vos clients apparaîtront ici.\n Booking.com reçoit tous les messages écrits ici et les traite selon sa Charte de confidentialité et informations sur les cookies Conditions "
const content = " 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 : \n 2\n Nombre d'hébergements \n 1\n Montant total € 186 Nom du client : \n Olga Korovina\n \n ru\n \n okorov.905387@guest.booking.com\n 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 \n russe\n Canal : Booking.com Code IATA/TIDS : \n PC029090\n Numéro de réservation : \n 4453602306\n Montant soumis à commission : € 177 Reçu jeu. 2 janv. 2025 Commission : € 31,86 Bloc-notes (usage interne) Ajoutez une note ici \n\n Maison 1 Chambre T2 - VillaFleurie au bourg du Gosier)\n € 186 jeu. 3 avr. 2025 dim. 6 avr. 2025 Nom du client \n Olga Korovina\n Occupation maximum 2 adultes, 2 enfants (3 personnes max.) Photo de l'hébergement Date Tarif Tarif par nuit \n 03 - 04 avril\n \n Standard Rate\n € 59\n 04 - 05 avril\n \n Standard Rate\n € 59\n 05 - 06 avril\n \n Standard Rate\n € 59Sous-total € 177\n Taxe de séjour\n € 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 \n Aucun message\n \n Les conversations avec vos clients apparaîtront ici.\n Booking.com reçoit tous les messages écrits ici et les traite selon sa Charte de confidentialité et informations sur les cookies Conditions "
func TestParseFromApi(t *testing.T) {
externalId := "4453602306"
@ -47,7 +47,9 @@ func TestParseFromApi(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual, err := booking.ParseBooking(tt.rawContent)
bs, _ := booking.NewService(&booking.MockStore{}, nil, nil, nil)
actual, err := bs.ParseFromApi(tt.rawContent)
if err != nil {
t.Errorf("unexpected error: %v", err)
}

View file

@ -14,6 +14,7 @@ import (
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/driver/calendar"
"github.com/rjNemo/rentease/internal/driver/database"
"github.com/rjNemo/rentease/internal/driver/parser"
"github.com/rjNemo/rentease/internal/driver/pdf"
bookingRepo "github.com/rjNemo/rentease/internal/repository/booking"
"github.com/rjNemo/rentease/internal/server"
@ -56,6 +57,8 @@ func run(c context.Context, getEnv func(string) string) error {
return fmt.Errorf("error migrating the database %w", err)
}
bookingStore := bookingRepo.NewPgStore(db)
gc, err := calendar.NewGoogleClient(ctx, getEnv("CALENDAR_CREDENTIALS"))
if err != nil {
return fmt.Errorf("error building calendar client %w", err)
@ -72,8 +75,9 @@ func run(c context.Context, getEnv func(string) string) error {
return fmt.Errorf("error starting pdf client %w", err)
}
bookingStore := bookingRepo.NewPgStore(db)
bookingService, err := booking.NewService(bookingStore, gc, pc)
parsingClient := parser.NewBookingAgentParser(getEnv("PARSER_BASE_URL"))
bookingService, err := booking.NewService(bookingStore, parsingClient, gc, pc)
if err != nil {
return fmt.Errorf("error creating booking service: %w", err)
}