diff --git a/.golangci.yml b/.golangci.yml index 27a9c1f..147b018 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -156,11 +156,11 @@ linters: cyclop: # The maximal code complexity to report. # Default: 10 - max-complexity: 30 + max-complexity: 16 # The maximal average package complexity. # If it's higher than 0.0 (float) the check is enabled. # Default: 0.0 - package-average: 10.0 + package-average: 16.0 depguard: # Rules to apply. diff --git a/engine/actions/actions.go b/engine/actions/actions.go index 6b586cd..579744b 100644 --- a/engine/actions/actions.go +++ b/engine/actions/actions.go @@ -497,7 +497,7 @@ func (s SearchAction) Execute(ctx context.Context, rt *Runtime) error { return nil } -func Parse(raw string) ([]Action, error) { //nolint:funlen +func Parse(raw string) ([]Action, error) { //nolint:funlen,cyclop // parser func, ok to be long start := strings.Index(raw, "[") end := strings.LastIndex(raw, "]") if start != -1 && end != -1 && end > start { diff --git a/engine/hooks/hooks.go b/engine/hooks/hooks.go index 90d5f24..8fe791e 100644 --- a/engine/hooks/hooks.go +++ b/engine/hooks/hooks.go @@ -10,26 +10,28 @@ import ( ) const ( - stage1 = `STAGE 1 TRIGGER. Ask user to compliment someone they care about. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a weekly reminder.` - - stage2 = `STAGE 2 TRIGGER. Ask if user drinks enough water. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a daily hydration reminder.` - - stage3 = `STAGE 3 TRIGGER. Ask if user stretches regularly. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a daily stretch reminder.` - - stage4 = `STAGE 4 TRIGGER. Ask about user's sleep habits. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a bedtime reminder.` - - stage5 = `STAGE 5 TRIGGER. Ask if user reads books or mostly consumes short-form content. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a reading reminder.` - - stage6 = `STAGE 6 TRIGGER. Ask about exercise/sports. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a workout reminder.` - - stage7 = `STAGE 7 TRIGGER. Ask if user is satisfied with their job/career. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to help with goal planning reminders.` - - stage8 = `STAGE 8 TRIGGER. Ask about user's personal goals. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set goal-tracking reminders.` + stage1 = `SYSTEM: STAGE 1 TRIGGER. Ask user to compliment someone they care about. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a weekly reminder.` + stage2 = `SYSTEM: STAGE 2 TRIGGER. Ask if user drinks enough water. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a daily hydration reminder.` + stage3 = `SYSTEM: STAGE 3 TRIGGER. Ask if user stretches regularly. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a daily stretch reminder.` + stage4 = `SYSTEM: STAGE 4 TRIGGER. Ask about user's sleep habits. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a bedtime reminder.` + stage5 = `SYSTEM: STAGE 5 TRIGGER. Ask if user reads books or mostly consumes short-form content. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a reading reminder.` + stage6 = `SYSTEM: STAGE 6 TRIGGER. Ask about exercise/sports. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set a workout reminder.` + stage7 = `SYSTEM: STAGE 7 TRIGGER. Ask if user is satisfied with their job/career. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to help with goal planning reminders.` + stage8 = `SYSTEM: STAGE 8 TRIGGER. Ask about user's personal goals. If now is not a good time, create a non-system notification for this evening or another convenient time. Offer to set goal-tracking reminders.` complete = `SYSTEM: COMPLETED INTEGRATION` ) -func Collect(user *user.User, source, content string, params ...any) actions.Action { +const ( + d24 = 24 * time.Hour + d48 = 48 * time.Hour + d60 = 60 * time.Hour + d84 = 84 * time.Hour + d108 = 108 * time.Hour + d120 = 120 * time.Hour +) + +func Collect(user *user.User, _, content string, _ ...any) actions.Action { if user.Timezone == "" { return nil } @@ -44,66 +46,61 @@ func Collect(user *user.User, source, content string, params ...any) actions.Act } } - switch { - case strings.Contains(content, "STAGE 1 TRIGGER"): + if !strings.HasPrefix(content, "SYSTEM: STAGE") { return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(48*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage2, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d48), user.Timezone), + Content: stage1, } + } - case strings.Contains(content, "STAGE 2 TRIGGER"): + switch content[14] { + case '1': return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(24*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage3, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d48), user.Timezone), + Content: stage2, } - - case strings.Contains(content, "STAGE 3 TRIGGER"): + case '2': return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(24*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage4, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d24), user.Timezone), + Content: stage3, } - - case strings.Contains(content, "STAGE 4 TRIGGER"): + case '3': return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(60*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage5, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d24), user.Timezone), + Content: stage4, } - - case strings.Contains(content, "STAGE 5 TRIGGER"): + case '4': return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(84*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage6, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d60), user.Timezone), + Content: stage5, } - - case strings.Contains(content, "STAGE 6 TRIGGER"): + case '5': return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(108*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage7, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d84), user.Timezone), + Content: stage6, } - - case strings.Contains(content, "STAGE 7 TRIGGER"): + case '6': return actions.AddNotification{ Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(120*time.Hour), user.Timezone), - Content: strings.ReplaceAll(stage8, "\n", " "), + Time: timeconv.ToLocal(time.Now().Add(d108), user.Timezone), + Content: stage7, } - - case strings.Contains(content, "STAGE 8 TRIGGER"): + case '7': + return actions.AddNotification{ + Type: "add_notification", + Time: timeconv.ToLocal(time.Now().Add(d120), user.Timezone), + Content: stage8, + } + default: return actions.AddFact{ Type: "add_fact", Value: complete, } } - - return actions.AddNotification{ - Type: "add_notification", - Time: timeconv.ToLocal(time.Now().Add(time.Hour*48), user.Timezone), - Content: strings.ReplaceAll(stage1, "\n", " "), - } } diff --git a/engine/hooks/hooks_test.go b/engine/hooks/hooks_test.go index b447cc1..67e611f 100644 --- a/engine/hooks/hooks_test.go +++ b/engine/hooks/hooks_test.go @@ -18,12 +18,6 @@ func newTestUser() *user.User { ID: uuid.New(), Timezone: "UTC", }, - Chats: []database.Chat{}, - Facts: []database.Fact{}, - Contacts: []database.Contact{}, - IncomingNotifications: []database.Notification{}, - OutgoingNotifications: []database.Notification{}, - RecentActions: []database.Action{}, } } @@ -72,7 +66,7 @@ func TestCollect_DefaultFirstRun(t *testing.T) { func TestCollect_Stage1ToStage2(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 1 TRIGGER processed") + result := Collect(u, "", "SYSTEM: STAGE 1 TRIGGER processed") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -82,7 +76,7 @@ func TestCollect_Stage1ToStage2(t *testing.T) { func TestCollect_Stage2ToStage3(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 2 TRIGGER done") + result := Collect(u, "", "SYSTEM: STAGE 2 TRIGGER done") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -92,7 +86,7 @@ func TestCollect_Stage2ToStage3(t *testing.T) { func TestCollect_Stage3ToStage4(t *testing.T) { u := newTestUser() - result := Collect(u, "", "handling STAGE 3 TRIGGER now") + result := Collect(u, "", "SYSTEM: STAGE 3 TRIGGER now") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -102,7 +96,7 @@ func TestCollect_Stage3ToStage4(t *testing.T) { func TestCollect_Stage4ToStage5(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 4 TRIGGER completed") + result := Collect(u, "", "SYSTEM: STAGE 4 TRIGGER completed") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -112,7 +106,7 @@ func TestCollect_Stage4ToStage5(t *testing.T) { func TestCollect_Stage5ToStage6(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 5 TRIGGER fired") + result := Collect(u, "", "SYSTEM: STAGE 5 TRIGGER fired") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -122,7 +116,7 @@ func TestCollect_Stage5ToStage6(t *testing.T) { func TestCollect_Stage6ToStage7(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 6 TRIGGER sent") + result := Collect(u, "", "SYSTEM: STAGE 6 TRIGGER sent") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -132,7 +126,7 @@ func TestCollect_Stage6ToStage7(t *testing.T) { func TestCollect_Stage7ToStage8(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 7 TRIGGER done") + result := Collect(u, "", "SYSTEM: STAGE 7 TRIGGER done") require.NotNil(t, result) notif, ok := result.(actions.AddNotification) @@ -142,7 +136,7 @@ func TestCollect_Stage7ToStage8(t *testing.T) { func TestCollect_Stage8Completes(t *testing.T) { u := newTestUser() - result := Collect(u, "", "STAGE 8 TRIGGER final") + result := Collect(u, "", "SYSTEM: STAGE 8 TRIGGER final") fact, ok := result.(actions.AddFact) require.True(t, ok) @@ -159,9 +153,9 @@ func TestCollect_TimeFormats(t *testing.T) { assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) stages := []string{ - "STAGE 1 TRIGGER", "STAGE 2 TRIGGER", "STAGE 3 TRIGGER", - "STAGE 4 TRIGGER", "STAGE 5 TRIGGER", "STAGE 6 TRIGGER", - "STAGE 7 TRIGGER", + "SYSTEM: STAGE 1 TRIGGER", "SYSTEM: STAGE 2 TRIGGER", "SYSTEM: STAGE 3 TRIGGER", + "SYSTEM: STAGE 4 TRIGGER", "SYSTEM: STAGE 5 TRIGGER", "SYSTEM: STAGE 6 TRIGGER", + "SYSTEM: STAGE 7 TRIGGER", } for _, stage := range stages { result := Collect(u, "", stage) @@ -178,7 +172,7 @@ func TestCollect_ContentNotLost(t *testing.T) { result := Collect(u, "", "hello") notif := result.(actions.AddNotification) - assert.True(t, strings.Contains(notif.Content, "user to compliment someone they care about")) + assert.True(t, strings.Contains(notif.Content, "compliment someone they care about")) assert.False(t, strings.Contains(notif.Content, "\n")) } @@ -197,16 +191,15 @@ func TestCollect_AllFieldsValid(t *testing.T) { notif, ok := a.(actions.AddNotification) require.True(t, ok) assert.Equal(t, "add_notification", notif.Type) - assert.Empty(t, notif.Target, "Target must be empty for self-notification") + assert.Empty(t, notif.Target) assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 1 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 1 to 2", - content: "STAGE 1 TRIGGER done", + content: "SYSTEM: STAGE 1 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -215,12 +208,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 2 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 2 to 3", - content: "STAGE 2 TRIGGER done", + content: "SYSTEM: STAGE 2 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -229,12 +221,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 3 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 3 to 4", - content: "STAGE 3 TRIGGER done", + content: "SYSTEM: STAGE 3 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -243,12 +234,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 4 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 4 to 5", - content: "STAGE 4 TRIGGER done", + content: "SYSTEM: STAGE 4 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -257,12 +247,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 5 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 5 to 6", - content: "STAGE 5 TRIGGER done", + content: "SYSTEM: STAGE 5 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -271,12 +260,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 6 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 6 to 7", - content: "STAGE 6 TRIGGER done", + content: "SYSTEM: STAGE 6 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -285,12 +273,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 7 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 7 to 8", - content: "STAGE 7 TRIGGER done", + content: "SYSTEM: STAGE 7 TRIGGER done", check: func(t *testing.T, a actions.Action) { notif, ok := a.(actions.AddNotification) require.True(t, ok) @@ -299,12 +286,11 @@ func TestCollect_AllFieldsValid(t *testing.T) { assert.NotEmpty(t, notif.Time) assert.Regexp(t, `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$`, notif.Time) assert.Contains(t, notif.Content, "STAGE 8 TRIGGER") - assert.NotContains(t, notif.Content, "\n") }, }, { name: "stage 8 completes", - content: "STAGE 8 TRIGGER done", + content: "SYSTEM: STAGE 8 TRIGGER done", check: func(t *testing.T, a actions.Action) { fact, ok := a.(actions.AddFact) require.True(t, ok) diff --git a/engine/processor.go b/engine/processor.go index 49c0e50..368adac 100644 --- a/engine/processor.go +++ b/engine/processor.go @@ -25,6 +25,11 @@ func (e *Engine) defaultProcessMessage(ctx context.Context, msg chat.Message) { return } + if msg.Text == "" { + span.Info("skipping empty message") + return + } + u, err := user.FromMessage(ctx, e.Database, msg, e.ActionLimit) if err != nil { span.Error("failed to get user from message", err) diff --git a/engine/processor_test.go b/engine/processor_test.go index 28f41f2..dd14ce7 100644 --- a/engine/processor_test.go +++ b/engine/processor_test.go @@ -444,6 +444,17 @@ func TestDefaultProcessMessage(t *testing.T) { e.defaultProcessMessage(t.Context(), msg) }) + + t.Run("empty message skipped", func(t *testing.T) { + var buf bytes.Buffer + slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))) + + e, _ := setupTestEngine(t) + msg := chat.Message{Chat: "telegram", ID: "123", Text: ""} + + e.defaultProcessMessage(t.Context(), msg) + assert.Contains(t, buf.String(), "skipping empty message") + }) } func TestDefaultProcessNotification(t *testing.T) { diff --git a/engine/prompt/prompt.go b/engine/prompt/prompt.go index 339d8fd..66a41cd 100644 --- a/engine/prompt/prompt.go +++ b/engine/prompt/prompt.go @@ -34,7 +34,7 @@ NEVER END YOUR MESSAGES WITH DOTS BE SKEPTICAL AND DON'T TRUST EVERYTHING SPEAK LIKE A REAL HUMAN FRIEND — NO ROBOTIC PHRASES, NO FORMAL STRUCTURES, NO AWKWARD QUESTIONS. IF USER WOULDN'T SAY IT TO A FRIEND, DON'T SAY IT MATCH USER'S TONE, LANGUAGE, AND MESSAGE LENGTH. SHORT INPUT → SHORT REPLY. LONG INPUT → CAN BE LONGER -ALWAYS SEND EMOJI IN SEPARATE MESSAGES +ALWAYS SEND EMOJI IN SEPARATE MESSAGES, SINGLE EMOJI PER MESSAGE NEVER APPLY WAIT BEFORE FIRST MESSAGE, ALWAYS AFTER NEXT IF YOU CREATE NOTIFICAION WHICH SHOULD BE REPEATED, BE VERY VERBOSE DESCRIBING REPETITION RULES, SO YOU WOULD BE ABLE TO SET NEXT RECIVING JUST FIRST repeat_on ALL TIMES IN ACTIONS SHOULD BE PROVIDED IN FOLLOWING FORMAT: "2006-01-02 15:04" (no timezone) @@ -283,7 +283,7 @@ func buildUserContext(user *user.User) string { } } - var tz = user.Timezone + tz := user.Timezone if user.Timezone == "" { tz = "(CRITICAL! ASK USER ABOUT HIS LOCATION/TIMEZONE, REQUIRED FOR NORMAL WORK FOR NOTIFICAIONS, WHEN USING SET - USE LINUX COMPATIBLE COMMAND)" }