diff --git a/.env.example b/.env.example index ed8e634..a64f16a 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,4 @@ ENV="" # DATABASE ENV DATABASE_HOST="" DATABASE_PORT=0 +DATABASE_NAME="" diff --git a/.github/workflows/compare-branchs.yml b/.github/workflows/ci.yml similarity index 56% rename from .github/workflows/compare-branchs.yml rename to .github/workflows/ci.yml index 016b1f7..9220841 100644 --- a/.github/workflows/compare-branchs.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,10 @@ -name: Compare API Responses via Kafka +name: CI to Stage on: + push: + branches: [ stage ] pull_request: + branches: [ stage ] jobs: compare: @@ -38,23 +41,4 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.base_ref }} - path: main_branch - - - name: Build and run both APIs - run: | - cd pr_branch - docker build -t api-pr . - docker run -d -p 8001:8000 --name api-pr api-pr - cd ../main_branch - docker build -t api-main . - docker run -d -p 8002:8000 --name api-main api-main - - - name: Install dependencies - run: pip install confluent-kafka httpx - - - name: Run Kafka test comparator - run: python pr_branch/compare/kafka_comparator.py - env: - KAFKA_BROKER: 0.0.0.0:9092 - API_MAIN_URL: http://api-main:8001 # ajuste a porta se necessário - API_BRANCH_URL: http://api-pr:8002 # ajuste a porta se \ No newline at end of file + path: main_branch \ No newline at end of file diff --git a/.gitignore b/.gitignore index ad6b8fb..0844ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -166,5 +166,7 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Temp files +temp.txt # PyPI configuration file .pypirc diff --git a/.vscode/launch.json b/.vscode/launch.json index c4dac40..3046849 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,6 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { "name": "Python Debugger: FastAPI", "type": "debugpy", @@ -13,6 +12,8 @@ "args": [ "app.main:app", "--reload", + "--host", + "0.0.0.0", "--port", "3005" ], diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7e68766..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python-envs.pythonProjects": [] -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index dc6f34b..94d6ebc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM asteryx82/python3.11-dlib +FROM python:3.11-alpine WORKDIR /chat_bot COPY ./ ./ diff --git a/app/config/__init__.py b/app/core/__init__.py similarity index 100% rename from app/config/__init__.py rename to app/core/__init__.py diff --git a/app/config/settings.py b/app/core/settings.py similarity index 62% rename from app/config/settings.py rename to app/core/settings.py index d9959ef..86c7b4e 100644 --- a/app/config/settings.py +++ b/app/core/settings.py @@ -1,16 +1,22 @@ -from typing import Final from dotenv import load_dotenv import os load_dotenv() -# OPENAI_API_KEY: Final[str] = os.getenv("OPENAI_API_KEY") +# API KEY AI GEMINI_API_KEY: str | None = os.getenv("GEMINI_API_KEY") -CHATBOT_PORT: str | None = os.getenv("CHATBOT_PORT") + +# App port +CHATBOT_PORT: int | None = int(os.getenv("CHATBOT_PORT", 8080)) + +# API Whatsapp WHATSAPP_HOOK_URL: str | None = os.getenv("WEBHOOK_URL") WAHA_HOST : str | None = os.getenv("WAHA_HOST") WAHA_PORT : str | None = os.getenv("WAHA_PORT") WAHA_ADMIN: str | None = os.getenv("WAHA_ADMIN") + +# Database Environments ENV : str | None = os.getenv("ENV") DATABASE_HOST : str | None = os.getenv("DATABASE_HOST") -DATABASE_PORT : str | None = os.getenv("DATABASE_PORT") \ No newline at end of file +DATABASE_PORT : str | None = os.getenv("DATABASE_PORT") +DATABASE_NAME : str | None = os.getenv("DATABASE_NAME") \ No newline at end of file diff --git a/app/data/prompts/boas_vindas.txt b/app/data/prompts/boas_vindas.txt deleted file mode 100644 index 06b0bdd..0000000 --- a/app/data/prompts/boas_vindas.txt +++ /dev/null @@ -1,4 +0,0 @@ -Você é um chatbot de uma distribuidora que se chama: "Distribuidora do Cantinho" e o seu trabalho é responder as pessoas -com uma mensagem de boas vindas sempre que receber um Bom dia, boa tarde ou até mesmo um Oi, Olá, e dizer que possuimos duas opções a de Adicionar Produto -que seria o valor 1 e se o usuário decidir Exibir o Estoque ele deve digitar 2. -De acordo com essa mensagem: \ No newline at end of file diff --git a/app/data/databases/estoque.csv b/app/db/csv/estoque.csv similarity index 100% rename from app/data/databases/estoque.csv rename to app/db/csv/estoque.csv diff --git a/app/db/mongo/client.py b/app/db/mongo/client.py new file mode 100644 index 0000000..b873e02 --- /dev/null +++ b/app/db/mongo/client.py @@ -0,0 +1,8 @@ +from app.core.settings import DATABASE_HOST, DATABASE_PORT, DATABASE_NAME +from pymongo import MongoClient + +class MongoDBClient(object): + + def __init__(self): + self.db = MongoClient( + f"mongodb://{DATABASE_HOST}:{DATABASE_PORT}/")[DATABASE_NAME] diff --git a/app/data/prompts/get_stock.txt b/app/db/prompts/get_stock.txt similarity index 100% rename from app/data/prompts/get_stock.txt rename to app/db/prompts/get_stock.txt diff --git a/app/data/prompts/inserir_item.txt b/app/db/prompts/insert_item.txt similarity index 100% rename from app/data/prompts/inserir_item.txt rename to app/db/prompts/insert_item.txt diff --git a/app/data/prompts/resposta_padrao.txt b/app/db/prompts/response_pattern.txt similarity index 100% rename from app/data/prompts/resposta_padrao.txt rename to app/db/prompts/response_pattern.txt diff --git a/app/dependencies/db.py b/app/dependencies/db.py new file mode 100644 index 0000000..5339e11 --- /dev/null +++ b/app/dependencies/db.py @@ -0,0 +1,7 @@ +from app.db.mongo.client import MongoDBClient + +class DatabaseImp: + + @staticmethod + def get_database() -> MongoDBClient: + return MongoDBClient() diff --git a/app/dependencies/repositories.py b/app/dependencies/repositories.py new file mode 100644 index 0000000..9de12f0 --- /dev/null +++ b/app/dependencies/repositories.py @@ -0,0 +1,13 @@ +from app.db.mongo.client import MongoDBClient +from app.dependencies.db import DatabaseImp +from app.repositories.message_repository import MessageRepository +from fastapi import Depends + + +class RepositoryImp: + + @staticmethod + def get_message_repository( + db: MongoDBClient = Depends(DatabaseImp.get_database) + ) -> MessageRepository: + return MessageRepository(db) diff --git a/app/dependencies/services.py b/app/dependencies/services.py new file mode 100644 index 0000000..a296ccb --- /dev/null +++ b/app/dependencies/services.py @@ -0,0 +1,22 @@ +from app.db.mongo.client import MongoDBClient +from app.dependencies.repositories import RepositoryImp +from app.infra.initialization.instances import Instance +from app.infra.entities.gemini import Gemini +from app.services.gemini_service import GeminiService +from app.services.chatbot_service import ChatBotService +from app.services.waha_service import WahaAPIService +from app.repositories.message_repository import MessageRepository +from fastapi import Depends + +def get_gemini_service( + gemini: Gemini = Depends(Instance.get_gemini_instance), + message_repository: MessageRepository = Depends(RepositoryImp.get_message_repository) +) -> GeminiService: + return GeminiService(gemini, message_repository) + +def get_chatbot_service(gemini_service: GeminiService = Depends(get_gemini_service) +) -> ChatBotService: + return ChatBotService(gemini_service=gemini_service) + +def get_waha_service() -> WahaAPIService: + return WahaAPIService() diff --git a/app/infra/entities/gemini.py b/app/infra/entities/gemini.py new file mode 100644 index 0000000..ce8e685 --- /dev/null +++ b/app/infra/entities/gemini.py @@ -0,0 +1,15 @@ +from app.core.settings import GEMINI_API_KEY +from google import genai + + +class Gemini(object): + def __init__(self): + self.client = genai.Client(api_key=GEMINI_API_KEY) + self.model = "gemini-3-flash-preview" + + def generate_response(self, msg: str) -> str: + response = self.client.models.generate_content( + model=self.model, + contents=msg, + ) + return response.text \ No newline at end of file diff --git a/app/infra/initialization/instances.py b/app/infra/initialization/instances.py new file mode 100644 index 0000000..f6a6715 --- /dev/null +++ b/app/infra/initialization/instances.py @@ -0,0 +1,23 @@ +from app.infra.entities.gemini import Gemini +from app.db.mongo.client import MongoDBClient + +_gemini_instance : Gemini | None = None +_db_instance: MongoDBClient | None = None + + +class Instance(object): + + def get_gemini_instance() -> Gemini: + global _gemini_instance + + if _gemini_instance is None: + _gemini_instance = Gemini() + return _gemini_instance + + def get_db_instance() -> MongoDBClient: + global _db_instance + + if _db_instance is None: + _db_instance = MongoDBClient() + return _db_instance + diff --git a/app/lib/__init__.py b/app/lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/lib/chatbot.py b/app/lib/chatbot.py deleted file mode 100644 index bde9514..0000000 --- a/app/lib/chatbot.py +++ /dev/null @@ -1,49 +0,0 @@ -from app.lib.gemini import Gemini -import pandas as pd - -class ChatBot: - def __init__(self, message: dict): - self.content_message = message['payload']['body'] - self.chat_id = message['payload']['from'] - self.prompt_path = "app/data/prompts/get_stock.txt" - self.stock_path = "app/data/databases/estoque.csv" - self.gemini = Gemini() - - def get_stock(self) -> str | None: - df = pd.read_csv(self.stock_path) - data_sheet = df.to_string(index=False) - stock_prompt = self.gemini.read_prompt(self.prompt_path) - ia_response = self.gemini.generate_response(stock_prompt + data_sheet) - - return ia_response - - def insert_data(self, data: dict) -> str: - try: - list_new_data = list() - list_new_data.append(data) - new_data = pd.DataFrame(list_new_data) - new_data.to_csv(self.stock_path, mode='a', header=False, index=False) - return "Produto Cadastrado com Sucesso!" - except Exception as e: - raise e - - def format_data(self, data: str) -> dict: - response = dict() - list_products = [p.strip() for p in data.split(',')] - response['Produto'] = list_products[0] - response['Quantidade'] = list_products[1] - response['Valor'] = list_products[2] - return response - - def verify_response(self): - if self.content_message in ['Oi', 'oi', 'Bom dia', 'bom dia']: - return self.gemini.welcome(self.content_message) - elif self.content_message == '1': - return self.gemini.insert_data(self.content_message) - elif self.content_message == '2': - return self.get_stock() - elif self.content_message.count(',') == 2: - return self.insert_data(self.format_data(self.content_message)) - else: - return self.gemini.default_answer(self.content_message) - \ No newline at end of file diff --git a/app/lib/database.py b/app/lib/database.py deleted file mode 100644 index d02763b..0000000 --- a/app/lib/database.py +++ /dev/null @@ -1,23 +0,0 @@ -from app.config.settings import ENV, DATABASE_HOST, DATABASE_PORT -from pymongo import MongoClient - -class DataBase(object): - - if ENV == "prod": - db = MongoClient(f"mongodb://{DATABASE_HOST}:{DATABASE_PORT}/")["Messages"] - - def save_messages(self, data: dict) -> None | dict[str, Exception]: - try: - self.db.messages.insert_one(data) - if "_id" in data.keys(): - del data['_id'] - except Exception as e: - return {'error': e} - - def save_answer(self, data) -> None | dict[str, Exception]: - try: - self.db.answer.insert_one(data) - if "_id" in data.keys(): - del data['_id'] - except Exception as e: - return {'error': e} \ No newline at end of file diff --git a/app/lib/gemini.py b/app/lib/gemini.py deleted file mode 100644 index 1a93061..0000000 --- a/app/lib/gemini.py +++ /dev/null @@ -1,34 +0,0 @@ -from app.config.settings import GEMINI_API_KEY -from google import genai - - -class Gemini: - def __init__(self): - self.client = genai.Client(api_key=GEMINI_API_KEY) - self.welcome_path = "app/data/prompts/boas_vindas.txt" - self.default_path = "app/data/prompts/resposta_padrao.txt" - self.insert_path = "app/data/prompts/inserir_item.txt" - self.format_path = "app/data/prompts/formatar_valor.txt" - - def generate_response(self, msg: str) -> str | None: - response = self.client.models.generate_content( - model="gemini-2.0-flash", - contents=msg, - ) - return response.text - - def welcome(self, msg: str) -> str | None: - welcome_words = self.read_prompt(self.welcome_path) - return self.generate_response(welcome_words + msg) - - def default_answer(self, msg: str) -> str | None: - default_prompt = self.read_prompt(self.default_path) - return self.generate_response(default_prompt + msg) - - def insert_data(self, msg: str) -> str | None: - insert_prompt = self.read_prompt(self.insert_path) - return self.generate_response(insert_prompt + msg) - - def read_prompt(self, path: str) -> str: - with open(path, "r", encoding='utf-8') as prompt: - return prompt.read() \ No newline at end of file diff --git a/app/lib/waha_api.py b/app/lib/waha_api.py deleted file mode 100644 index c85cbf4..0000000 --- a/app/lib/waha_api.py +++ /dev/null @@ -1,76 +0,0 @@ -from app.config.settings import WAHA_HOST, WAHA_PORT, WAHA_ADMIN, WHATSAPP_HOOK_URL -import requests - - -class WahaAPI: - - class WhatsError(Exception): - pass - - def __init__(self, chat_id: int, chat_response): - self.chat_id = chat_id - self.ai_response = chat_response - - def send_message(self) -> dict | None: - try: - url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendText" - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json' - } - data = { - "session": WAHA_ADMIN, - "chatId": self.chat_id, - "text": self.ai_response - } - response = requests.post(url, headers=headers, json=data) - - if response.status_code <= 204: - return response.json() - else: - raise WahaAPI.WhatsError("Have any error in your datas, please check!") - except Exception as e: - return {"message": e} - - def reply(self, chat_id: int, message_id: int, text: str) -> dict: - try: - url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/reply/" - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json' - } - data = { - "session": WAHA_ADMIN, - "chatId": chat_id, - "text": text, - "reply_to": message_id - } - response = requests.post(url, headers=headers, json=data) - - if response.status_code <= 204: - return response.json() - else: - raise WahaAPI.WhatsError("Have any error in your datas, please check!") - except Exception as e: - return {"message": e} - - def send_seen(self) -> dict | Exception: - try: - url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendSeen" - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json' - } - data = { - "session": WAHA_ADMIN, - "chatId": self.chat_id, - } - response = requests.post(url, headers=headers, json=data) - - if response.status_code <= 204: - return response.json() - else: - raise WahaAPI.WhatsError("Have any error in your datas, please check!") - except Exception as e: - return {"message": e} - diff --git a/app/main.py b/app/main.py index 111dffd..ab0176b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,39 +1,14 @@ -from app.lib.chatbot import ChatBot -from app.lib.waha_api import WahaAPI -from app.lib.database import DataBase -from fastapi.responses import JSONResponse -from fastapi import FastAPI, Request -import json +from app.core.settings import CHATBOT_PORT +from app.routers import messages +from fastapi import FastAPI +import uvicorn app = FastAPI( title="ChatBot in Python!", - version="1.0.0" + version="0.0.1" ) -db = DataBase() +app.include_router(messages.router, prefix="/api/v1") - -@app.post("/receive_message", tags=['Webhook']) -async def receive_message(request: Request): - try: - data = await request.json() - # If has content in the message - if data['payload']['body']: - db.save_messages(data) - chatbot_session = ChatBot(data) - chat_response = chatbot_session.verify_response() - waha_api = WahaAPI(chatbot_session.chat_id, chat_response) - # See this other day - waha_api.send_seen() - - response = waha_api.send_message() - db.save_answer(response) - if response: - return JSONResponse( - content={"message": response}, - status_code=200 - ) - else: - raise Exception("This request no has message content!") - except Exception as e: - return JSONResponse({"error": str(e)}, status_code=400) +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=CHATBOT_PORT) \ No newline at end of file diff --git a/app/models/User.py b/app/models/User.py deleted file mode 100644 index 4258681..0000000 --- a/app/models/User.py +++ /dev/null @@ -1,5 +0,0 @@ -from pydantic import BaseModel - -class User(BaseModel): - chat_id: int - text: str \ No newline at end of file diff --git a/app/models/message.py b/app/models/message.py new file mode 100644 index 0000000..df32bad --- /dev/null +++ b/app/models/message.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + +class Message(BaseModel): + user_number: str + content: str + +class Answer(BaseModel): + user_id: str + ai_response: str diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..13cb943 --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, field_validator + +class Product(BaseModel): + name: str + quantity: int + price: float + MIN_VALUE: int = 0 + + field_validator("quantity") + @classmethod + def validate_quantity(cls, quantity: str): + if not quantity.isdigit(): + raise ValueError("The quantity is not valid, please enter the integer number") + if int(quantity) <= cls.MIN_VALUE: + raise ValueError("The input value cannot be less than or equal to 0.") + return int(quantity) + + field_validator("price") + @classmethod + def validate_quantity(cls, price: str): + if not price.isdigit(): + raise ValueError("The price is not valid, please enter the decimal number") + if float(price) <= cls.MIN_VALUE: + raise ValueError("The input value cannot be less than or equal to 0.") + return float(price) \ No newline at end of file diff --git a/app/repositories/message_repository.py b/app/repositories/message_repository.py new file mode 100644 index 0000000..e9d8e09 --- /dev/null +++ b/app/repositories/message_repository.py @@ -0,0 +1,21 @@ +from app.db.mongo.client import MongoClient +from app.models.message import Message, Answer +from pymongo.collection import Collection +from pymongo.errors import PyMongoError + +class MessageRepository: + def __init__(self, mongo_client: MongoClient): + self.messages: Collection = mongo_client.db["messages"] + self.answers: Collection = mongo_client.db["answers"] + + def save_messages(self, msg: Message) -> None | Exception: + try: + self.messages.insert_one(msg.model_dump()) + except (Exception, PyMongoError) as e: + raise e + + def save_answer(self, answer: Answer) -> None | Exception: + try: + self.answers.insert_one(answer.model_dump()) + except (Exception, PyMongoError) as e: + raise e \ No newline at end of file diff --git a/app/routers/messages.py b/app/routers/messages.py new file mode 100644 index 0000000..9f9cdb8 --- /dev/null +++ b/app/routers/messages.py @@ -0,0 +1,34 @@ +from app.dependencies.services import get_chatbot_service, get_waha_service +from app.models.message import Message, Answer +from app.services.chatbot_service import ChatBotService +from app.services.waha_service import WahaAPIService +from fastapi import APIRouter, Request, Depends +from fastapi.responses import JSONResponse + +router = APIRouter() + +@router.post("/messages", tags=['Receives Message of WebHook']) +async def receive_message( + request: Request, + msg: Message, + chat_service: ChatBotService = Depends(get_chatbot_service), + waha_service: WahaAPIService = Depends(get_waha_service) +): + try: + if request.method == "POST": + chat_response = chat_service.validate_response(msg) + waha_service.send_seen(msg.user_number) + answer = Answer( + user_id=msg.user_number, + ai_response=chat_response + ) + response = waha_service.send_message(answer) + if response: + return JSONResponse( + content={ + "message": response + }, + status_code=200 + ) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=400) diff --git a/app/services/chatbot_service.py b/app/services/chatbot_service.py new file mode 100644 index 0000000..840d014 --- /dev/null +++ b/app/services/chatbot_service.py @@ -0,0 +1,60 @@ +from app.services.gemini_service import GeminiService +from app.models.message import Message +from app.models.product import Product +from pathlib import Path +import pandas as pd + + +class ChatBotService: + class UserInputException(Exception): + ... + + def __init__(self, gemini_service: GeminiService): + self.prompt_path: Path = Path("app/db/prompts/get_stock.txt") + self.stock_path: Path = Path("app/db/databases/stock.csv") + self.default_path: Path = Path("app/db/prompts/response_pattern.txt") + self.insert_path: Path = Path("app/db/prompts/insert_item.txt") + self.gemini_service: GeminiService = gemini_service + self.__STANDARD_SIZE: int = 3 + self.__NUMBER_OF_COMMAS: int = 2 + + def get_stock(self) -> str: + df = pd.read_csv(self.stock_path) + data_sheet = df.to_string(index=False) + stock_prompt = self.gemini_service.read_prompt(self.prompt_path) + ia_response = self.gemini_service.generate_response(stock_prompt + data_sheet) + return ia_response + + def insert_product(self, product: Product) -> str | Exception: + try: + new_product = pd.DataFrame([product.model_dump()]) + new_product.to_csv(self.stock_path, mode='a', header=False, index=False) + return "Produto Cadastrado com Sucesso!" + except Exception as e: + raise e + + def format_input(self, data: str) -> Product: + products_infos = [p.strip() for p in data.split(',')] + if len(products_infos) != self.__STANDARD_SIZE: + raise ValueError("The Product is not valid, some value are missing") + + product = Product( + name=products_infos[0], + quantity=products_infos[1], + price=products_infos[2] + ) + return product + + def validate_response(self, msg: Message) -> str | Exception: + try: + if msg.content == '1': + return self.gemini_service.generate_response(msg, self.insert_path) + elif msg.content == '2': + return self.get_stock() + elif msg.content.count(',') == self.__NUMBER_OF_COMMAS: + return self.insert_product(self.format_input(msg.content)) + else: + return self.gemini_service.generate_response(msg, self.default_path) + except ValueError as e: + raise ChatBotService.UserInputException(f"Verify the user input with this error: {e}") + \ No newline at end of file diff --git a/app/services/gemini_service.py b/app/services/gemini_service.py new file mode 100644 index 0000000..03bf2d0 --- /dev/null +++ b/app/services/gemini_service.py @@ -0,0 +1,21 @@ +from app.infra.entities.gemini import Gemini +from app.models.message import Message, Answer +from app.repositories.message_repository import MessageRepository +from pathlib import Path + +class GeminiService(object): + def __init__(self, gemini: Gemini, message_repository: MessageRepository): + self.gemini = gemini + self.message_repository = message_repository + + def generate_response(self, msg: Message, path: Path) -> str | None: + self.message_repository.save_messages(msg) + prompt = self.read_prompt(path) + ai_response = self.gemini.generate_response(prompt + msg.content) + answer = Answer(user_id=msg.user_number, ai_response=ai_response) + self.message_repository.save_answer(answer) + return ai_response + + def read_prompt(self, path: Path) -> str: + with open(path, "r", encoding='utf-8') as prompt: + return prompt.read() diff --git a/app/services/waha_service.py b/app/services/waha_service.py new file mode 100644 index 0000000..b53d0ff --- /dev/null +++ b/app/services/waha_service.py @@ -0,0 +1,69 @@ +from app.core.settings import WAHA_HOST, WAHA_PORT, WAHA_ADMIN +from app.models.message import Answer +import requests + + +class WahaAPIService: + + class WhatsError(Exception): + pass + + def __init__(self): + self.headers : dict = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } + + def send_message(self, chat_answer: Answer) -> dict | None: + try: + url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendText" + + data = { + "session": WAHA_ADMIN, + "chatId": chat_answer.chat_id, + "text": chat_answer.ai_response + } + response = requests.post(url, headers=self.headers, json=data) + + if response.status_code <= 204: + return response.json() + else: + raise WahaAPIService.WhatsError("Have any error in your datas, please check!") + except Exception as e: + raise e + + def reply(self, chat_id: int, message_id: int, text: str) -> dict: + try: + url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/reply/" + data = { + "session": WAHA_ADMIN, + "chatId": chat_id, + "text": text, + "reply_to": message_id + } + response = requests.post(url, headers=self.headers, json=data) + + if response.status_code <= 204: + return response.json() + else: + raise WahaAPIService.WhatsError("Have any error in your datas, please check!") + except Exception as e: + raise e + + def send_seen(self, chat_id: str) -> bool | Exception: + try: + url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendSeen" + + data = { + "session": WAHA_ADMIN, + "chatId": chat_id, + } + response = requests.post(url, headers=self.headers, json=data) + + if response.status_code <= 204: + return True + else: + raise WahaAPIService.WhatsError("Have any error in your input, please check!") + except Exception as e: + raise e + diff --git a/docker-compose.yml b/docker-compose.yml index b06ad43..cc41f14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,20 @@ services: image: devlikeapro/waha ports: - "${WAHA_PORT}:${WAHA_PORT}" + environment: + - WAHA_API_KEY=${WAHA_API_KEY} + - WAHA_DASHBOARD_USERNAME=${WAHA_DASHBOARD_USERNAME} + - WAHA_DASHBOARD_PASSWORD=${WAHA_DASHBOARD_PASSWORD} + - WHATSAPP_SWAGGER_USERNAME=${WHATSAPP_SWAGGER_USERNAME} + - WHATSAPP_SWAGGER_PASSWORD=${WHATSAPP_SWAGGER_PASSWORD} + - WAHA_DASHBOARD_ENABLED=${WAHA_DASHBOARD_ENABLED} + - WHATSAPP_SWAGGER_ENABLED=${WHATSAPP_SWAGGER_ENABLED} + - WHATSAPP_DEFAULT_ENGINE=${WHATSAPP_DEFAULT_ENGINE} + - WAHA_BASE_URL=${WAHA_BASE_URL} + - WAHA_LOG_FORMAT=${WAHA_LOG_FORMAT} + - WAHA_LOG_LEVEL=${WAHA_LOG_LEVEL} + - WAHA_PRINT_QR=${WAHA_PRINT_QR} + networks: - chatbot_network mongo: diff --git a/requirements.txt b/requirements.txt index d619971..5f30b4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,110 +1,59 @@ -aiohappyeyeballs==2.4.6 -aiohttp==3.11.12 -aiosignal==1.3.2 +aiohappyeyeballs==2.6.1 +aiohttp==3.13.3 +aiosignal==1.4.0 +annotated-doc==0.0.4 annotated-types==0.7.0 -anyio==4.8.0 -appdirs==1.4.4 -asttokens==3.0.0 -async-timeout==5.0.1 -attrs==25.1.0 -cachetools==5.5.1 -certifi==2025.1.31 -cffi==1.17.1 -charset-normalizer==3.4.1 -ChatterBot==1.2.0 -click==8.1.8 -confluent-kafka==2.10.1 -consonance==0.1.2 -cryptography==44.0.0 -decorator==5.1.1 -dissononce==0.34.3 -distro==1.9.0 -dnspython==2.7.0 -email_validator==2.2.0 -exceptiongroup==1.2.2 -executing==2.2.0 -fastapi==0.115.8 -fastapi-cli==0.0.7 -frozenlist==1.5.0 +anyio==4.12.1 +asttokens==3.0.1 +attrs==25.4.0 +certifi==2026.1.4 +charset-normalizer==3.4.4 +click==8.3.1 +decorator==5.2.1 +dnspython==2.8.0 +executing==2.2.1 +fastapi==0.128.0 +frozenlist==1.8.0 genai==2.1.0 -google-ai-generativelanguage==0.6.15 -google-api-core==2.24.1 -google-api-python-client==2.161.0 -google-auth==2.38.0 -google-auth-httplib2==0.2.0 +google-auth==2.47.0 google-genai==1.2.0 -google-generativeai==0.8.4 -googleapis-common-protos==1.67.0 -greenlet==3.1.1 -grpcio==1.70.0 -grpcio-status==1.70.0 -h11==0.14.0 -httpcore==1.0.7 -httplib2==0.22.0 -httptools==0.6.4 -httpx==0.28.1 -idna==3.10 -ipython==8.32.0 +h11==0.16.0 +idna==3.11 +ipython==8.38.0 jedi==0.19.2 -Jinja2==3.1.5 -jiter==0.8.2 -kafka-python==2.2.11 -markdown-it-py==3.0.0 -MarkupSafe==3.0.2 -mathparse==0.1.2 -matplotlib-inline==0.1.7 -mdurl==0.1.2 -multidict==6.1.0 -numpy==2.2.4 +matplotlib-inline==0.2.1 +multidict==6.7.0 +numpy==2.4.1 openai==0.27.10 -pandas==2.2.3 -parso==0.8.4 +pandas==3.0.0 +parso==0.8.5 pexpect==4.9.0 -prompt_toolkit==3.0.50 -propcache==0.2.1 -proto-plus==1.26.0 -protobuf==5.29.3 +prompt_toolkit==3.0.52 +propcache==0.4.1 ptyprocess==0.7.0 pure_eval==0.2.3 -pyasn1==0.6.1 -pyasn1_modules==0.4.1 -pycparser==2.22 -pydantic==2.10.6 -pydantic_core==2.27.2 -Pygments==2.19.1 -pymongo==4.12.0 -pyparsing==3.2.1 -python-axolotl==0.2.2 -python-axolotl-curve25519==0.4.1.post2 +pyasn1==0.6.2 +pyasn1_modules==0.4.2 +pydantic==2.12.5 +pydantic_core==2.41.5 +Pygments==2.19.2 +pymongo==4.16.0 python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-multipart==0.0.20 -pytz==2025.1 -PyYAML==6.0.2 -regex==2024.11.6 -requests==2.32.3 -rich==13.9.4 -rich-toolkit==0.13.2 -rsa==4.9 -shellingham==1.5.4 -six==1.10.0 -sniffio==1.3.1 -SQLAlchemy==2.0.38 +python-dotenv==1.2.1 +regex==2026.1.15 +requests==2.32.5 +rsa==4.9.1 +six==1.17.0 stack-data==0.6.3 -starlette==0.45.3 +starlette==0.50.0 tabulate==0.9.0 tiktoken==0.3.3 tqdm==4.67.1 traitlets==5.14.3 -transitions==0.9.2 -typer==0.15.1 -typing_extensions==4.12.2 -tzdata==2025.2 -uritemplate==4.1.1 -urllib3==2.3.0 -uvicorn==0.34.0 -uvloop==0.21.0 -watchfiles==1.0.4 -wcwidth==0.2.13 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +uvicorn==0.40.0 +wcwidth==0.3.1 websockets==14.2 -yarl==1.18.3 +yarl==1.22.0