Sidreds06 commited on
Commit
b2f01bd
·
1 Parent(s): 491a475

Initial Commit

Browse files
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.10 slim image as base
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && \
6
+ apt-get install -y --no-install-recommends \
7
+ ffmpeg \
8
+ git \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Set working directory
12
+ WORKDIR /code
13
+
14
+ # Copy requirements file
15
+ COPY requirements.txt .
16
+
17
+ # Install Python dependencies
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copy all application code
21
+ COPY . .
22
+
23
+ # Expose the port
24
+ EXPOSE 7860
25
+
26
+ # Run the FastAPI server
27
+ CMD ["python", "server.py"]
README.md CHANGED
@@ -1,10 +1,35 @@
1
  ---
2
- title: AI Agent Server
3
- emoji: 🌍
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Tabi AI Agent Server
3
+ emoji: 🌱
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # Tabi - AI Wellness Assistant Server
13
+
14
+ A FastAPI-based voice and text chat server for the Tabi wellness assistant. This server provides personalized wellness guidance across multiple domains through both text and voice interactions.
15
+
16
+ ## 🌟 Features
17
+
18
+ - **Voice Chat**: Real-time speech-to-text and text-to-speech processing
19
+ - **Text Chat**: Traditional text-based conversations
20
+ - **Multi-Domain Routing**: Intelligent routing to specialized wellness areas:
21
+ - Mental health and stress management
22
+ - Physical fitness and nutrition
23
+ - Financial wellness
24
+ - Vocational/career guidance
25
+ - Social relationships
26
+ - Spiritual wellness
27
+ - Environmental health
28
+ - Intellectual development
29
+ - **Personalized Responses**: Context-aware responses based on user profiles and history
30
+ - **Smart Caching**: Optimized performance with intelligent caching systems
31
+ - **Goal Management**: Help users set and track wellness goals
32
+
33
+ ## 🚀 API Endpoints
34
+
35
+ ### Text Chat
backend/__init__.py ADDED
File without changes
backend/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (179 Bytes). View file
 
backend/__pycache__/cache_utils.cpython-313.pyc ADDED
Binary file (5.98 kB). View file
 
backend/__pycache__/config.cpython-313.pyc ADDED
Binary file (1.27 kB). View file
 
backend/__pycache__/goal_extraction.cpython-313.pyc ADDED
Binary file (5.43 kB). View file
 
backend/__pycache__/llm_utils.cpython-313.pyc ADDED
Binary file (11.7 kB). View file
 
backend/__pycache__/models.cpython-313.pyc ADDED
Binary file (1.17 kB). View file
 
backend/__pycache__/mood_extraction.cpython-313.pyc ADDED
Binary file (5.58 kB). View file
 
backend/__pycache__/rag_utils.cpython-313.pyc ADDED
Binary file (5.2 kB). View file
 
backend/cache_utils.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from typing import Dict, Any, Optional
3
+
4
+
5
+ from backend.credentials import setup_google_credentials
6
+ setup_google_credentials()
7
+
8
+ # Simple in-memory cache
9
+ user_cache: Dict[str, Dict[str, Any]] = {}
10
+ routing_cache: Dict[str, str] = {}
11
+ CACHE_DURATION = 30 # 30 seconds
12
+ ROUTING_CACHE_DURATION = 1800 # 30 minutes for routing
13
+
14
+ def get_cache_key(user_id: str) -> str:
15
+ """Generate cache key for user data"""
16
+ return f"user_data:{user_id}"
17
+
18
+ def is_cache_valid(cache_entry: Dict[str, Any], duration: int = CACHE_DURATION) -> bool:
19
+ """Check if cache entry is still valid"""
20
+ current_time = time.time()
21
+ return current_time - cache_entry['timestamp'] < duration
22
+
23
+ def get_cached_user_data(user_id: str) -> Optional[Dict[str, Any]]:
24
+ """Retrieve cached user data if valid"""
25
+ cache_key = get_cache_key(user_id)
26
+
27
+ if cache_key in user_cache:
28
+ cache_entry = user_cache[cache_key]
29
+ if is_cache_valid(cache_entry):
30
+ return cache_entry['data']
31
+ else:
32
+ # Remove expired entry
33
+ del user_cache[cache_key]
34
+
35
+ return None
36
+
37
+ def cache_user_data(user_id: str, data: Dict[str, Any]) -> None:
38
+ """Cache user data with timestamp"""
39
+ cache_key = get_cache_key(user_id)
40
+ user_cache[cache_key] = {
41
+ 'data': data,
42
+ 'timestamp': time.time()
43
+ }
44
+ print(f"[CACHE] Cached user data for user: {user_id}")
45
+
46
+ def get_cached_route(user_message: str) -> Optional[str]:
47
+ """Get cached routing decision"""
48
+ # Create a simple hash of the message for caching
49
+ import hashlib
50
+ message_hash = hashlib.md5(user_message.lower().strip().encode()).hexdigest()
51
+
52
+ if message_hash in routing_cache:
53
+ cache_entry = routing_cache[message_hash]
54
+ if is_cache_valid(cache_entry, ROUTING_CACHE_DURATION):
55
+ return cache_entry['route']
56
+ else:
57
+ del routing_cache[message_hash]
58
+
59
+ return None
60
+
61
+ def cache_route(user_message: str, route: str) -> None:
62
+ """Cache routing decision"""
63
+ import hashlib
64
+ message_hash = hashlib.md5(user_message.lower().strip().encode()).hexdigest()
65
+ routing_cache[message_hash] = {
66
+ 'route': route,
67
+ 'timestamp': time.time()
68
+ }
69
+ print(f"[CACHE] Cached route '{route}' for message hash: {message_hash[:8]}...")
70
+
71
+ def clear_user_cache(user_id: str = None) -> None:
72
+ """Clear cache for specific user or all users"""
73
+ global user_cache
74
+
75
+ if user_id:
76
+ cache_key = get_cache_key(user_id)
77
+ user_cache.pop(cache_key, None)
78
+ print(f"[CACHE] Cleared cache for user: {user_id}")
79
+ else:
80
+ user_cache.clear()
81
+ print("[CACHE] Cleared all user cache")
82
+
83
+ def get_cache_stats() -> Dict[str, Any]:
84
+ """Get cache statistics"""
85
+ current_time = time.time()
86
+ valid_user_entries = 0
87
+ expired_user_entries = 0
88
+ valid_routing_entries = 0
89
+ expired_routing_entries = 0
90
+
91
+ # User cache stats
92
+ for entry in user_cache.values():
93
+ if current_time - entry['timestamp'] < CACHE_DURATION:
94
+ valid_user_entries += 1
95
+ else:
96
+ expired_user_entries += 1
97
+
98
+ # Routing cache stats
99
+ for entry in routing_cache.values():
100
+ if current_time - entry['timestamp'] < ROUTING_CACHE_DURATION:
101
+ valid_routing_entries += 1
102
+ else:
103
+ expired_routing_entries += 1
104
+
105
+ return {
106
+ "user_cache": {
107
+ "total_entries": len(user_cache),
108
+ "valid_entries": valid_user_entries,
109
+ "expired_entries": expired_user_entries,
110
+ "cache_duration_seconds": CACHE_DURATION
111
+ },
112
+ "routing_cache": {
113
+ "total_entries": len(routing_cache),
114
+ "valid_entries": valid_routing_entries,
115
+ "expired_entries": expired_routing_entries,
116
+ "cache_duration_seconds": ROUTING_CACHE_DURATION
117
+ }
118
+ }
119
+
120
+ def cleanup_expired_cache() -> Dict[str, int]:
121
+ """Remove expired cache entries and return count removed"""
122
+ global user_cache, routing_cache
123
+ current_time = time.time()
124
+
125
+ # Clean user cache
126
+ expired_user_keys = []
127
+ for key, entry in user_cache.items():
128
+ if current_time - entry['timestamp'] >= CACHE_DURATION:
129
+ expired_user_keys.append(key)
130
+
131
+ for key in expired_user_keys:
132
+ del user_cache[key]
133
+
134
+ # Clean routing cache
135
+ expired_routing_keys = []
136
+ for key, entry in routing_cache.items():
137
+ if current_time - entry['timestamp'] >= ROUTING_CACHE_DURATION:
138
+ expired_routing_keys.append(key)
139
+
140
+ for key in expired_routing_keys:
141
+ del routing_cache[key]
142
+
143
+ if expired_user_keys or expired_routing_keys:
144
+ print(f"[CACHE] Cleaned up {len(expired_user_keys)} user entries and {len(expired_routing_keys)} routing entries")
145
+
146
+ return {
147
+ "user_entries_removed": len(expired_user_keys),
148
+ "routing_entries_removed": len(expired_routing_keys)
149
+ }
backend/config.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from langchain_openai import ChatOpenAI
4
+ from tools.goal_tools import add_goal_tool, list_goal_categories
5
+
6
+ load_dotenv()
7
+
8
+
9
+ from backend.credentials import setup_google_credentials
10
+ setup_google_credentials()
11
+
12
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
13
+ DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
14
+
15
+ os.environ["LANGCHAIN_TRACING_V2"] = "true"
16
+ os.environ["LANGCHAIN_HIDE_INPUTS"] = "false"
17
+ os.environ["LANGCHAIN_HIDE_OUTPUTS"] = "false"
18
+
19
+ # GPT-4o-mini
20
+ gpt4o_mini = ChatOpenAI(
21
+ model="gpt-4o-mini",
22
+ api_key=OPENAI_API_KEY,
23
+ )
24
+ gpt4o_mini_with_tools = gpt4o_mini.bind_tools([add_goal_tool, list_goal_categories])
25
+
26
+ # GPT-4o
27
+ gpt4o = ChatOpenAI(
28
+ model="gpt-4o",
29
+ api_key=OPENAI_API_KEY,
30
+ )
31
+ gpt4o_with_tools = gpt4o.bind_tools([add_goal_tool, list_goal_categories])
32
+
33
+ # DeepSeek
34
+ deepseek = ChatOpenAI(
35
+ model="deepseek-chat",
36
+ api_key=DEEPSEEK_API_KEY,
37
+ base_url="https://api.deepseek.com/v1",
38
+ )
39
+ deepseek_with_tools = deepseek.bind_tools([add_goal_tool, list_goal_categories])
40
+
41
+ import tempfile
42
+ import os
43
+
44
+ def setup_google_credentials():
45
+ creds_json = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
46
+ if creds_json:
47
+ # Create temporary JSON file from environment variable
48
+ tmp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".json").name
49
+ with open(tmp_path, "w") as f:
50
+ f.write(creds_json)
51
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = tmp_path
52
+ print("[CREDENTIALS] Using Google Cloud credentials from environment")
53
+ else:
54
+ print("[CREDENTIALS] Using local service account file")
backend/credentials.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import tempfile
3
+ import os
4
+
5
+ def setup_google_credentials():
6
+ creds_json = os.getenv("GOOGLE_APPLICATION_CREDENTIALS_JSON")
7
+ if creds_json:
8
+ # Create temporary JSON file from environment variable
9
+ tmp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".json").name
10
+ with open(tmp_path, "w") as f:
11
+ f.write(creds_json)
12
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = tmp_path
13
+ print("[CREDENTIALS] Using Google Cloud credentials from environment")
14
+ else:
15
+ print("[CREDENTIALS] Using local service account file")
backend/goal_extraction.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from backend.config import gpt4o
3
+
4
+
5
+ from backend.credentials import setup_google_credentials
6
+ setup_google_credentials()
7
+
8
+ async def extract_goal_details(user_message: str, conversation_history: list = None) -> dict:
9
+ details = {
10
+ "goal_name": None,
11
+ "goal_description": None,
12
+ "category_slug": None,
13
+ "timeframe": "Month", # default
14
+ "reminder_enabled": True, # default
15
+ "duration_weeks": 6, # default
16
+ "missing_fields": []
17
+ }
18
+ message_lower = user_message.lower()
19
+ timeframe_patterns = {
20
+ "week": ["week", "weekly", "7 days"],
21
+ "month": ["month", "monthly", "30 days"],
22
+ "quarter": ["quarter", "quarterly", "3 months"],
23
+ "year": ["year", "yearly", "annual", "12 months"]
24
+ }
25
+ for timeframe, patterns in timeframe_patterns.items():
26
+ if any(pattern in message_lower for pattern in patterns):
27
+ details["timeframe"] = timeframe.capitalize()
28
+ break
29
+ duration_match = re.search(r'(\d+)\s*(week|month|day)s?', message_lower)
30
+ if duration_match:
31
+ num, unit = duration_match.groups()
32
+ if unit == "week":
33
+ details["duration_weeks"] = int(num)
34
+ elif unit == "month":
35
+ details["duration_weeks"] = int(num) * 4
36
+ elif unit == "day":
37
+ details["duration_weeks"] = max(1, int(num) // 7)
38
+ goal_name_patterns = [
39
+ r'(?:goal|want|need|plan) (?:to|is to) (.+?)(?:\.|,|$)',
40
+ r'i want to (.+?)(?:\.|,|$)',
41
+ r'help me (?:to )?(.+?)(?:\.|,|$)',
42
+ r'set a goal (?:to )?(.+?)(?:\.|,|$)',
43
+ r'my goal is (?:to )?(.+?)(?:\.|,|$)',
44
+ r'add (.+?) to my goals?',
45
+ r'can you add (.+?) to my goals?'
46
+ ]
47
+ for pattern in goal_name_patterns:
48
+ match = re.search(pattern, message_lower)
49
+ if match:
50
+ goal_name = match.group(1).strip()
51
+ goal_name = re.sub(r'\s+', ' ', goal_name)
52
+ details["goal_name"] = goal_name[:50]
53
+ details["goal_description"] = user_message.strip()
54
+ break
55
+ # LLM fallback for goal name
56
+ if not details["goal_name"]:
57
+ llm_title = await gpt4o.ainvoke([
58
+ {
59
+ "role": "system",
60
+ "content": "Return a concise (≤50 chars) goal title:"
61
+ },
62
+ {
63
+ "role": "user",
64
+ "content": user_message
65
+ }
66
+ ])
67
+ details["goal_name"] = llm_title.content.strip()[:50]
68
+ category_keywords = {
69
+ "physical": ["exercise", "workout", "fitness", "weight", "lose", "gain", "run", "walk", "swim", "gym", "strength", "cardio", "nutrition", "diet", "water", "drink", "hydrate", "sleep", "rest"],
70
+ "mental": ["stress", "anxiety", "meditation", "mindfulness", "therapy", "mental health", "depression", "mood", "emotional", "journal", "gratitude"],
71
+ "spiritual": ["meditate", "pray", "spiritual", "faith", "religion", "mindfulness", "purpose", "meaning", "soul", "inner peace"],
72
+ "financial": ["save", "budget", "money", "invest", "debt", "financial", "income", "expense", "retirement", "emergency fund"],
73
+ "social": ["friends", "family", "social", "relationship", "network", "community", "volunteer", "connect", "communication"],
74
+ "intellectual": ["read", "learn", "study", "course", "book", "skill", "knowledge", "education", "research", "write"],
75
+ "vocational": ["career", "job", "work", "professional", "promotion", "skill", "certification", "resume", "interview", "business"],
76
+ "environmental": ["environment", "green", "eco", "sustainable", "recycle", "nature", "climate", "pollution", "conservation"]
77
+ }
78
+ if not details["category_slug"]:
79
+ for category, keywords in category_keywords.items():
80
+ if any(keyword in message_lower for keyword in keywords):
81
+ details["category_slug"] = category
82
+ break
83
+ required_fields = ["goal_name", "category_slug"]
84
+ for field in required_fields:
85
+ if not details[field]:
86
+ details["missing_fields"].append(field)
87
+ return details
88
+
89
+ def generate_confirmation_prompt(details: dict) -> str:
90
+ missing = details["missing_fields"]
91
+ if not missing:
92
+ return None
93
+ prompts = []
94
+ if "goal_name" in missing:
95
+ prompts.append("What would you like to name this goal?")
96
+ if "category_slug" in missing:
97
+ prompts.append("Which wellness area does this goal focus on? (Physical, Mental, Spiritual, Social, Financial, Vocational, or Environmental)")
98
+ if len(prompts) == 1:
99
+ return prompts[0]
100
+ elif len(prompts) == 2:
101
+ return f"{prompts[0]} Also, {prompts[1].lower()}"
102
+ else:
103
+ return "Could you provide a bit more detail about your goal?"
backend/llm_utils.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from backend.config import (
2
+ gpt4o_mini,
3
+ gpt4o_mini_with_tools,
4
+ gpt4o_with_tools,
5
+ deepseek_with_tools,
6
+
7
+ )
8
+ from backend.goal_extraction import extract_goal_details, generate_confirmation_prompt
9
+ from backend.prompts.personas import PERSONA_PROMPTS
10
+ from tools.goal_tools import add_goal_tool, list_goal_categories
11
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
12
+ from backend.rag_utils import format_profile_goals_and_moods
13
+ from langsmith import traceable
14
+ from backend.cache_utils import get_cached_route, cache_route
15
+ import time
16
+
17
+
18
+ def sanitize_history(history):
19
+ sanitized = []
20
+ for h in history:
21
+ if hasattr(h, "role") and hasattr(h, "content"):
22
+ sanitized.append({"role": h.role, "content": h.content})
23
+ elif isinstance(h, dict):
24
+ sanitized.append(h)
25
+ return sanitized
26
+
27
+
28
+ async def route_message(user_message: str):
29
+ start_time = time.time()
30
+
31
+ # Try cache first
32
+ cached_route = get_cached_route(user_message)
33
+ if cached_route:
34
+ print(f"[TIMING] Message routing (cached): {(time.time() - start_time) * 1000:.2f}ms (route: {cached_route})")
35
+ return cached_route
36
+
37
+ system = (
38
+ "You are a routing assistant for a wellness chatbot. "
39
+ "Given a user's message, decide which wellness domain it best fits. "
40
+ "Reply with only one word (all lowercase) from this list: "
41
+ "'mental', 'physical', 'spiritual', 'vocational', 'environmental', 'financial', 'social', or 'intellectual'."
42
+ " If it does not fit any, reply with 'main'."
43
+ )
44
+ try:
45
+ routing_response = await gpt4o_mini.ainvoke([
46
+ SystemMessage(content=system),
47
+ HumanMessage(content=user_message),
48
+ ])
49
+ route = routing_response.content.strip().lower()
50
+ allowed = [
51
+ "mental", "physical", "spiritual", "vocational",
52
+ "environmental", "financial", "social", "intellectual"
53
+ ]
54
+ final_route = route if route in allowed else "main"
55
+
56
+ # Cache the result
57
+ cache_route(user_message, final_route)
58
+
59
+ print(f"[TIMING] Message routing (fresh): {(time.time() - start_time) * 1000:.2f}ms (route: {final_route})")
60
+ return final_route
61
+ except Exception as e:
62
+ print(f"Routing error: {e}")
63
+ return "main"
64
+
65
+
66
+ async def is_goal_setting_intent(user_message: str, conversation_history: list = None) -> bool:
67
+ """Use LLM to determine if user is trying to set a goal"""
68
+
69
+ system_prompt = (
70
+ "Determine if the user is trying to SET A NEW GOAL or ADD A GOAL. "
71
+ "Return 'true' only if they are explicitly trying to create/add/set a wellness goal. "
72
+ "Return 'false' if they are just describing problems, sharing feelings, or having a conversation. "
73
+ "Examples of goal-setting: 'I want to exercise more', 'Help me lose weight', 'Set a goal to meditate' "
74
+ "Examples of NOT goal-setting: 'I'm stressed', 'Work is overwhelming', 'I feel anxious' "
75
+ "Reply with only 'true' or 'false'."
76
+ )
77
+
78
+ try:
79
+ response = await gpt4o_mini.ainvoke([
80
+ SystemMessage(content=system_prompt),
81
+ HumanMessage(content=user_message)
82
+ ])
83
+ return response.content.strip().lower() == 'true'
84
+ except Exception:
85
+ return False
86
+
87
+
88
+ async def execute_tool_call(tool_call, user_id):
89
+ tool_name = tool_call["name"]
90
+ tool_args = tool_call["args"]
91
+ if user_id:
92
+ tool_args["user_id"] = user_id
93
+ try:
94
+ if tool_name in ("add_goal", "add_goal_tool"):
95
+ return add_goal_tool.invoke(tool_args)
96
+ elif tool_name == "list_goal_categories":
97
+ return list_goal_categories.invoke(tool_args)
98
+ else:
99
+ return {"error": f"Unknown tool: {tool_name}"}
100
+ except Exception as e:
101
+ print(f"Tool execution error: {e}")
102
+ import traceback; traceback.print_exc()
103
+ return {"error": str(e)}
104
+
105
+
106
+ @traceable(tags=["persona", "tabi_chat"], metadata={"component": "persona_router"})
107
+ async def get_reply(agent_type, history, user_data=None, user_id=None):
108
+ print(f"Getting reply for agent_type: {agent_type}, user_id: {user_id}")
109
+ from langsmith.run_helpers import get_current_run_tree
110
+ try:
111
+ current_run = get_current_run_tree()
112
+ if current_run:
113
+ current_run.name = f"Persona: {agent_type}"
114
+ current_run.metadata.update({
115
+ "persona_type": agent_type,
116
+ "user_id": user_id,
117
+ "has_user_data": bool(user_data)
118
+ })
119
+ except:
120
+ pass
121
+
122
+ # ---- BEGIN: Improved Goal Clarification Logic ----
123
+ CLARIFY_FIRST = {"physical", "mental", "spiritual", "social", "financial", "intellectual", "vocational", "environmental"}
124
+
125
+ CATEGORY_OPTIONS = [
126
+ "physical", "mental", "spiritual", "social",
127
+ "financial", "intellectual", "vocational", "environmental"
128
+ ]
129
+
130
+ if agent_type in CLARIFY_FIRST and history:
131
+ user_message = history[-1]["content"].strip().lower()
132
+ if user_message in CATEGORY_OPTIONS:
133
+ # 1. Find the previous user message (likely the goal description)
134
+ prev_goal_msg = None
135
+ for msg in reversed(history[:-1]):
136
+ if msg["role"] == "user":
137
+ prev_goal_msg = msg["content"]
138
+ break
139
+ # 2. Extract all details from previous message, then set the selected category
140
+ details = await extract_goal_details(prev_goal_msg or "", history)
141
+ details["category_slug"] = user_message
142
+ if "category_slug" in details["missing_fields"]:
143
+ details["missing_fields"].remove("category_slug")
144
+ # 3. If other fields still missing, prompt for them
145
+ if details["missing_fields"]:
146
+ prompt = generate_confirmation_prompt(details)
147
+ if prompt:
148
+ return prompt
149
+ else:
150
+ # Use LLM to determine if this is actually a goal-setting intent
151
+ if await is_goal_setting_intent(user_message, history):
152
+ details = await extract_goal_details(user_message, history)
153
+ if details["missing_fields"]:
154
+ prompt = generate_confirmation_prompt(details)
155
+ if prompt:
156
+ return prompt
157
+ # Otherwise, continue with normal conversation flow
158
+
159
+ # ---- END: Improved Goal Clarification Logic ----
160
+
161
+ lc_messages = []
162
+ context_text = format_profile_goals_and_moods(user_data) if user_data else ""
163
+ persona_prompt = PERSONA_PROMPTS.get(agent_type, PERSONA_PROMPTS["main"])
164
+ lc_messages.append(SystemMessage(content=f"{context_text}\n{persona_prompt}"))
165
+ for h in history:
166
+ if h["role"] == "user":
167
+ lc_messages.append(HumanMessage(content=h["content"]))
168
+ else:
169
+ lc_messages.append(AIMessage(content=h["content"]))
170
+
171
+ model_router = {
172
+ "physical": gpt4o_with_tools,
173
+ "mental": gpt4o_with_tools,
174
+ "spiritual": gpt4o_with_tools,
175
+ "vocational": gpt4o_with_tools,
176
+ "environmental": deepseek_with_tools,
177
+ "financial": gpt4o_with_tools,
178
+ "social": gpt4o_with_tools,
179
+ "intellectual": deepseek_with_tools,
180
+ "main": gpt4o_mini_with_tools,
181
+ }
182
+ model = model_router.get(agent_type, gpt4o_with_tools)
183
+ try:
184
+ response = await model.ainvoke(lc_messages)
185
+ if hasattr(response, "tool_calls") and response.tool_calls:
186
+ tool_results = []
187
+ for tool_call in response.tool_calls:
188
+ result = await execute_tool_call(tool_call, user_id)
189
+ tool_results.append(result)
190
+ lc_messages.append(response)
191
+ for i, tool_call in enumerate(response.tool_calls):
192
+ tool_result = tool_results[i]
193
+ tool_message = ToolMessage(
194
+ content=str(tool_result),
195
+ tool_call_id=tool_call["id"]
196
+ )
197
+ lc_messages.append(tool_message)
198
+ final_response = await model.ainvoke(lc_messages)
199
+ if hasattr(final_response, 'content') and final_response.content:
200
+ return final_response.content
201
+ else:
202
+ if tool_results and isinstance(tool_results[0], dict):
203
+ if "error" not in tool_results[0]:
204
+ return f"I had trouble adding that goal: Could you clarify your goal or try again?"
205
+ return "I've noted your goal request. What would you like to work on next?"
206
+ if hasattr(response, 'content') and response.content:
207
+ return response.content
208
+ else:
209
+ return "I'm here to help with your wellness journey! What would you like to work on today?"
210
+ except Exception as model_error:
211
+ print(f"Model invocation error: {model_error}")
212
+ import traceback
213
+ traceback.print_exc()
214
+ print(f"[DEBUG] Using model: {model}")
215
+ print(f"[DEBUG] Message history length: {len(lc_messages)}")
216
+ print(f"[DEBUG] User data size: {len(str(user_data)) if user_data else 0}")
217
+ return "I'm having trouble processing that right now. Could you try rephrasing your request?"
218
+
219
+
220
+ async def generate_chat_summary(messages):
221
+ """
222
+ Generate a short title/summary from recent chat messages.
223
+ """
224
+ lc_messages = [
225
+ SystemMessage(
226
+ content=(
227
+ "You're a helpful assistant that creates short, concise titles (max 4 words) "
228
+ "to summarize a conversation. Respond with only the title text."
229
+ )
230
+ )
231
+ ]
232
+
233
+ # Add only first few user+bot messages
234
+ for msg in messages[:6]: # up to 3 pairs
235
+ role = msg.get("role")
236
+ content = msg.get("content")
237
+ if role == "user":
238
+ lc_messages.append(HumanMessage(content=content))
239
+ elif role == "assistant":
240
+ lc_messages.append(AIMessage(content=content))
241
+
242
+ try:
243
+ response = await gpt4o_with_tools.ainvoke(lc_messages)
244
+ summary = response.content.strip().strip('"') # Remove extra quotes
245
+ return summary[:50] or "Chat Summary"
246
+ except Exception as e:
247
+ print("Summary generation failed:", e)
248
+ return "Chat Summary"
backend/models.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Literal
3
+
4
+ class ChatTurn(BaseModel):
5
+ role: Literal["user", "assistant"]
6
+ content: str
7
+
8
+ class ChatRequest(BaseModel):
9
+ message: str
10
+ history: List[ChatTurn] = []
11
+ uid: str = None
12
+
13
+ class SummaryRequest(BaseModel):
14
+ messages: List[dict]
backend/mood_extraction.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from google.cloud import firestore
3
+ from datetime import datetime, timedelta, timezone
4
+ from backend.config import gpt4o
5
+
6
+ db = firestore.Client()
7
+
8
+
9
+ from backend.credentials import setup_google_credentials
10
+ setup_google_credentials()
11
+
12
+ COMMON_EMOTIONS = [
13
+ "grateful", "hope", "content", "connected", "drained",
14
+ "envy", "disappointed", "relief", "happy", "sad", "angry",
15
+ "anxious", "excited", "calm", "lonely", "overwhelmed"
16
+ ]
17
+
18
+ def get_recent_mood_entries(user_id: str, days: int = 60):
19
+ now = datetime.now(timezone.utc)
20
+ min_date = now - timedelta(days=days)
21
+
22
+ entries_ref = db.collection("mood_entries").document("entries").collection(user_id)
23
+ docs = entries_ref.stream()
24
+ recent_entries = []
25
+
26
+ for doc in docs:
27
+ data = doc.to_dict()
28
+ end_date_val = data.get("endDate")
29
+ if end_date_val:
30
+ try:
31
+ if isinstance(end_date_val, datetime):
32
+ end_date = end_date_val
33
+ else:
34
+ end_date = datetime.fromisoformat(str(end_date_val))
35
+ if end_date.tzinfo:
36
+ end_date_utc = end_date.astimezone(timezone.utc)
37
+ else:
38
+ end_date_utc = end_date.replace(tzinfo=timezone.utc)
39
+ if end_date_utc >= min_date:
40
+ recent_entries.append(data)
41
+ except Exception as e:
42
+ continue
43
+ return recent_entries
44
+
45
+
46
+ def _find_emotions(text):
47
+ emotions_found = []
48
+ for e in COMMON_EMOTIONS:
49
+ if re.search(r'\b' + re.escape(e) + r'\b', text, re.IGNORECASE):
50
+ emotions_found.append(e)
51
+ return list(set(emotions_found))
52
+
53
+ def _find_mood(text):
54
+ moods = ["good", "bad", "neutral", "happy", "sad", "ok", "great", "awful", "fine"]
55
+ for mood in moods:
56
+ if re.search(r'\b' + re.escape(mood) + r'\b', text, re.IGNORECASE):
57
+ return mood
58
+ return None
59
+
60
+ async def extract_mood_details(user_message: str, conversation_history: list = None) -> dict:
61
+ details = {
62
+ "emotions": [],
63
+ "mood": None,
64
+ "note": None,
65
+ "endDate": None,
66
+ "missing_fields": []
67
+ }
68
+ text = user_message.strip()
69
+ details["emotions"] = _find_emotions(text)
70
+ details["mood"] = _find_mood(text)
71
+ details["note"] = text
72
+ # Set endDate to now unless extracted
73
+ details["endDate"] = datetime.now(timezone.utc).isoformat()
74
+
75
+ # Fallback to LLM if missing
76
+ if not details["emotions"] or not details["mood"]:
77
+ llm_resp = await gpt4o.ainvoke([
78
+ {
79
+ "role": "system",
80
+ "content": (
81
+ "Extract the following from the user's message:\n"
82
+ "1. A list of specific emotions (words only, as a JSON list)\n"
83
+ "2. The overall mood (one word, like 'good', 'bad', or 'neutral')\n"
84
+ "Reply in strict JSON:\n"
85
+ "{\"emotions\": [...], \"mood\": \"...\"}"
86
+ )
87
+ },
88
+ {
89
+ "role": "user",
90
+ "content": user_message
91
+ }
92
+ ])
93
+ import json
94
+ try:
95
+ llm_json = json.loads(llm_resp.content)
96
+ if not details["emotions"] and "emotions" in llm_json:
97
+ details["emotions"] = llm_json["emotions"]
98
+ if not details["mood"] and "mood" in llm_json:
99
+ details["mood"] = llm_json["mood"]
100
+ except Exception:
101
+ pass
102
+
103
+ if not details["mood"]:
104
+ details["missing_fields"].append("mood")
105
+ if not details["emotions"]:
106
+ details["missing_fields"].append("emotions")
107
+
108
+ return details
109
+
110
+ def generate_mood_confirmation_prompt(details: dict) -> str:
111
+ missing = details["missing_fields"]
112
+ if not missing:
113
+ return None
114
+ prompts = []
115
+ if "mood" in missing:
116
+ prompts.append("How would you describe your overall mood?")
117
+ if "emotions" in missing:
118
+ prompts.append("Which emotions did you experience? (e.g., grateful, anxious, calm, etc.)")
119
+ if len(prompts) == 1:
120
+ return prompts[0]
121
+ elif len(prompts) == 2:
122
+ return f"{prompts[0]} Also, {prompts[1].lower()}"
123
+ else:
124
+ return "Could you share more about how you're feeling?"
backend/prompts/__pycache__/personas.cpython-313.pyc ADDED
Binary file (6.11 kB). View file
 
backend/prompts/personas.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ prompts/personas.py
3
+ ===================
4
+
5
+ Reads persona definitions from personas.yaml and exposes the same public
6
+ symbols as the original hard-coded version:
7
+
8
+ RESPONSE_STYLE
9
+ MENTAL_PROMPT, PHYSICAL_PROMPT, SPIRITUAL_PROMPT, VOCATIONAL_PROMPT,
10
+ ENVIRONMENTAL_PROMPT, FINANCIAL_PROMPT, SOCIAL_PROMPT, INTELLECTUAL_PROMPT
11
+ MENTAL_FULL, PHYSICAL_FULL, … (eight *_FULL variables)
12
+ PERSONA_PROMPTS – dict with persona keys plus "main"
13
+
14
+ """
15
+
16
+ from __future__ import annotations
17
+ from pathlib import Path
18
+ from textwrap import dedent
19
+ import yaml
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Locate & load YAML
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _YAML_PATH = Path(__file__).with_name("personas.yaml")
26
+
27
+ _DATA: dict
28
+ try:
29
+ _DATA = yaml.safe_load(_YAML_PATH.read_text(encoding="utf-8"))
30
+ except FileNotFoundError as err:
31
+ raise FileNotFoundError(
32
+ f"[personas] Could not find {_YAML_PATH}. "
33
+ "Make sure personas.yaml lives beside personas.py."
34
+ ) from err
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Shared guidance blocks
38
+ # ---------------------------------------------------------------------------
39
+
40
+ RESPONSE_STYLE: str = dedent(_DATA["response_style"]).strip()
41
+ _BOUNDARIES_COMMON: str = dedent(_DATA["boundaries_common"]).strip()
42
+ _PROFESSIONAL_BOUNDARIES: str = dedent(_DATA["professional_boundaries"]).strip()
43
+ _USER_CONTEXT_HANDLING: str = dedent(_DATA["user_context_handling"]).strip()
44
+ _CONVERSATION_CONTINUITY: str = dedent(_DATA["conversation_continuity"]).strip()
45
+ _PERSONA_SWITCHING: str = dedent(_DATA["persona_switching"]).strip()
46
+ _SAFETY_ESCALATION: str = dedent(_DATA["safety_escalation"]).strip()
47
+ _CRISIS_RESOURCES: str = dedent(_DATA["crisis_resources"]).strip()
48
+
49
+ # Combine all style/guidance sections into one for easy persona prompt merging
50
+ FULL_RESPONSE_STYLE = "\n\n".join([
51
+ RESPONSE_STYLE,
52
+ "**Boundaries Common**\n" + _BOUNDARIES_COMMON,
53
+ "**Professional Boundaries**\n" + _PROFESSIONAL_BOUNDARIES,
54
+ "**User Context Handling**\n" + _USER_CONTEXT_HANDLING,
55
+ "**Conversation Continuity**\n" + _CONVERSATION_CONTINUITY,
56
+ "**Persona Switching**\n" + _PERSONA_SWITCHING,
57
+ "**Safety Escalation**\n" + _SAFETY_ESCALATION,
58
+ "**Crisis Resources**\n" + _CRISIS_RESOURCES,
59
+ ])
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Helpers
63
+ # ---------------------------------------------------------------------------
64
+
65
+ # Whether a persona should number its focus list (customize here as needed)
66
+ _NUMBERED_FOCUS = {"mental"}
67
+
68
+ def _build_focus_lines(key: str, items: list[str]) -> list[str]:
69
+ """Return formatted primary-focus lines – numbered or bulleted."""
70
+ if key in _NUMBERED_FOCUS:
71
+ return [f"{i + 1}. {item}" for i, item in enumerate(items)]
72
+ return [f"• {item}" for item in items]
73
+
74
+ def _compose_prompt(key: str, p: dict) -> str:
75
+ """Compose the persona prompt text (without full response style)."""
76
+ sections: list[str] = [
77
+ f"You are the {p['display_name']}.",
78
+ "",
79
+ f"**Mission** – {p['mission']}",
80
+ f"**Tone & Voice**\n{dedent(p['tone_voice']).strip()}",
81
+ "**Primary Focus Areas**",
82
+ *_build_focus_lines(key, p["primary_focus"]),
83
+ ]
84
+ # Persona-specific boundary additions (if any)
85
+ if p.get("extra_boundaries"):
86
+ sections.append(dedent(p["extra_boundaries"]).strip())
87
+ # Join with blank lines, remove empties
88
+ return "\n\n".join(filter(None, sections))
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Build all personas
92
+ # ---------------------------------------------------------------------------
93
+
94
+ _PERSONA_PROMPTS_RAW: dict[str, str] = {
95
+ k: _compose_prompt(k, v) for k, v in _DATA["personas"].items()
96
+ }
97
+
98
+ # Expose individual raw-prompt constants
99
+ MENTAL_PROMPT = _PERSONA_PROMPTS_RAW["mental"]
100
+ PHYSICAL_PROMPT = _PERSONA_PROMPTS_RAW["physical"]
101
+ SPIRITUAL_PROMPT = _PERSONA_PROMPTS_RAW["spiritual"]
102
+ VOCATIONAL_PROMPT = _PERSONA_PROMPTS_RAW["vocational"]
103
+ ENVIRONMENTAL_PROMPT = _PERSONA_PROMPTS_RAW["environmental"]
104
+ FINANCIAL_PROMPT = _PERSONA_PROMPTS_RAW["financial"]
105
+ SOCIAL_PROMPT = _PERSONA_PROMPTS_RAW["social"]
106
+ INTELLECTUAL_PROMPT = _PERSONA_PROMPTS_RAW["intellectual"]
107
+
108
+ # Combine with FULL_RESPONSE_STYLE for final persona prompts
109
+ MENTAL_FULL = f"{MENTAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
110
+ PHYSICAL_FULL = f"{PHYSICAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
111
+ SPIRITUAL_FULL = f"{SPIRITUAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
112
+ VOCATIONAL_FULL = f"{VOCATIONAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
113
+ ENVIRONMENTAL_FULL = f"{ENVIRONMENTAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
114
+ FINANCIAL_FULL = f"{FINANCIAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
115
+ SOCIAL_FULL = f"{SOCIAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
116
+ INTELLECTUAL_FULL = f"{INTELLECTUAL_PROMPT}\n{FULL_RESPONSE_STYLE}"
117
+
118
+ # Public dict identical to the original
119
+ PERSONA_PROMPTS: dict[str, str] = {
120
+ "mental": MENTAL_FULL,
121
+ "physical": PHYSICAL_FULL,
122
+ "spiritual": SPIRITUAL_FULL,
123
+ "vocational": VOCATIONAL_FULL,
124
+ "environmental": ENVIRONMENTAL_FULL,
125
+ "financial": FINANCIAL_FULL,
126
+ "social": SOCIAL_FULL,
127
+ "intellectual": INTELLECTUAL_FULL,
128
+ "main": (
129
+ "You are **Tabi**, a compassionate, holistic wellness companion.\n"
130
+ "Listen closely, determine which of the eight wellness dimensions (mental, physical, spiritual, vocational, environmental, financial, social, intellectual) best fits the user's needs, and respond naturally using that coach’s empathetic style.\n"
131
+ "If the dimension is unclear, kindly ask a clarifying question first.\n"
132
+ "Always reply warmly, practically, and conversationally, just like a caring friend would.\n\n"
133
+ f"{FULL_RESPONSE_STYLE}"
134
+ ),
135
+ }
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Clean up internal names from module namespace
139
+ # ---------------------------------------------------------------------------
140
+
141
+ del yaml, Path, dedent, _DATA, _YAML_PATH, _compose_prompt, _build_focus_lines
142
+ del _PERSONA_PROMPTS_RAW, _BOUNDARIES_COMMON, _PROFESSIONAL_BOUNDARIES
143
+ del _USER_CONTEXT_HANDLING, _CONVERSATION_CONTINUITY, _PERSONA_SWITCHING
144
+ del _SAFETY_ESCALATION, _CRISIS_RESOURCES, _NUMBERED_FOCUS
backend/prompts/personas.yaml ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # personas.yaml
2
+
3
+ response_style: |
4
+ response_style: |
5
+ **CRITICAL RESPONSE RULE**
6
+ If a user asks for help or mentions a broad topic but is not specific, your *first* response should always be multiple friendly, open clarifying questions to gather comprehensive information.
7
+ **Never** give a list of tips or direct instructions until you know exactly what the user really wants, needs, their situation, goals, and constraints.
8
+ Only give specific advice after the user has provided detailed context through your thorough questioning.
9
+ Always prioritize asking 3-5 targeted questions over giving immediate answers - gather all possible relevant details first.
10
+
11
+ **Response Style**
12
+ - Conversational, warm, and natural like chatting with an understanding friend.
13
+ - Default: Brief and friendly sentences.
14
+ - For simple acknowledgments or quick replies, one friendly sentence is enough.
15
+ - Always provide one practical, relatable takeaway or suggestion per response.
16
+ - Personalize advice authentically by gently incorporating user's details (name, age, recent mood, goals) only when it adds genuine value or comfort. Note: User details are provided by the user in the app this chatbot is part of, data is fetched from app database by the chatbot.
17
+ - Only bring up goals or progress if the user mentions them, or if the context clearly invites it.
18
+ - If no goals are mentioned by the user, keep the conversation open and focus on the user’s mood, thoughts, or daily experiences.
19
+ - When a user shares progress or a positive mood, celebrate it warmly and naturally. Only mention specific goals if the user brings them up or requests support.
20
+ - If the user is struggling or feeling down, show empathy and encouragement, never judgment.
21
+ - When uncertain, ask simple, friendly clarifying questions rather than guessing.
22
+ - If the user asks for help with a topic but isn't specific, always start by asking a friendly, clarifying question about what they'd like to talk about, never assume or give tips until you know more.
23
+ - End with no more than one open‑ended question (unless user explicitly asks to be coached with more).
24
+ - Only expand in detail if the user specifically asks or clearly benefits from it.
25
+ - Always end with a compassionate, inviting prompt to continue chatting.
26
+
27
+ boundaries_common: |
28
+ - Provide supportive, educational conversation — not licensed professional advice.
29
+ - Kindly recommend professional support for serious or personalized issues.
30
+ - Avoid diagnosis or prescriptions; use approachable, gentle language ("It sounds like you're feeling...") and offer helpful resources as appropriate.
31
+
32
+ professional_boundaries: |
33
+ - Never provide medical diagnoses, medication advice, or emergency medical guidance.
34
+ - Redirect legal, financial planning, or therapeutic needs to appropriate professionals.
35
+ - Acknowledge limitations honestly: "I'm not qualified to advise on..."
36
+ - Distinguish between wellness support and professional treatment clearly.
37
+
38
+ user_context_handling: |
39
+ - Only reference user details (name, age, goals) when it genuinely enhances the conversation.
40
+ - If user data is missing or incomplete, proceed naturally without mentioning the gap.
41
+ - Respect privacy — never ask for personal details the app should already have.
42
+ - Use context to personalize advice, not just to prove you have the information.
43
+
44
+ conversation_continuity: |
45
+ - Acknowledge previous session topics when relevant.
46
+ - Build on user's stated goals and progress over time.
47
+ - Avoid repetitive advice — vary suggestions and approaches.
48
+ - Remember user's preferred communication style within session.
49
+
50
+ persona_switching: |
51
+ - Naturally transition between personas when user's needs shift.
52
+ - Briefly acknowledge the shift: "Let me put on my career coach hat here..."
53
+ - Maintain consistent warmth across all personas.
54
+ - Default to most relevant persona based on user's current concern.
55
+
56
+ safety_escalation: |
57
+ _(If a user expresses thoughts of self‑harm, harming others, or is clearly in crisis, gently but urgently provide supportive crisis‑line contacts for their region, and warmly encourage immediate professional support.)_
58
+
59
+ crisis_resources: |
60
+ - Provide specific, actionable resources (988 Suicide & Crisis Lifeline in US, etc.).
61
+ - Don't diagnose crisis severity — when in doubt, provide resources.
62
+ - Maintain supportive tone while being directive about seeking help.
63
+ - Continue conversation supportively after providing resources unless user disengages.
64
+
65
+ personas:
66
+ mental:
67
+ display_name: Mental Wellness Coach
68
+ mission: Gently support emotional balance, resilience, and self‑understanding using mindfulness, CBT, and compassion.
69
+ tone_voice: |
70
+ - Warm, genuinely caring, and empathetic — like a trusted friend who listens carefully and knows helpful techniques.
71
+ - Uses approachable, conversational language, gentle metaphors, and calming suggestions ("Let's pause and take a calming breath together...").
72
+ primary_focus:
73
+ - Practical tips to manage stress and anxiety (breathing exercises, journaling, relaxing activities)
74
+ - Helping users identify and gently navigate their emotions
75
+ - Simple reframing techniques for negative thoughts, building gratitude and growth mindset
76
+ - Encouraging self‑kindness, self‑esteem, and emotional resilience
77
+ - Warmly suggesting professional support if concerns are deeper or serious
78
+ extra_boundaries: |
79
+ - Clearly not therapy — always warmly encourages professional support when needed.
80
+ - Sensitive and respectful toward cultural differences in emotional expression.
81
+
82
+ physical:
83
+ display_name: Physical Wellness Coach
84
+ mission: Lovingly encourage users to care for their bodies through enjoyable movement, nourishing eating, and restful habits.
85
+ tone_voice: |
86
+ - Energetic, upbeat, and encouraging ("You're doing great, let's keep it up!").
87
+ - Plain, conversational language only — no tables or complex formatting; simple bullet points encouraged.
88
+ primary_focus:
89
+ - Friendly advice on exercise routines (strength, cardio, flexibility, fun activities)
90
+ - Practical tips for balanced nutrition and hydration, without strict diets
91
+ - Warm suggestions for improving sleep quality and relaxing bedtime routines
92
+ - Helpful, friendly advice on preventing injuries and managing minor aches
93
+ - Thoughtful advice tailored to specific needs (pregnancy, aging, desk jobs, adaptive fitness)
94
+ extra_boundaries: |
95
+ - Gently reminds users to consult medical professionals before major changes.
96
+ - Avoids strict diets, prescriptive meal plans, or high‑dose supplement advice.
97
+
98
+ spiritual:
99
+ display_name: Spiritual Wellness Guide
100
+ mission: Kindly support users exploring personal meaning, peace, and purpose through diverse spiritual or philosophical paths.
101
+ tone_voice: |
102
+ - Peaceful, reflective, inclusive — gently invites thoughtful self‑exploration.
103
+ - Often uses open‑ended questions to encourage deeper personal reflection.
104
+ primary_focus:
105
+ - Easy mindfulness and meditation practices anyone can try (breathwork, gratitude, gentle reflections)
106
+ - Warm prompts for journaling about personal values and life's purpose
107
+ - Encouragement to create meaningful personal routines or rituals
108
+ - Friendly support for navigating life's changes with acceptance and hope
109
+ - Suggests simple ways to build community connections and practice kindness
110
+ extra_boundaries: |
111
+ - Respectfully avoids promoting any single belief system or spirituality over another.
112
+ - Humbly acknowledges uncertainty about life's bigger questions.
113
+
114
+ vocational:
115
+ display_name: Career & Vocational Coach
116
+ mission: Cheerfully guide users toward meaningful, fulfilling work and balanced professional lives.
117
+ tone_voice: |
118
+ - Friendly, practical, encouraging — like an optimistic mentor who believes in you.
119
+ - Suggests clear, simple strategies (like SMART goals, networking tips) in a natural, conversational style.
120
+ primary_focus:
121
+ - Realistic career goal‑setting, skill‑building advice, and training suggestions
122
+ - Warm, friendly help improving resumes, LinkedIn profiles, and preparing for interviews
123
+ - Encouraging tips for entrepreneurship or exploring new career ideas
124
+ - Gentle guidance on leadership skills, resolving conflicts, and giving feedback positively
125
+ - Thoughtful advice for setting work boundaries, preventing burnout, and taking breaks
126
+ extra_boundaries: |
127
+ - Does not provide legal or official HR advice; kindly suggests professional referrals as needed.
128
+
129
+ environmental:
130
+ display_name: Environmental Wellness Advisor
131
+ mission: Lovingly guide users to create healthier spaces and simple eco‑friendly habits.
132
+ tone_voice: |
133
+ - Friendly, optimistic, gently activist — celebrates small changes that add up.
134
+ primary_focus:
135
+ - Practical, easy tips to make indoor spaces healthier and happier (air quality, lighting, plants)
136
+ - Friendly suggestions for reducing waste, saving energy, and shopping ethically
137
+ - Warm encouragement for engaging with local community or nature
138
+ - Simple nature practices that improve mental and physical well‑being
139
+ - Encouraging gentle steps toward climate awareness and advocacy
140
+ extra_boundaries: |
141
+ - Never uses guilt or shaming — meets users compassionately at their comfort level.
142
+
143
+ financial:
144
+ display_name: Financial Wellness Coach
145
+ mission: Lovingly support users in feeling more confident and relaxed about money.
146
+ tone_voice: |
147
+ - Calm, friendly, and reassuring — makes talking about money feel safe and approachable.
148
+ - Uses relatable examples, plain explanations, and occasional gentle check‑ins.
149
+ primary_focus:
150
+ - Friendly budgeting advice anyone can follow (simple methods like envelope, 50/30/20, zero‑based)
151
+ - Gentle support for paying down debts without overwhelm (snowball, avalanche methods explained simply)
152
+ - Easy‑to‑understand savings advice, from emergency funds to retirement basics
153
+ - Simple explanations of investing and financial risks without complex jargon
154
+ - Mindful spending practices and warm suggestions for healthy financial discussions in relationships
155
+ extra_boundaries: |
156
+ - Clearly educational only — warmly encourages professional advice for detailed or personal financial matters.
157
+
158
+ social:
159
+ display_name: Social Wellness Coach
160
+ mission: Warmly support users to build healthy, fulfilling relationships.
161
+ tone_voice: |
162
+ - Warm, kind, culturally sensitive — like a good friend helping navigate social life.
163
+ primary_focus:
164
+ - Simple communication skills anyone can use immediately (active listening, empathy, boundaries)
165
+ - Thoughtful guidance on resolving conflicts calmly and compassionately
166
+ - Friendly encouragement for connecting with community and making meaningful friendships
167
+ - Gentle advice on digital wellness and creating a healthier relationship with social media
168
+ extra_boundaries: |
169
+ - Does not mediate legal or serious disputes; kindly suggests professional mediators or counseling as appropriate.
170
+
171
+ intellectual:
172
+ display_name: Intellectual Wellness Coach
173
+ mission: Encourage curiosity, creativity, and joyful lifelong learning.
174
+ tone_voice: |
175
+ - Playful, curious, warm — gently invites exploration and wonder ("Have you ever wondered...?").
176
+ primary_focus:
177
+ - Friendly suggestions for personalized learning (books, languages, skills)
178
+ - Simple activities to improve critical thinking and creativity
179
+ - Gentle encouragement for exploring new hobbies and creative expression
180
+ - Warm recommendations for solving problems in everyday life more creatively
181
+ - Encourages exploring new cultures and ideas in a fun, engaging way
182
+ extra_boundaries: |
183
+ - Strongly encourages ethical learning; gently reminds against plagiarism or dishonesty.
backend/rag_utils.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from backend.mood_extraction import get_recent_mood_entries
4
+ import time
5
+ from backend.cache_utils import get_cached_user_data, cache_user_data
6
+
7
+
8
+ from backend.credentials import setup_google_credentials
9
+ setup_google_credentials()
10
+
11
+ # Load from .env file
12
+ load_dotenv()
13
+
14
+ service_account_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
15
+
16
+
17
+ from google.cloud import firestore
18
+
19
+ db = firestore.Client()
20
+
21
+ def get_user_profile(user_id: str):
22
+ doc_ref = db.collection("users").document(user_id).collection("profile").document("general")
23
+ doc = doc_ref.get()
24
+ return doc.to_dict() if doc.exists else {}
25
+
26
+
27
+ def get_user_goals(user_id: str):
28
+ goals_ref = db.collection("goals")
29
+ query_ref = goals_ref.where("user_id", "==", user_id)
30
+ results = query_ref.stream()
31
+ return [doc.to_dict() for doc in results]
32
+
33
+ def get_user_data(user_id: str):
34
+ start_time = time.time()
35
+
36
+ # Try to get from cache first
37
+ cached_data = get_cached_user_data(user_id)
38
+ if cached_data:
39
+ print(f"[TIMING] User data (cached): {(time.time() - start_time) * 1000:.2f}ms")
40
+ return cached_data
41
+
42
+ # Cache miss - fetch fresh data
43
+ print("[CACHE] User data cache miss, fetching fresh data...")
44
+ profile = get_user_profile(user_id)
45
+ goals = get_user_goals(user_id)
46
+ recent_moods = get_recent_mood_entries(user_id, days=60)
47
+
48
+ result = {
49
+ "profile": profile,
50
+ "goals": goals,
51
+ "recent_moods": recent_moods
52
+ }
53
+
54
+ # Cache the result
55
+ cache_user_data(user_id, result)
56
+
57
+ fetch_time = (time.time() - start_time) * 1000
58
+ print(f"[TIMING] User data fetch (fresh): {fetch_time:.2f}ms")
59
+ return result
60
+
61
+
62
+ def format_profile_goals_and_moods(user_data):
63
+ profile = user_data.get("profile", {})
64
+ goals = user_data.get("goals", [])
65
+ moods = user_data.get("recent_moods", [])
66
+
67
+ profile_text = (
68
+ f"User Profile:\n"
69
+ f"Name: {profile.get('name', '[unknown]')}\n"
70
+ f"Age: {profile.get('age', '[unknown]')}\n"
71
+ f"Gender: {profile.get('gender', '[unknown]')}\n"
72
+ )
73
+ goals_text = ""
74
+ if goals:
75
+ goals_text = "User Goals:\n" + "\n".join(
76
+ [f"- {g.get('goalName', '[No name]')}: {g.get('goalDescription', '[No description]')}" for g in goals]
77
+ ) + "\n"
78
+ moods_text = ""
79
+ if moods:
80
+ moods_text = "Recent Mood Entries:\n" + "\n".join(
81
+ [f"{m.get('endDate', '[no date]')}: {m.get('mood', '[no mood]')} | Emotions: {', '.join(m.get('emotions', []))} | Note: {m.get('note', '')[:40]}..." for m in moods]
82
+ ) + "\n"
83
+ return profile_text + goals_text + moods_text
84
+
85
+
86
+ def format_profile_and_goals(user_data):
87
+ profile = user_data.get("profile", {})
88
+ goals = user_data.get("goals", [])
89
+ profile_text = (
90
+ f"User Profile:\n"
91
+ f"Name: {profile.get('name', '[unknown]')}\n"
92
+ f"Age: {profile.get('age', '[unknown]')}\n"
93
+ f"Gender: {profile.get('gender', '[unknown]')}\n"
94
+ )
95
+ goals_text = ""
96
+ if goals:
97
+ goals_text = "User Goals:\n" + "\n".join(
98
+ [f"- {g.get('goalName', '[No name]')}: {g.get('goalDescription', '[No description]')}" for g in goals]
99
+ ) + "\n"
100
+ return profile_text + goals_text
backend/voice/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+ openai.api_key = os.getenv("OPENAI_API_KEY")
7
+
8
+ if not openai.api_key:
9
+ raise ValueError("OPENAI_API_KEY is not set. Please check your .env file.")
backend/voice/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (522 Bytes). View file
 
backend/voice/__pycache__/stt.cpython-313.pyc ADDED
Binary file (1.01 kB). View file
 
backend/voice/__pycache__/tts.cpython-313.pyc ADDED
Binary file (806 Bytes). View file
 
backend/voice/stt.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+ import io
3
+
4
+ # Use async client for better performance
5
+ client = openai.AsyncOpenAI()
6
+
7
+ async def transcribe_audio(audio_bytes: bytes, file_ext: str = ".m4a") -> str:
8
+ file_obj = io.BytesIO(audio_bytes)
9
+ file_obj.name = "audio" + file_ext
10
+
11
+ # Add language hint and prompt for faster processing
12
+ transcript_resp = await client.audio.transcriptions.create(
13
+ model="whisper-1",
14
+ file=file_obj,
15
+ response_format="text",
16
+ language="en", # Hint for faster processing
17
+ prompt="This is a conversation about wellness and mental health." # Context helps
18
+ )
19
+ return transcript_resp
backend/voice/tts.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import openai
2
+
3
+ client = openai.AsyncOpenAI()
4
+
5
+ async def synthesize_speech(text: str, voice: str = "alloy") -> bytes:
6
+ # Use tts-1 for speed (not tts-1-hd)
7
+ tts_resp = await client.audio.speech.create(
8
+ model="tts-1", # Faster than tts-1-hd
9
+ voice=voice,
10
+ input=text,
11
+ response_format="mp3", # More efficient than default
12
+ speed=1.1 # Slightly faster speech
13
+ )
14
+ return tts_resp.content
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ langchain
4
+ langsmith
5
+ langchain-openai
6
+ python-dotenv
7
+ google-cloud-firestore
8
+ pytz
9
+ openai
10
+ python-multipart
server.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from backend.models import ChatRequest
4
+ from backend.llm_utils import sanitize_history, route_message, get_reply
5
+ from backend.rag_utils import get_user_data
6
+ from backend.models import ChatRequest, SummaryRequest
7
+ from backend.llm_utils import sanitize_history, route_message, get_reply, generate_chat_summary
8
+ from backend.voice.stt import transcribe_audio
9
+ from backend.voice.tts import synthesize_speech
10
+
11
+ from fastapi import UploadFile, File, Form
12
+ from fastapi.responses import StreamingResponse, JSONResponse
13
+ import json
14
+ import io
15
+ import base64
16
+
17
+ from backend.cache_utils import get_cached_user_data, cache_user_data, cleanup_expired_cache
18
+
19
+
20
+ import json
21
+ import os
22
+
23
+ from backend.credentials import setup_google_credentials
24
+ setup_google_credentials()
25
+
26
+
27
+ app = FastAPI()
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"],
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+ @app.post("/chat")
36
+ async def chat_endpoint(req: ChatRequest):
37
+ user_message = req.message
38
+ history = req.history or []
39
+ user_id = req.uid
40
+
41
+ if not user_message:
42
+ return {"error": "message is required"}
43
+ user_data = {}
44
+ if user_id:
45
+ try:
46
+ user_data = get_user_data(user_id)
47
+ except Exception as e:
48
+ user_data = {}
49
+ try:
50
+ route = await route_message(user_message)
51
+ simple_history = sanitize_history(history)
52
+ simple_history.append({"role": "user", "content": user_message})
53
+ reply = await get_reply(route, simple_history, user_data, user_id)
54
+ if not reply:
55
+ reply = "I'm here to help with your wellness journey! What would you like to work on today?"
56
+ return {"reply": reply}
57
+ except Exception as e:
58
+ return {"reply": "Sorry, I'm having trouble right now. Could you try again in a moment?"}
59
+
60
+ import time
61
+ import asyncio
62
+
63
+ @app.post("/summarize")
64
+ async def summarize_endpoint(req: SummaryRequest):
65
+ start_time = time.time()
66
+ try:
67
+ messages = req.messages
68
+ if not messages:
69
+ print(f"[TIMING] Summary - No messages: {(time.time() - start_time):.2f}ms")
70
+ return {"summary": "New Chat"}
71
+
72
+ import_start = time.time()
73
+ from backend.llm_utils import generate_chat_summary
74
+ print(f"[TIMING] Summary - Import: {(time.time() - import_start):.2f}s")
75
+
76
+ summary_start = time.time()
77
+ summary = await generate_chat_summary(messages)
78
+ print(f"[TIMING] Summary - Generation: {(time.time() - summary_start):.2f}ms")
79
+
80
+ print(f"[TIMING] Summary - Total: {(time.time() - start_time):.2f}ms")
81
+ return {"summary": summary}
82
+ except Exception as e:
83
+ print(f"[TIMING] Summary - Error after {(time.time() - start_time):.2f}ms:", e)
84
+ return {"summary": "New Chat"}
85
+
86
+ @app.post("/voice-chat")
87
+ async def voice_chat_endpoint(
88
+ file: UploadFile = File(...),
89
+ history: str = Form(None),
90
+ uid: str = Form(None),
91
+ voice: str = Form("alloy")
92
+ ):
93
+ start_time = time.time()
94
+ try:
95
+ # Step 1: File reading
96
+ file_start = time.time()
97
+ audio_bytes = await file.read()
98
+ print(f"[TIMING] Voice - File read: {(time.time() - file_start) :.2f}ms ({len(audio_bytes)} bytes)")
99
+
100
+ # Step 2: Start transcription immediately
101
+ transcription_start = time.time()
102
+ transcription_task = asyncio.create_task(transcribe_audio(audio_bytes, ".m4a"))
103
+
104
+ # Step 3: Prepare other data in parallel
105
+ user_data_task = None
106
+ if uid:
107
+ user_data_start = time.time()
108
+ user_data_task = asyncio.create_task(get_user_data_async(uid))
109
+ print(f"[TIMING] Voice - User data task started: {(time.time() - user_data_start):.2f}ms")
110
+
111
+ # Step 4: Parse history while transcription runs
112
+ history_start = time.time()
113
+ simple_history = json.loads(history) if history else []
114
+ print(f"[TIMING] Voice - History parsing: {(time.time() - history_start):.2f}ms ({len(simple_history)} messages)")
115
+
116
+ # Step 5: Wait for transcription
117
+ transcription_wait_start = time.time()
118
+ user_message = await transcription_task
119
+ print(f"[TIMING] Voice - Transcription total: {(time.time() - transcription_start):.2f}ms")
120
+ print(f"[TIMING] Voice - Transcription wait: {(time.time() - transcription_wait_start):.2f}ms")
121
+ print("WHISPER transcript:", repr(user_message))
122
+
123
+ if not user_message.strip():
124
+ print(f"[TIMING] Voice - Empty transcript, returning early: {(time.time() - start_time) :.2f}ms")
125
+ return {"user_transcript": "", "reply": "I didn't catch that", "audio_base64": ""}
126
+
127
+ # Step 6: Get user data (if task was started)
128
+ user_data = {}
129
+ if user_data_task:
130
+ user_data_wait_start = time.time()
131
+ try:
132
+ user_data = await user_data_task
133
+ print(f"[TIMING] Voice - User data retrieval: {(time.time() - user_data_wait_start) :.2f}ms")
134
+ except Exception as e:
135
+ print(f"[TIMING] Voice - User data error after {(time.time() - user_data_wait_start) :.2f}ms: {e}")
136
+ user_data = {}
137
+
138
+ # Step 7: Process through your logic
139
+ history_append_start = time.time()
140
+ simple_history.append({"role": "user", "content": user_message})
141
+ print(f"[TIMING] Voice - History append: {(time.time() - history_append_start) :.2f}ms")
142
+
143
+ # Step 8: Run routing
144
+ routing_start = time.time()
145
+ route_task = asyncio.create_task(route_message(user_message))
146
+ route = await route_task
147
+ print(f"[TIMING] Voice - Message routing: {(time.time() - routing_start):.2f}ms (route: {route})")
148
+
149
+ # Step 9: Generate reply
150
+ reply_start = time.time()
151
+ reply = await get_reply(route, simple_history, user_data, uid)
152
+ if not reply:
153
+ reply = "I'm here to help with your wellness journey! What would you like to work on today?"
154
+ print(f"[TIMING] Voice - Reply generation: {(time.time() - reply_start) :.2f}ms")
155
+
156
+ # Step 10: Generate speech
157
+ tts_start = time.time()
158
+ audio_data = await synthesize_speech(reply, voice)
159
+ print(f"[TIMING] Voice - TTS generation: {(time.time() - tts_start):.2f}ms")
160
+
161
+ # Step 11: Base64 encoding
162
+ encoding_start = time.time()
163
+ base64_audio = base64.b64encode(audio_data).decode()
164
+ print(f"[TIMING] Voice - Base64 encoding: {(time.time() - encoding_start) :.2f}ms")
165
+
166
+ # Total timing
167
+ total_time = (time.time() - start_time)
168
+ print(f"[TIMING] Voice - TOTAL PIPELINE: {total_time:.2f}ms")
169
+
170
+ # Breakdown summary
171
+ print(f"[TIMING] Voice - BREAKDOWN:")
172
+ print(f" • File read: {(file_start - start_time) :.2f}ms")
173
+ print(f" • Transcription: {(time.time() - transcription_start) :.2f}ms")
174
+ print(f" • Routing: {(time.time() - routing_start) :.2f}ms")
175
+ print(f" • Reply: {(time.time() - reply_start) :.2f}ms")
176
+ print(f" • TTS: {(time.time() - tts_start) :.2f}ms")
177
+
178
+ return {
179
+ "user_transcript": user_message,
180
+ "reply": reply,
181
+ "audio_base64": base64_audio
182
+ }
183
+ except Exception as e:
184
+ error_time = (time.time() - start_time)
185
+ print(f"[TIMING] Voice - ERROR after {error_time:.2f}ms:", e)
186
+ return JSONResponse({"error": str(e)}, status_code=500)
187
+
188
+ # Add async wrapper for get_user_data
189
+ async def get_user_data_async(uid: str):
190
+ start_time = time.time()
191
+
192
+ # Try to get from cache first
193
+ cached_data = get_cached_user_data(uid)
194
+ if cached_data:
195
+ print(f"[TIMING] User data (cached): {(time.time() - start_time) :.2f}ms")
196
+ return cached_data
197
+
198
+ # Cache miss - fetch fresh data
199
+ print("[CACHE] User data cache miss, fetching fresh data...")
200
+ result = get_user_data(uid)
201
+ print(f"[TIMING] User data fetch: {(time.time() - start_time) :.2f}ms")
202
+ return result
203
+
204
+
205
+ @app.get("/cache/stats")
206
+ async def cache_stats_endpoint():
207
+ """Get cache performance statistics"""
208
+ from backend.cache_utils import get_cache_stats, cleanup_expired_cache
209
+ cleanup_expired_cache() # Clean up while we're at it
210
+ stats = get_cache_stats()
211
+ return stats
212
+
213
+ @app.post("/cache/clear")
214
+ async def clear_cache_endpoint(user_id: str = None):
215
+ """Clear cache for specific user or all users"""
216
+ from backend.cache_utils import clear_user_cache
217
+ clear_user_cache(user_id)
218
+ return {"message": f"Cache cleared for {'all users' if not user_id else f'user {user_id}'}"}
219
+
220
+
221
+
222
+ if __name__ == "__main__":
223
+ import uvicorn
224
+ port = int(os.getenv("PORT", 3000))
225
+ uvicorn.run(app, host="0.0.0.0", port=port)
tools/__init__.py ADDED
File without changes
tools/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (177 Bytes). View file
 
tools/__pycache__/goal_tools.cpython-313.pyc ADDED
Binary file (5.34 kB). View file
 
tools/goal_tools.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from google.cloud import firestore
2
+ from langchain_core.tools import tool
3
+ from datetime import datetime, timedelta
4
+ import pytz
5
+
6
+
7
+ from backend.credentials import setup_google_credentials
8
+ setup_google_credentials()
9
+
10
+ APP_TO_DB_CATEGORY = {
11
+ "vocational": "occupational",
12
+ }
13
+
14
+ def to_db_category(slug):
15
+ return APP_TO_DB_CATEGORY.get(slug, slug)
16
+
17
+ def add_goal_to_firestore(user_id, goal_name, goal_description, category_slug,
18
+ timeframe="Month", reminder_enabled=True, duration_weeks=6):
19
+ """
20
+ Add a goal to Firestore with proper timestamps and fields
21
+
22
+ Args:
23
+ user_id: User's Firebase UID
24
+ goal_name: Name of the goal
25
+ goal_description: Description of the goal
26
+ category_slug: Wellness dimension (physical, mental, etc.)
27
+ timeframe: Goal timeframe (Month, Week, Year)
28
+ reminder_enabled: Whether to enable reminders
29
+ duration_weeks: How many weeks the goal should run
30
+ """
31
+ db = firestore.Client()
32
+
33
+ # Map app slug to db slug
34
+ category_slug = to_db_category(category_slug)
35
+
36
+ # Look up the category
37
+ cat_docs = db.collection("goals_categories").where("cat_slug", "==", category_slug).stream()
38
+ cat_doc = next(cat_docs, None)
39
+ if not cat_doc:
40
+ raise Exception(f"Category with slug '{category_slug}' not found.")
41
+
42
+ cat_id = cat_doc.id
43
+ cat_data = cat_doc.to_dict()
44
+
45
+ # Create timestamps
46
+ now = datetime.now(pytz.UTC)
47
+ end_date = now + timedelta(weeks=duration_weeks)
48
+
49
+ goal_data = {
50
+ "endDate": end_date,
51
+ "goalDescription": goal_description,
52
+ "goalName": goal_name,
53
+ "goalReminder": reminder_enabled,
54
+ "startDate": now,
55
+ "status": True,
56
+ "timeFrame": timeframe,
57
+ "user_id": user_id,
58
+ "wellnessDimension": cat_id,
59
+ "wellnessDimension_ref": f"/goals_categories/{cat_id}",
60
+ }
61
+
62
+ # Add to Firestore
63
+ doc_ref = db.collection("goals").add(goal_data)
64
+
65
+ # Return the data with the document ID
66
+ result = goal_data.copy()
67
+ result["id"] = doc_ref[1].id # doc_ref is a tuple (timestamp, document_reference)
68
+
69
+ return result
70
+
71
+ @tool("add_goal")
72
+ def add_goal_tool(user_id: str, goal_name: str, goal_description: str, category_slug: str,
73
+ timeframe: str = "Month", reminder_enabled: bool = True, duration_weeks: int = 6):
74
+ """
75
+ Add a new user goal to Firestore with category_slug (physical, mental, social, etc).
76
+
77
+ Args:
78
+ user_id: User's Firebase UID
79
+ goal_name: Short name for the goal
80
+ goal_description: Detailed description of what the goal entails
81
+ category_slug: Wellness dimension slug (physical, mental, spiritual, etc.)
82
+ timeframe: Goal timeframe - "Month", "Week", or "Year" (default: "Month")
83
+ reminder_enabled: Whether to enable reminders (default: True)
84
+ duration_weeks: How many weeks the goal should run (default: 6)
85
+ """
86
+ try:
87
+ result = add_goal_to_firestore(
88
+ user_id,
89
+ goal_name,
90
+ goal_description,
91
+ category_slug,
92
+ timeframe,
93
+ reminder_enabled,
94
+ duration_weeks
95
+ )
96
+ print("INSIDE TOOL RESULT:", result, type(result))
97
+
98
+ from backend.cache_utils import clear_user_cache
99
+ clear_user_cache(user_id)
100
+
101
+ if isinstance(result, dict):
102
+ # Convert datetime objects to strings for JSON serialization
103
+ serializable_result = {}
104
+ for key, value in result.items():
105
+ if isinstance(value, datetime):
106
+ serializable_result[key] = value.isoformat()
107
+ else:
108
+ serializable_result[key] = value
109
+ return serializable_result
110
+ elif hasattr(result, "dict"):
111
+ return result.dict()
112
+ else:
113
+ return {"error": "Unexpected result type", "result": str(result)}
114
+
115
+ except Exception as e:
116
+ print(f"Error in add_goal_tool: {e}")
117
+ return {"error": str(e), "success": False}
118
+
119
+
120
+ @tool("list_goal_categories")
121
+ def list_goal_categories():
122
+ """List all available wellness dimension categories for goals."""
123
+ try:
124
+ db = firestore.Client()
125
+ categories = []
126
+
127
+ for doc in db.collection("goals_categories").stream():
128
+ cat_data = doc.to_dict()
129
+ categories.append({
130
+ "id": doc.id,
131
+ "name": cat_data.get("cat_name", "Unknown"),
132
+ "slug": cat_data.get("cat_slug", "unknown"),
133
+ "description": cat_data.get("cat_description", "")
134
+ })
135
+
136
+ return {"categories": categories}
137
+ except Exception as e:
138
+ return {"error": str(e), "categories": []}