From 618ea533ed15a486a643011018e6144b88f6982d Mon Sep 17 00:00:00 2001 From: OdyAsh Date: Wed, 13 Nov 2024 00:16:45 +0200 Subject: [PATCH 1/3] Handle WhatsApp Unsupported Msgs, Also: add new logger utility and refine .gitignore --- .env.example | 8 ++ .github/workflows/python-app.yml | 1 + .gitignore | 4 +- data/mawsuah/strip_tashkeel.py | 8 +- ...p_api_structure_of_a_reply_msg_status.json | 2 +- ...sapp_structure_of_a_user_incoming_msg.json | 112 ++++++++++++++++++ setup_database.py | 12 +- src/ansari/agents/ansari.py | 14 +-- src/ansari/agents/ansari_workflow.py | 18 +-- src/ansari/ansari_db.py | 16 ++- src/ansari/ansari_logger.py | 31 +++++ src/ansari/app/main_api.py | 47 +++----- src/ansari/app/main_whatsapp.py | 57 ++++++--- src/ansari/config.py | 14 +-- src/ansari/presenters/whatsapp_presenter.py | 50 +++++--- ...sapp_structure_of_a_user_incoming_msg.json | 39 ------ tests/test_answer_quality.py | 34 ++---- tests/test_main_api.py | 39 +++--- 18 files changed, 324 insertions(+), 182 deletions(-) rename {src/ansari/resources => docs}/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json (94%) create mode 100644 docs/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json create mode 100644 src/ansari/ansari_logger.py delete mode 100644 src/ansari/resources/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json diff --git a/.env.example b/.env.example index 3fab9eb..ccc2421 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,9 @@ export ORIGINS="https://beta.ansari.chat,http://beta.ansari.chat,https://ansari. export PGPASSWORD="" # Password for PostgreSQL database export VECTARA_API_KEY="" # Authentication token for Vectara API +# Related to our PostgreSQL database +export QURAN_DOT_COM_API_KEY="" + # Directory for storing templates export template_dir="." # Directory path for templates @@ -29,3 +32,8 @@ export WHATSAPP_API_VERSION="< export WHATSAPP_BUSINESS_PHONE_NUMBER_ID="<>" export WHATSAPP_ACCESS_TOKEN_FROM_SYS_USER="<" export WHATSAPP_VERIFY_TOKEN_FOR_WEBHOOK="<>" + +# Related to internal code logic +# Leave the values below when locally debugging the application +export LOGGING_LEVEL="DEBUG" +export DEBUG_MODE="True" \ No newline at end of file diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 655361a..379556d 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -26,6 +26,7 @@ jobs: WHATSAPP_RECIPIENT_WAID: ${{ secrets.WHATSAPP_RECIPIENT_WAID }} WHATSAPP_API_VERSION: ${{ secrets.WHATSAPP_API_VERSION }} WHATSAPP_BUSINESS_PHONE_NUMBER_ID: ${{ secrets.WHATSAPP_BUSINESS_PHONE_NUMBER_ID }} + WHATSAPP_TEST_BUSINESS_PHONE_NUMBER_ID: ${{ secrets.WHATSAPP_TEST_BUSINESS_PHONE_NUMBER_ID }} WHATSAPP_ACCESS_TOKEN_FROM_SYS_USER: ${{ secrets.WHATSAPP_ACCESS_TOKEN_FROM_SYS_USER }} WHATSAPP_VERIFY_TOKEN_FOR_WEBHOOK: ${{ secrets.WHATSAPP_VERIFY_TOKEN_FOR_WEBHOOK }} PYTHONPATH: src diff --git a/.gitignore b/.gitignore index d7b1eab..c20858b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ .conda/ +.venv/ .vscode/ +src/ansari_backend.egg-info/* +diskcache_dir/ abandoned/ bin/ -data/ datasources/ etc/ example_projects/ diff --git a/data/mawsuah/strip_tashkeel.py b/data/mawsuah/strip_tashkeel.py index 7b082c3..dd4e17d 100644 --- a/data/mawsuah/strip_tashkeel.py +++ b/data/mawsuah/strip_tashkeel.py @@ -4,6 +4,10 @@ import textract from tqdm.auto import tqdm +from ansari.ansari_logger import get_logger + +logger = get_logger(__name__) + def strip_tashkeel_from_doc(input_file, output_file): text = textract.process(input_file).decode("utf-8") # Extract text from .doc file @@ -24,8 +28,8 @@ def strip_tashkeel_from_doc(input_file, output_file): # iterate over all files in the directory for input_file in tqdm(input_dir.glob("*.doc")): if input_file.is_file() and input_file.suffix == ".doc": - print(f"Processing {input_file.name}...") + logger.info(f"Processing {input_file.name}...") strip_tashkeel_from_doc( input_file, output_dir.joinpath(input_file.with_suffix(".txt").name) ) - print(f"Done processing {input_file.name}") + logger.info(f"Done processing {input_file.name}") diff --git a/src/ansari/resources/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json b/docs/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json similarity index 94% rename from src/ansari/resources/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json rename to docs/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json index c3a04b3..e1e3802 100644 --- a/src/ansari/resources/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json +++ b/docs/structure_of_api_responses/meta_whatsapp_api_structure_of_a_reply_msg_status.json @@ -24,7 +24,7 @@ } }, "pricing": { - "billable": "True (actually sent as a boolean value, so no quotes)", + "billable": "True/False (actually sent as a boolean value, so no quotes)", "pricing_model": "CBP", "category": "service" } diff --git a/docs/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json b/docs/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json new file mode 100644 index 0000000..7aac800 --- /dev/null +++ b/docs/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json @@ -0,0 +1,112 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "<>", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "< 15555555555)>>", + "phone_number_id": "<>" + }, + "contacts": [ + { + "profile": { + "name": "<>" + }, + "wa_id": "<>" + } + ], + "messages": [ + { + "from": "<>", + "id": "wamid.<>", + "timestamp": "<>", + "type": "<>", + // The following field is only present if "type" has value "text" + "text": { + "body": "<>" + }, + // The following field is only present if "type" has value "audio" + "audio": { + "mime_type": "audio/ogg", + "codecs": "opus", + "sha256": "<>", + "id": "<>", + "voice": "True/False (actually sent as a boolean value, so no quotes)" + }, + // The following field is only present if "type" has value "image" + "image": { + "mime_type": "image/jpeg", + "sha256": "<>", + "id": "<>" + }, + // The following field is only present if "type" has value "sticker" + "sticker": { + "mime_type": "image/webp", + "sha256": "<>", + "id": "<>", + "animated": "True/False (actually sent as a boolean value, so no quotes)" + }, + // The following field is only present if "type" has value "video" (when sending a video/gif) + "video": { + "mime_type": "video/mp4", + "sha256": "<>", + "id": "<>" + }, + // the following field is only present if "type" has value "document" + "document": { + "filename": "<>", + "mime_type": "application/pdf , application/vnd.openxmlformats-officedocument.wordprocessingml.document , etc.", + "sha256": "<>", + "id": "<>" + }, + // the following field is only present if "type" has value "location" + "location": { + "address": "<
>", + "latitude": "<>", + "longitude": "<>", + "name": "<>", + "url": "<>" + }, + // the following field is only present if "type" has value "contacts" + "contacts":[ + { + "name": { + "first_name": "<>", + "middle_name": "<>", + "last_name": "<>", + "formatted_name": "<>" + }, + "phones": [ + { + "phone": "<>", + "wa_id": "<>", + "type": "<>" + } + ], + } + ], + // The following field is only present if "type" has value "unsupported" + // as of 2024-11-12, video notes, gifs sent from giphy (whatsapp keyboard), and polls are not supported + "errors": [ + { + "code": 131051, + "title": "Message type unknown", + "message": "Message type unknown", + "error_data": { + "details": "Message type is currently not supported." + } + } + ] + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/setup_database.py b/setup_database.py index 0021045..21ee898 100644 --- a/setup_database.py +++ b/setup_database.py @@ -1,7 +1,11 @@ import os + import psycopg2 -from config import get_settings +from ansari.ansari_logger import get_logger +from ansari.config import get_settings + +logger = get_logger(__name__) def import_sql_files(directory, db_url): @@ -20,7 +24,7 @@ def import_sql_files(directory, db_url): for filename in sorted_files: if filename.endswith(".sql"): file_path = os.path.join(directory, filename) - print("Importing:", file_path) + logger.info(f"Importing: {file_path}") # Read the SQL file with open(file_path, "r") as f: @@ -29,7 +33,7 @@ def import_sql_files(directory, db_url): # Execute the SQL query cursor.execute(sql_query) except psycopg2.Error as error: - print("Error executing", filename, ":", error) + logger.error(f"Error executing {filename}: {error}") conn.rollback() # Rollback the transaction in case of error # Commit changes to the database @@ -39,7 +43,7 @@ def import_sql_files(directory, db_url): cursor.close() except (Exception, psycopg2.DatabaseError) as error: - print("Error:", error) + logger.error(f"Error: {error}") finally: if conn is not None: conn.close() diff --git a/src/ansari/agents/ansari.py b/src/ansari/agents/ansari.py index eb4ddfa..02ae377 100644 --- a/src/ansari/agents/ansari.py +++ b/src/ansari/agents/ansari.py @@ -1,6 +1,5 @@ import hashlib import json -import logging import os import time import traceback @@ -10,20 +9,13 @@ import litellm from langfuse.decorators import langfuse_context, observe -from ansari.config import get_settings +from ansari.ansari_logger import get_logger from ansari.tools.search_hadith import SearchHadith -from ansari.tools.search_vectara import SearchVectara from ansari.tools.search_quran import SearchQuran +from ansari.tools.search_vectara import SearchVectara from ansari.util.prompt_mgr import PromptMgr -logger = logging.getLogger(__name__ + ".Ansari") -logging_level = get_settings().LOGGING_LEVEL.upper() -logger.setLevel(logging_level) - -# # Uncomment below when logging above doesn't output to std, and you want to see the logs in the console -# console_handler = logging.StreamHandler() -# console_handler.setLevel(logging_mode) -# logger.addHandler(console_handler) +logger = get_logger(__name__ + ".Ansari") class Ansari: diff --git a/src/ansari/agents/ansari_workflow.py b/src/ansari/agents/ansari_workflow.py index 2bfc113..394d882 100644 --- a/src/ansari/agents/ansari_workflow.py +++ b/src/ansari/agents/ansari_workflow.py @@ -6,24 +6,18 @@ import litellm +from ansari.ansari_logger import get_logger from ansari.tools.search_hadith import SearchHadith -from ansari.tools.search_vectara import SearchVectara from ansari.tools.search_quran import SearchQuran +from ansari.tools.search_vectara import SearchVectara from ansari.util.prompt_mgr import PromptMgr -logger = logging.getLogger(__name__ + ".AnsariWorkflow") - if not sys.argv[0].endswith("main_api.py"): - logging_mode = logging.DEBUG + logging_level = logging.DEBUG else: - logging_mode = logging.INFO - -logger.setLevel(logging_mode) + logging_level = logging.INFO -# # Uncomment below when logging above doesn't output to std, and you want to see the logs in the console -# console_handler = logging.StreamHandler() -# console_handler.setLevel(logging_mode) -# logger.addHandler(console_handler) +logger = get_logger(__name__ + ".AnsariWorkflow", logging_level) class AnsariWorkflow: @@ -118,7 +112,7 @@ def _execute_search_step(self, step_params, prev_outputs): elif "query_from_prev_output_index" in step_params: results = tool.run_as_string( prev_outputs[step_params["query_from_prev_output_index"]], - metadata_filter=step_params.get("metadata_filter") + metadata_filter=step_params.get("metadata_filter"), ) else: raise ValueError( diff --git a/src/ansari/ansari_db.py b/src/ansari/ansari_db.py index bfb8641..1d9f4ef 100644 --- a/src/ansari/ansari_db.py +++ b/src/ansari/ansari_db.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from datetime import datetime, timedelta, timezone from typing import Union + import bcrypt import jwt import psycopg2 @@ -10,11 +11,10 @@ from fastapi import HTTPException, Request from jwt import ExpiredSignatureError, InvalidTokenError +from ansari.ansari_logger import get_logger from ansari.config import Settings, get_settings -logger = logging.getLogger(__name__) -logging_level = get_settings().LOGGING_LEVEL.upper() -logger.setLevel(logging_level) +logger = get_logger(__name__) class MessageLogger: @@ -576,7 +576,9 @@ def convert_message_llm(self, msg): else: return {"role": msg[0], "content": msg[1]} - def store_quran_answer(self, surah: int, ayah: int, question: str, ansari_answer: str): + def store_quran_answer( + self, surah: int, ayah: int, question: str, ansari_answer: str + ): with self.get_connection() as conn: with conn.cursor() as cur: cur.execute( @@ -584,11 +586,13 @@ def store_quran_answer(self, surah: int, ayah: int, question: str, ansari_answer INSERT INTO quran_answers (surah, ayah, question, ansari_answer, review_result, final_answer) VALUES (%s, %s, %s, %s, 'pending', NULL) """, - (surah, ayah, question, ansari_answer) + (surah, ayah, question, ansari_answer), ) conn.commit() - def get_quran_answer(self, surah: int, ayah: int, question: str) -> Union[str, None]: + def get_quran_answer( + self, surah: int, ayah: int, question: str + ) -> Union[str, None]: """ Retrieve the stored answer for a given surah, ayah, and question. diff --git a/src/ansari/ansari_logger.py b/src/ansari/ansari_logger.py new file mode 100644 index 0000000..e0536f6 --- /dev/null +++ b/src/ansari/ansari_logger.py @@ -0,0 +1,31 @@ +import logging + +from ansari.config import get_settings + + +def get_logger( + caller_file_name: str, logging_level=None, debug_mode=None +) -> logging.Logger: + """ + Creates and returns a logger instance for the specified caller file. + + Args: + caller_file_name (str): The name of the file requesting the logger. + logging_level (Optional[str]): The logging level to be set for the logger. + If None, it defaults to the LOGGING_LEVEL from settings. + debug_mode (Optional[bool]): If True, adds a console handler to the logger. + If None, it defaults to the DEBUG_MODE from settings. + Returns: + logging.Logger: Configured logger instance. + """ + logger = logging.getLogger(caller_file_name) + if logging_level is None: + logging_level = get_settings().LOGGING_LEVEL.upper() + logger.setLevel(logging_level) + + if debug_mode is not False and get_settings().DEBUG_MODE: + console_handler = logging.StreamHandler() + console_handler.setLevel(logging_level) + logger.addHandler(console_handler) + + return logger diff --git a/src/ansari/app/main_api.py b/src/ansari/app/main_api.py index 3b5e951..fabe219 100644 --- a/src/ansari/app/main_api.py +++ b/src/ansari/app/main_api.py @@ -1,7 +1,7 @@ import logging import os -from typing import Union import uuid +from typing import Union import psycopg2 import psycopg2.extras @@ -17,16 +17,15 @@ from sendgrid.helpers.mail import Mail from zxcvbn import zxcvbn -from ansari.agents import Ansari -from ansari.agents import AnsariWorkflow +from ansari.agents import Ansari, AnsariWorkflow from ansari.ansari_db import AnsariDB, MessageLogger -from ansari.config import Settings, get_settings +from ansari.ansari_logger import get_logger from ansari.app.main_whatsapp import router as whatsapp_router +from ansari.config import Settings, get_settings from ansari.presenters.api_presenter import ApiPresenter -logger = logging.getLogger(__name__) -logging_level = get_settings().LOGGING_LEVEL.upper() -logger.setLevel(logging_level) +logger = get_logger(__name__) + # Register the UUID type globally psycopg2.extras.register_uuid() @@ -35,7 +34,8 @@ def main(): - add_app_middleware() + add_app_middleware() + def add_app_middleware(): app.add_middleware( @@ -46,6 +46,7 @@ def add_app_middleware(): allow_headers=["*"], ) + main() db = AnsariDB(get_settings()) ansari = Ansari(get_settings()) @@ -58,7 +59,7 @@ def add_app_middleware(): # Include the WhatsApp router app.include_router(whatsapp_router) -if __name__ == "__main__" and get_settings().LOGGING_LEVEL.upper() == "DEBUG": +if __name__ == "__main__" and get_settings().DEBUG_MODE: # Programatically start a Uvicorn server while debugging (development) for easier control/accessibility # Note: if you instead run # uvicorn main_api:app --host YOUR_HOST --port YOUR_PORT @@ -684,18 +685,20 @@ async def complete(request: Request, cors_ok: bool = Depends(validate_cors)): else: raise HTTPException(status_code=403, detail="CORS not permitted") + class AyahQuestionRequest(BaseModel): surah: int ayah: int question: str - augment_question: Union[bool,None] = False + augment_question: Union[bool, None] = False apikey: str + @app.post("/api/v2/ayah") async def answer_ayah_question( req: AyahQuestionRequest, settings: Settings = Depends(get_settings), - db: AnsariDB = Depends(lambda: AnsariDB(get_settings())) + db: AnsariDB = Depends(lambda: AnsariDB(get_settings())), ): if req.apikey != settings.QURAN_DOT_COM_API_KEY.get_secret_value(): raise HTTPException(status_code=401, detail="Unauthorized") @@ -705,7 +708,7 @@ async def answer_ayah_question( logging.debug("Creating AnsariWorkflow instance for {req.surah}:{req.ayah}") ansari_workflow = AnsariWorkflow(settings) - ayah_id = req.surah*1000 + req.ayah + ayah_id = req.surah * 1000 + req.ayah # Check if the answer is already stored in the database stored_answer = db.get_quran_answer(req.surah, req.ayah, req.question) @@ -719,23 +722,11 @@ async def answer_ayah_question( { "query": req.question, "tool_name": "search_tafsir", - "metadata_filter": f"part.from_ayah_int<={ayah_id} AND part.to_ayah_int>={ayah_id}" - } - ), - ( - "gen_query", - { - "input": req.question, - "target_corpus": "tafsir" - } + "metadata_filter": f"part.from_ayah_int<={ayah_id} AND part.to_ayah_int>={ayah_id}", + }, ), - ( - "gen_answer", - { - "input": req.question, - "search_results_indices": [0] - } - ) + ("gen_query", {"input": req.question, "target_corpus": "tafsir"}), + ("gen_answer", {"input": req.question, "search_results_indices": [0]}), ] if not req.augment_question: workflow_steps.pop(1) diff --git a/src/ansari/app/main_whatsapp.py b/src/ansari/app/main_whatsapp.py index d93d44d..97dabeb 100644 --- a/src/ansari/app/main_whatsapp.py +++ b/src/ansari/app/main_whatsapp.py @@ -1,18 +1,14 @@ -import logging from typing import Optional from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse from ansari.agents import Ansari +from ansari.ansari_logger import get_logger from ansari.config import get_settings from ansari.presenters.whatsapp_presenter import WhatsAppPresenter -# Initialize logging -logger = logging.getLogger(__name__) -logging_level = get_settings().LOGGING_LEVEL.upper() -logger.setLevel(logging_level) - +logger = get_logger(__name__) # Create a router in order to make the FastAPI functions here an extension of the main FastAPI app router = APIRouter() @@ -20,11 +16,17 @@ # Initialize the agent ansari = Ansari(get_settings()) +chosen_whatsapp_biz_num = ( + get_settings().WHATSAPP_BUSINESS_PHONE_NUMBER_ID.get_secret_value() + if not get_settings().DEBUG_MODE + else get_settings().WHATSAPP_TEST_BUSINESS_PHONE_NUMBER_ID.get_secret_value() +) + # Initialize the presenter with the agent and credentials presenter = WhatsAppPresenter( agent=ansari, access_token=get_settings().WHATSAPP_ACCESS_TOKEN_FROM_SYS_USER.get_secret_value(), - business_phone_number_id=get_settings().WHATSAPP_BUSINESS_PHONE_NUMBER_ID.get_secret_value(), + business_phone_number_id=chosen_whatsapp_biz_num, api_version=get_settings().WHATSAPP_API_VERSION, ) presenter.present() @@ -78,21 +80,40 @@ async def main_webhook(request: Request) -> None: # Terminate if incoming webhook message is empty/invalid/msg-status-update(sent,delivered,read) result = await presenter.extract_relevant_whatsapp_message_details(data) - if not result: - logger.debug( - f"whatsapp incoming message that will not be considered by the webhook: \n{data}" - ) + if isinstance(result, str): + if "error" in result.lower(): + presenter.send_whatsapp_message( + "There's a problem with the server. Kindly send again later..." + ) + return return - logger.info(data) # Get relevant info from Meta's API - business_phone_number_id, from_whatsapp_number, incoming_msg_body = result + ( + business_phone_number_id, + from_whatsapp_number, + incoming_msg_type, + incoming_msg_body, + ) = result + + if incoming_msg_type != "text": + msg_type = ( + incoming_msg_type + "s" + if not incoming_msg_type.endswith("s") + else incoming_msg_type + ) + msg_type = msg_type.replace("unsupporteds", "this media type") + await presenter.send_whatsapp_message( + from_whatsapp_number, + f"Sorry, I can't process {msg_type} yet. Please send me a text message.", + ) + return - # # Send acknowledgment message - # # (uncomment this and comment any function(s) below it when you want to quickly test that the webhook works) - # await presenter.send_whatsapp_message( - # from_whatsapp_number, f"Ack: {incoming_msg_body}" - # ) + # Send acknowledgment message + if get_settings().DEBUG_MODE: + await presenter.send_whatsapp_message( + from_whatsapp_number, f"Ack: {incoming_msg_body}" + ) # Actual code to process the incoming message using Ansari agent then reply to the sender await presenter.process_and_reply_to_whatsapp_sender( diff --git a/src/ansari/config.py b/src/ansari/config.py index f27def1..b4a7bb5 100644 --- a/src/ansari/config.py +++ b/src/ansari/config.py @@ -1,13 +1,12 @@ import logging -from pathlib import Path from functools import lru_cache +from pathlib import Path from typing import Literal, Optional, Union -from pydantic import (DirectoryPath, Field, PostgresDsn, SecretStr, - field_validator) - +from pydantic import DirectoryPath, Field, PostgresDsn, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict +# Can't use get_logger() here due to circular import logger = logging.getLogger(__name__) @@ -38,7 +37,7 @@ def get_resource_path(filename): # Get the directory of the current script script_dir = Path(__file__).resolve() # Construct the path to the resources directory - resources_dir = script_dir.parent / 'resources' + resources_dir = script_dir.parent / "resources" # Construct the full path to the resource file path = resources_dir / filename return path @@ -56,7 +55,7 @@ def get_resource_path(filename): REFRESH_TOKEN_EXPIRY_HOURS: int = Field(default=24 * 90) ORIGINS: Union[str, list[str]] = Field( - default=["https://ansari.chat", "http://ansari.chat"], env="ORIGINS" + default=["https://ansari.chat", "http://ansari.chat"] ) API_SERVER_PORT: int = Field(default=8000) @@ -121,6 +120,7 @@ def get_resource_path(filename): WHATSAPP_RECIPIENT_WAID: Optional[SecretStr] = Field(default=None) WHATSAPP_API_VERSION: Optional[str] = Field(default="v21.0") WHATSAPP_BUSINESS_PHONE_NUMBER_ID: Optional[SecretStr] = Field(default=None) + WHATSAPP_TEST_BUSINESS_PHONE_NUMBER_ID: Optional[SecretStr] = Field(default=None) WHATSAPP_ACCESS_TOKEN_FROM_SYS_USER: Optional[SecretStr] = Field(default=None) WHATSAPP_VERIFY_TOKEN_FOR_WEBHOOK: Optional[SecretStr] = Field(default=None) @@ -133,8 +133,8 @@ def get_resource_path(filename): SYSTEM_PROMPT_FILE_NAME: str = Field(default="system_msg_tool") PROMPT_PATH: str = Field(default=str(get_resource_path("prompts"))) - LOGGING_LEVEL: str = Field(default="INFO") + DEBUG_MODE: bool = Field(default=False) @field_validator("ORIGINS") def parse_origins(cls, v): diff --git a/src/ansari/presenters/whatsapp_presenter.py b/src/ansari/presenters/whatsapp_presenter.py index 2a852a2..35f3309 100644 --- a/src/ansari/presenters/whatsapp_presenter.py +++ b/src/ansari/presenters/whatsapp_presenter.py @@ -1,17 +1,15 @@ import copy -import logging -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Tuple import httpx from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from ansari.ansari_logger import get_logger from ansari.config import get_settings -# Initialize logging -logger = logging.getLogger(__name__) -logging_level = get_settings().LOGGING_LEVEL.upper() -logger.setLevel(logging_level) +logger = get_logger(__name__) + # Initialize FastAPI app app = FastAPI() @@ -28,7 +26,7 @@ class WhatsAppPresenter: def __init__( - self, agent, access_token, business_phone_number_id, api_version="v13.0" + self, agent, access_token, business_phone_number_id, api_version="v21.0" ): self.agent = agent self.access_token = access_token @@ -70,7 +68,7 @@ async def process_and_reply_to_whatsapp_sender( async def extract_relevant_whatsapp_message_details( self, body: Dict[str, Any], - ) -> Optional[Tuple[str, str, str]]: + ) -> Tuple[str, str, str] | str | None: """ Extracts relevant whatsapp message details from the incoming webhook payload. @@ -88,18 +86,40 @@ async def extract_relevant_whatsapp_message_details( and (changes := entry[0].get("changes", [])) and (value := changes[0].get("value", {})) and (messages := value.get("messages", [])) - and messages[0] + and (incoming_msg := messages[0]) ): - return None + logger.error( + f"Invalid received payload from WhatsApp user and/or problem with Meta's API :\n{body}" + ) + return "error" + elif "statuses" in value: + logger.debug( + f"WhatsApp status update received:\n({value["statuses"]["status"]} at {value["statuses"]["timestamp"]}.)" + ) + return "status update" + else: + logger.info(f"Received payload from WhatsApp user:\n{body}") # Extract the business phone number ID from the webhook payload business_phone_number_id = value["metadata"]["phone_number_id"] # Extract the phone number of the WhatsApp sender - from_whatsapp_number = messages[0]["from"] - # Extract the message text of the WhatsApp sender - incoming_msg_body = messages[0]["text"]["body"] - - return business_phone_number_id, from_whatsapp_number, incoming_msg_body + from_whatsapp_number = incoming_msg["from"] + # Meta API note: Meta sends "errors" key when receiving unsupported message types + # (e.g., video notes, gifs sent from giphy, or polls) + incoming_msg_type = ( + incoming_msg["type"] + if incoming_msg["type"] in incoming_msg.keys() + else "errors" + ) + # Extract the message of the WhatsApp sender (could be text, image, etc.) + incoming_msg_body = incoming_msg[incoming_msg_type] + + return ( + business_phone_number_id, + from_whatsapp_number, + incoming_msg_type, + incoming_msg_body, + ) async def send_whatsapp_message( self, from_whatsapp_number: str, msg_body: str diff --git a/src/ansari/resources/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json b/src/ansari/resources/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json deleted file mode 100644 index f47311d..0000000 --- a/src/ansari/resources/structure_of_api_responses/meta_whatsapp_structure_of_a_user_incoming_msg.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "object": "whatsapp_business_account", - "entry": [ - { - "id": "<>", - "changes": [ - { - "value": { - "messaging_product": "whatsapp", - "metadata": { - "display_phone_number": "< 15555555555)>>", - "phone_number_id": "<>" - }, - "contacts": [ - { - "profile": { - "name": "<>" - }, - "wa_id": "<>" - } - ], - "messages": [ - { - "from": "<>", - "id": "wamid.<>", - "timestamp": "<>", - "text": { - "body": "<>" - }, - "type": "<>" - } - ] - }, - "field": "messages" - } - ] - } - ] -} \ No newline at end of file diff --git a/tests/test_answer_quality.py b/tests/test_answer_quality.py index eb0564c..425a079 100644 --- a/tests/test_answer_quality.py +++ b/tests/test_answer_quality.py @@ -1,24 +1,14 @@ import json -import logging -from logging import StreamHandler import pandas as pd import pytest from jinja2 import Environment, FileSystemLoader from ansari.agents import Ansari +from ansari.ansari_logger import get_logger from ansari.config import get_settings -LOGGER = logging.getLogger(__name__) -logging_level = get_settings().LOGGING_LEVEL.upper() -LOGGER.setLevel(logging_level) - -# Create a handler and set the logging level -console_handler = StreamHandler() -console_handler.setLevel(logging.INFO) - -# Add the handler to the logger -LOGGER.addHandler(console_handler) +logger = get_logger(__name__) @pytest.fixture(scope="module") @@ -31,15 +21,15 @@ def data(): def answer_question(question, q_temp, cache): - LOGGER.info(f'Answering question: {question["question"]}') + logger.info(f'Answering question: {question["question"]}') options = [o.strip() for o in question["options"].split(",")] prompt = q_temp.render(question=question["question"], options=options) if prompt in cache.keys(): - LOGGER.info(f'Found {question["question"]} in cache') + logger.info(f'Found {question["question"]} in cache') return cache[prompt] ansari = Ansari(get_settings()) result = "".join(filter(lambda x: x is not None, ansari.process_input(prompt))) - LOGGER.info(f"Answer: {result}") + logger.info(f"Answer: {result}") cache[prompt] = result return result @@ -52,7 +42,7 @@ def extract_prediction(row): raw = "{" + raw.split("{")[1] raw = raw.split("}")[0] + "}" raw = raw.strip() - LOGGER.info(f"raw is: {raw}") + logger.info(f"raw is: {raw}") raw_dict = json.loads(raw) return str(raw_dict["answer"]) except IndexError: @@ -75,16 +65,16 @@ def test_ansari_agent(data): df["predicted"] = df.apply(extract_prediction, axis=1) df["correct_prediction"] = df.apply(is_correct, axis=1) correct_percentage = df["correct_prediction"].mean() * 100 - LOGGER.info(f"Percentage of correct predictions: {correct_percentage:.2f}%") + logger.info(f"Percentage of correct predictions: {correct_percentage:.2f}%") wrong_predictions = df[~df["correct_prediction"]] if not wrong_predictions.empty: - LOGGER.info("\nQuestions with wrong predictions:") + logger.info("\nQuestions with wrong predictions:") for index, row in wrong_predictions.iterrows(): - LOGGER.info(f"Question: {row['question']}") - LOGGER.info(f"Correct Answer: {row['correct']}") - LOGGER.info(f"Predicted Answer: {row['predicted']}") - LOGGER.info("---------------------------------------") + logger.info(f"Question: {row['question']}") + logger.info(f"Correct Answer: {row['correct']}") + logger.info(f"Predicted Answer: {row['predicted']}") + logger.info("---------------------------------------") assert ( correct_percentage >= 80 diff --git a/tests/test_main_api.py b/tests/test_main_api.py index 8836880..8348b2a 100644 --- a/tests/test_main_api.py +++ b/tests/test_main_api.py @@ -5,15 +5,14 @@ import pytest from fastapi.testclient import TestClient +from ansari.ansari_db import AnsariDB +from ansari.ansari_logger import get_logger from ansari.app.main_api import app from ansari.config import get_settings -from ansari.ansari_db import AnsariDB -client = TestClient(app) +logger = get_logger(__name__) -# Configure logging -logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) +client = TestClient(app) # Test data valid_email_base = "test@example.com" @@ -550,20 +549,26 @@ async def test_add_feedback(login_user, create_thread): assert response.status_code == 200 assert response.json() == {"status": "success"} + @pytest.fixture(scope="module") def settings(): return get_settings() + @pytest.fixture(scope="module") def db(settings): return AnsariDB(settings) + @pytest.mark.integration -@pytest.mark.parametrize("surah,ayah,question", [ - (1, 1, "What is the meaning of bismillah?"), - (2, 255, "What is the significance of Ayat al-Kursi?"), - (112, 1, "What does this ayah teach about Allah?") -]) +@pytest.mark.parametrize( + "surah,ayah,question", + [ + (1, 1, "What is the meaning of bismillah?"), + (2, 255, "What is the significance of Ayat al-Kursi?"), + (112, 1, "What does this ayah teach about Allah?"), + ], +) def test_answer_ayah_question_integration(settings, db, surah, ayah, question): api_key = settings.QURAN_DOT_COM_API_KEY.get_secret_value() @@ -576,14 +581,16 @@ def test_answer_ayah_question_integration(settings, db, surah, ayah, question): "ayah": ayah, "question": question, "augment_question": False, - "apikey": api_key - } + "apikey": api_key, + }, ) end_time = time.time() - assert response.status_code == 200, f"Failed with status code {response.status_code}" + assert ( + response.status_code == 200 + ), f"Failed with status code {response.status_code}" assert "response" in response.json(), "Response doesn't contain 'response' key" - + answer = response.json()["response"] assert isinstance(answer, str), "Answer is not a string" assert len(answer) > 0, "Answer is empty" @@ -603,7 +610,7 @@ def test_answer_ayah_question_integration(settings, db, surah, ayah, question): "ayah": ayah, "question": question, "augment_question": False, - "apikey": "wrong_api_key" - } + "apikey": "wrong_api_key", + }, ) assert error_response.status_code == 401, "Incorrect API key should return 401" From 4b5631eb44f0467bca478cd9bf959d69e9eadd14 Mon Sep 17 00:00:00 2001 From: OdyAsh Date: Wed, 13 Nov 2024 00:21:29 +0200 Subject: [PATCH 2/3] Fix Lint Issue with flake8 --- src/ansari/presenters/whatsapp_presenter.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ansari/presenters/whatsapp_presenter.py b/src/ansari/presenters/whatsapp_presenter.py index 35f3309..9a5ab70 100644 --- a/src/ansari/presenters/whatsapp_presenter.py +++ b/src/ansari/presenters/whatsapp_presenter.py @@ -93,8 +93,10 @@ async def extract_relevant_whatsapp_message_details( ) return "error" elif "statuses" in value: + status = value["statuses"]["status"] + timestamp = value["statuses"]["timestamp"] logger.debug( - f"WhatsApp status update received:\n({value["statuses"]["status"]} at {value["statuses"]["timestamp"]}.)" + f"WhatsApp status update received:\n({status} at {timestamp}.)" ) return "status update" else: From 45664df0401198c7d81a2ffebdba7daf683ace7f Mon Sep 17 00:00:00 2001 From: OdyAsh Date: Wed, 13 Nov 2024 09:56:08 +0200 Subject: [PATCH 3/3] Update CORS Validation and Tests with Secret Key --- .env.example | 1 + src/ansari/app/main_api.py | 6 +++- src/ansari/config.py | 4 +++ tests/test_main_api.py | 64 +++++++++++++++++++------------------- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index ccc2421..8967a8d 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,7 @@ export WHATSAPP_ACCESS_TOKEN_FROM_SYS_USER="<" export WHATSAPP_VERIFY_TOKEN_FOR_WEBHOOK="<>" # Related to internal code logic +export PYTEST_API_KEY="<>" # Leave the values below when locally debugging the application export LOGGING_LEVEL="DEBUG" export DEBUG_MODE="True" \ No newline at end of file diff --git a/src/ansari/app/main_api.py b/src/ansari/app/main_api.py index fabe219..187c14a 100644 --- a/src/ansari/app/main_api.py +++ b/src/ansari/app/main_api.py @@ -77,7 +77,11 @@ def validate_cors(request: Request, settings: Settings = Depends(get_settings)) logger.info(f"Raw request is {request.headers}") origin = request.headers.get("origin", "") mobile = request.headers.get("x-mobile-ansari", "") - if origin and origin in settings.ORIGINS or mobile == "ANSARI": + if ( + origin + and origin in settings.ORIGINS + or mobile == get_settings().PYTEST_API_KEY.get_secret_value() + ): logger.debug("CORS OK") return True else: diff --git a/src/ansari/config.py b/src/ansari/config.py index b4a7bb5..a80fe59 100644 --- a/src/ansari/config.py +++ b/src/ansari/config.py @@ -133,6 +133,10 @@ def get_resource_path(filename): SYSTEM_PROMPT_FILE_NAME: str = Field(default="system_msg_tool") PROMPT_PATH: str = Field(default=str(get_resource_path("prompts"))) + PYTEST_API_KEY: SecretStr = Field(default="") + + # the two default settings below are used in production, so don't change them from here + # and instead, change them from .env when developing/testing/debugging locally LOGGING_LEVEL: str = Field(default="INFO") DEBUG_MODE: bool = Field(default=False) diff --git a/tests/test_main_api.py b/tests/test_main_api.py index 8348b2a..b615823 100644 --- a/tests/test_main_api.py +++ b/tests/test_main_api.py @@ -31,7 +31,7 @@ def register_user(): # Register a user before each test that requires a valid token response = client.post( "/api/v2/users/register", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": email, "password": valid_password, @@ -53,7 +53,7 @@ def register_another_user(): # Register a user before each test that requires a valid token response = client.post( "/api/v2/users/register", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": email, "password": valid_password, @@ -71,7 +71,7 @@ def login_user(register_user): # Log in the user before each test that requires a valid token response = client.post( "/api/v2/users/login", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": register_user["email"], "password": valid_password, @@ -86,7 +86,7 @@ def login_another_user(register_another_user): # Log in the user before each test that requires a valid token response = client.post( "/api/v2/users/login", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": register_another_user["email"], "password": valid_password, @@ -103,7 +103,7 @@ def create_thread(login_user): "/api/v2/threads", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -117,7 +117,7 @@ async def test_register_new_user(): email = f"{base}+{uuid.uuid4()}@{domain}" response = client.post( "/api/v2/users/register", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": email, "password": valid_password, @@ -135,7 +135,7 @@ async def test_register_new_user(): # # Test registering a new user with an invalid email # response = client.post( # "/api/v2/users/register", -# headers={"x-mobile-ansari": "ANSARI"}, +# headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, # json={ # "email": "invalid_email", # "password": valid_password, @@ -155,7 +155,7 @@ async def test_register_new_user(): # email = f"{base}+{uuid.uuid4()}@{domain}" # response = client.post( # "/api/v2/users/register", -# headers={"x-mobile-ansari": "ANSARI"}, +# headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, # json={ # "email": email, # "password": "", @@ -172,7 +172,7 @@ async def test_login_valid_credentials(register_user): # Test logging in with valid credentials response = client.post( "/api/v2/users/login", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": register_user["email"], "password": valid_password, @@ -188,7 +188,7 @@ async def test_login_invalid_credentials(): # Test logging in with invalid credentials response = client.post( "/api/v2/users/login", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": "invalid@example.com", "password": "wrongpassword", @@ -204,7 +204,7 @@ async def test_login_from_several_devices(register_user, login_user): time.sleep(1) response = client.post( "/api/v2/users/login", - headers={"x-mobile-ansari": "ANSARI"}, + headers={"x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value()}, json={ "email": register_user["email"], "password": valid_password, @@ -226,7 +226,7 @@ async def test_login_from_several_devices(register_user, login_user): "/api/v2/users/logout", headers={ "Authorization": f"Bearer {response.json()['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -241,7 +241,7 @@ async def test_logout(login_user, create_thread): "/api/v2/users/logout", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -252,7 +252,7 @@ async def test_logout(login_user, create_thread): f"/api/v2/threads/{create_thread}", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 401 @@ -270,7 +270,7 @@ async def test_refresh_token_request(login_user): "/api/v2/users/refresh_token", headers={ "Authorization": f"Bearer {login_user['refresh_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -283,7 +283,7 @@ async def test_refresh_token_request(login_user): "/api/v2/users/logout", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 401 @@ -294,7 +294,7 @@ async def test_refresh_token_request(login_user): "/api/v2/users/refresh_token", headers={ "Authorization": f"Bearer {login_user['refresh_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 401 @@ -304,7 +304,7 @@ async def test_refresh_token_request(login_user): "/api/v2/users/logout", headers={ "Authorization": f"Bearer {new_tokens['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -316,7 +316,7 @@ async def test_invalid_refresh_token(): "/api/v2/users/refresh_token", headers={ "Authorization": "Bearer invalid_refresh_token", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 401 @@ -331,14 +331,14 @@ async def test_concurrent_refresh_requests(login_user): "/api/v2/users/refresh_token", headers={ "Authorization": f"Bearer {login_user['refresh_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) response2 = client.post( "/api/v2/users/refresh_token", headers={ "Authorization": f"Bearer {login_user['refresh_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) @@ -362,7 +362,7 @@ async def test_refresh_token_cache_expiry(login_user): "/api/v2/users/refresh_token", headers={ "Authorization": f"Bearer {login_user['refresh_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) time.sleep(3) # cache expiry is 3 seconds @@ -370,7 +370,7 @@ async def test_refresh_token_cache_expiry(login_user): "/api/v2/users/refresh_token", headers={ "Authorization": f"Bearer {login_user['refresh_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) @@ -385,7 +385,7 @@ async def test_create_thread(login_user): "/api/v2/threads", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -399,7 +399,7 @@ async def test_delete_thread(login_user, create_thread): f"/api/v2/threads/{create_thread}", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -410,7 +410,7 @@ async def test_delete_thread(login_user, create_thread): f"/api/v2/threads/{create_thread}", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 404 @@ -424,7 +424,7 @@ async def test_share_thread(login_user, create_thread): f"/api/v2/share/{create_thread}", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) logging.info(f"Response is {response}") @@ -441,7 +441,7 @@ async def test_share_thread(login_user, create_thread): headers={ # NOTE: We do not need to pass the Authorization header here # Accessing a shared thread does not require authentication - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 200 @@ -456,7 +456,7 @@ async def test_thread_access(login_user, create_thread, login_another_user): f"/api/v2/threads/{create_thread}", headers={ "Authorization": f"Bearer {login_another_user}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) assert response.status_code == 404 @@ -466,7 +466,7 @@ async def test_thread_access(login_user, create_thread, login_another_user): f"/api/v2/threads/{create_thread}", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, ) # This should return a 200 OK response @@ -521,7 +521,7 @@ async def test_add_feedback(login_user, create_thread): f"/api/v2/threads/{create_thread}", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, json=message_data, ) @@ -542,7 +542,7 @@ async def test_add_feedback(login_user, create_thread): "/api/v2/feedback", headers={ "Authorization": f"Bearer {login_user['access_token']}", - "x-mobile-ansari": "ANSARI", + "x-mobile-ansari": get_settings().PYTEST_API_KEY.get_secret_value(), }, json=feedback_data, )