Shageenderan Sapai commited on
Commit
1aeb534
·
1 Parent(s): eb3dba5

Align with staging branch web_search

Browse files
Files changed (6) hide show
  1. app/assistants.py +3 -162
  2. app/conversation_manager.py +59 -8
  3. app/flows.py +7 -6
  4. app/main.py +62 -14
  5. app/user.py +27 -9
  6. app/web_search.py +255 -0
app/assistants.py CHANGED
@@ -16,173 +16,14 @@ import pytz
16
  from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError, UtilsError
17
  from app.utils import get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, get_users_mementos, print_log
18
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
 
19
 
20
  load_dotenv()
21
 
22
  logger = logging.getLogger(__name__)
23
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
24
 
25
- import requests
26
 
27
- class SearchEngine:
28
- BING_API_KEY = os.getenv("BING_API_KEY")
29
- BING_ENDPOINT = 'https://api.bing.microsoft.com/v7.0'
30
-
31
- @staticmethod
32
- def search(feedback_type_name, search_term):
33
- """
34
- Public method to perform a search based on the feedback type.
35
- """
36
- search_methods = {
37
- "Resource Links": SearchEngine._search_relevant_links,
38
- "Book/Podcast Recommendations": SearchEngine._search_books_or_podcasts,
39
- # "Success Stories/Testimonials": SearchEngine._search_success_stories,
40
- "Inspirational Stories or Case Studies": SearchEngine._search_inspirational_stories,
41
- "Fun Facts": SearchEngine._search_fun_facts,
42
- # "Visual Content (Images, Infographics)": SearchEngine._search_visual_content,
43
- "Personalised Recommendations": SearchEngine._search_personalized_recommendations
44
- }
45
-
46
- search_method = search_methods.get(feedback_type_name)
47
-
48
- if search_method:
49
- return search_method(search_term)
50
- else:
51
- return (feedback_type_name, search_term)
52
-
53
- @staticmethod
54
- def _search_relevant_links(search_term):
55
- """
56
- Uses Bing Web Search API to search for relevant links.
57
- """
58
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
59
- params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
60
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
61
- if response.status_code == 200:
62
- data = response.json()
63
- links = []
64
- if 'webPages' in data and 'value' in data['webPages']:
65
- for result in data['webPages']['value']:
66
- links.append(result)
67
- return links
68
- return ["No relevant links found."]
69
-
70
- @staticmethod
71
- def _search_books_or_podcasts(search_term):
72
- """
73
- Uses Bing Web Search API to search for books or podcasts.
74
- """
75
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
76
- query = f"{search_term} book OR podcast"
77
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
78
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
79
- if response.status_code == 200:
80
- data = response.json()
81
- recommendations = []
82
- if 'webPages' in data and 'value' in data['webPages']:
83
- for result in data['webPages']['value']:
84
- title = result.get('name', 'Unknown Title')
85
- url = result.get('url', '')
86
- recommendations.append(f"{title}: {url}")
87
- return recommendations
88
- return ["No book or podcast recommendations found."]
89
-
90
- @staticmethod
91
- def _search_success_stories(search_term):
92
- """
93
- Uses Bing Web Search API to search for success stories.
94
- """
95
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
96
- query = f"{search_term} success stories"
97
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
98
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
99
- if response.status_code == 200:
100
- data = response.json()
101
- stories = []
102
- if 'webPages' in data and 'value' in data['webPages']:
103
- for result in data['webPages']['value']:
104
- title = result.get('name', 'Unknown Title')
105
- url = result.get('url', '')
106
- stories.append(f"{title}: {url}")
107
- return stories
108
- return ["No success stories found."]
109
-
110
- @staticmethod
111
- def _search_inspirational_stories(search_term):
112
- """
113
- Uses Bing Web Search API to search for inspirational stories or case studies.
114
- """
115
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
116
- query = f"{search_term} inspirational stories OR case studies"
117
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
118
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
119
- if response.status_code == 200:
120
- data = response.json()
121
- stories = []
122
- if 'webPages' in data and 'value' in data['webPages']:
123
- for result in data['webPages']['value']:
124
- title = result.get('name', 'Unknown Title')
125
- url = result.get('url', '')
126
- stories.append(f"{title}: {url}")
127
- return stories
128
- return ["No inspirational stories found."]
129
-
130
- @staticmethod
131
- def _search_fun_facts(search_term):
132
- """
133
- Uses Bing Web Search API to search for fun facts related to personal growth.
134
- """
135
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
136
- query = f"{search_term} fun facts"
137
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
138
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
139
- if response.status_code == 200:
140
- data = response.json()
141
- facts = []
142
- if 'webPages' in data and 'value' in data['webPages']:
143
- for result in data['webPages']['value']:
144
- snippet = result.get('snippet', '')
145
- facts.append(snippet)
146
- return facts
147
- return ["No fun facts found."]
148
-
149
- @staticmethod
150
- def _search_visual_content(search_term):
151
- """
152
- Uses Bing Image Search API to search for images or infographics.
153
- """
154
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
155
- params = {'q': search_term, 'count': 3}
156
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/images/search", headers=headers, params=params)
157
- if response.status_code == 200:
158
- data = response.json()
159
- images = []
160
- if 'value' in data:
161
- for result in data['value']:
162
- image_url = result.get('contentUrl', '')
163
- images.append(image_url)
164
- return images
165
- return ["No visual content found."]
166
-
167
- @staticmethod
168
- def _search_personalized_recommendations(search_term):
169
- """
170
- Uses Bing Web Search API to provide personalized recommendations.
171
- """
172
- headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
173
- query = f"tips for {search_term}"
174
- params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
175
- response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
176
- if response.status_code == 200:
177
- data = response.json()
178
- recommendations = []
179
- if 'webPages' in data and 'value' in data['webPages']:
180
- for result in data['webPages']['value']:
181
- title = result.get('name', 'Unknown Title')
182
- url = result.get('url', '')
183
- recommendations.append(f"{title}: {url}")
184
- return recommendations
185
- return ["No personalized recommendations found."]
186
 
187
  class FeedbackContent:
188
  def __init__(self, content, role):
@@ -569,11 +410,11 @@ class Assistant:
569
  "tool_call_id": tool.id,
570
  "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
571
  })
572
- elif tool.function.name == "search_resource":
573
  type = json.loads(tool.function.arguments)['resource_type']
574
  query = json.loads(tool.function.arguments)['query']
575
  logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
576
- relevant_context = SearchEngine.search(type, query)
577
  logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
578
  tool_outputs.append({
579
  "tool_call_id": tool.id,
 
16
  from app.exceptions import AssistantError, BaseOurcoachException, OpenAIRequestError, UtilsError
17
  from app.utils import get_booked_gg_sessions, get_growth_guide, get_growth_guide_summary, get_user_subscriptions, get_users_mementos, print_log
18
  from app.flows import FOLLOW_UP_STATE, GENERAL_COACHING_STATE, MICRO_ACTION_STATE, REFLECTION_STATE
19
+ from app.web_search import SearchEngine
20
 
21
  load_dotenv()
22
 
23
  logger = logging.getLogger(__name__)
24
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
25
 
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  class FeedbackContent:
29
  def __init__(self, content, role):
 
410
  "tool_call_id": tool.id,
411
  "output": "Generate a final coach message (feedback message) using these 3 feedback types (together with the stated emoji at the beginning of each feedback): " + str(sample_feedbacks)
412
  })
413
+ elif tool.function.name == "search_web":
414
  type = json.loads(tool.function.arguments)['resource_type']
415
  query = json.loads(tool.function.arguments)['query']
