Working LLM responses and Twilio status tracking
The responses aren't good, but they do exist.
This commit is contained in:
parent
407b478637
commit
b8e7b9b7fd
13 changed files with 497 additions and 287 deletions
|
|
@ -18,6 +18,7 @@ func twilioStatusPost(w http.ResponseWriter, r *http.Request) {
|
|||
message_sid := r.PostFormValue("MessageSid")
|
||||
message_status := r.PostFormValue("MessageStatus")
|
||||
log.Info().Str("sid", message_sid).Str("status", message_status).Msg("Updated message status")
|
||||
platform.UpdateMessageStatus(message_sid, message_status)
|
||||
fmt.Fprintf(w, "")
|
||||
}
|
||||
func twilioTextPost(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ func ParsePhoneNumber(input string) (*E164, error) {
|
|||
return phonenumbers.Parse(input, "US")
|
||||
}
|
||||
|
||||
func SendText(ctx context.Context, source string, destination string, message string) error {
|
||||
func SendText(ctx context.Context, source string, destination string, message string) (string, error) {
|
||||
client := twilio.NewRestClient()
|
||||
|
||||
params := &twilioApi.CreateMessageParams{}
|
||||
|
|
@ -29,15 +29,14 @@ func SendText(ctx context.Context, source string, destination string, message st
|
|||
resp, err := client.Api.CreateMessage(params)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create message to %s: %w", destination, err)
|
||||
} else {
|
||||
if resp.Body != nil {
|
||||
log.Info().Str("dest", destination).Str("body", *resp.Body).Msg("Text message response")
|
||||
} else {
|
||||
log.Info().Str("dest", destination).Msg("Text message response is nil")
|
||||
}
|
||||
return "", fmt.Errorf("Failed to create message to %s: %w", destination, err)
|
||||
}
|
||||
return nil
|
||||
//log.Info().Str("dest", destination).Str("sid", *resp.Body).Msg("Text message response")
|
||||
if resp.Sid == nil {
|
||||
log.Warn().Str("src", source).Str("dst", destination).Msg("Text message sid is nil")
|
||||
return "", nil
|
||||
}
|
||||
return *resp.Sid, nil
|
||||
}
|
||||
|
||||
func sendSMS(destination, source, message string) error {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,17 @@ var CommsTextLogErrors = &commsTextLogErrors{
|
|||
columns: []string{"id"},
|
||||
s: "text_log_pkey",
|
||||
},
|
||||
|
||||
ErrUniqueTextLogTwilioSidKey: &UniqueConstraintError{
|
||||
schema: "comms",
|
||||
table: "text_log",
|
||||
columns: []string{"twilio_sid"},
|
||||
s: "text_log_twilio_sid_key",
|
||||
},
|
||||
}
|
||||
|
||||
type commsTextLogErrors struct {
|
||||
ErrUniqueTextLogPkey *UniqueConstraintError
|
||||
|
||||
ErrUniqueTextLogTwilioSidKey *UniqueConstraintError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,24 @@ var CommsTextLogs = Table[
|
|||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
TwilioSid: column{
|
||||
Name: "twilio_sid",
|
||||
DBType: "text",
|
||||
Default: "NULL",
|
||||
Comment: "",
|
||||
Nullable: true,
|
||||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
TwilioStatus: column{
|
||||
Name: "twilio_status",
|
||||
DBType: "text",
|
||||
Default: "",
|
||||
Comment: "",
|
||||
Nullable: false,
|
||||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
},
|
||||
Indexes: commsTextLogIndexes{
|
||||
TextLogPkey: index{
|
||||
|
|
@ -97,6 +115,23 @@ var CommsTextLogs = Table[
|
|||
Where: "",
|
||||
Include: []string{},
|
||||
},
|
||||
TextLogTwilioSidKey: index{
|
||||
Type: "btree",
|
||||
Name: "text_log_twilio_sid_key",
|
||||
Columns: []indexColumn{
|
||||
{
|
||||
Name: "twilio_sid",
|
||||
Desc: null.FromCond(false, true),
|
||||
IsExpression: false,
|
||||
},
|
||||
},
|
||||
Unique: true,
|
||||
Comment: "",
|
||||
NullsFirst: []bool{false},
|
||||
NullsDistinct: false,
|
||||
Where: "",
|
||||
Include: []string{},
|
||||
},
|
||||
},
|
||||
PrimaryKey: &constraint{
|
||||
Name: "text_log_pkey",
|
||||
|
|
@ -123,33 +158,43 @@ var CommsTextLogs = Table[
|
|||
ForeignColumns: []string{"e164"},
|
||||
},
|
||||
},
|
||||
Uniques: commsTextLogUniques{
|
||||
TextLogTwilioSidKey: constraint{
|
||||
Name: "text_log_twilio_sid_key",
|
||||
Columns: []string{"twilio_sid"},
|
||||
Comment: "",
|
||||
},
|
||||
},
|
||||
|
||||
Comment: "Used to track text messages that were sent.",
|
||||
}
|
||||
|
||||
type commsTextLogColumns struct {
|
||||
Content column
|
||||
Created column
|
||||
Destination column
|
||||
ID column
|
||||
IsWelcome column
|
||||
Origin column
|
||||
Source column
|
||||
Content column
|
||||
Created column
|
||||
Destination column
|
||||
ID column
|
||||
IsWelcome column
|
||||
Origin column
|
||||
Source column
|
||||
TwilioSid column
|
||||
TwilioStatus column
|
||||
}
|
||||
|
||||
func (c commsTextLogColumns) AsSlice() []column {
|
||||
return []column{
|
||||
c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source,
|
||||
c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, c.TwilioSid, c.TwilioStatus,
|
||||
}
|
||||
}
|
||||
|
||||
type commsTextLogIndexes struct {
|
||||
TextLogPkey index
|
||||
TextLogPkey index
|
||||
TextLogTwilioSidKey index
|
||||
}
|
||||
|
||||
func (i commsTextLogIndexes) AsSlice() []index {
|
||||
return []index{
|
||||
i.TextLogPkey,
|
||||
i.TextLogPkey, i.TextLogTwilioSidKey,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,10 +209,14 @@ func (f commsTextLogForeignKeys) AsSlice() []foreignKey {
|
|||
}
|
||||
}
|
||||
|
||||
type commsTextLogUniques struct{}
|
||||
type commsTextLogUniques struct {
|
||||
TextLogTwilioSidKey constraint
|
||||
}
|
||||
|
||||
func (u commsTextLogUniques) AsSlice() []constraint {
|
||||
return []constraint{}
|
||||
return []constraint{
|
||||
u.TextLogTwilioSidKey,
|
||||
}
|
||||
}
|
||||
|
||||
type commsTextLogChecks struct{}
|
||||
|
|
|
|||
|
|
@ -347,6 +347,8 @@ const (
|
|||
CommsTextoriginDistrict CommsTextorigin = "district"
|
||||
CommsTextoriginLLM CommsTextorigin = "llm"
|
||||
CommsTextoriginWebsiteAction CommsTextorigin = "website-action"
|
||||
CommsTextoriginCustomer CommsTextorigin = "customer"
|
||||
CommsTextoriginReiteration CommsTextorigin = "reiteration"
|
||||
)
|
||||
|
||||
func AllCommsTextorigin() []CommsTextorigin {
|
||||
|
|
@ -354,6 +356,8 @@ func AllCommsTextorigin() []CommsTextorigin {
|
|||
CommsTextoriginDistrict,
|
||||
CommsTextoriginLLM,
|
||||
CommsTextoriginWebsiteAction,
|
||||
CommsTextoriginCustomer,
|
||||
CommsTextoriginReiteration,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,7 +371,9 @@ func (e CommsTextorigin) Valid() bool {
|
|||
switch e {
|
||||
case CommsTextoriginDistrict,
|
||||
CommsTextoriginLLM,
|
||||
CommsTextoriginWebsiteAction:
|
||||
CommsTextoriginWebsiteAction,
|
||||
CommsTextoriginCustomer,
|
||||
CommsTextoriginReiteration:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -368,6 +368,8 @@ func (f *Factory) FromExistingCommsTextLog(m *models.CommsTextLog) *CommsTextLog
|
|||
o.IsWelcome = func() bool { return m.IsWelcome }
|
||||
o.Origin = func() enums.CommsTextorigin { return m.Origin }
|
||||
o.Source = func() string { return m.Source }
|
||||
o.TwilioSid = func() null.Val[string] { return m.TwilioSid }
|
||||
o.TwilioStatus = func() string { return m.TwilioStatus }
|
||||
|
||||
ctx := context.Background()
|
||||
if m.R.DestinationPhone != nil {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import (
|
|||
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
models "github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/aarondl/opt/null"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/jaswdr/faker/v2"
|
||||
"github.com/stephenafamo/bob"
|
||||
)
|
||||
|
|
@ -36,13 +38,15 @@ func (mods CommsTextLogModSlice) Apply(ctx context.Context, n *CommsTextLogTempl
|
|||
// CommsTextLogTemplate is an object representing the database table.
|
||||
// all columns are optional and should be set by mods
|
||||
type CommsTextLogTemplate struct {
|
||||
Content func() string
|
||||
Created func() time.Time
|
||||
Destination func() string
|
||||
ID func() int32
|
||||
IsWelcome func() bool
|
||||
Origin func() enums.CommsTextorigin
|
||||
Source func() string
|
||||
Content func() string
|
||||
Created func() time.Time
|
||||
Destination func() string
|
||||
ID func() int32
|
||||
IsWelcome func() bool
|
||||
Origin func() enums.CommsTextorigin
|
||||
Source func() string
|
||||
TwilioSid func() null.Val[string]
|
||||
TwilioStatus func() string
|
||||
|
||||
r commsTextLogR
|
||||
f *Factory
|
||||
|
|
@ -120,6 +124,14 @@ func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter {
|
|||
val := o.Source()
|
||||
m.Source = omit.From(val)
|
||||
}
|
||||
if o.TwilioSid != nil {
|
||||
val := o.TwilioSid()
|
||||
m.TwilioSid = omitnull.FromNull(val)
|
||||
}
|
||||
if o.TwilioStatus != nil {
|
||||
val := o.TwilioStatus()
|
||||
m.TwilioStatus = omit.From(val)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
@ -163,6 +175,12 @@ func (o CommsTextLogTemplate) Build() *models.CommsTextLog {
|
|||
if o.Source != nil {
|
||||
m.Source = o.Source()
|
||||
}
|
||||
if o.TwilioSid != nil {
|
||||
m.TwilioSid = o.TwilioSid()
|
||||
}
|
||||
if o.TwilioStatus != nil {
|
||||
m.TwilioStatus = o.TwilioStatus()
|
||||
}
|
||||
|
||||
o.setModelRels(m)
|
||||
|
||||
|
|
@ -207,6 +225,10 @@ func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) {
|
|||
val := random_string(nil)
|
||||
m.Source = omit.From(val)
|
||||
}
|
||||
if !(m.TwilioStatus.IsValue()) {
|
||||
val := random_string(nil)
|
||||
m.TwilioStatus = omit.From(val)
|
||||
}
|
||||
}
|
||||
|
||||
// insertOptRels creates and inserts any optional the relationships on *models.CommsTextLog
|
||||
|
|
@ -351,6 +373,8 @@ func (m commsTextLogMods) RandomizeAllColumns(f *faker.Faker) CommsTextLogMod {
|
|||
CommsTextLogMods.RandomIsWelcome(f),
|
||||
CommsTextLogMods.RandomOrigin(f),
|
||||
CommsTextLogMods.RandomSource(f),
|
||||
CommsTextLogMods.RandomTwilioSid(f),
|
||||
CommsTextLogMods.RandomTwilioStatus(f),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -571,6 +595,90 @@ func (m commsTextLogMods) RandomSource(f *faker.Faker) CommsTextLogMod {
|
|||
})
|
||||
}
|
||||
|
||||
// Set the model columns to this value
|
||||
func (m commsTextLogMods) TwilioSid(val null.Val[string]) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioSid = func() null.Val[string] { return val }
|
||||
})
|
||||
}
|
||||
|
||||
// Set the Column from the function
|
||||
func (m commsTextLogMods) TwilioSidFunc(f func() null.Val[string]) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioSid = f
|
||||
})
|
||||
}
|
||||
|
||||
// Clear any values for the column
|
||||
func (m commsTextLogMods) UnsetTwilioSid() CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioSid = nil
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
// The generated value is sometimes null
|
||||
func (m commsTextLogMods) RandomTwilioSid(f *faker.Faker) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioSid = func() null.Val[string] {
|
||||
if f == nil {
|
||||
f = &defaultFaker
|
||||
}
|
||||
|
||||
val := random_string(f)
|
||||
return null.From(val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
// The generated value is never null
|
||||
func (m commsTextLogMods) RandomTwilioSidNotNull(f *faker.Faker) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioSid = func() null.Val[string] {
|
||||
if f == nil {
|
||||
f = &defaultFaker
|
||||
}
|
||||
|
||||
val := random_string(f)
|
||||
return null.From(val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Set the model columns to this value
|
||||
func (m commsTextLogMods) TwilioStatus(val string) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioStatus = func() string { return val }
|
||||
})
|
||||
}
|
||||
|
||||
// Set the Column from the function
|
||||
func (m commsTextLogMods) TwilioStatusFunc(f func() string) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioStatus = f
|
||||
})
|
||||
}
|
||||
|
||||
// Clear any values for the column
|
||||
func (m commsTextLogMods) UnsetTwilioStatus() CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioStatus = nil
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
func (m commsTextLogMods) RandomTwilioStatus(f *faker.Faker) CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) {
|
||||
o.TwilioStatus = func() string {
|
||||
return random_string(f)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m commsTextLogMods) WithParentsCascading() CommsTextLogMod {
|
||||
return CommsTextLogModFunc(func(ctx context.Context, o *CommsTextLogTemplate) {
|
||||
if isDone, _ := commsTextLogWithParentsCascadingCtx.Value(ctx); isDone {
|
||||
|
|
|
|||
6
db/migrations/00044_comms_text_origin_user.sql
Normal file
6
db/migrations/00044_comms_text_origin_user.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- +goose Up
|
||||
ALTER TYPE comms.TextOrigin ADD VALUE 'customer';
|
||||
ALTER TYPE comms.TextOrigin ADD VALUE 'reiteration';
|
||||
-- +goose Down
|
||||
ALTER TYPE comms.TextOrigin DROP VALUE 'reiteration';
|
||||
ALTER TYPE comms.TextOrigin DROP VALUE 'customer';
|
||||
8
db/migrations/00045_comms_text_sid.sql
Normal file
8
db/migrations/00045_comms_text_sid.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- +goose Up
|
||||
ALTER TABLE comms.text_log ADD COLUMN twilio_sid TEXT UNIQUE;
|
||||
ALTER TABLE comms.text_log ADD COLUMN twilio_status TEXT;
|
||||
UPDATE comms.text_log SET twilio_status = '';
|
||||
ALTER TABLE comms.text_log ALTER COLUMN twilio_status SET NOT NULL;
|
||||
-- +goose Down
|
||||
ALTER TABLE comms.text_log DROP COLUMN twilio_status;
|
||||
ALTER TABLE comms.text_log DROP COLUMN twilio_sid;
|
||||
|
|
@ -10,7 +10,9 @@ import (
|
|||
"time"
|
||||
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/aarondl/opt/null"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/psql"
|
||||
"github.com/stephenafamo/bob/dialect/psql/dialect"
|
||||
|
|
@ -25,13 +27,15 @@ import (
|
|||
|
||||
// CommsTextLog is an object representing the database table.
|
||||
type CommsTextLog struct {
|
||||
Content string `db:"content" `
|
||||
Created time.Time `db:"created" `
|
||||
Destination string `db:"destination" `
|
||||
ID int32 `db:"id,pk" `
|
||||
IsWelcome bool `db:"is_welcome" `
|
||||
Origin enums.CommsTextorigin `db:"origin" `
|
||||
Source string `db:"source" `
|
||||
Content string `db:"content" `
|
||||
Created time.Time `db:"created" `
|
||||
Destination string `db:"destination" `
|
||||
ID int32 `db:"id,pk" `
|
||||
IsWelcome bool `db:"is_welcome" `
|
||||
Origin enums.CommsTextorigin `db:"origin" `
|
||||
Source string `db:"source" `
|
||||
TwilioSid null.Val[string] `db:"twilio_sid" `
|
||||
TwilioStatus string `db:"twilio_status" `
|
||||
|
||||
R commsTextLogR `db:"-" `
|
||||
}
|
||||
|
|
@ -55,29 +59,33 @@ type commsTextLogR struct {
|
|||
func buildCommsTextLogColumns(alias string) commsTextLogColumns {
|
||||
return commsTextLogColumns{
|
||||
ColumnsExpr: expr.NewColumnsExpr(
|
||||
"content", "created", "destination", "id", "is_welcome", "origin", "source",
|
||||
"content", "created", "destination", "id", "is_welcome", "origin", "source", "twilio_sid", "twilio_status",
|
||||
).WithParent("comms.text_log"),
|
||||
tableAlias: alias,
|
||||
Content: psql.Quote(alias, "content"),
|
||||
Created: psql.Quote(alias, "created"),
|
||||
Destination: psql.Quote(alias, "destination"),
|
||||
ID: psql.Quote(alias, "id"),
|
||||
IsWelcome: psql.Quote(alias, "is_welcome"),
|
||||
Origin: psql.Quote(alias, "origin"),
|
||||
Source: psql.Quote(alias, "source"),
|
||||
tableAlias: alias,
|
||||
Content: psql.Quote(alias, "content"),
|
||||
Created: psql.Quote(alias, "created"),
|
||||
Destination: psql.Quote(alias, "destination"),
|
||||
ID: psql.Quote(alias, "id"),
|
||||
IsWelcome: psql.Quote(alias, "is_welcome"),
|
||||
Origin: psql.Quote(alias, "origin"),
|
||||
Source: psql.Quote(alias, "source"),
|
||||
TwilioSid: psql.Quote(alias, "twilio_sid"),
|
||||
TwilioStatus: psql.Quote(alias, "twilio_status"),
|
||||
}
|
||||
}
|
||||
|
||||
type commsTextLogColumns struct {
|
||||
expr.ColumnsExpr
|
||||
tableAlias string
|
||||
Content psql.Expression
|
||||
Created psql.Expression
|
||||
Destination psql.Expression
|
||||
ID psql.Expression
|
||||
IsWelcome psql.Expression
|
||||
Origin psql.Expression
|
||||
Source psql.Expression
|
||||
tableAlias string
|
||||
Content psql.Expression
|
||||
Created psql.Expression
|
||||
Destination psql.Expression
|
||||
ID psql.Expression
|
||||
IsWelcome psql.Expression
|
||||
Origin psql.Expression
|
||||
Source psql.Expression
|
||||
TwilioSid psql.Expression
|
||||
TwilioStatus psql.Expression
|
||||
}
|
||||
|
||||
func (c commsTextLogColumns) Alias() string {
|
||||
|
|
@ -92,17 +100,19 @@ func (commsTextLogColumns) AliasedAs(alias string) commsTextLogColumns {
|
|||
// All values are optional, and do not have to be set
|
||||
// Generated columns are not included
|
||||
type CommsTextLogSetter struct {
|
||||
Content omit.Val[string] `db:"content" `
|
||||
Created omit.Val[time.Time] `db:"created" `
|
||||
Destination omit.Val[string] `db:"destination" `
|
||||
ID omit.Val[int32] `db:"id,pk" `
|
||||
IsWelcome omit.Val[bool] `db:"is_welcome" `
|
||||
Origin omit.Val[enums.CommsTextorigin] `db:"origin" `
|
||||
Source omit.Val[string] `db:"source" `
|
||||
Content omit.Val[string] `db:"content" `
|
||||
Created omit.Val[time.Time] `db:"created" `
|
||||
Destination omit.Val[string] `db:"destination" `
|
||||
ID omit.Val[int32] `db:"id,pk" `
|
||||
IsWelcome omit.Val[bool] `db:"is_welcome" `
|
||||
Origin omit.Val[enums.CommsTextorigin] `db:"origin" `
|
||||
Source omit.Val[string] `db:"source" `
|
||||
TwilioSid omitnull.Val[string] `db:"twilio_sid" `
|
||||
TwilioStatus omit.Val[string] `db:"twilio_status" `
|
||||
}
|
||||
|
||||
func (s CommsTextLogSetter) SetColumns() []string {
|
||||
vals := make([]string, 0, 7)
|
||||
vals := make([]string, 0, 9)
|
||||
if s.Content.IsValue() {
|
||||
vals = append(vals, "content")
|
||||
}
|
||||
|
|
@ -124,6 +134,12 @@ func (s CommsTextLogSetter) SetColumns() []string {
|
|||
if s.Source.IsValue() {
|
||||
vals = append(vals, "source")
|
||||
}
|
||||
if !s.TwilioSid.IsUnset() {
|
||||
vals = append(vals, "twilio_sid")
|
||||
}
|
||||
if s.TwilioStatus.IsValue() {
|
||||
vals = append(vals, "twilio_status")
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +165,12 @@ func (s CommsTextLogSetter) Overwrite(t *CommsTextLog) {
|
|||
if s.Source.IsValue() {
|
||||
t.Source = s.Source.MustGet()
|
||||
}
|
||||
if !s.TwilioSid.IsUnset() {
|
||||
t.TwilioSid = s.TwilioSid.MustGetNull()
|
||||
}
|
||||
if s.TwilioStatus.IsValue() {
|
||||
t.TwilioStatus = s.TwilioStatus.MustGet()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) {
|
||||
|
|
@ -157,7 +179,7 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) {
|
|||
})
|
||||
|
||||
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
|
||||
vals := make([]bob.Expression, 7)
|
||||
vals := make([]bob.Expression, 9)
|
||||
if s.Content.IsValue() {
|
||||
vals[0] = psql.Arg(s.Content.MustGet())
|
||||
} else {
|
||||
|
|
@ -200,6 +222,18 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) {
|
|||
vals[6] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
if !s.TwilioSid.IsUnset() {
|
||||
vals[7] = psql.Arg(s.TwilioSid.MustGetNull())
|
||||
} else {
|
||||
vals[7] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
if s.TwilioStatus.IsValue() {
|
||||
vals[8] = psql.Arg(s.TwilioStatus.MustGet())
|
||||
} else {
|
||||
vals[8] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
|
||||
}))
|
||||
}
|
||||
|
|
@ -209,7 +243,7 @@ func (s CommsTextLogSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
|
|||
}
|
||||
|
||||
func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression {
|
||||
exprs := make([]bob.Expression, 0, 7)
|
||||
exprs := make([]bob.Expression, 0, 9)
|
||||
|
||||
if s.Content.IsValue() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
|
|
@ -260,6 +294,20 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression {
|
|||
}})
|
||||
}
|
||||
|
||||
if !s.TwilioSid.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
psql.Quote(append(prefix, "twilio_sid")...),
|
||||
psql.Arg(s.TwilioSid),
|
||||
}})
|
||||
}
|
||||
|
||||
if s.TwilioStatus.IsValue() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
psql.Quote(append(prefix, "twilio_status")...),
|
||||
psql.Arg(s.TwilioStatus),
|
||||
}})
|
||||
}
|
||||
|
||||
return exprs
|
||||
}
|
||||
|
||||
|
|
@ -631,13 +679,15 @@ func (commsTextLog0 *CommsTextLog) AttachSourcePhone(ctx context.Context, exec b
|
|||
}
|
||||
|
||||
type commsTextLogWhere[Q psql.Filterable] struct {
|
||||
Content psql.WhereMod[Q, string]
|
||||
Created psql.WhereMod[Q, time.Time]
|
||||
Destination psql.WhereMod[Q, string]
|
||||
ID psql.WhereMod[Q, int32]
|
||||
IsWelcome psql.WhereMod[Q, bool]
|
||||
Origin psql.WhereMod[Q, enums.CommsTextorigin]
|
||||
Source psql.WhereMod[Q, string]
|
||||
Content psql.WhereMod[Q, string]
|
||||
Created psql.WhereMod[Q, time.Time]
|
||||
Destination psql.WhereMod[Q, string]
|
||||
ID psql.WhereMod[Q, int32]
|
||||
IsWelcome psql.WhereMod[Q, bool]
|
||||
Origin psql.WhereMod[Q, enums.CommsTextorigin]
|
||||
Source psql.WhereMod[Q, string]
|
||||
TwilioSid psql.WhereNullMod[Q, string]
|
||||
TwilioStatus psql.WhereMod[Q, string]
|
||||
}
|
||||
|
||||
func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] {
|
||||
|
|
@ -646,13 +696,15 @@ func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] {
|
|||
|
||||
func buildCommsTextLogWhere[Q psql.Filterable](cols commsTextLogColumns) commsTextLogWhere[Q] {
|
||||
return commsTextLogWhere[Q]{
|
||||
Content: psql.Where[Q, string](cols.Content),
|
||||
Created: psql.Where[Q, time.Time](cols.Created),
|
||||
Destination: psql.Where[Q, string](cols.Destination),
|
||||
ID: psql.Where[Q, int32](cols.ID),
|
||||
IsWelcome: psql.Where[Q, bool](cols.IsWelcome),
|
||||
Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin),
|
||||
Source: psql.Where[Q, string](cols.Source),
|
||||
Content: psql.Where[Q, string](cols.Content),
|
||||
Created: psql.Where[Q, time.Time](cols.Created),
|
||||
Destination: psql.Where[Q, string](cols.Destination),
|
||||
ID: psql.Where[Q, int32](cols.ID),
|
||||
IsWelcome: psql.Where[Q, bool](cols.IsWelcome),
|
||||
Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin),
|
||||
Source: psql.Where[Q, string](cols.Source),
|
||||
TwilioSid: psql.WhereNull[Q, string](cols.TwilioSid),
|
||||
TwilioStatus: psql.Where[Q, string](cols.TwilioStatus),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package llm
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"context"
|
||||
"fmt"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
|
|
@ -9,14 +11,10 @@ type Message struct {
|
|||
IsFromCustomer bool
|
||||
}
|
||||
|
||||
func GenerateNextMessage(history []Message, current Message) (Message, error) {
|
||||
// In general our history
|
||||
for i, msg := range history {
|
||||
log.Info().Int("i", i).Bool("is_customer", msg.IsFromCustomer).Msg("History")
|
||||
func GenerateNextMessage(ctx context.Context, history []Message, customer_phone string) (Message, error) {
|
||||
next, err := client.continueConversation(ctx, history, customer_phone)
|
||||
if err != nil {
|
||||
return Message{}, fmt.Errorf("Failed to generate next message: %w", err)
|
||||
}
|
||||
|
||||
return Message{
|
||||
Content: "hey there. :)",
|
||||
IsFromCustomer: false,
|
||||
}, nil
|
||||
return next, nil
|
||||
}
|
||||
|
|
|
|||
162
llm/openai.go
162
llm/openai.go
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/maruel/genai"
|
||||
"github.com/maruel/genai/adapters"
|
||||
|
|
@ -12,21 +11,6 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type openAIClient struct {
|
||||
client *openaichat.Client
|
||||
conversations map[string][]genai.Message
|
||||
log *Logger
|
||||
}
|
||||
|
||||
var client *openAIClient
|
||||
|
||||
type AIRequest struct {
|
||||
Displayname string
|
||||
Message string
|
||||
Sender string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func CreateOpenAIClient(ctx context.Context) error {
|
||||
logger := createLogger()
|
||||
|
||||
|
|
@ -45,131 +29,73 @@ func CreateOpenAIClient(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *openAIClient) continueConversation(ctx context.Context, req AIRequest) error {
|
||||
msgs, ok := c.conversations["roomid"]
|
||||
if !ok {
|
||||
msgs = genai.Messages{
|
||||
c.startConversation(ctx, req),
|
||||
}
|
||||
} else {
|
||||
msgs = append(msgs, genai.NewTextMessage(fmt.Sprintf("(%s) user: %s\nbot: ", req.Timestamp.String(), req.Message)))
|
||||
}
|
||||
type openAIClient struct {
|
||||
client *openaichat.Client
|
||||
conversations map[string][]genai.Message
|
||||
log *Logger
|
||||
}
|
||||
|
||||
c.log.Debug().Msg("Generating response...")
|
||||
type QueryReportStatusInput struct {
|
||||
ReportID string `json:"report_id"`
|
||||
}
|
||||
|
||||
var client *openAIClient
|
||||
|
||||
func (c *openAIClient) continueConversation(ctx context.Context, history []Message, customer_phone string) (Message, error) {
|
||||
opts := genai.OptionsTools{
|
||||
Tools: []genai.ToolDef{
|
||||
{
|
||||
Name: "followup_timer",
|
||||
Description: "This should be used to indicate that the bot should follow up with the user in the future to check on task progress.",
|
||||
Callback: func(ctx2 context.Context, input *FollowupTimerInput) (string, error) {
|
||||
return c.followupSchedule(ctx2, req, input)
|
||||
},
|
||||
}, {
|
||||
Name: "switch_task",
|
||||
Description: "Any time the user indicates they change tasks this must be called to update the record of what tasks are being done.",
|
||||
Callback: func(ctx2 context.Context, input *SwitchTaskInput) (string, error) {
|
||||
return c.switchTask(ctx2, req, input)
|
||||
Name: "query_report_status",
|
||||
Description: "This is used to answer any questions about the current state of the mosquito nuisance report.",
|
||||
Callback: func(ctx2 context.Context, input *QueryReportStatusInput) (string, error) {
|
||||
return c.queryReportStatus(ctx2, customer_phone)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, msgs, &opts)
|
||||
msg := c.convertHistory(history)
|
||||
res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, genai.Messages{msg}, &opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to continue conversation: %v", err)
|
||||
return Message{}, fmt.Errorf("Failed to continue conversation: %v", err)
|
||||
}
|
||||
|
||||
for _, m := range res {
|
||||
msgs = append(msgs, m)
|
||||
// Empty responses are tool call related.
|
||||
if m.String() == "" {
|
||||
log.Debug().Msg("Tool called")
|
||||
} else {
|
||||
//c.log.Info().Str("room", req.RoomID.String()).Msg(m.String())
|
||||
var toSay string = m.String()
|
||||
toSay = strings.Replace(toSay, "bot: ", "", 1)
|
||||
log.Info().Str("to say", toSay).Msg("Responding")
|
||||
/*c.aiResponseChannel <- AIResponse{
|
||||
Message: toSay,
|
||||
RoomID: req.RoomID,
|
||||
}*/
|
||||
toSay = strings.Replace(toSay, "report-mosquitoes-online: ", "", 1)
|
||||
return Message{
|
||||
Content: toSay,
|
||||
IsFromCustomer: false,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
//c.conversations[req.RoomID.String()] = msgs
|
||||
|
||||
return nil
|
||||
return Message{}, nil
|
||||
}
|
||||
|
||||
type FollowupTimerInput struct {
|
||||
DelayInSeconds int64 `json:"delay_in_seconds"`
|
||||
}
|
||||
|
||||
func (c *openAIClient) followupFire(ctx context.Context, req AIRequest, duration time.Duration) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
//c.log.Info().Str("room", req.RoomID.String()).Msg("Context canceled")
|
||||
return
|
||||
func (c *openAIClient) convertHistory(history []Message) genai.Message {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(
|
||||
`This is a text chat conversation between a customer that's a member of the public and a mosquito abatement district.
|
||||
The customer has reported a mosquito nuisance or mosquito breeding through the website report.mosquitoes.online.
|
||||
Messages from the customer are prefixed with 'customer:' and reponses from the service agent servicing the request are prefixed with 'agent:'.
|
||||
The agent wants to provide clear, confident, and succint information about the state of the customer's request. The agent also provides general information about how members of the public can help with controlling mosquitoes. For complex or highly specific requests, the agent will need to defer to the mosquito abatement district. This will take some time because contacting the district may take a few hours to get a response. When the agent needs to contact the district, the agent should tell the customer they are reaching out to the district and to expect a delay.
|
||||
Transcript starts:`,
|
||||
)
|
||||
for _, h := range history {
|
||||
if h.IsFromCustomer {
|
||||
sb.WriteString(fmt.Sprintf("\n\ncustomer (%s): %s\n", h.Content))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("\n\nagent (%s): %s\n", h.Content))
|
||||
}
|
||||
}
|
||||
msgs, ok := c.conversations["roomid"]
|
||||
if !ok {
|
||||
//c.log.Warn().Str("room", req.RoomID.String()).Str("elapsed", duration.String()).Msg("No messages for room")
|
||||
return
|
||||
}
|
||||
msgs = append(msgs, genai.NewTextMessage(fmt.Sprintf("<%s passed>", duration.String())))
|
||||
res, err := c.client.GenSync(ctx, msgs)
|
||||
if err != nil {
|
||||
//c.log.Error().Str("room", req.RoomID.String()).Err(err).Msg("Failed to continue after timer")
|
||||
return
|
||||
}
|
||||
msgs = append(msgs, res.Message)
|
||||
var toSay string = res.String()
|
||||
toSay = strings.Replace(toSay, "bot: ", "", 1)
|
||||
log.Info().Str("to say", toSay).Msg("To say")
|
||||
/*c.aiResponseChannel <- AIResponse{
|
||||
Message: toSay,
|
||||
RoomID: req.RoomID,
|
||||
}
|
||||
c.conversations[req.RoomID.String()] = msgs
|
||||
*/
|
||||
return genai.NewTextMessage(sb.String())
|
||||
}
|
||||
|
||||
func (c *openAIClient) followupSchedule(ctx context.Context, req AIRequest, input *FollowupTimerInput) (string, error) {
|
||||
//c.log.Info().Str("room", req.RoomID.String()).Int64("delay", input.DelayInSeconds).Msg("Followup timer scheduled.")
|
||||
duration, err := time.ParseDuration(fmt.Sprintf("%ds", input.DelayInSeconds))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to parse %d as a valid duration: %v", input.DelayInSeconds, err)
|
||||
}
|
||||
/*c.aiResponseChannel <- AIResponse{
|
||||
Message: fmt.Sprintf("⌛ followup scheduled '%s'", duration.String()),
|
||||
RoomID: req.RoomID,
|
||||
}*/
|
||||
time.AfterFunc(duration, func() {
|
||||
c.followupFire(ctx, req, duration)
|
||||
})
|
||||
return fmt.Sprintf("Followup timer set for %s in the future", duration.String()), nil
|
||||
}
|
||||
|
||||
type SwitchTaskInput struct {
|
||||
TaskName string `json:"task_name"`
|
||||
}
|
||||
|
||||
func (c *openAIClient) switchTask(ctx context.Context, req AIRequest, input *SwitchTaskInput) (string, error) {
|
||||
//c.log.Info().Str("room", req.RoomID.String()).Str("task", input.TaskName).Msg("Task Switched")
|
||||
/*c.aiResponseChannel <- AIResponse{
|
||||
Message: fmt.Sprintf("📋 notes task '%s'", input.TaskName),
|
||||
RoomID: req.RoomID,
|
||||
}*/
|
||||
|
||||
return fmt.Sprintf("Recorded a switch to task %s at %s", input.TaskName, time.Now().String()), nil
|
||||
}
|
||||
|
||||
func (c *openAIClient) startConversation(ctx context.Context, req AIRequest) genai.Message {
|
||||
return genai.NewTextMessage(fmt.Sprintf(
|
||||
`This is a text chat conversation between an employee and a chatbot helping to manage timecards.
|
||||
The user's name is '%[1]s'.
|
||||
Messages from the user will start with '(timestamp) %[1]s:'.
|
||||
Messages from the bot will start with 'bot:'.
|
||||
Sometimes the user won't say anything for a long time and the chatbot needs to follow-up with them.
|
||||
When time passes, there will be a prompt like '<200s passed>'.
|
||||
The bot should then prompt the user to provide a bit of information about what they've been working on during that time.
|
||||
The bot should be interested to know what the user's goals are at a high level and should pay attention to any difficulties or frustrations the user experiences.\n\n
|
||||
(%[2]s) user: %[3]s\nbot:`, req.Displayname, req.Timestamp.String(), req.Message))
|
||||
func (c *openAIClient) queryReportStatus(ctx context.Context, customer_phone string) (string, error) {
|
||||
return "Report is scheduled for work in 3 days at 2:00pm by the district", nil
|
||||
}
|
||||
|
|
|
|||
210
platform/text.go
210
platform/text.go
|
|
@ -19,12 +19,97 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func HandleTextMessage(from string, to string, body string) {
|
||||
ctx := context.Background()
|
||||
type_, src := splitPhoneSource(from)
|
||||
dst, err := getDst(ctx, to)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("to", to).Msg("Failed to get dst")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = insertTextLog(ctx, body, dst, src, enums.CommsTextoriginCustomer, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dst", dst).Msg("Failed to add text message log")
|
||||
return
|
||||
}
|
||||
subscribed, err := isSubscribed(ctx, src)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to handle message")
|
||||
return
|
||||
}
|
||||
// We don't know if they're subscribed or not.
|
||||
if subscribed == nil {
|
||||
body_l := strings.TrimSpace(strings.ToLower(body))
|
||||
switch body_l {
|
||||
case "stop":
|
||||
setSubscribed(ctx, src, false)
|
||||
case "yes":
|
||||
setSubscribed(ctx, src, true)
|
||||
handleWaitingTextJobs(ctx, src)
|
||||
default:
|
||||
content := "I have to start with either 'YES' or 'STOP' first, Which do you want?"
|
||||
/*err := insertTextLog(ctx, body, src, dst, enums.CommsTextoriginReiteration, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to add reiteration to the text log")
|
||||
return
|
||||
}*/
|
||||
err = sendText(ctx, src, dst, content, enums.CommsTextoriginReiteration, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to resend initial prompt.")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
previous_messages, err := loadPreviousMessages(ctx, dst, src)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages")
|
||||
return
|
||||
}
|
||||
log.Info().Int("len", len(previous_messages)).Msg("passing")
|
||||
next_message, err := llm.GenerateNextMessage(ctx, previous_messages, src)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message")
|
||||
return
|
||||
}
|
||||
/*
|
||||
err = insertTextLog(ctx, next_message.Content, src, dst, enums.CommsTextoriginLLM, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dst", dst).Msg("Failed to insert new text message to the text log")
|
||||
return
|
||||
}
|
||||
*/
|
||||
err = sendText(ctx, dst, src, next_message.Content, enums.CommsTextoriginLLM, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("src", src).Str("dst", dst).Str("content", next_message.Content).Msg("Failed to send response text")
|
||||
return
|
||||
}
|
||||
log.Info().Str("from", from).Str("from-type", type_).Str("to", to).Str("src", src).Str("dst", dst).Str("body", body).Str("reply", next_message.Content).Msg("Handled text message")
|
||||
}
|
||||
|
||||
func TextStoreSources() error {
|
||||
ctx := context.TODO()
|
||||
src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164)
|
||||
return ensureInDB(ctx, src)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
func delayMessage(ctx context.Context, source string, destination string, content string, type_ enums.CommsTextjobtype) error {
|
||||
job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{
|
||||
Content: omit.From(content),
|
||||
|
|
@ -81,20 +166,6 @@ func ensureInDB(ctx context.Context, destination string) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (err error) {
|
||||
_, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{
|
||||
//ID:
|
||||
Content: omit.From(content),
|
||||
Created: omit.From(time.Now()),
|
||||
Destination: omit.From(destination),
|
||||
IsWelcome: omit.From(is_welcome),
|
||||
Origin: omit.From(origin),
|
||||
Source: omit.From(source),
|
||||
}).One(ctx, db.PGInstance.BobDB)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Translate from Twilio's representation of a RCS message sender to our concept of a phone number
|
||||
// From: rcs:dev_report_mosquitoes_online_dosrvwxm_agent
|
||||
// To: +16235525879
|
||||
|
|
@ -113,6 +184,39 @@ func getDst(ctx context.Context, to string) (string, error) {
|
|||
return "", fmt.Errorf("Cannot match phone number to '%s'", to)
|
||||
}
|
||||
|
||||
func handleWaitingTextJobs(ctx context.Context, src string) {
|
||||
log.Info().Str("src", src).Msg("Pretend handle waiting jobs")
|
||||
|
||||
}
|
||||
|
||||
func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (log *models.CommsTextLog, err error) {
|
||||
log, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{
|
||||
//ID:
|
||||
Content: omit.From(content),
|
||||
Created: omit.From(time.Now()),
|
||||
Destination: omit.From(destination),
|
||||
IsWelcome: omit.From(is_welcome),
|
||||
Origin: omit.From(origin),
|
||||
Source: omit.From(source),
|
||||
TwilioSid: omitnull.FromPtr[string](nil),
|
||||
TwilioStatus: omit.From(""),
|
||||
}).One(ctx, db.PGInstance.BobDB)
|
||||
|
||||
return log, err
|
||||
}
|
||||
|
||||
func isSubscribed(ctx context.Context, src string) (*bool, error) {
|
||||
phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err)
|
||||
}
|
||||
if phone.IsSubscribed.IsNull() {
|
||||
return nil, nil
|
||||
}
|
||||
result := phone.IsSubscribed.MustGet()
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func loadPreviousMessages(ctx context.Context, dst, src string) ([]llm.Message, error) {
|
||||
messages, err := sql.TextsBySenders(dst, src).All(ctx, db.PGInstance.BobDB)
|
||||
results := make([]llm.Message, 0)
|
||||
|
|
@ -135,11 +239,19 @@ func sendText(ctx context.Context, source string, destination string, message st
|
|||
if err != nil {
|
||||
return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err)
|
||||
}
|
||||
err = insertTextLog(ctx, message, destination, source, origin, is_welcome)
|
||||
log, err := insertTextLog(ctx, message, destination, source, origin, is_welcome)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to insert text message in the DB: %w", err)
|
||||
}
|
||||
err = text.SendText(ctx, source, destination, message)
|
||||
sid, err := text.SendText(ctx, source, destination, message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to send text message: %w", err)
|
||||
}
|
||||
err = log.Update(ctx, db.PGInstance.BobDB, &models.CommsTextLogSetter{
|
||||
TwilioSid: omitnull.From(sid),
|
||||
TwilioStatus: omit.From("created"),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -159,18 +271,6 @@ func splitPhoneSource(s string) (string, string) {
|
|||
|
||||
}
|
||||
|
||||
func isSubscribed(ctx context.Context, src string) (*bool, error) {
|
||||
phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err)
|
||||
}
|
||||
if phone.IsSubscribed.IsNull() {
|
||||
return nil, nil
|
||||
}
|
||||
result := phone.IsSubscribed.MustGet()
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func setSubscribed(ctx context.Context, src string, is_subscribed bool) error {
|
||||
phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src)
|
||||
if err != nil {
|
||||
|
|
@ -182,57 +282,3 @@ func setSubscribed(ctx context.Context, src string, is_subscribed bool) error {
|
|||
log.Info().Str("src", src).Bool("is_subscribed", is_subscribed).Msg("Set number subscribed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleWaitingTextJobs(ctx context.Context, src string) {
|
||||
log.Info().Str("src", src).Msg("Pretend handle waiting jobs")
|
||||
|
||||
}
|
||||
func HandleTextMessage(from string, to string, body string) {
|
||||
ctx := context.Background()
|
||||
type_, src := splitPhoneSource(from)
|
||||
dst, err := getDst(ctx, to)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("to", to).Msg("Failed to get dst")
|
||||
return
|
||||
}
|
||||
subscribed, err := isSubscribed(ctx, src)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to handle message")
|
||||
return
|
||||
}
|
||||
// We don't know if they're subscribed or not.
|
||||
if subscribed == nil {
|
||||
body_l := strings.TrimSpace(strings.ToLower(body))
|
||||
switch body_l {
|
||||
case "stop":
|
||||
setSubscribed(ctx, src, false)
|
||||
case "yes":
|
||||
setSubscribed(ctx, src, true)
|
||||
handleWaitingTextJobs(ctx, src)
|
||||
default:
|
||||
content := "I have to start with either 'YES' or 'STOP' first, Which do you want?"
|
||||
err := text.SendText(ctx, src, dst, content)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to resend initial prompt.")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
previous_messages, err := loadPreviousMessages(ctx, dst, src)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages")
|
||||
return
|
||||
}
|
||||
current := llm.Message{
|
||||
Content: body,
|
||||
IsFromCustomer: true,
|
||||
}
|
||||
log.Info().Int("len", len(previous_messages)).Msg("passing")
|
||||
next_message, err := llm.GenerateNextMessage(previous_messages, current)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message")
|
||||
return
|
||||
}
|
||||
text.SendTextFromLLM(next_message.Content)
|
||||
log.Info().Str("from", from).Str("from-type", type_).Str("to", to).Str("src", src).Str("dst", dst).Str("body", body).Str("reply", next_message.Content).Msg("Handling text message")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue