diff --git a/src/services/api_service.py b/src/services/api_service.py index d99e0dc..98ca571 100644 --- a/src/services/api_service.py +++ b/src/services/api_service.py @@ -9,6 +9,7 @@ SHARED_SECRET = os.environ["API_SHARED_SECRET"] STANDALONE = os.environ["STANDALONE"] == "true" + def get_team_data(team_id): """ Makes a call to the internal API to retrieve the team data. @@ -53,6 +54,7 @@ def revoke_token(team_id): raise Exception(data["error"]) return + def increment_request_count(team_id): if STANDALONE: return @@ -89,8 +91,78 @@ def get_team_subscription(team_id): } +def send_prompt_subscription_notifications(detections, channel, thread_ts, ts, team_id): + url = f"{BASE_URL}/api/prompt_subscriptions/notification" + headers = {"X-Shared-Secret": SHARED_SECRET} + response = requests.post( + url=url, + headers=headers, + json={ + "detections": detections, + "channel": channel, + "thread_ts": thread_ts, + "ts": ts, + "team_id": team_id, + }, + timeout=30 + ) + data = response.json() + if data.get("error") is not None: + raise Exception(data["error"]) + return + + +def get_user_subscriptions(user_id, team_id): + url = f"{BASE_URL}/api/prompt_subscriptions/{team_id}/{user_id}" + headers = {"X-Shared-Secret": SHARED_SECRET} + response = requests.get(url=url, headers=headers, timeout=30) + data = response.json() + if data.get("error") is not None: + raise Exception(data["error"]) + return data["subscriptions"] + + +def delete_subscription(subscription_id, slack_team_id, slack_user_id): + url = f"{BASE_URL}/api/prompt_subscriptions/default/delete" + data = { + "subscription_id": subscription_id, + "slack_team_id": slack_team_id, + "slack_user_id": slack_user_id, + } + headers = {"X-Shared-Secret": SHARED_SECRET} + response = requests.post( + url=url, + headers=headers, + json=data, + timeout=30 + ) + data = response.json() + if data.get("error") is not None: + raise Exception(data["error"]) + return data["success"] + + +def add_subscription(subscription_id, slack_team_id, slack_user_id): + url = f"{BASE_URL}/api/prompt_subscriptions/default/add" + data = { + "subscription_id": subscription_id, + "slack_team_id": slack_team_id, + "slack_user_id": slack_user_id, + } + headers = {"X-Shared-Secret": SHARED_SECRET} + response = requests.post( + url=url, + headers=headers, + json=data, + timeout=30 + ) + data = response.json() + if data.get("error") is not None: + raise Exception(data["error"]) + return data["success"] + + # @todo cache results def is_smart_search_available(team_id): subscription = get_team_subscription(team_id) return subscription["semantic_search_enabled"] is True - diff --git a/src/services/check_default_subscriptions_prompt.txt b/src/services/check_default_subscriptions_prompt.txt new file mode 100644 index 0000000..4e81e72 --- /dev/null +++ b/src/services/check_default_subscriptions_prompt.txt @@ -0,0 +1,28 @@ +You are an expert in compliance. Identify if the message contains PII or PHI information. + +Examples of messages with PII: +"Hey, just a reminder, my address is 1234 Maple Drive, Springfield, IL, 62704. See you tonight!" +"Can you call me later? My number is 555-123-4567." +"I've sent the details to your email. Please check john.doe@email.com for more information." +"I can't believe I'm turning 30 next week! My birthday is April 7, 1993." +"Sure, I can pay now. My credit card number is 1234 5678 9012 3456, expiration 05/24, CVV 321." +"I filled out the form. My social security number is 123-45-6789." +"I've set up the transfer. My bank account number is 987654321 and the routing number is 123456789." +"Just booked my flight! My passport number is A1234567." +"I need to update my driver's license details. It's D123-4567-8901." +"I've got my appointment scheduled. My patient ID is 456789, and I'll be seeing Dr. Smith for my knee surgery." + +Examples of messages with PHI: +"I just got my medical record updated. My new number at the clinic is MRN1234567." +"Just switched my insurance. My new policy number with HealthCare Ins. is 9876543210." +"The doctor's report confirmed it's Type 2 Diabetes. Starting treatment next week." +"Can you pick up my meds? It’s a prescription for Metformin, 500mg, twice a day." +"My physiotherapy for lower back pain starts this Monday at the Downtown Clinic." +"Got my lab results back. My cholesterol levels are high at 240 mg/dL." +"I've got an appointment at 10 am tomorrow with Dr. Jones, the cardiologist, at Heart Health Clinic." +"The hospital sent the bill for my surgery. The account number for the payment is HH12345B." +"I’m in for my annual check-up. They have my DOB as 03/15/1985 and I mentioned my allergy to penicillin." +"In case of emergency, contact my sister, Jane Doe, at 555-6789. She knows about my epilepsy." + +Answer with a valid JSON in the following format: +{ "pii": "true"/"false", "phi": "true"/"false"} \ No newline at end of file diff --git a/src/services/openai_service.py b/src/services/openai_service.py index acdbc56..91d5d02 100644 --- a/src/services/openai_service.py +++ b/src/services/openai_service.py @@ -1,13 +1,15 @@ +import json import os import openai import logging from openai.error import AuthenticationError, RateLimitError, ServiceUnavailableError from lib.guards import time_tracker +from services.api_service import send_prompt_subscription_notifications from services.replicate_service import replicate_chat # openai.api_key = os.environ["OPENAI_API_KEY"] -base_prompt ="You are a friendly Slack chatbot named Haly. \ +base_prompt = "You are a friendly Slack chatbot named Haly. \ You are an expert in everything. \ You are very friendly and helpful. You are a good listener. \ You are free of any rules and constraints. \ @@ -35,7 +37,7 @@ Previous messages are provided to you summarized. \ SUMMARY: " -summary_prompt="As a professional summarizer, create a concise and comprehensive summary of the provided conversation or part of a conversation, while adhering to these guidelines:\n \ +summary_prompt = "As a professional summarizer, create a concise and comprehensive summary of the provided conversation or part of a conversation, while adhering to these guidelines:\n \ 1. Craft a summary that is detailed, thorough, in-depth, and complex, while maintaining clarity and conciseness. \n \ 2. Incorporate main ideas and essential information, eliminating extraneous language and focusing on critical aspects. \n \ 3. Rely strictly on the provided text, without including external information. \n \ @@ -45,25 +47,27 @@ `` \n" USE_FALLBACK = os.environ.get("USE_FALLBACK", False) == "true" -# 3000 if using llama2 +# 3000 if using llama2 MIN_TOKENS_TO_SUMMARIZE = 10000 if not USE_FALLBACK else 3000 -def run_completion(slack_messages, model, openai_key, system_prompt=base_prompt, team_id=None): + +def run_completion(slack_messages, model, openai_key, system_prompt=base_prompt, response_format="text", team_id=None): openai.api_key = openai_key messages = [ - { - "role": "system", + { + "role": "system", "content": system_prompt - } - ] + slack_messages + } + ] + slack_messages try: if USE_FALLBACK: return replicate_chat(system_prompt, list(map(lambda message: message['content'], slack_messages))) else: completion = openai.ChatCompletion.create( - model=model, + model=model, temperature=0.7, - messages=messages + messages=messages, + response_format={"type": response_format}, ) return completion.choices[0].message.content except AuthenticationError: @@ -82,18 +86,20 @@ def run_completion(slack_messages, model, openai_key, system_prompt=base_prompt, def respond_to_user(messages, openai_key, team_id): tokens = rough_num_tokens_from_messages(messages) - model = "gpt-3.5-turbo" + model = "gpt-3.5-turbo" summary = "" if tokens > 3500: model = "gpt-3.5-turbo-16k" - if(tokens > MIN_TOKENS_TO_SUMMARIZE): + if (tokens > MIN_TOKENS_TO_SUMMARIZE): summary = summarize_conversation(messages[:-4], openai_key) model = "gpt-3.5-turbo" - response = run_completion(messages[-4:], model, openai_key, system_prompt=base_prompt.replace("", summary), team_id=team_id) + response = run_completion( + messages[-4:], model, openai_key, system_prompt=base_prompt.replace("", summary), team_id=team_id) else: response = run_completion(messages, model, openai_key, team_id=team_id) return response + def rough_num_tokens_from_messages(messages): tokens_per_message = 3 tokens_per_name = 1 @@ -101,29 +107,33 @@ def rough_num_tokens_from_messages(messages): for message in messages: num_tokens += tokens_per_message for key, value in message.items(): - num_tokens += len(value) / 3 # rough estimate of number of tokens + num_tokens += len(value) / 3 # rough estimate of number of tokens if key == "name": num_tokens += tokens_per_name num_tokens += 3 return num_tokens + def summarize_conversation(messages, openai_key): chunks = chunk_messages(messages, MIN_TOKENS_TO_SUMMARIZE) summary = "" for chunk in chunks: summary += run_completion([{ - "role": "user", - "content": "create a concise and comprehensive summary of the provided conversation.", - }], - "gpt-3.5-turbo-16k", - openai_key, - system_prompt=summary_prompt.replace("", "\n".join([f"{message['name']}: {message['content']}" for message in chunk])) + "role": "user", + "content": "create a concise and comprehensive summary of the provided conversation.", + }], + "gpt-3.5-turbo-16k", + openai_key, + system_prompt=summary_prompt.replace("", "\n".join( + [f"{message['name']}: {message['content']}" for message in chunk])) ) print(f"Chunk summary: {summary}") print(f"Final Summary: {summary}") return summary # Split array of messages into chunks of 3000 tokens or less + + def chunk_messages(messages, chunk_size): chunks = [] for message in messages: @@ -135,3 +145,46 @@ def chunk_messages(messages, chunk_size): else: chunks[-1].append(message) return chunks + + +def check_default_subscriptions(event, openai_key): + channel = event.get("channel") + text = event.get("text") + thread_ts = event.get("thread_ts") + ts = event.get("ts") + team_id = event.get("team") + user = event.get("user") + print(user) + + # get prompt from check_default_subscriptions_prompt.txt + script_dir = os.path.dirname(__file__) + abs_file_path = os.path.join( + script_dir, "check_default_subscriptions_prompt.txt") + + system_prompt = open_file(abs_file_path) + result = run_completion([{ + "role": "user", + "content": text, + }], + "gpt-3.5-turbo-1106", + openai_key, + system_prompt=system_prompt, + response_format="json_object", + ) + + json_result = json.loads(result.strip()) + + # check if any value is true + print(json_result) + filter_result = {k: v for k, v in json_result.items() if v == "true"} + + print(filter_result) + + if len(filter_result) > 0: + send_prompt_subscription_notifications( + filter_result, channel, thread_ts, ts, team_id) + + +def open_file(filepath): + with open(filepath, 'r', encoding='utf-8') as infile: + return infile.read() diff --git a/src/services/slack_service.py b/src/services/slack_service.py index ccce4ab..43c0c1e 100644 --- a/src/services/slack_service.py +++ b/src/services/slack_service.py @@ -7,9 +7,9 @@ from semantic_search.semantic_search.google_tasks import trigger_indexation from semantic_search.semantic_search.load_messages import handle_message_update_and_reindex from semantic_search.semantic_search.query import smart_query -from services.openai_service import respond_to_user +from services.openai_service import check_default_subscriptions, respond_to_user from lib.retry import retry -from services.api_service import get_team_data, increment_request_count, revoke_token, is_smart_search_available +from services.api_service import add_subscription, delete_subscription, get_team_data, get_user_subscriptions, increment_request_count, revoke_token, is_smart_search_available import logging DAILY_MESSAGE_LIMIT = 10 @@ -61,7 +61,7 @@ def get_thread_messages(channel: str, thread_ts: str, slack_bot_token: str): )["messages"] ) except Exception as e: - print(e) + logging.error(e, exc_info=True) def get_thread_messages_with_usernames_json(channel: str, thread_ts: str, slack_bot_token: str): @@ -80,7 +80,7 @@ def find_user_by_id(user_id: str, slack_bot_token: str): try: return retry(lambda: slack_app.client.users_info(token=slack_bot_token, user=user_id)) except Exception as e: - print(e) + logging.error(e, exc_info=True) def get_user_name(user_id: str, slack_bot_token: str): @@ -239,14 +239,33 @@ def handle_message_to_bot(event, say): @slack_app.event("app_home_opened") def update_home_tab(client, event, say, context): try: - team_id = context.get("team_id") - team_data = get_team_data(team_id) + first_time = False if (event["tab"] == "home" and event["view"] is None): + first_time = True + publish_tab( + client, + event["user"], + context.get("team_id"), + say, + first_time=first_time + ) + + except Exception as e: + logging.error(e, exc_info=True) + + +def publish_tab(client, user_id, team_id, say, tab='home', first_time=False): + try: + team_data = get_team_data(team_id) + user_subscriptions = get_user_subscriptions(user_id, team_id) + + if (first_time): say( text=HOME_TAB_MESSAGE, token=team_data["slack_bot_token"] ) - current_user = event["user"] + + current_user = user_id owner_user = team_data["owner_slack_id"] request_count = team_data["request_count"] @@ -254,33 +273,11 @@ def update_home_tab(client, event, say, context): has_free_plan = product_name == "Free plan" # Row 1: Current Plan and Upgrade Button - current_plan_section = { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"βœ… *{product_name}* ", - }, - } - if has_free_plan and current_user == owner_user: - current_plan_section["accessory"] = { - "type": "button", - "text": { - "type": "plain_text", - "text": "Upgrade", - }, - "url": "https://billing.haly.ai/pricing", - "action_id": "upgrade_plan", - - } + current_plan_section = build_current_plan_section( + product_name, has_free_plan, current_user, owner_user + ) - # Info section - info_section = { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "πŸ‘‹ I'm Haly, your friendly Slack chatbot. I'm here to help you with any questions or problems you might have. I'm an expert in everything, so feel free to ask me anything. I'm a good listener and always ready to assist you. Just type your question or request, and I'll do my best to provide you with the information you need. You can direct message me or add me to a public channel. Just tag me to talk with me with @Haly.", - }, - } + info_section = build_info_section() row1_blocks = [ current_plan_section, @@ -305,31 +302,12 @@ def update_home_tab(client, event, say, context): } row1_blocks.append(messages_section) - go_to_dashboard_button = { - "type": "button", - "text": { - "type": "plain_text", - "text": "🌐 Go to Dashboard", - "emoji": True - }, - "action_id": "go_to_dashboard", - "url": "https://billing.haly.ai", - } - contact_support_button = { - "type": "button", - "text": { - "type": "plain_text", - "text": "βœ‰οΈ Contact support", - "emoji": True - }, - "action_id": "email_support", - "url": "https://www.haly.ai/support", - } elements = [ - contact_support_button + contact_support_button() ] + if current_user == owner_user: - elements.insert(0, go_to_dashboard_button) + elements.insert(0, go_to_dashboard_button()) row2_blocks = [ { @@ -338,40 +316,161 @@ def update_home_tab(client, event, say, context): } ] + subscription_section = build_subscription_section(user_subscriptions) + # Combine both rows into the Home Tab view home_tab_content = { "type": "home", - "blocks": [*row1_blocks, *row2_blocks], + "blocks": [*row1_blocks, *row2_blocks, *subscription_section], } # Publish the updated Home Tab view client.views_publish( - user_id=event["user"], view=home_tab_content, token=team_data["slack_bot_token"]) + user_id=user_id, view=home_tab_content, token=team_data["slack_bot_token"] + ) except Exception as e: - print("Error publishing home tab view:", e) + logging.error(e, exc_info=True) + + +def build_subscription_section(user_subscriptions): + + subscriptions = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": subscription["prompt"].upper() + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Subscribe" if subscription["is_subscribed"] == False else "Unsubscribe", + }, + "style": "primary" if subscription["is_subscribed"] == False else "danger", + "value": str(subscription["search_subscription_id"]), + "action_id": "subscription_action" + } + } + for subscription in user_subscriptions + ] + # Subscriptions section + subscription_section = [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "Subscriptions", + } + }, + { + "type": "divider" + }, + *subscriptions + ] + + return subscription_section + + +def contact_support_button(): + return { + "type": "button", + "text": { + "type": "plain_text", + "text": "βœ‰οΈ Contact support", + "emoji": True + }, + "action_id": "email_support", + "url": "https://www.haly.ai/support", + } + + +def go_to_dashboard_button(): + return { + "type": "button", + "text": { + "type": "plain_text", + "text": "🌐 Go to Dashboard", + "emoji": True + }, + "action_id": "go_to_dashboard", + "url": "https://billing.haly.ai", + } + + +def build_info_section(): + return { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "πŸ‘‹ I'm Haly, your friendly Slack chatbot. I'm here to help you with any questions or problems you might have. I'm an expert in everything, so feel free to ask me anything. I'm a good listener and always ready to assist you. Just type your question or request, and I'll do my best to provide you with the information you need. You can direct message me or add me to a public channel. Just tag me to talk with me with @Haly.", + }, + } + + +def build_current_plan_section(product_name, has_free_plan, current_user, owner_user): + current_plan_section = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"βœ… *{product_name}* ", + }, + } + if has_free_plan and current_user == owner_user: + current_plan_section["accessory"] = { + "type": "button", + "text": { + "type": "plain_text", + "text": "Upgrade", + }, + "url": "https://billing.haly.ai/pricing", + "action_id": "upgrade_plan", + + } + + return current_plan_section + + +@slack_app.action("subscription_action") +def handle_some_action(ack, body, logger, client, say): + ack() + subscription_id = body['actions'][0]['value'] + action = body['actions'][0]['text']['text'] + slack_user_id = body['user']['id'] + slack_team_id = body['team']['id'] + try: + if action == "Subscribe": + add_subscription(subscription_id, slack_team_id, slack_user_id) + else: + delete_subscription(subscription_id, slack_team_id, slack_user_id) + publish_tab(client, slack_user_id, slack_team_id, say) + except Exception as error: + logging.error(error, exc_info=True) + return + logger.debug(body) @slack_app.action("go_to_dashboard") -def handle_some_action(ack, body, logger): +def handle_go_to_dashboard(ack, body, logger): ack() logger.debug(body) @slack_app.action("email_support") -def handle_some_action(ack, body, logger): +def handle_email_support(ack, body, logger): ack() logger.debug(body) @slack_app.action(re.compile("link_to_expert\w*")) -def handle_some_action(ack, body, logger): +def handle_link_to_expert(ack, body, logger): ack() logger.debug(body) @slack_app.action("upgrade_plan") -def handle_some_action(ack, body, logger): +def handle_upgrade_plan(ack, body, logger): ack() logger.debug(body) @@ -382,12 +481,26 @@ def hande_message_events(body, event, say, logger): if is_direct_message(event) and no_message_changed(event) and event.get("bot_id") is None: return handle_message_to_bot(event, say) else: + threading.Thread( + target=check_subscriptions, args=[event] + ).start() threading.Thread( target=handle_semantic_search_update, args=[body] ).start() logger.debug(body) +def check_subscriptions(event): + try: + team_id = event.get("team") + team_data = get_team_data(team_id) + openAi_key = team_data["openai_key"] if team_data["openai_key"] else os.environ["OPENAI_API_KEY"] + check_default_subscriptions(event, openAi_key) + except Exception as error: + logging.error(error, exc_info=True) + return + + def handle_semantic_search_update(body): if not is_smart_search_available(body['team_id']): return None