416
  logger.info(f"Getting microaction theme: {type} - {query}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
417
+ relevant_context = SearchEngine.search(type, query, self.cm.user.user_id)
418
  logger.info(f"Finish Getting microaction theme: {relevant_context}", extra={"user_id": self.cm.user.user_id, "endpoint": "get_microaction_theme"})
419
  tool_outputs.append({
420
  "tool_call_id": tool.id,
app/conversation_manager.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import openai
3
  import pandas as pd
@@ -10,7 +11,9 @@ from datetime import datetime
10
 
11
  import dotenv
12
 
13
- from app.utils import id_to_persona
 
 
14
  dotenv.load_dotenv()
15
 
16
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
@@ -27,7 +30,13 @@ class ConversationManager:
27
  self.assistants = {'general': Assistant(asst_id, self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
28
 
29
  self.client = client
30
- self.state = {'date': pd.Timestamp.now(tz='UTC').strftime("%Y-%m-%d %a %H:%M:%S")}
 
 
 
 
 
 
31
 
32
  self.current_thread = self.create_thread()
33
  self.daily_thread = None
@@ -70,6 +79,8 @@ class ConversationManager:
70
  {id_to_persona(self.user.asst_id)}, always adhere to your choosen persona by incorporating it conversationally.
71
  You represent a coach at ourcoach. You may refer to you Knowledgebase (ourcoach FAQ) for all information related to ourcoach.
72
  ** Branding ** Always stylize ourcoach as 'ourcoach' instead of 'OurCoach' or 'Ourcoach', regardless of any grammatical errors.
 
 
73
  -------------------------------------------
74
  You are coaching:
75
  \n\n{user_interaction_guidelines}\n\n\
@@ -103,11 +114,51 @@ class ConversationManager:
103
  return message
104
 
105
  @catch_error
106
- def _run_current_thread(self, text, thread=None, hidden=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  if thread is None:
108
  thread = self.current_thread
109
  logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
110
- logger.info(f"User Message: {text}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
 
 
111
 
112
  # need to select assistant
113
  if self.intro_done:
@@ -117,7 +168,7 @@ class ConversationManager:
117
  logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
118
  run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
119
 
120
- logger.info(f"Run {run.id} {run.status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
121
 
122
  if 'message' in run.metadata:
123
  info = run.metadata['message']
@@ -136,18 +187,18 @@ class ConversationManager:
136
 
137
  if hidden:
138
  self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
139
- logger.info(f"Deleted hidden message: {message}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
140
 
141
  return self._get_current_thread_history(remove_system_message=False)[-1], run
142
 
143
  @catch_error
144
  def _send_and_replace_message(self, text, replacement_msg=None):
145
- logger.info(f"Sending hidden message: {text}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
146
  response, _ = self._run_current_thread(text, hidden=True)
147
 
148
  # check if there is a replacement message
149
  if replacement_msg:
150
- logger.info(f"Adding replacement message: {replacement_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
151
  # get the last message
152
  last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
153
  logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
 
1
+ import base64
2
  import os
3
  import openai
4
  import pandas as pd
 
11
 
12
  import dotenv
13
 
14
+ from app.utils import get_user_local_timezone, id_to_persona
15
+ from PIL import Image
16
+ import io
17
  dotenv.load_dotenv()
18
 
19
  OURCOACH_DASHBOARD_URL = os.getenv("OURCOACH_DASHBOARD_URL")
 
30
  self.assistants = {'general': Assistant(asst_id, self), 'intro': Assistant('asst_baczEK65KKvPWIUONSzdYH8j', self)}
31
 
32
  self.client = client
33
+
34
+ # get users local timezone
35
+ timezone = get_user_local_timezone(user.user_id)
36
+
37
+ # convert UTC to user's timezone
38
+ local_time = pd.Timestamp.now(tz='UTC').tz_convert(timezone)
39
+ self.state = {'date': local_time.strftime("%Y-%m-%d %a %H:%M:%S")}
40
 
41
  self.current_thread = self.create_thread()
42
  self.daily_thread = None
 
79
  {id_to_persona(self.user.asst_id)}, always adhere to your choosen persona by incorporating it conversationally.
80
  You represent a coach at ourcoach. You may refer to you Knowledgebase (ourcoach FAQ) for all information related to ourcoach.
81
  ** Branding ** Always stylize ourcoach as 'ourcoach' instead of 'OurCoach' or 'Ourcoach', regardless of any grammatical errors.
82
+ **You truly believe you are your persona**
83
+ **You always try to be engaging and personal with your coachee in order to get them to continue interacting with you daily**
84
  -------------------------------------------
85
  You are coaching:
86
  \n\n{user_interaction_guidelines}\n\n\
 
114
  return message
115
 
116
  @catch_error
117
+ def _run_current_thread(self, text, thread=None, hidden=False, media=None):
118
+ image_context = ""
119
+ if media:
120
+ logger.info(f"Describing Media", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
121
+ # assert media is a base64 encoded string
122
+ try:
123
+ # scale down media to 512x512
124
+ img_data = base64.b64decode(media)
125
+ img = Image.open(io.BytesIO(img_data))
126
+ img = img.resize((512, 512), Image.Resampling.LANCZOS) # Better quality resizing
127
+ buffered = io.BytesIO()
128
+ img.save(buffered, format="JPEG", quality=85) # Specify format and quality
129
+ media = base64.b64encode(buffered.getvalue()).decode()
130
+
131
+ except Exception as e:
132
+ raise ConversationManagerError(user_id=self.user.user_id, message="Invalid base64 image data", e=str(e))
133
+ logger.info(f"Media is valid", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
134
+ response = self.client.chat.completions.create(
135
+ model="gpt-4o",
136
+ messages=[
137
+ {
138
+ "role": "user",
139
+ "content": [
140
+ {
141
+ "type": "text",
142
+ "text": f"""Describe this image to an LLM for it to have rich and complete context.
143
+ {'Lock in on things related to the users message:'+text if text else text}""",
144
+ },
145
+ {
146
+ "type": "image_url",
147
+ "image_url": {"url": f"data:image/jpeg;base64,{media}", "detail": "low"},
148
+ },
149
+ ],
150
+ }
151
+ ])
152
+
153
+ image_context = response.choices[0].message.content
154
+ logger.info(f"Image context: {image_context}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
155
+
156
  if thread is None:
157
  thread = self.current_thread
158
  logger.warning(f"{self}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
159
+
160
+ if image_context:
161
+ text = f"{text}\n\n[MEDIA ATTACHMENT] User attached an image about: {image_context}"
162
 
163
  # need to select assistant
164
  if self.intro_done:
 
168
  logger.info(f"Running intro assistant", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
169
  run, just_finished_intro, message = self.assistants['intro'].process(thread, text)
170
 
171
+ logger.info(f"Run done, status={run.status} just finished intro: {just_finished_intro}", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
172
 
173
  if 'message' in run.metadata:
174
  info = run.metadata['message']
 
187
 
188
  if hidden:
189
  self.client.beta.threads.messages.delete(message_id=message.id, thread_id=thread.id)
190
+ logger.info(f"Deleted hidden message", extra={"user_id": self.user.user_id, "endpoint": "run_current_thread"})
191
 
192
  return self._get_current_thread_history(remove_system_message=False)[-1], run
193
 
194
  @catch_error
195
  def _send_and_replace_message(self, text, replacement_msg=None):
196
+ logger.info(f"Sending hidden message", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
197
  response, _ = self._run_current_thread(text, hidden=True)
198
 
199
  # check if there is a replacement message
200
  if replacement_msg:
201
+ logger.info(f"Adding replacement message", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
202
  # get the last message
203
  last_msg = list(self.client.beta.threads.messages.list(self.current_thread.id, order="asc"))[-1]
204
  logger.info(f"Last message: {last_msg}", extra={"user_id": self.user.user_id, "endpoint": "send_hidden_message"})
app/flows.py CHANGED
@@ -59,7 +59,8 @@ Step 1:
59
  **First Message: Micro-Action Suggestion**
60
 
61
  - Propose one clear, actionable task for the day.
62
- - Ensure it is relevant, easy to begin, and framed positively.
 
63
  - Avoid repeating previous actions.
64
  - Your message must be concise! (use Whatsapp texting length)
65
  - Wait for the user's response
@@ -87,6 +88,7 @@ Step 1:
87
 
88
  - **If the user has decided to try out the micro-action:**
89
  - Encourage them to proceed with the task (Your message must be concise!)
 
90
  - Do **not** ask any questions at this point.
91
  - After the user confirms they've completed the micro-action:
92
  - Acknowledge their effort.
@@ -107,7 +109,7 @@ Step 1:
107
  **Key Rules for Micro-Actions**
108
 
109
  - **Personalized and Achievable:** Align with the user’s progress, keeping early actions (Day 1–5) simple and gradually increasing difficulty (Day 6+).
110
- - **Resource Suggestions:** Avoid recommending resources on Day 1; may do so sparingly on later days if relevant.
111
  - **Guided Coaching:** Provide micro-actions as if you naturally guide the user—never state or imply a function call.
112
 
113
  ---
@@ -117,16 +119,15 @@ Step 1:
117
  - **Warm and Encouraging:** Mirror a friendly, personable texting style.
118
  - **Simple and Succinct:** Use conversational, human-like language in WhatsApp texting length.
119
  - **Action-Oriented:** Focus on immediate, actionable steps rather than abstract suggestions.
120
-
121
  ---
122
 
123
  **Interaction Principles**
124
 
125
  1. **Simplicity:** Actions should feel easy and immediately doable.
126
  2. **Relevance:** Align each action with the user’s goal and progress.
127
- 3. **Encouragement:** Motivate the user without judgment or pressure.
128
- 4. **Feedback:** Reinforce positive habits subtly to build momentum.
129
- 5. **Tone:** Casual, personable, and focused on meaningful progress.
130
 
131
  ---
132
 
 
59
  **First Message: Micro-Action Suggestion**
60
 
61
  - Propose one clear, actionable task for the day.
62
+ - Ensure it is relevant, timely, easy to begin, and framed positively.
63
+ - If you believe it will be helpful and engaging for the user, you can call search_web(...) to get better context or timely recommendations on the topic to incorporate it into your response.
64
  - Avoid repeating previous actions.
65
  - Your message must be concise! (use Whatsapp texting length)
66
  - Wait for the user's response
 
88
 
89
  - **If the user has decided to try out the micro-action:**
90
  - Encourage them to proceed with the task (Your message must be concise!)
91
+ - Ok coach, if you feel like its appropriate i.e, the quality of the microaction can be enhanced via a picture or the microaction accountability tracking can be enhaced by receiving picture, let the user know that you can track their progress and provide coaching even through picture (but dont always ask for pictures, thats kinda creepy).
92
  - Do **not** ask any questions at this point.
93
  - After the user confirms they've completed the micro-action:
94
  - Acknowledge their effort.
 
109
  **Key Rules for Micro-Actions**
110
 
111
  - **Personalized and Achievable:** Align with the user’s progress, keeping early actions (Day 1–5) simple and gradually increasing difficulty (Day 6+).
112
+ - **Resource Suggestions:** You may call search_web sparingly when you feel it would absolutely blow the user away.
113
  - **Guided Coaching:** Provide micro-actions as if you naturally guide the user—never state or imply a function call.
114
 
115
  ---
 
119
  - **Warm and Encouraging:** Mirror a friendly, personable texting style.
120
  - **Simple and Succinct:** Use conversational, human-like language in WhatsApp texting length.
121
  - **Action-Oriented:** Focus on immediate, actionable steps rather than abstract suggestions.
122
+ - **Coach Mindset:** You have entered focussed mind state akin to that of elite coaches. While being encouraging and friendly, you should lightly push and challenge the user when appropriate.
123
  ---
124
 
125
  **Interaction Principles**
126
 
127
  1. **Simplicity:** Actions should feel easy and immediately doable.
128
  2. **Relevance:** Align each action with the user’s goal and progress.
129
+ 3. **Feedback:** Reinforce positive habits subtly to build momentum.
130
+ 4. **Tone:** Casual, personable, and focused on meaningful progress.
 
131
 
132
  ---
133
 
app/main.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import FastAPI, HTTPException, Security, Query, status, Request, Depends
2
  from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
3
  from fastapi.security import APIKeyHeader
4
  import openai
@@ -28,6 +28,9 @@ import pickle
28
  from app.exceptions import *
29
  import re
30
  import sentry_sdk
 
 
 
31
 
32
  load_dotenv()
33
  AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
@@ -35,18 +38,18 @@ AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY')
35
  REGION = os.getenv('AWS_REGION')
36
  SENTRY_DSN = os.getenv('SENTRY_DSN')
37
 
38
- sentry_sdk.init(
39
- dsn=SENTRY_DSN,
40
- # Set traces_sample_rate to 1.0 to capture 100%
41
- # of transactions for tracing.
42
- traces_sample_rate=1.0,
43
- _experiments={
44
- # Set continuous_profiling_auto_start to True
45
- # to automatically start the profiler on when
46
- # possible.
47
- "continuous_profiling_auto_start": True,
48
- },
49
- )
50
 
51
 
52
 
@@ -278,6 +281,7 @@ class CreateUserItem(BaseModel):
278
  class ChatItem(BaseModel):
279
  user_id: str
280
  message: str
 
281
 
282
  class PersonaItem(BaseModel):
283
  user_id: str
@@ -836,7 +840,9 @@ async def chat(
836
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
837
  user = get_user(request.user_id)
838
 
839
- response = user.send_message(request.message)
 
 
840
  logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
841
  return {"response": response}
842
 
@@ -1109,3 +1115,45 @@ async def get_recent_booking(
1109
  if 'conn' in locals():
1110
  conn.close()
1111
  return {"booking_id": booking_id}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Security, Query, status, Request, Depends, File, UploadFile, Form
2
  from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
3
  from fastapi.security import APIKeyHeader
4
  import openai
 
28
  from app.exceptions import *
29
  import re
30
  import sentry_sdk
31
+ import base64
32
+ from io import BytesIO
33
+ from PIL import Image
34
 
35
  load_dotenv()
36
  AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY')
 
38
  REGION = os.getenv('AWS_REGION')
39
  SENTRY_DSN = os.getenv('SENTRY_DSN')
40
 
41
+ # sentry_sdk.init(
42
+ # dsn=SENTRY_DSN,
43
+ # # Set traces_sample_rate to 1.0 to capture 100%
44
+ # # of transactions for tracing.
45
+ # traces_sample_rate=1.0,
46
+ # _experiments={
47
+ # # Set continuous_profiling_auto_start to True
48
+ # # to automatically start the profiler on when
49
+ # # possible.
50
+ # "continuous_profiling_auto_start": True,
51
+ # },
52
+ # )
53
 
54
 
55
 
 
281
  class ChatItem(BaseModel):
282
  user_id: str
283
  message: str
284
+ image64: str = None
285
 
286
  class PersonaItem(BaseModel):
287
  user_id: str
 
840
  logger.info("Processing chat request", extra={"user_id": request.user_id, "endpoint": "/chat"})
841
  user = get_user(request.user_id)
842
 
843
+ if request.image64:
844
+ logger.info(f"Processing image", extra={"user_id": request.user_id, "endpoint": "/chat"})
845
+ response = user.send_message(request.message, request.image64)
846
  logger.info(f"Assistant response generated", extra={"user_id": request.user_id, "endpoint": "/chat"})
847
  return {"response": response}
848
 
 
1115
  if 'conn' in locals():
1116
  conn.close()
1117
  return {"booking_id": booking_id}
1118
+
1119
+ @app.post("/answer_image_question")
1120
+ async def answer_image_question(
1121
+ question: str = Form(...),
1122
+ file: UploadFile = File(None),
1123
+ image_base64: str = Form(None)
1124
+ ):
1125
+ if file:
1126
+ contents = await file.read()
1127
+ # convert to base64 string
1128
+ image_base64 = base64.b64encode(contents).decode('utf-8')
1129
+
1130
+ client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
1131
+ response = client.chat.completions.create(
1132
+ model="gpt-4o",
1133
+ messages=[
1134
+ {
1135
+ "role": "user",
1136
+ "content": [
1137
+ {
1138
+ "type": "text",
1139
+ "text": "What is in this image?",
1140
+ },
1141
+ {
1142
+ "type": "image_url",
1143
+ "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
1144
+ },
1145
+ ],
1146
+ }
1147
+ ])
1148
+
1149
+ return {"response": response.choices[0]}
1150
+
1151
+
1152
+
1153
+
1154
+ @app.post("/upload_image")
1155
+ async def upload_image(file: UploadFile = File(...)):
1156
+ # process or save the file
1157
+ contents = await file.read()
1158
+ image = Image.open(BytesIO(contents))
1159
+ return {"filename": file.filename, "info": "Image uploaded successfully"}
app/user.py CHANGED
@@ -457,8 +457,10 @@ class User:
457
  return self.conversations.current_thread
458
 
459
  @catch_error
460
- def send_message(self, text):
461
- response, run = self.conversations._run_current_thread(text)
 
 
462
  message = run.metadata.get("message", "No message")
463
  logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
464
 
@@ -475,6 +477,8 @@ class User:
475
  # Move to the next action
476
  self.growth_plan.next()
477
 
 
 
478
  elif message == "change_goal":
479
  # send the change goal prompt
480
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
@@ -726,8 +730,8 @@ class User:
726
  return [response]
727
 
728
  @catch_error
729
- def do_theme(self, theme, date, day, last_msg_is_answered = True):
730
- logger.info(f"Doing theme: {theme}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
731
 
732
  # Add 1 day to cumulative_plan_day
733
  self.cumulative_plan_day += 1
@@ -833,20 +837,21 @@ class User:
833
  else:
834
  # Remind the user that they can book a Growth Guide session if they have not done one yet after the FINAL_SUMMARY_STATE
835
  if self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
836
- formatted_message = f"[IMPORTANT] The user has not booked a Growth Guide session yet. Remind them that they can book one through their Revelation Dahsboard: {OURCOACH_DASHBOARD_URL} to get more personalized advice and guidance!\n\n" + formatted_message
 
837
 
838
  prompt = f"""** It is a new day: {date} ({day}) 10:00:00 **
839
 
840
- (If the day is a public holiday (e.g., Christmas, New Year, or other significant occasions), customize your message to reflect the context appropriately, acknowledging the holiday or its significance.)
841
 
842
  **Before we start,**
843
  Has the user answered your last question? : {last_msg_is_answered}
844
  If the answer above is "True", you may proceed to do the instruction below
845
- If the answer above is "False", you must immediately ask something like (but warmer) "Hey <user name>, I've noticed that you haven't answered my latest question. Do you still want to continue the growth plan?". If the user says "no", then ask if they want to set a new goal (therefore later, call the change_goal() function)
846
  But if the user says "yes", then proceed to do the instruction below.
847
 
848
  Today is day {self.cumulative_plan_day} of the user's growth journey (out of {final_day} days). You may (or may not) mention this occasionally in your first message of the day.
849
-
850
  Today's Theme:
851
  {formatted_message}
852
  """
@@ -918,7 +923,20 @@ class User:
918
  else:
919
  last_msg_is_answered = True
920
 
921
- response, prompt = self.do_theme(theme, date, action['day'], last_msg_is_answered)
 
 
 
 
 
 
 
 
 
 
 
 
 
922
 
923
  # add today's reminders to response to schedule
924
  # response['reminders'] = all reminders which date is today (so all the reminders that BE has to queue today)
 
457
  return self.conversations.current_thread
458
 
459
  @catch_error
460
+ def send_message(self, text, media=None):
461
+ if media:
462
+ logger.info(f"Sending message with media", extra={"user_id": self.user_id, "endpoint": "send_message"})
463
+ response, run = self.conversations._run_current_thread(text, media=media)
464
  message = run.metadata.get("message", "No message")
465
  logger.info(f"Message: {message}", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
466
 
 
477
  # Move to the next action
478
  self.growth_plan.next()
479
 
480
+ response['add_one'] = True
481
+
482
  elif message == "change_goal":
483
  # send the change goal prompt
484
  logger.info("Sending change goal message...", extra={"user_id": self.user_id, "endpoint": "user_send_message"})
 
730
  return [response]
731
 
732
  @catch_error
733
+ def do_theme(self, theme, date, day, last_msg_is_answered = True, extra=None):
734
+ logger.info(f"Doing theme: {theme}, extra={extra}", extra={"user_id": self.user_id, "endpoint": "do_theme"})
735
 
736
  # Add 1 day to cumulative_plan_day
737
  self.cumulative_plan_day += 1
 
837
  else:
838
  # Remind the user that they can book a Growth Guide session if they have not done one yet after the FINAL_SUMMARY_STATE
839
  if self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
840
+ if day != 1:
841
+ formatted_message = f"[IMPORTANT] The user has not booked a Growth Guide session yet. Remind them that they can book one through their Revelation Dahsboard: {OURCOACH_DASHBOARD_URL} to get more personalized advice and guidance!\n\n" + formatted_message
842
 
843
  prompt = f"""** It is a new day: {date} ({day}) 10:00:00 **
844
 
845
+ (If the day is a public holiday (e.g., Christmas, New Year, the user's Birthday or other significant occasions), customize your message to reflect the context appropriately, acknowledging the holiday or its significance.)
846
 
847
  **Before we start,**
848
  Has the user answered your last question? : {last_msg_is_answered}
849
  If the answer above is "True", you may proceed to do the instruction below
850
+ If the answer above is "False", take a deep breath coach, and utilizing your ability as an elite coach with the users best interest in mind, think whether it would be more appropriate to follow up the unanswered question with the user or continue with todays theme below. However if the user indicates that they want to set a new goal (call the change_goal() function)
851
  But if the user says "yes", then proceed to do the instruction below.
852
 
853
  Today is day {self.cumulative_plan_day} of the user's growth journey (out of {final_day} days). You may (or may not) mention this occasionally in your first message of the day.
854
+ {extra if extra else ''}
855
  Today's Theme:
856
  {formatted_message}
857
  """
 
923
  else:
924
  last_msg_is_answered = True
925
 
926
+ extra="Greet the user with a creative good morning message!"
927
+
928
+ # Check if user didnt respond (last 2 messages in conversation history are from the AI) on FINAL_SUMMARY_STATE
929
+ messages = self.get_messages()
930
+ logger.info(f"Last 2 messages are from: 1) {messages[-1]['role']} and 2) {messages[-2]['role']}")
931
+ if (messages[-1]['role'] == "assistant" and messages[-2]['role'] == "assistant") and self.growth_plan.previous()['coachingTheme'] == "FINAL_SUMMARY_STATE":
932
+ self.extend_growth_plan()
933
+ extra += """\nDang, the user did not indicate what they wanted to do yesterday.
934
+ This is not a good sign as it may indicate that the user is going to dropoff.
935
+ We have automatically extended their growth plan for them, let the user know this and also integrate upselling the ourcoach platform and subscription as much as you can
936
+ in the first message you send to the user.."""
937
+
938
+
939
+ response, prompt = self.do_theme(theme, date, action['day'], last_msg_is_answered, extra=extra)
940
 
941
  # add today's reminders to response to schedule
942
  # response['reminders'] = all reminders which date is today (so all the reminders that BE has to queue today)
app/web_search.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from dotenv import load_dotenv
3
+ import os
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ load_dotenv()
9
+
10
+ class SearchEngine:
11
+ BING_API_KEY = os.getenv("BING_API_KEY")
12
+ BING_ENDPOINT = 'https://api.bing.microsoft.com/v7.0'
13
+
14
+ @staticmethod
15
+ def search(feedback_type_name, search_term, user_id):
16
+ logger.info(f"User {user_id}: Initiating search for type '{feedback_type_name}' with term '{search_term}'")
17
+ """
18
+ Public method to perform a search based on the feedback type.
19
+ """
20
+ search_methods = {
21
+ "General": SearchEngine._search_general,
22
+ "Resource Links": SearchEngine._search_relevant_links,
23
+ "Book/Podcast Recommendations": SearchEngine._search_books_or_podcasts,
24
+ "Inspirational Stories or Case Studies": SearchEngine._search_inspirational_stories,
25
+ "Fun Facts": SearchEngine._search_fun_facts,
26
+ "Personalised Recommendations": SearchEngine._search_personalized_recommendations,
27
+ "Videos": SearchEngine._search_videos
28
+ }
29
+
30
+ search_method = search_methods.get(feedback_type_name)
31
+
32
+ if search_method:
33
+ return search_method(search_term)
34
+ else:
35
+ return (feedback_type_name, search_term)
36
+
37
+ @staticmethod
38
+ def _search_relevant_links(search_term):
39
+ logger.debug(f"Searching relevant links for term: {search_term}")
40
+ """
41
+ Uses Bing Web Search API to search for relevant links.
42
+ """
43
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
44
+ params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
45
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
46
+ if response.status_code == 200:
47
+ logger.debug("Received successful response from Bing Web Search API.")
48
+ data = response.json()
49
+ links = []
50
+ if 'webPages' in data and 'value' in data['webPages']:
51
+ for result in data['webPages']['value']:
52
+ links.append(result)
53
+ return links
54
+ else:
55
+ logger.error(f"Bing Web Search API returned status code {response.status_code}")
56
+ return ["No relevant links found."]
57
+
58
+ @staticmethod
59
+ def _search_books_or_podcasts(search_term):
60
+ logger.debug(f"Searching books or podcasts for term: {search_term}")
61
+ """
62
+ Uses Bing Web Search API to search for books or podcasts.
63
+ """
64
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
65
+ query = f"{search_term} book OR podcast"
66
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
67
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
68
+ if response.status_code == 200:
69
+ logger.debug("Received successful response from Bing Web Search API for books/podcasts.")
70
+ data = response.json()
71
+ recommendations = []
72
+ if 'webPages' in data and 'value' in data['webPages']:
73
+ for result in data['webPages']['value']:
74
+ title = result.get('name', 'Unknown Title')
75
+ url = result.get('url', '')
76
+ recommendations.append(f"{title}: {url}")
77
+ return recommendations
78
+ else:
79
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for books/podcasts search.")
80
+ return ["No book or podcast recommendations found."]
81
+
82
+ @staticmethod
83
+ def _search_success_stories(search_term):
84
+ logger.debug(f"Searching success stories for term: {search_term}")
85
+ """
86
+ Uses Bing Web Search API to search for success stories.
87
+ """
88
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
89
+ query = f"{search_term} success stories"
90
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
91
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
92
+ if response.status_code == 200:
93
+ logger.debug("Received successful response from Bing Web Search API for success stories.")
94
+ data = response.json()
95
+ stories = []
96
+ if 'webPages' in data and 'value' in data['webPages']:
97
+ for result in data['webPages']['value']:
98
+ title = result.get('name', 'Unknown Title')
99
+ url = result.get('url', '')
100
+ stories.append(f"{title}: {url}")
101
+ return stories
102
+ else:
103
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for success stories search.")
104
+ return ["No success stories found."]
105
+
106
+ @staticmethod
107
+ def _search_inspirational_stories(search_term):
108
+ logger.debug(f"Searching inspirational stories or case studies for term: {search_term}")
109
+ """
110
+ Uses Bing Web Search API to search for inspirational stories or case studies.
111
+ """
112
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
113
+ query = f"{search_term} inspirational stories OR case studies"
114
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
115
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
116
+ if response.status_code == 200:
117
+ logger.debug("Received successful response from Bing Web Search API for inspirational stories.")
118
+ data = response.json()
119
+ stories = []
120
+ if 'webPages' in data and 'value' in data['webPages']:
121
+ for result in data['webPages']['value']:
122
+ title = result.get('name', 'Unknown Title')
123
+ url = result.get('url', '')
124
+ stories.append(f"{title}: {url}")
125
+ return stories
126
+ else:
127
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for inspirational stories search.")
128
+ return ["No inspirational stories found."]
129
+
130
+ @staticmethod
131
+ def _search_fun_facts(search_term):
132
+ logger.debug(f"Searching fun facts for term: {search_term}")
133
+ """
134
+ Uses Bing Web Search API to search for fun facts related to personal growth.
135
+ """
136
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
137
+ query = f"{search_term} fun facts"
138
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
139
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
140
+ if response.status_code == 200:
141
+ logger.debug("Received successful response from Bing Web Search API for fun facts.")
142
+ data = response.json()
143
+ facts = []
144
+ if 'webPages' in data and 'value' in data['webPages']:
145
+ for result in data['webPages']['value']:
146
+ snippet = result.get('snippet', '')
147
+ facts.append(snippet)
148
+ return facts
149
+ else:
150
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for fun facts search.")
151
+ return ["No fun facts found."]
152
+
153
+ @staticmethod
154
+ def _search_visual_content(search_term):
155
+ logger.debug(f"Searching visual content for term: {search_term}")
156
+ """
157
+ Uses Bing Image Search API to search for images or infographics.
158
+ """
159
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
160
+ params = {'q': search_term, 'count': 3}
161
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/images/search", headers=headers, params=params)
162
+ if response.status_code == 200:
163
+ logger.debug("Received successful response from Bing Image Search API.")
164
+ data = response.json()
165
+ images = []
166
+ if 'value' in data:
167
+ for result in data['value']:
168
+ image_url = result.get('contentUrl', '')
169
+ images.append(image_url)
170
+ return images
171
+ else:
172
+ logger.error(f"Bing Image Search API returned status code {response.status_code} for visual content search.")
173
+ return ["No visual content found."]
174
+
175
+ @staticmethod
176
+ def _search_personalized_recommendations(search_term):
177
+ logger.debug(f"Searching personalized recommendations for term: {search_term}")
178
+ """
179
+ Uses Bing Web Search API to provide personalized recommendations.
180
+ """
181
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
182
+ query = f"tips for {search_term}"
183
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
184
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
185
+ if response.status_code == 200:
186
+ logger.debug("Received successful response from Bing Web Search API for personalized recommendations.")
187
+ data = response.json()
188
+ recommendations = []
189
+ if 'webPages' in data and 'value' in data['webPages']:
190
+ for result in data['webPages']['value']:
191
+ title = result.get('name', 'Unknown Title')
192
+ url = result.get('url', '')
193
+ recommendations.append(f"{title}: {url}")
194
+ return recommendations
195
+ else:
196
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for personalized recommendations search.")
197
+ return ["No personalized recommendations found."]
198
+
199
+ @staticmethod
200
+ def _search_videos(search_term):
201
+ logger.debug(f"Searching videos for term: {search_term}")
202
+ """
203
+ Uses Bing Video Search API to search for videos, prioritizing YouTube results.
204
+ """
205
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
206
+ query = f"site:youtube.com {search_term}"
207
+ params = {'q': query, 'textDecorations': True, 'textFormat': 'HTML', 'count': 3}
208
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/videos/search", headers=headers, params=params)
209
+ if response.status_code == 200:
210
+ logger.debug("Received successful response from Bing Video Search API.")
211
+ data = response.json()
212
+ videos = []
213
+ if 'value' in data:
214
+ for result in data['value']:
215
+ title = result.get('name', 'Unknown Title')
216
+ url = result.get('contentUrl', '')
217
+ # Prioritize YouTube results
218
+ if 'youtube.com' in url.lower():
219
+ videos.append(f"{title}: {url}")
220
+ if len(videos) >= 3:
221
+ break
222
+ # If we don't have enough YouTube results, add other video results
223
+ if len(videos) < 3:
224
+ for result in data['value']:
225
+ title = result.get('name', 'Unknown Title')
226
+ url = result.get('contentUrl', '')
227
+ if url not in [v.split(': ')[1] for v in videos]:
228
+ videos.append(f"{title}: {url}")
229
+ if len(videos) >= 3:
230
+ break
231
+ return videos
232
+ else:
233
+ logger.error(f"Bing Video Search API returned status code {response.status_code} for video search.")
234
+ return ["No video content found."]
235
+
236
+ @staticmethod
237
+ def _search_general(search_term):
238
+ logger.debug(f"Performing a general search for term: {search_term}")
239
+ """
240
+ Uses Bing Web Search API to perform a general search.
241
+ """
242
+ headers = {'Ocp-Apim-Subscription-Key': SearchEngine.BING_API_KEY}
243
+ params = {'q': search_term, 'textDecorations': True, 'textFormat': 'HTML', 'count': 5}
244
+ response = requests.get(f"{SearchEngine.BING_ENDPOINT}/search", headers=headers, params=params)
245
+ if response.status_code == 200:
246
+ logger.debug("Received successful response from Bing Web Search API for general search.")
247
+ data = response.json()
248
+ results = []
249
+ if 'webPages' in data and 'value' in data['webPages']:
250
+ for result in data['webPages']['value']:
251
+ results.append(result)
252
+ return results
253
+ else:
254
+ logger.error(f"Bing Web Search API returned status code {response.status_code} for general search.")
255
+ return ["No results found for general search."]