Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions internal/app/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ type Command string

const (
// User commands
Start Command = "/start" // Start the bot
Why Command = "/why" // Tells the purpose of the bot
Note Command = "/note" // Add a new note
Last Command = "/last" // Get the last note
Analysis Command = "/analysis" // Get the analysis of the last notes
TherapySession Command = "/therapy_session" // Start a real-time therapy session
Language Command = "/language" // Change the language
Settings Command = "/settings" // Show the settings
Help Command = "/help" // Show the help
Version Command = "/version" // Show the version
Start Command = "/start" // Start the bot
Why Command = "/why" // Tells the purpose of the bot
Note Command = "/note" // Add a new note
Last Command = "/last" // Get the last note
Analysis Command = "/analysis" // Get the analysis of the last notes
Therapy Command = "/therapy" // Start a real-time therapy session
Language Command = "/language" // Change the language
Settings Command = "/settings" // Show the settings
Help Command = "/help" // Show the help
Version Command = "/version" // Show the version

// Hidden user commands
MissingNote Command = "/missing_note" // Ask to put a note from the previous text
Expand Down
12 changes: 6 additions & 6 deletions internal/app/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func handleSession(session *Session) {
command := session.Job.Command
commandStr := string(command)
// Keep therapy session as the effective last command during active window
if command == None && session.User.TherapySessionEndAt != nil && now.Before(*session.User.TherapySessionEndAt) && session.Job.LastCommand == TherapySession {
commandStr = string(TherapySession)
if command == None && session.User.TherapySessionEndAt != nil && now.Before(*session.User.TherapySessionEndAt) && session.Job.LastCommand == Therapy {
commandStr = string(Therapy)
}
session.User.LastCommand = &commandStr
session.User.Timestamp = &now
Expand All @@ -71,7 +71,7 @@ func handleSession(session *Session) {
}

// If another command is called while therapy is active, end therapy
if command != None && command != TherapySession && session.User.TherapySessionEndAt != nil {
if command != None && command != Therapy && session.User.TherapySessionEndAt != nil {
endTherapySession(session)
}

Expand All @@ -87,7 +87,7 @@ func handleSession(session *Session) {
handleWhy(session)
case Note:
startNote(session)
case TherapySession:
case Therapy:
startTherapySession(session)
case MissingNote:
handleMissingNote(session, noteStorage)
Expand Down Expand Up @@ -161,7 +161,7 @@ func handleSession(session *Session) {
switch session.Job.LastCommand {
case Note:
finishNote(*session.Job.Input, session, noteStorage)
case TherapySession:
case Therapy:
relayTherapyMessage(*session.Job.Input, session)
case Support:
finishFeedback(session, feedbackStorage)
Expand All @@ -176,7 +176,7 @@ func handleSession(session *Session) {
// If the user is not in typing mode but therapy session is active, keep the session alive and forward
if session.User.TherapySessionEndAt != nil && now.Before(*session.User.TherapySessionEndAt) {
session.User.IsTyping = true
session.Job.LastCommand = TherapySession
session.Job.LastCommand = Therapy
relayTherapyMessage(*session.Job.Input, session)
} else {
handleInputWithoutCommand(session)
Expand Down
29 changes: 28 additions & 1 deletion internal/app/therapy.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ func callTherapySessionEndpoint(text string, session *Session) *string {
// Relay a user message to the therapy session backend and append the reply
func relayTherapyMessage(text string, session *Session) {
//coverage:ignore
// Send immediate typing acknowledgement is already enabled via IsTyping
// Send immediate feedback message to let user know we're processing
thinkingMessageKey := getRandomThinkingMessage(session)
setOutputText(thinkingMessageKey, session)

// Send the actual request to the therapy endpoint
reply := callTherapySessionEndpoint(text, session)
if reply != nil && *reply != "" {
setOutputRawText(*reply, session)
Expand Down Expand Up @@ -250,6 +254,29 @@ func parseRunResponse(respStr string) *string {
return &respStr
}

// Get a random thinking message to show immediate feedback
func getRandomThinkingMessage(session *Session) string {
thinkingMessages := []string{
"therapy_thinking_1",
"therapy_thinking_2",
"therapy_thinking_3",
"therapy_thinking_4",
"therapy_thinking_5",
}

// Use a simple hash of the user ID to get consistent randomness per user
userIDHash := 0
for _, char := range session.User.ID {
userIDHash += int(char)
}

// Add current time to make it more random
userIDHash += int(time.Now().Unix())

selectedIndex := userIDHash % len(thinkingMessages)
return thinkingMessages[selectedIndex]
}

// End the therapy session and notify the user
func endTherapySession(session *Session) {
session.User.IsTyping = false
Expand Down
212 changes: 199 additions & 13 deletions internal/app/therapy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

Expand All @@ -15,7 +16,7 @@ import (
func TestStartTherapySession(t *testing.T) {
ctx := context.Background()
user := &database.User{IsTyping: false}
session := createSession(&Job{Command: TherapySession}, user, nil, &ctx)
session := createSession(&Job{Command: Therapy}, user, nil, &ctx)

startTherapySession(session)

Expand All @@ -37,7 +38,7 @@ func TestEndTherapySession(t *testing.T) {
ctx := context.Background()
endAt := time.Now().Add(10 * time.Minute)
user := &database.User{IsTyping: true, TherapySessionEndAt: &endAt}
session := createSession(&Job{Command: TherapySession}, user, nil, &ctx)
session := createSession(&Job{Command: Therapy}, user, nil, &ctx)

endTherapySession(session)

Expand Down Expand Up @@ -89,11 +90,35 @@ func TestRelayTherapyMessage(t *testing.T) {

relayTherapyMessage("hi", session)

if len(session.Job.Output) == 0 {
t.Fatalf("expected at least one output")
if len(session.Job.Output) < 2 {
t.Fatalf("expected at least two outputs (immediate feedback + response)")
}

// First output should be a thinking message
firstOutput := session.Job.Output[0]
validThinkingKeys := []string{
"therapy_thinking_1",
"therapy_thinking_2",
"therapy_thinking_3",
"therapy_thinking_4",
"therapy_thinking_5",
}

found := false
for _, validKey := range validThinkingKeys {
if firstOutput.TextID == validKey {
found = true
break
}
}

if !found {
t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID)
}
if session.Job.Output[0].TextID != "Hello, I'm here for you." {
t.Fatalf("unexpected relay text: %s", session.Job.Output[0].TextID)

// Second output should be the actual therapy response
if session.Job.Output[1].TextID != "Hello, I'm here for you." {
t.Fatalf("unexpected relay text: %s", session.Job.Output[1].TextID)
}
}

Expand Down Expand Up @@ -143,13 +168,40 @@ func TestHandleSession_ForwardDuringActive(t *testing.T) {
locale := "en"
user := &database.User{ID: "u1", TherapySessionEndAt: &future, IsTyping: true, Locale: &locale}
input := "some text"
job := &Job{Command: None, LastCommand: TherapySession, Input: &input}
job := &Job{Command: None, LastCommand: Therapy, Input: &input}
session := createSession(job, user, nil, &ctx)

handleSession(session)

if len(session.Job.Output) == 0 || session.Job.Output[0].TextID != "Therapist reply" {
t.Fatalf("expected Therapist reply, got %v", session.Job.Output)
if len(session.Job.Output) < 2 {
t.Fatalf("expected at least two outputs (immediate feedback + response), got %v", session.Job.Output)
}

// First output should be a thinking message
firstOutput := session.Job.Output[0]
validThinkingKeys := []string{
"therapy_thinking_1",
"therapy_thinking_2",
"therapy_thinking_3",
"therapy_thinking_4",
"therapy_thinking_5",
}

found := false
for _, validKey := range validThinkingKeys {
if firstOutput.TextID == validKey {
found = true
break
}
}

if !found {
t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID)
}

// Second output should be the actual therapy response
if session.Job.Output[1].TextID != "Therapist reply" {
t.Fatalf("expected Therapist reply in second output, got %v", session.Job.Output)
}
}

Expand Down Expand Up @@ -187,11 +239,35 @@ func TestRelayTherapyMessage_ExistingSessionContinues(t *testing.T) {

relayTherapyMessage("hi", session)

if len(session.Job.Output) == 0 {
t.Fatalf("expected at least one output")
if len(session.Job.Output) < 2 {
t.Fatalf("expected at least two outputs (immediate feedback + response)")
}
if session.Job.Output[0].TextID != "Hello again" {
t.Fatalf("unexpected relay text: %s", session.Job.Output[0].TextID)

// First output should be a thinking message
firstOutput := session.Job.Output[0]
validThinkingKeys := []string{
"therapy_thinking_1",
"therapy_thinking_2",
"therapy_thinking_3",
"therapy_thinking_4",
"therapy_thinking_5",
}

found := false
for _, validKey := range validThinkingKeys {
if firstOutput.TextID == validKey {
found = true
break
}
}

if !found {
t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID)
}

// Second output should be the actual therapy response
if session.Job.Output[1].TextID != "Hello again" {
t.Fatalf("unexpected relay text: %s", session.Job.Output[1].TextID)
}
}

Expand All @@ -215,3 +291,113 @@ func TestHandleSession_EndOnOtherCommand(t *testing.T) {
t.Fatalf("expected commands_hint second, got %s", session.Job.Output[1].TextID)
}
}

func TestGetRandomThinkingMessage(t *testing.T) {
ctx := context.Background()
user := &database.User{ID: "test_user_123"}
session := createSession(&Job{Command: Therapy}, user, nil, &ctx)

// Test that we get a valid thinking message key
messageKey := getRandomThinkingMessage(session)

validKeys := []string{
"therapy_thinking_1",
"therapy_thinking_2",
"therapy_thinking_3",
"therapy_thinking_4",
"therapy_thinking_5",
}

found := false
for _, validKey := range validKeys {
if messageKey == validKey {
found = true
break
}
}

if !found {
t.Fatalf("expected one of %v, got %s", validKeys, messageKey)
}
}

func TestRelayTherapyMessageWithImmediateFeedback(t *testing.T) {
// Create a mock server that simulates the therapy endpoint
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate a successful therapy response
response := `{"content": {"parts": [{"text": "I understand your concerns. Let's work through this together."}]}}`
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(response))
}))
defer server.Close()

// Set environment variables for testing
originalURL := os.Getenv("CAPY_THERAPY_SESSION_URL")
originalCloud := os.Getenv("CLOUD")
originalToken := os.Getenv("CAPY_AGENT_TOKEN")

os.Setenv("CAPY_THERAPY_SESSION_URL", server.URL)
os.Setenv("CLOUD", "false")
os.Setenv("CAPY_AGENT_TOKEN", "test_token")

defer func() {
os.Setenv("CAPY_THERAPY_SESSION_URL", originalURL)
os.Setenv("CLOUD", originalCloud)
os.Setenv("CAPY_AGENT_TOKEN", originalToken)
}()

ctx := context.Background()
user := &database.User{
ID: "test_user",
TherapySessionId: stringPtr("test_session_id"),
TherapySessionEndAt: timePtr(time.Now().Add(30 * time.Minute)),
IsTyping: true,
}
session := createSession(&Job{Command: Therapy}, user, nil, &ctx)

// Call relayTherapyMessage
relayTherapyMessage("I'm feeling anxious about work", session)

// Check that we got at least 2 outputs: immediate feedback + actual response
if len(session.Job.Output) < 2 {
t.Fatalf("expected at least 2 outputs, got %d", len(session.Job.Output))
}

// Check that the first output is a thinking message
firstOutput := session.Job.Output[0]
validThinkingKeys := []string{
"therapy_thinking_1",
"therapy_thinking_2",
"therapy_thinking_3",
"therapy_thinking_4",
"therapy_thinking_5",
}

found := false
for _, validKey := range validThinkingKeys {
if firstOutput.TextID == validKey {
found = true
break
}
}

if !found {
t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID)
}

// Check that the second output is the actual therapy response
secondOutput := session.Job.Output[1]
if !strings.Contains(secondOutput.TextID, "I understand your concerns") {
t.Fatalf("expected therapy response in second output TextID, got: %s", secondOutput.TextID)
}
}

// Helper functions for creating test data
func stringPtr(s string) *string {
return &s
}

func timePtr(t time.Time) *time.Time {
return &t
}
Loading