From b69b4517f85a46c292bd5da36a90389dc3f6600b Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Sun, 28 Dec 2025 23:20:04 -0400 Subject: [PATCH 1/6] refactor: Update Arquiteture of project --- .env.example | 1 + .vscode/launch.json | 3 +- .vscode/settings.json | 3 -- app/{config => core}/__init__.py | 0 app/core/dependencies.py | 9 +++++ app/{config => core}/settings.py | 14 +++++-- app/{data/databases => db/csv}/estoque.csv | 0 app/db/mongo/client.py | 10 +++++ app/{data => db}/prompts/get_stock.txt | 0 .../prompts/insert_item.txt} | 0 .../prompts/response_pattern.txt} | 0 .../prompts/welcome.txt} | 0 app/infra/gemini/config.py | 11 ++++++ app/infra/gemini/provider.py | 11 ++++++ app/{lib => infra/whatsapp}/waha_api.py | 34 +++++++---------- app/lib/__init__.py | 0 app/lib/database.py | 23 ----------- app/lib/gemini.py | 34 ----------------- app/main.py | 38 ++++--------------- app/repositories/message_repository.py | 21 ++++++++++ app/routers/messages.py | 30 +++++++++++++++ .../chatbot_service.py} | 12 +++--- app/services/gemini_service.py | 21 ++++++++++ docker-compose.yml | 14 +++++++ temp.txt | 10 +++++ 25 files changed, 177 insertions(+), 122 deletions(-) delete mode 100644 .vscode/settings.json rename app/{config => core}/__init__.py (100%) create mode 100644 app/core/dependencies.py rename app/{config => core}/settings.py (63%) rename app/{data/databases => db/csv}/estoque.csv (100%) create mode 100644 app/db/mongo/client.py rename app/{data => db}/prompts/get_stock.txt (100%) rename app/{data/prompts/inserir_item.txt => db/prompts/insert_item.txt} (100%) rename app/{data/prompts/resposta_padrao.txt => db/prompts/response_pattern.txt} (100%) rename app/{data/prompts/boas_vindas.txt => db/prompts/welcome.txt} (100%) create mode 100644 app/infra/gemini/config.py create mode 100644 app/infra/gemini/provider.py rename app/{lib => infra/whatsapp}/waha_api.py (70%) delete mode 100644 app/lib/__init__.py delete mode 100644 app/lib/database.py delete mode 100644 app/lib/gemini.py create mode 100644 app/repositories/message_repository.py create mode 100644 app/routers/messages.py rename app/{lib/chatbot.py => services/chatbot_service.py} (80%) create mode 100644 app/services/gemini_service.py create mode 100644 temp.txt 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/.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/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/core/dependencies.py b/app/core/dependencies.py new file mode 100644 index 0000000..b5fc0e4 --- /dev/null +++ b/app/core/dependencies.py @@ -0,0 +1,9 @@ +from fastapi import Depends +from app.infra.gemini.provider import get_gemini_instance +from app.infra.gemini.config import Gemini +from app.services.gemini_service import GeminiService + +def get_gemini_service( + gemini: Gemini = Depends(get_gemini_instance) +) -> GeminiService: + return GeminiService(gemini) diff --git a/app/config/settings.py b/app/core/settings.py similarity index 63% rename from app/config/settings.py rename to app/core/settings.py index d9959ef..c5fee95 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 Enviroments 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/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..7ef9961 --- /dev/null +++ b/app/db/mongo/client.py @@ -0,0 +1,10 @@ +from app.core.settings import ENV, DATABASE_HOST, DATABASE_PORT, DATABASE_NAME +from pymongo import MongoClient +from typing import Dict + +class MongoDBClient(object): + + def __init__(self): + self.client = MongoClient(f"mongodb://{DATABASE_HOST}:{DATABASE_PORT}/") + self.db = self.client[DATABASE_NAME] + \ No newline at end of file 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/data/prompts/boas_vindas.txt b/app/db/prompts/welcome.txt similarity index 100% rename from app/data/prompts/boas_vindas.txt rename to app/db/prompts/welcome.txt diff --git a/app/infra/gemini/config.py b/app/infra/gemini/config.py new file mode 100644 index 0000000..d91d3a2 --- /dev/null +++ b/app/infra/gemini/config.py @@ -0,0 +1,11 @@ +from app.core.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/welcome.txt" + self.default_path = "app/data/prompts/response_pattern.txt" + self.insert_path = "app/data/prompts/insert_item.txt" + self.model = "gemini-2.0-flash" \ No newline at end of file diff --git a/app/infra/gemini/provider.py b/app/infra/gemini/provider.py new file mode 100644 index 0000000..577f333 --- /dev/null +++ b/app/infra/gemini/provider.py @@ -0,0 +1,11 @@ +from app.infra.gemini.config import Gemini + +_gemini_instance = Gemini | None = None + +def get_gemini_instance() -> Gemini: + global _gemini_instance + + if _gemini_instance is None: + _gemini_instance = Gemini() + return _gemini_instance + diff --git a/app/lib/waha_api.py b/app/infra/whatsapp/waha_api.py similarity index 70% rename from app/lib/waha_api.py rename to app/infra/whatsapp/waha_api.py index c85cbf4..00e8bce 100644 --- a/app/lib/waha_api.py +++ b/app/infra/whatsapp/waha_api.py @@ -1,4 +1,4 @@ -from app.config.settings import WAHA_HOST, WAHA_PORT, WAHA_ADMIN, WHATSAPP_HOOK_URL +from app.core.settings import WAHA_HOST, WAHA_PORT, WAHA_ADMIN, WHATSAPP_HOOK_URL import requests @@ -7,70 +7,64 @@ class WahaAPI: class WhatsError(Exception): pass - def __init__(self, chat_id: int, chat_response): + def __init__(self, chat_id: int, chat_response: str): self.chat_id = chat_id self.ai_response = chat_response + self.headers : dict = { + 'accept': 'application/json', + 'Content-Type': 'application/json' + } 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) + response = requests.post(url, headers=self.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} + raise 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) + response = requests.post(url, headers=self.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} + raise 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) + response = requests.post(url, headers=self.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} + raise e diff --git a/app/lib/__init__.py b/app/lib/__init__.py deleted file mode 100644 index e69de29..0000000 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/main.py b/app/main.py index 111dffd..2742bbb 100644 --- a/app/main.py +++ b/app/main.py @@ -1,39 +1,15 @@ -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 app.core.settings import CHATBOT_PORT +from app.routers import messages from fastapi import FastAPI, Request import json +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/repositories/message_repository.py b/app/repositories/message_repository.py new file mode 100644 index 0000000..651d29c --- /dev/null +++ b/app/repositories/message_repository.py @@ -0,0 +1,21 @@ +from app.db.mongo.client import MongoClient +from pymongo.collection import Collection +from pymongo.errors import PyMongoError +from typing import Dict + +class MessageRepository: + def __init__(self, db: MongoClient): + self.messages: Collection = db["messages"] + self.answers: Collection = db["answers"] + + def save_messages(self, data: Dict) -> None | Exception: + try: + self.messages.insert_one(data) + except (Exception, PyMongoError) as e: + raise e + + def save_answer(self, data: Dict) -> None | Exception: + try: + self.answers.insert_one(data) + 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..99141d7 --- /dev/null +++ b/app/routers/messages.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse +from typing import Dict + +router = APIRouter() + +@router.post("/messages", tags=['Receives Message of WebHook']) +async def receive_message(request: Request, body: Dict): + try: + data = request.json() if request.json() else body + # If has content in the message + # if data['payload']['body']: + # ... + # 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) diff --git a/app/lib/chatbot.py b/app/services/chatbot_service.py similarity index 80% rename from app/lib/chatbot.py rename to app/services/chatbot_service.py index bde9514..157130f 100644 --- a/app/lib/chatbot.py +++ b/app/services/chatbot_service.py @@ -1,19 +1,19 @@ -from app.lib.gemini import Gemini +from app.services.gemini_service import GeminiService 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() + self.prompt_path = "app/db/prompts/get_stock.txt" + self.stock_path = "app/db/databases/estoque.csv" + self.gemini_service = GeminiService() 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) + stock_prompt = self.gemini_service.read_prompt(self.prompt_path) + ia_response = self.gemini_service.generate_response(stock_prompt + data_sheet) return ia_response diff --git a/app/services/gemini_service.py b/app/services/gemini_service.py new file mode 100644 index 0000000..f54616f --- /dev/null +++ b/app/services/gemini_service.py @@ -0,0 +1,21 @@ +from app.infra.gemini.config import Gemini + +class GeminiService(object): + def __init__(self, gemini: Gemini): + self.gemini = gemini + + def welcome(self, msg: str) -> str | None: + welcome_words = self.read_prompt(self.gemini.welcome_path) + return self.gemini.generate_response(welcome_words + msg) + + def default_answer(self, msg: str) -> str | None: + default_prompt = self.read_prompt(self.gemini.default_path) + return self.gemini.generate_response(default_prompt + msg) + + def insert_data(self, msg: str) -> str | None: + insert_prompt = self.read_prompt(self.gemini.insert_path) + return self.gemini.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/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/temp.txt b/temp.txt new file mode 100644 index 0000000..86ce41a --- /dev/null +++ b/temp.txt @@ -0,0 +1,10 @@ +Credentials generated. + +Dashboard and Swagger: + - Username: admin + - Password: 448dc6d67102432d8468a3b397537130 + +API key: + - b3296596f30c4bb583cc9a8e51496d19 + +Use it in the Dashboard connection flow: https://waha.devlike.pro/docs/how-to/dashboard/#api-key \ No newline at end of file From 610c361cbe66e7924be6c9c538168fcfbd2db70d Mon Sep 17 00:00:00 2001 From: Lucas de Lima Canto Date: Mon, 5 Jan 2026 09:05:49 -0400 Subject: [PATCH 2/6] fix: update spaces and generate_response logic --- .idea/.gitignore | 10 ++++++++++ .idea/chat_bot.iml | 10 ++++++++++ .idea/inspectionProfiles/profiles_settings.xml | 6 ++++++ .idea/misc.xml | 7 +++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ app/core/settings.py | 4 ++-- app/db/mongo/client.py | 3 +-- app/infra/gemini/config.py | 5 ++++- app/infra/gemini/provider.py | 2 +- app/repositories/message_repository.py | 4 ++-- app/services/chatbot_service.py | 2 +- 12 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/chat_bot.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/chat_bot.iml b/.idea/chat_bot.iml new file mode 100644 index 0000000..8fb1ecc --- /dev/null +++ b/.idea/chat_bot.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..18023e3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1391c84 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/core/settings.py b/app/core/settings.py index c5fee95..86c7b4e 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -9,13 +9,13 @@ # App port CHATBOT_PORT: int | None = int(os.getenv("CHATBOT_PORT", 8080)) -#API Whatsapp +# 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 Enviroments +# Database Environments ENV : str | None = os.getenv("ENV") DATABASE_HOST : str | None = os.getenv("DATABASE_HOST") DATABASE_PORT : str | None = os.getenv("DATABASE_PORT") diff --git a/app/db/mongo/client.py b/app/db/mongo/client.py index 7ef9961..d030bfe 100644 --- a/app/db/mongo/client.py +++ b/app/db/mongo/client.py @@ -1,6 +1,5 @@ -from app.core.settings import ENV, DATABASE_HOST, DATABASE_PORT, DATABASE_NAME +from app.core.settings import DATABASE_HOST, DATABASE_PORT, DATABASE_NAME from pymongo import MongoClient -from typing import Dict class MongoDBClient(object): diff --git a/app/infra/gemini/config.py b/app/infra/gemini/config.py index d91d3a2..d40919d 100644 --- a/app/infra/gemini/config.py +++ b/app/infra/gemini/config.py @@ -8,4 +8,7 @@ def __init__(self): self.welcome_path = "app/data/prompts/welcome.txt" self.default_path = "app/data/prompts/response_pattern.txt" self.insert_path = "app/data/prompts/insert_item.txt" - self.model = "gemini-2.0-flash" \ No newline at end of file + self.model = "gemini-2.0-flash" + + def generate_response(self, param): + pass \ No newline at end of file diff --git a/app/infra/gemini/provider.py b/app/infra/gemini/provider.py index 577f333..4048126 100644 --- a/app/infra/gemini/provider.py +++ b/app/infra/gemini/provider.py @@ -1,6 +1,6 @@ from app.infra.gemini.config import Gemini -_gemini_instance = Gemini | None = None +_gemini_instance = Gemini = None def get_gemini_instance() -> Gemini: global _gemini_instance diff --git a/app/repositories/message_repository.py b/app/repositories/message_repository.py index 651d29c..7b3fb46 100644 --- a/app/repositories/message_repository.py +++ b/app/repositories/message_repository.py @@ -10,12 +10,12 @@ def __init__(self, db: MongoClient): def save_messages(self, data: Dict) -> None | Exception: try: - self.messages.insert_one(data) + self.messages.insert_one(data) except (Exception, PyMongoError) as e: raise e def save_answer(self, data: Dict) -> None | Exception: try: - self.answers.insert_one(data) + self.answers.insert_one(data) except (Exception, PyMongoError) as e: raise e \ No newline at end of file diff --git a/app/services/chatbot_service.py b/app/services/chatbot_service.py index 157130f..f097fbb 100644 --- a/app/services/chatbot_service.py +++ b/app/services/chatbot_service.py @@ -6,7 +6,7 @@ def __init__(self, message: dict): self.content_message = message['payload']['body'] self.chat_id = message['payload']['from'] self.prompt_path = "app/db/prompts/get_stock.txt" - self.stock_path = "app/db/databases/estoque.csv" + self.stock_path = "app/db/databases/stock.csv" self.gemini_service = GeminiService() def get_stock(self) -> str | None: From 0197547bf1ce03f4034884d83eb117b2804d86b6 Mon Sep 17 00:00:00 2001 From: Lucas de Lima Canto Date: Thu, 22 Jan 2026 11:35:48 -0400 Subject: [PATCH 3/6] feat: Update DI to get instances --- app/core/dependencies.py | 21 ++++-- .../{gemini/config.py => entities/gemini.py} | 10 ++- app/infra/gemini/provider.py | 11 --- app/infra/initialization/instances.py | 25 +++++++ app/main.py | 3 +- app/models/message.py | 9 +++ app/models/product.py | 25 +++++++ app/repositories/message_repository.py | 22 +++--- app/routers/messages.py | 46 ++++++------ app/services/chatbot_service.py | 72 ++++++++++--------- app/services/gemini_service.py | 35 +++++---- .../waha_api.py => services/waha_service.py} | 25 ++++--- 12 files changed, 193 insertions(+), 111 deletions(-) rename app/infra/{gemini/config.py => entities/gemini.py} (63%) delete mode 100644 app/infra/gemini/provider.py create mode 100644 app/infra/initialization/instances.py create mode 100644 app/models/message.py create mode 100644 app/models/product.py rename app/{infra/whatsapp/waha_api.py => services/waha_service.py} (68%) diff --git a/app/core/dependencies.py b/app/core/dependencies.py index b5fc0e4..3011f0a 100644 --- a/app/core/dependencies.py +++ b/app/core/dependencies.py @@ -1,9 +1,20 @@ -from fastapi import Depends -from app.infra.gemini.provider import get_gemini_instance -from app.infra.gemini.config import Gemini +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(get_gemini_instance) + gemini: Gemini = Depends(Instance.get_gemini_instance), + message_repository: MessageRepository = Depends(Instance.get_repository_instance) ) -> GeminiService: - return GeminiService(gemini) + 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() \ No newline at end of file diff --git a/app/infra/gemini/config.py b/app/infra/entities/gemini.py similarity index 63% rename from app/infra/gemini/config.py rename to app/infra/entities/gemini.py index d40919d..489a199 100644 --- a/app/infra/gemini/config.py +++ b/app/infra/entities/gemini.py @@ -2,7 +2,7 @@ from google import genai -class Gemini: +class Gemini(object): def __init__(self): self.client = genai.Client(api_key=GEMINI_API_KEY) self.welcome_path = "app/data/prompts/welcome.txt" @@ -10,5 +10,9 @@ def __init__(self): self.insert_path = "app/data/prompts/insert_item.txt" self.model = "gemini-2.0-flash" - def generate_response(self, param): - pass \ No newline at end of file + 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/gemini/provider.py b/app/infra/gemini/provider.py deleted file mode 100644 index 4048126..0000000 --- a/app/infra/gemini/provider.py +++ /dev/null @@ -1,11 +0,0 @@ -from app.infra.gemini.config import Gemini - -_gemini_instance = Gemini = None - -def get_gemini_instance() -> Gemini: - global _gemini_instance - - if _gemini_instance is None: - _gemini_instance = Gemini() - return _gemini_instance - diff --git a/app/infra/initialization/instances.py b/app/infra/initialization/instances.py new file mode 100644 index 0000000..e338fa5 --- /dev/null +++ b/app/infra/initialization/instances.py @@ -0,0 +1,25 @@ +from app.infra.entities.gemini import Gemini +from app.repositories.message_repository import MessageRepository +from app.db.mongo.client import MongoDBClient + +_gemini_instance : Gemini | None = None +_repository_instance: MessageRepository | 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_repository_instance() -> MessageRepository: + global _repository_instance + + if _repository_instance is None: + _repository_instance = MessageRepository() + return _repository_instance + diff --git a/app/main.py b/app/main.py index 2742bbb..ab0176b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,6 @@ from app.core.settings import CHATBOT_PORT from app.routers import messages -from fastapi import FastAPI, Request -import json +from fastapi import FastAPI import uvicorn app = FastAPI( 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..2497187 --- /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("pricce") + @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 index 7b3fb46..3e54afa 100644 --- a/app/repositories/message_repository.py +++ b/app/repositories/message_repository.py @@ -1,21 +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 -from typing import Dict class MessageRepository: def __init__(self, db: MongoClient): - self.messages: Collection = db["messages"] - self.answers: Collection = db["answers"] + self.messages: Collection = db["messages"] + self.answers: Collection = db["answers"] - def save_messages(self, data: Dict) -> None | Exception: + def save_messages(self, msg: Message) -> None | Exception: try: - self.messages.insert_one(data) + self.messages.insert_one(msg.model_dump()) except (Exception, PyMongoError) as e: - raise e + raise e - def save_answer(self, data: Dict) -> None | Exception: - try: - self.answers.insert_one(data) - except (Exception, PyMongoError) as e: - raise e \ No newline at end of file + 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 index 99141d7..3d8a85f 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -1,30 +1,32 @@ -from fastapi import APIRouter, Request +from app.core.dependencies 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 -from typing import Dict router = APIRouter() @router.post("/messages", tags=['Receives Message of WebHook']) -async def receive_message(request: Request, body: Dict): +async def receive_message( + request: Request, + msg: Message, + chat_service: ChatBotService = Depends(get_chatbot_service), + waha_service: WahaAPIService = Depends(get_waha_service) +): try: - data = request.json() if request.json() else body - # If has content in the message - # if data['payload']['body']: - # ... - # 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!") + data = request.json() if request.json() else msg + chat_response = chat_service.validate_response(data) + # See this other day + waha_service.send_seen() + answer = Answer(user_id="5592991957450", 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 index f097fbb..fdb83de 100644 --- a/app/services/chatbot_service.py +++ b/app/services/chatbot_service.py @@ -1,49 +1,57 @@ from app.services.gemini_service import GeminiService +from app.models.message import Message +from app.models.product import Product 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/db/prompts/get_stock.txt" - self.stock_path = "app/db/databases/stock.csv" - self.gemini_service = GeminiService() - def get_stock(self) -> str | None: +class ChatBotService: + class UserInputException(Exception): + ... + + def __init__(self, gemini_service: GeminiService): + self.prompt_path: str = "app/db/prompts/get_stock.txt" + self.stock_path: str = "app/db/databases/stock.csv" + 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_data(self, data: dict) -> str: + def insert_product(self, product: Product) -> str | Exception: 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) + 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_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 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 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) + def validate_response(self, msg: Message) -> str | Exception: + try: + if msg.content == '1': + return self.gemini_service.insert_data(self.msg.content) + 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.default_answer(msg.content) + 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 index f54616f..0daf981 100644 --- a/app/services/gemini_service.py +++ b/app/services/gemini_service.py @@ -1,21 +1,32 @@ -from app.infra.gemini.config import Gemini +from app.infra.entities.gemini import Gemini +from app.repositories.message_repository import MessageRepository class GeminiService(object): - def __init__(self, gemini: Gemini): - self.gemini = gemini + def __init__(self, gemini: Gemini, message_repository: MessageRepository): + self.gemini = gemini + self.message_repository = message_repository def welcome(self, msg: str) -> str | None: - welcome_words = self.read_prompt(self.gemini.welcome_path) - return self.gemini.generate_response(welcome_words + msg) + self.message_repository.save_messages(msg) + welcome_words = self.read_prompt(self.gemini.welcome_path) + ai_response = self.gemini.generate_response(welcome_words + msg) + self.message_repository.save_answer(ai_response) + return ai_response def default_answer(self, msg: str) -> str | None: - default_prompt = self.read_prompt(self.gemini.default_path) - return self.gemini.generate_response(default_prompt + msg) - + self.message_repository.save_messages(msg) + default_prompt = self.read_prompt(self.gemini.default_path) + ai_response = self.gemini.generate_response(default_prompt + msg) + self.message_repository.save_answer(ai_response) + return ai_response + def insert_data(self, msg: str) -> str | None: - insert_prompt = self.read_prompt(self.gemini.insert_path) - return self.gemini.generate_response(insert_prompt + msg) + self.message_repository.save_messages(msg) + insert_prompt = self.read_prompt(self.gemini.insert_path) + ai_response = self.gemini.generate_response(insert_prompt + msg) + self.message_repository.save_answer(ai_response) + return ai_response 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 + with open(path, "r", encoding='utf-8') as prompt: + return prompt.read() \ No newline at end of file diff --git a/app/infra/whatsapp/waha_api.py b/app/services/waha_service.py similarity index 68% rename from app/infra/whatsapp/waha_api.py rename to app/services/waha_service.py index 00e8bce..c7164e3 100644 --- a/app/infra/whatsapp/waha_api.py +++ b/app/services/waha_service.py @@ -1,35 +1,34 @@ -from app.core.settings import WAHA_HOST, WAHA_PORT, WAHA_ADMIN, WHATSAPP_HOOK_URL +from app.core.settings import WAHA_HOST, WAHA_PORT, WAHA_ADMIN +from app.models.message import Answer import requests -class WahaAPI: +class WahaAPIService: class WhatsError(Exception): pass - def __init__(self, chat_id: int, chat_response: str): - self.chat_id = chat_id - self.ai_response = chat_response + def __init__(self): self.headers : dict = { 'accept': 'application/json', 'Content-Type': 'application/json' } - def send_message(self) -> dict | None: + def send_message(self, chat_answer: Answer) -> dict | None: try: url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendText" data = { "session": WAHA_ADMIN, - "chatId": self.chat_id, - "text": self.ai_response + "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 WahaAPI.WhatsError("Have any error in your datas, please check!") + raise WahaAPIService.WhatsError("Have any error in your datas, please check!") except Exception as e: raise e @@ -47,24 +46,24 @@ def reply(self, chat_id: int, message_id: int, text: str) -> dict: if response.status_code <= 204: return response.json() else: - raise WahaAPI.WhatsError("Have any error in your datas, please check!") + raise WahaAPIService.WhatsError("Have any error in your datas, please check!") except Exception as e: raise e - def send_seen(self) -> dict | Exception: + def send_seen(self, chat_id: str) -> dict | Exception: try: url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendSeen" data = { "session": WAHA_ADMIN, - "chatId": self.chat_id, + "chatId": chat_id, } response = requests.post(url, headers=self.headers, json=data) if response.status_code <= 204: return response.json() else: - raise WahaAPI.WhatsError("Have any error in your datas, please check!") + raise WahaAPIService.WhatsError("Have any error in your datas, please check!") except Exception as e: raise e From b5cc32122e235bd6ad779abfc0e080c6882cc821 Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Sun, 25 Jan 2026 12:50:09 -0400 Subject: [PATCH 4/6] fix: update response and generate models --- .../workflows/{compare-branchs.yml => ci.yml} | 26 +--- .gitignore | 2 + .idea/.gitignore | 10 -- .idea/chat_bot.iml | 10 -- .../inspectionProfiles/profiles_settings.xml | 6 - .idea/misc.xml | 7 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - Dockerfile | 2 +- app/db/mongo/client.py | 5 +- app/dependencies/db.py | 7 + app/dependencies/repositories.py | 13 ++ .../services.py} | 6 +- app/infra/entities/gemini.py | 6 +- app/infra/initialization/instances.py | 12 +- app/repositories/message_repository.py | 6 +- app/routers/messages.py | 29 ++-- app/services/chatbot_service.py | 4 +- app/services/gemini_service.py | 22 +-- requirements.txt | 139 ++++++------------ temp.txt | 10 -- 21 files changed, 120 insertions(+), 216 deletions(-) rename .github/workflows/{compare-branchs.yml => ci.yml} (56%) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/chat_bot.iml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 app/dependencies/db.py create mode 100644 app/dependencies/repositories.py rename app/{core/dependencies.py => dependencies/services.py} (76%) delete mode 100644 temp.txt 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/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index ab1f416..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/chat_bot.iml b/.idea/chat_bot.iml deleted file mode 100644 index 8fb1ecc..0000000 --- a/.idea/chat_bot.iml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 18023e3..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 1391c84..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ 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/db/mongo/client.py b/app/db/mongo/client.py index d030bfe..b873e02 100644 --- a/app/db/mongo/client.py +++ b/app/db/mongo/client.py @@ -4,6 +4,5 @@ class MongoDBClient(object): def __init__(self): - self.client = MongoClient(f"mongodb://{DATABASE_HOST}:{DATABASE_PORT}/") - self.db = self.client[DATABASE_NAME] - \ No newline at end of file + self.db = MongoClient( + f"mongodb://{DATABASE_HOST}:{DATABASE_PORT}/")[DATABASE_NAME] 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/core/dependencies.py b/app/dependencies/services.py similarity index 76% rename from app/core/dependencies.py rename to app/dependencies/services.py index 3011f0a..a296ccb 100644 --- a/app/core/dependencies.py +++ b/app/dependencies/services.py @@ -1,3 +1,5 @@ +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 @@ -8,7 +10,7 @@ def get_gemini_service( gemini: Gemini = Depends(Instance.get_gemini_instance), - message_repository: MessageRepository = Depends(Instance.get_repository_instance) + message_repository: MessageRepository = Depends(RepositoryImp.get_message_repository) ) -> GeminiService: return GeminiService(gemini, message_repository) @@ -17,4 +19,4 @@ def get_chatbot_service(gemini_service: GeminiService = Depends(get_gemini_servi return ChatBotService(gemini_service=gemini_service) def get_waha_service() -> WahaAPIService: - return WahaAPIService() \ No newline at end of file + return WahaAPIService() diff --git a/app/infra/entities/gemini.py b/app/infra/entities/gemini.py index 489a199..d69f6c1 100644 --- a/app/infra/entities/gemini.py +++ b/app/infra/entities/gemini.py @@ -5,9 +5,9 @@ class Gemini(object): def __init__(self): self.client = genai.Client(api_key=GEMINI_API_KEY) - self.welcome_path = "app/data/prompts/welcome.txt" - self.default_path = "app/data/prompts/response_pattern.txt" - self.insert_path = "app/data/prompts/insert_item.txt" + self.welcome_path = "app/db/prompts/welcome.txt" + self.default_path = "app/db/prompts/response_pattern.txt" + self.insert_path = "app/db/prompts/insert_item.txt" self.model = "gemini-2.0-flash" def generate_response(self, msg: str) -> str: diff --git a/app/infra/initialization/instances.py b/app/infra/initialization/instances.py index e338fa5..f6a6715 100644 --- a/app/infra/initialization/instances.py +++ b/app/infra/initialization/instances.py @@ -1,9 +1,7 @@ from app.infra.entities.gemini import Gemini -from app.repositories.message_repository import MessageRepository from app.db.mongo.client import MongoDBClient _gemini_instance : Gemini | None = None -_repository_instance: MessageRepository | None = None _db_instance: MongoDBClient | None = None @@ -16,10 +14,10 @@ def get_gemini_instance() -> Gemini: _gemini_instance = Gemini() return _gemini_instance - def get_repository_instance() -> MessageRepository: - global _repository_instance + def get_db_instance() -> MongoDBClient: + global _db_instance - if _repository_instance is None: - _repository_instance = MessageRepository() - return _repository_instance + if _db_instance is None: + _db_instance = MongoDBClient() + return _db_instance diff --git a/app/repositories/message_repository.py b/app/repositories/message_repository.py index 3e54afa..e9d8e09 100644 --- a/app/repositories/message_repository.py +++ b/app/repositories/message_repository.py @@ -4,9 +4,9 @@ from pymongo.errors import PyMongoError class MessageRepository: - def __init__(self, db: MongoClient): - self.messages: Collection = db["messages"] - self.answers: Collection = db["answers"] + 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: diff --git a/app/routers/messages.py b/app/routers/messages.py index 3d8a85f..40c2d10 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -1,4 +1,4 @@ -from app.core.dependencies import get_chatbot_service, get_waha_service +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 @@ -15,18 +15,21 @@ async def receive_message( waha_service: WahaAPIService = Depends(get_waha_service) ): try: - data = request.json() if request.json() else msg - chat_response = chat_service.validate_response(data) - # See this other day - waha_service.send_seen() - answer = Answer(user_id="5592991957450", ai_response=chat_response) - response = waha_service.send_message(answer) - if response: - return JSONResponse( - content={ - "message": response - }, - status_code=200 + if request.method == "POST": + chat_response = chat_service.validate_response(msg) + # See this other day + waha_service.send_seen() + 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 index fdb83de..cb78841 100644 --- a/app/services/chatbot_service.py +++ b/app/services/chatbot_service.py @@ -45,13 +45,13 @@ def format_input(self, data: str) -> Product: def validate_response(self, msg: Message) -> str | Exception: try: if msg.content == '1': - return self.gemini_service.insert_data(self.msg.content) + return self.gemini_service.insert_data(msg) 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.default_answer(msg.content) + return self.gemini_service.default_answer(msg) 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 index 0daf981..8812500 100644 --- a/app/services/gemini_service.py +++ b/app/services/gemini_service.py @@ -1,4 +1,5 @@ from app.infra.entities.gemini import Gemini +from app.models.message import Message, Answer from app.repositories.message_repository import MessageRepository class GeminiService(object): @@ -6,25 +7,28 @@ def __init__(self, gemini: Gemini, message_repository: MessageRepository): self.gemini = gemini self.message_repository = message_repository - def welcome(self, msg: str) -> str | None: + def welcome(self, msg: Message) -> str | None: self.message_repository.save_messages(msg) welcome_words = self.read_prompt(self.gemini.welcome_path) - ai_response = self.gemini.generate_response(welcome_words + msg) - self.message_repository.save_answer(ai_response) + ai_response = self.gemini.generate_response(welcome_words + msg.content) + answer = Answer(msg.user_number, ai_response) + self.message_repository.save_answer(answer) return ai_response - def default_answer(self, msg: str) -> str | None: + def default_answer(self, msg: Message) -> str | None: self.message_repository.save_messages(msg) default_prompt = self.read_prompt(self.gemini.default_path) - ai_response = self.gemini.generate_response(default_prompt + msg) - self.message_repository.save_answer(ai_response) + ai_response = self.gemini.generate_response(default_prompt + msg.content) + answer = Answer(msg.user_number, ai_response) + self.message_repository.save_answer(answer) return ai_response - def insert_data(self, msg: str) -> str | None: + def insert_data(self, msg: Message) -> str | None: self.message_repository.save_messages(msg) insert_prompt = self.read_prompt(self.gemini.insert_path) - ai_response = self.gemini.generate_response(insert_prompt + msg) - self.message_repository.save_answer(ai_response) + ai_response = self.gemini.generate_response(insert_prompt + msg.content) + answer = Answer(msg.user_number, ai_response) + self.message_repository.save_answer(answer) return ai_response def read_prompt(self, path: str) -> str: 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 diff --git a/temp.txt b/temp.txt deleted file mode 100644 index 86ce41a..0000000 --- a/temp.txt +++ /dev/null @@ -1,10 +0,0 @@ -Credentials generated. - -Dashboard and Swagger: - - Username: admin - - Password: 448dc6d67102432d8468a3b397537130 - -API key: - - b3296596f30c4bb583cc9a8e51496d19 - -Use it in the Dashboard connection flow: https://waha.devlike.pro/docs/how-to/dashboard/#api-key \ No newline at end of file From f28dbb817362b5ede7bae1456fcd1d3abd60ad02 Mon Sep 17 00:00:00 2001 From: Lucas Lima Date: Tue, 27 Jan 2026 00:39:22 -0400 Subject: [PATCH 5/6] fix: update model name and remove boilerplate in services --- app/db/prompts/welcome.txt | 4 ---- app/infra/entities/gemini.py | 5 +---- app/routers/messages.py | 5 ++--- app/services/chatbot_service.py | 11 +++++++---- app/services/gemini_service.py | 29 +++++++---------------------- app/services/waha_service.py | 6 +++--- 6 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 app/db/prompts/welcome.txt diff --git a/app/db/prompts/welcome.txt b/app/db/prompts/welcome.txt deleted file mode 100644 index 06b0bdd..0000000 --- a/app/db/prompts/welcome.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/infra/entities/gemini.py b/app/infra/entities/gemini.py index d69f6c1..ce8e685 100644 --- a/app/infra/entities/gemini.py +++ b/app/infra/entities/gemini.py @@ -5,10 +5,7 @@ class Gemini(object): def __init__(self): self.client = genai.Client(api_key=GEMINI_API_KEY) - self.welcome_path = "app/db/prompts/welcome.txt" - self.default_path = "app/db/prompts/response_pattern.txt" - self.insert_path = "app/db/prompts/insert_item.txt" - self.model = "gemini-2.0-flash" + self.model = "gemini-3-flash-preview" def generate_response(self, msg: str) -> str: response = self.client.models.generate_content( diff --git a/app/routers/messages.py b/app/routers/messages.py index 40c2d10..9f9cdb8 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -17,8 +17,7 @@ async def receive_message( try: if request.method == "POST": chat_response = chat_service.validate_response(msg) - # See this other day - waha_service.send_seen() + waha_service.send_seen(msg.user_number) answer = Answer( user_id=msg.user_number, ai_response=chat_response @@ -28,7 +27,7 @@ async def receive_message( return JSONResponse( content={ "message": response - }, + }, status_code=200 ) except Exception as e: diff --git a/app/services/chatbot_service.py b/app/services/chatbot_service.py index cb78841..840d014 100644 --- a/app/services/chatbot_service.py +++ b/app/services/chatbot_service.py @@ -1,6 +1,7 @@ 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 @@ -9,8 +10,10 @@ class UserInputException(Exception): ... def __init__(self, gemini_service: GeminiService): - self.prompt_path: str = "app/db/prompts/get_stock.txt" - self.stock_path: str = "app/db/databases/stock.csv" + 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 @@ -45,13 +48,13 @@ def format_input(self, data: str) -> Product: def validate_response(self, msg: Message) -> str | Exception: try: if msg.content == '1': - return self.gemini_service.insert_data(msg) + 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.default_answer(msg) + 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 index 8812500..83ddc4a 100644 --- a/app/services/gemini_service.py +++ b/app/services/gemini_service.py @@ -1,36 +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 welcome(self, msg: Message) -> str | None: - self.message_repository.save_messages(msg) - welcome_words = self.read_prompt(self.gemini.welcome_path) - ai_response = self.gemini.generate_response(welcome_words + msg.content) - answer = Answer(msg.user_number, ai_response) - self.message_repository.save_answer(answer) - return ai_response - - def default_answer(self, msg: Message) -> str | None: - self.message_repository.save_messages(msg) - default_prompt = self.read_prompt(self.gemini.default_path) - ai_response = self.gemini.generate_response(default_prompt + msg.content) - answer = Answer(msg.user_number, ai_response) - self.message_repository.save_answer(answer) - return ai_response - - def insert_data(self, msg: Message) -> str | None: + + def generate_response(self, msg: Message, path: Path) -> str | None: self.message_repository.save_messages(msg) - insert_prompt = self.read_prompt(self.gemini.insert_path) - ai_response = self.gemini.generate_response(insert_prompt + msg.content) - answer = Answer(msg.user_number, ai_response) + 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: str) -> str: + def read_prompt(self, path: Path) -> str: with open(path, "r", encoding='utf-8') as prompt: return prompt.read() \ No newline at end of file diff --git a/app/services/waha_service.py b/app/services/waha_service.py index c7164e3..b53d0ff 100644 --- a/app/services/waha_service.py +++ b/app/services/waha_service.py @@ -50,7 +50,7 @@ def reply(self, chat_id: int, message_id: int, text: str) -> dict: except Exception as e: raise e - def send_seen(self, chat_id: str) -> dict | Exception: + def send_seen(self, chat_id: str) -> bool | Exception: try: url = f"http://{WAHA_HOST}:{WAHA_PORT}/api/sendSeen" @@ -61,9 +61,9 @@ def send_seen(self, chat_id: str) -> dict | Exception: response = requests.post(url, headers=self.headers, json=data) if response.status_code <= 204: - return response.json() + return True else: - raise WahaAPIService.WhatsError("Have any error in your datas, please check!") + raise WahaAPIService.WhatsError("Have any error in your input, please check!") except Exception as e: raise e From c640e63f60763e8d4800c3520103ea95b1aaa607 Mon Sep 17 00:00:00 2001 From: eulucaslim Date: Wed, 4 Feb 2026 20:18:39 -0400 Subject: [PATCH 6/6] fix: update fields product --- app/models/User.py | 5 ----- app/models/product.py | 2 +- app/services/gemini_service.py | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 app/models/User.py 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/product.py b/app/models/product.py index 2497187..13cb943 100644 --- a/app/models/product.py +++ b/app/models/product.py @@ -15,7 +15,7 @@ def validate_quantity(cls, quantity: str): raise ValueError("The input value cannot be less than or equal to 0.") return int(quantity) - field_validator("pricce") + field_validator("price") @classmethod def validate_quantity(cls, price: str): if not price.isdigit(): diff --git a/app/services/gemini_service.py b/app/services/gemini_service.py index 83ddc4a..03bf2d0 100644 --- a/app/services/gemini_service.py +++ b/app/services/gemini_service.py @@ -18,4 +18,4 @@ def generate_response(self, msg: Message, path: Path) -> str | None: def read_prompt(self, path: Path) -> str: with open(path, "r", encoding='utf-8') as prompt: - return prompt.read() \ No newline at end of file + return prompt.read()