nidus-sync/platform/text/text.go

233 lines
7.5 KiB
Go
Raw Normal View History

package text
import (
"context"
"fmt"
"strings"
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/platform/background"
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/nyaruka/phonenumbers"
"github.com/rs/zerolog/log"
)
func HandleTextMessage(ctx context.Context, source string, destination string, content string) error {
src, err := ParsePhoneNumber(source)
if err != nil {
return fmt.Errorf("parse source '%s': %w", source, err)
}
dst, err := ParsePhoneNumber(destination)
if err != nil {
return fmt.Errorf("parse destination '%s': %w", destination, err)
}
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("start txn: %w", err)
}
defer txn.Rollback(ctx)
status, err := phoneStatus(ctx, *src)
if err != nil {
return fmt.Errorf("Failed to get phone status")
}
is_visible_to_llm := true
if status == enums.CommsPhonestatustypeUnconfirmed {
is_visible_to_llm = false
}
l, err := models.CommsTextLogs.Insert(&models.CommsTextLogSetter{
//ID:
Content: omit.From(content),
Created: omit.From(time.Now()),
Destination: omit.From(dst.PhoneString()),
IsVisibleToLLM: omit.From(is_visible_to_llm),
IsWelcome: omit.From(false),
Origin: omit.From(enums.CommsTextoriginCustomer),
Source: omit.From(src.PhoneString()),
TwilioSid: omitnull.FromPtr[string](nil),
TwilioStatus: omit.From(""),
}).One(ctx, txn)
if err != nil {
return fmt.Errorf("insert text log: %w", err)
}
log.Debug().Int32("id", l.ID).Msg("insert comms text log")
err = background.NewTextRespond(ctx, txn, l.ID)
if err != nil {
return fmt.Errorf("text respond: %w", err)
}
txn.Commit(ctx)
return err
}
func respondText(ctx context.Context, log_id int32) error {
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer txn.Rollback(ctx)
l, err := models.FindCommsTextLog(ctx, txn, log_id)
if err != nil {
return fmt.Errorf("find comms: %w", err)
}
src, err := ParsePhoneNumber(l.Source)
if err != nil {
return fmt.Errorf("parse source: %w", err)
}
status, err := phoneStatus(ctx, *src)
if err != nil {
return fmt.Errorf("Failed to get phone status")
}
body_l := strings.TrimSpace(strings.ToLower(l.Content))
// If the user isn't confirmed for sending regular texts ensure they get a reprompt
if status == enums.CommsPhonestatustypeUnconfirmed {
switch body_l {
case "yes":
err = setPhoneStatus(ctx, txn, *src, enums.CommsPhonestatustypeOkToSend)
if err != nil {
return fmt.Errorf("set phone status: %w", err)
}
content := "Thanks, we've confirmed your phone number. You can text STOP at any time if you change your mind"
err = sendTextCommandResponse(ctx, txn, *src, content)
if err != nil {
return fmt.Errorf("send response: %w", err)
}
handleWaitingTextJobs(ctx, *src)
// We don't handle 'stop' here because we allow them to say 'stop' at any time, regardless of
// phone status.
//case "stop":
default:
content := "I have to start with either 'YES' or 'STOP' first, Which do you want?"
err = sendTextCommandResponse(ctx, txn, *src, content)
if err != nil {
log.Error().Err(err).Msg("Failed to resend initial prompt.")
}
}
return nil
}
switch body_l {
case "stop":
content := "You have successfully been unsubscribed. You will not receive any more messages from this number. Reply START to resubscribe."
err = sendTextCommandResponse(ctx, txn, *src, content)
if err != nil {
log.Error().Err(err).Msg("Failed to send unsubscribe acknowledgement.")
}
setPhoneStatus(ctx, txn, *src, enums.CommsPhonestatustypeStopped)
return nil
case "reset conversation":
err = handleResetConversation(ctx, txn, *src)
if err != nil {
log.Error().Err(err).Msg("Failed to wipe memory")
content := "Failed to wipe memory"
sendTextCommandResponse(ctx, txn, *src, content)
return fmt.Errorf("reset conversation: %w", err)
}
return nil
}
// If we've got an open public report from this phone number then we'll let the district respond
reports, err := reportsForTextRecipient(ctx, txn, *src)
if err != nil {
return fmt.Errorf("has open report: %w", err)
}
for _, report := range reports {
models.PublicreportReportLogs.Insert(&models.PublicreportReportLogSetter{
Created: omit.From(time.Now()),
EmailLogID: omitnull.FromPtr[int32](nil),
// ID
ReportID: omit.From(report.ID),
TextLogID: omitnull.From(log_id),
Type: omit.From(enums.PublicreportReportlogtypeMessageText),
UserID: omitnull.FromPtr[int32](nil),
}).One(ctx, txn)
event.Updated(event.TypeRMOPublicReport, report.OrganizationID, report.PublicID)
}
// If humans are involved, wait for them.
if len(reports) > 0 {
return nil
}
// Otherwise let the LLM handle the response
return respondTextLLM(ctx, *src)
}
func respondTextLLM(ctx context.Context, src types.E164) error {
previous_messages, err := loadPreviousMessagesForLLM(ctx, src)
if err != nil {
return fmt.Errorf("Failed to get previous messages: %w", err)
}
log.Info().Int("len", len(previous_messages)).Msg("passing")
next_message, err := generateNextMessage(ctx, previous_messages, src)
if err != nil {
return fmt.Errorf("Failed to generate next message: %w", err)
}
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("start txn: %w", err)
}
defer txn.Rollback(ctx)
_, err = sendTextDirect(ctx, txn, enums.CommsTextoriginLLM, src.PhoneString(), next_message.Content, true, false)
if err != nil {
return fmt.Errorf("Failed to send response text: %w", err)
}
txn.Commit(ctx)
return nil
}
func ParsePhoneNumber(input string) (*types.E164, error) {
n, err := phonenumbers.Parse(input, "US")
if err != nil {
return nil, err
}
return types.NewE164(n), nil
}
func StoreSources() error {
ctx := context.TODO()
for _, n := range []string{config.PhoneNumberReportStr, config.PhoneNumberSupportStr, config.VoipMSNumber} {
var err error
// Deal with Voip.ms not expecting API calls with the prefixed +1
if !strings.HasPrefix(n, "+1") {
dest, err := ParsePhoneNumber("+1" + n)
if err != nil {
return fmt.Errorf("Failed to parse +1'%s' as phone number: %w", n, err)
}
err = EnsureInDB(ctx, db.PGInstance.BobDB, *dest)
} else {
dest, err := ParsePhoneNumber(n)
if err != nil {
return fmt.Errorf("Failed to parse '%s' as phone number: %w", n, err)
}
err = EnsureInDB(ctx, db.PGInstance.BobDB, *dest)
}
if err != nil {
return fmt.Errorf("Failed to add number '%s' to DB: %w", n, err)
}
}
return nil
}
func UpdateMessageStatus(twilio_sid string, status string) {
ctx := context.TODO()
l, err := models.CommsTextLogs.Query(
models.SelectWhere.CommsTextLogs.TwilioSid.EQ(twilio_sid),
).One(ctx, db.PGInstance.BobDB)
if err != nil {
log.Error().Err(err).Str("twilio_sid", twilio_sid).Str("status", status).Msg("Failed to update message status query failed")
return
}
err = l.Update(ctx, db.PGInstance.BobDB, &models.CommsTextLogSetter{
TwilioStatus: omit.From(status),
})
if err != nil {
log.Error().Err(err).Str("twilio_sid", twilio_sid).Str("status", status).Msg("Failed to update message status update failed")
return
}
}