diff --git a/requirements.txt b/requirements.txt index 27ad852..2bd7127 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,6 @@ transformers>=4.35.0,<5.0.0 tokenizers>=0.15.0,<1.0.0 scikit-learn>=1.5.2,<2.0.0 numpy>=1.21.0,<1.27.0 + +matplotlib==3.9.2 +pandas==2.2.3 \ No newline at end of file diff --git a/src/api/controller/AskController.py b/src/api/controller/AskController.py index ba55acc..523d814 100644 --- a/src/api/controller/AskController.py +++ b/src/api/controller/AskController.py @@ -1,6 +1,13 @@ from src.assets.pattern.singleton import SingletonMeta from src.api.models import Question, Response +# Novas imports para gráficos +import io +import os +import uuid +import matplotlib.pyplot as plt +import pandas as pd + from langchain_google_genai import ChatGoogleGenerativeAI from langchain.schema import SystemMessage, HumanMessage from google import genai @@ -11,21 +18,25 @@ GEMINI_API_KEY = env["GEMINI_API_KEY"] GEMINI_MODEL_NAME = env["GEMINI_MODEL_NAME"] + class AskController(metaclass=SingletonMeta): + STATIC_DIR = "src/api/static/graficos/" + def __init__(self): self.client = genai.Client(api_key=GEMINI_API_KEY) - self.gen = ChatGoogleGenerativeAI(model=GEMINI_MODEL_NAME, + self.gen = ChatGoogleGenerativeAI( + model=GEMINI_MODEL_NAME, google_api_key=GEMINI_API_KEY, temperature=0, max_tokens=None, timeout=None, max_retries=2, convert_system_message_to_human=True - ) + ) self.vn = MyVanna(config={ - 'print_prompt': False, + 'print_prompt': False, 'print_sql': False, 'api_key': GEMINI_API_KEY, 'model_name': GEMINI_MODEL_NAME @@ -33,59 +44,125 @@ def __init__(self): self.vn.prepare() - def ask(self, question: Question): + # Garantir que a pasta para gráficos exista + if not os.path.exists(self.STATIC_DIR): + os.makedirs(self.STATIC_DIR) + def ask(self, question: Question): try: - - mensagem = [SystemMessage(content="""Você é um assistente em um sistema que de chat AI, - seu trabalho é receber uma mensagem de um humano e tratar ela para ser processada - por outra IA que possui falhas. Dentro dos tratamentos necessários estão: - -Trocar datas que são dadas em formato que não sejam dias, por exemplo 3 mêses, - e transformar em dias, 90 dias. Outro exemplo, 1 ano e 2 meses, trocar por 425 dias - (serão considerados que os mêses separados terão 30 dias) - -Quando usada a expressão "mudança" referente ao repositório, você trocara por "commit", - por exemplo, "qual foi a ultima mudança feita no repositório?" será trocado por - "qual foi o ultimo commit feito no repositório?) - NÃO explique, NÃO confirme, NÃO dê exemplos. Apenas RETORNE a mensagem tratada. - Mensagem a ser processada:"""),HumanMessage(content=question.question)] - - ai_mensagem = self.gen.invoke(mensagem) - - print(ai_mensagem.content) - - sql_gerado = self.vn.generate_sql(ai_mensagem.content) - - if "SELECT" not in sql_gerado.upper(): - return {"output": "Não consegui entender sua pergunta bem o suficiente para gerar uma resposta SQL válida."} - - resultado = self.vn.run_sql(sql_gerado) - - if not resultado: - return {"output": "A consulta foi feita, mas não há dados correspondentes no banco."} - - # Prompt mais informativo - prompt = f""" - Você é um assistente que responde perguntas sobre dados extraídos do GitHub. - - Pergunta do usuário: "{question.question}" - - Resultado da consulta SQL: {resultado} - - Gere uma resposta clara e útil para o usuário, explicando o que o resultado significa. - """ - - - response = self.client.models.generate_content( - model=GEMINI_MODEL_NAME, - contents=prompt, - config={ - "response_mime_type": "application/json", - "response_schema": list[Response], - } + # Detectar se usuário quer gráfico + gerar_grafico = any( + kw in question.question.lower() + for kw in ["gráfico", "grafico", "chart", "visualizar", "plot"] ) - texto = response.parsed[0].texto - return {"output": texto} + # ----------------------------- + # Pergunta padrão (sem gráfico) + # ----------------------------- + if not gerar_grafico: + mensagem = [ + SystemMessage(content="""Você é um assistente em um sistema que de chat AI, + seu trabalho é receber uma mensagem de um humano e tratar ela para ser processada + por outra IA que possui falhas. Dentro dos tratamentos necessários estão: + -Trocar datas que são dadas em formato que não sejam dias, por exemplo 3 mêses, + e transformar em dias, 90 dias. Outro exemplo, 1 ano e 2 meses, trocar por 425 dias + (serão considerados que os mêses separados terão 30 dias) + -Quando usada a expressão "mudança" referente ao repositório, você trocara por "commit", + por exemplo, "qual foi a ultima mudança feita no repositório?" será trocado por + "qual foi o ultimo commit feito no repositório?") + NÃO explique, NÃO confirme, NÃO dê exemplos. Apenas RETORNE a mensagem tratada. + Mensagem a ser processada:"""), + HumanMessage(content=question.question) + ] + + ai_mensagem = self.gen.invoke(mensagem) + sql_gerado = self.vn.generate_sql(ai_mensagem.content) + + if "SELECT" not in sql_gerado.upper(): + return {"output": "Não consegui entender sua pergunta bem o suficiente para gerar uma resposta SQL válida."} + + resultado = self.vn.run_sql(sql_gerado) + + if not resultado: + return {"output": "A consulta foi feita, mas não há dados correspondentes no banco."} + + prompt = f""" + Você é um assistente que responde perguntas sobre dados extraídos do GitHub. + Pergunta do usuário: "{question.question}" + Resultado da consulta SQL: {resultado} + Gere uma resposta clara e útil para o usuário, explicando o que o resultado significa. + """ + response = self.client.models.generate_content( + model=GEMINI_MODEL_NAME, + contents=prompt, + config={ + "response_mime_type": "application/json", + "response_schema": list[Response], + } + ) + texto = response.parsed[0].texto + return {"output": texto} + + # ----------------------------- + # Pergunta com gráfico + # ----------------------------- + else: + mensagem = [ + SystemMessage(content="""Você é um assistente em um sistema que de chat AI, + seu trabalho é receber uma mensagem de um humano e tratar ela para ser processada + por outra IA que possui falhas. Dentro dos tratamentos necessários estão: + -Trocar datas que são dadas em formato que não sejam dias, por exemplo 3 mêses, + e transformar em dias, 90 dias. Outro exemplo, 1 ano e 2 meses, trocar por 425 dias + (serão considerados que os mêses separados terão 30 dias) + -Quando usada a expressão "mudança" referente ao repositório, você trocara por "commit", + por exemplo, "qual foi a ultima mudança feita no repositório?" será trocado por + "qual foi o ultimo commit feito no repositório?") + retire as palavras faça gráfico da mensagem, + NÃO explique, NÃO confirme, NÃO dê exemplos. Apenas RETORNE a mensagem tratada. + Mensagem a ser processada:"""), + HumanMessage(content=question.question) + ] + + ai_mensagem = self.gen.invoke(mensagem) + sql_gerado = self.vn.generate_sql(ai_mensagem.content) + + if "SELECT" not in sql_gerado.upper(): + return {"output": "Não consegui entender sua pergunta bem o suficiente para gerar uma resposta SQL válida."} + + resultado = self.vn.run_sql(sql_gerado) + + if not resultado: + return {"output": "A consulta foi feita, mas não há dados correspondentes no banco."} + + # ----------------------------- + # Geração do gráfico + # ----------------------------- + df = pd.DataFrame(resultado) + if df.empty: + return {"output": "Não há dados suficientes para gerar um gráfico."} + + plt.figure(figsize=(8,5)) + if df.shape[1] >= 2: + x = df.columns[0] + y = df.columns[1] + plt.bar(df[x], df[y]) + plt.xlabel(x) + plt.ylabel(y) + plt.title("Gráfico gerado a partir dos dados") + else: + plt.plot(df[df.columns[0]]) + plt.title("Gráfico gerado a partir dos dados") + + # Salvar gráfico como arquivo + filename = f"{uuid.uuid4()}.png" + filepath = os.path.join(self.STATIC_DIR, filename) + plt.tight_layout() + plt.savefig(filepath) + plt.close() + + # Montar link clicável + link = f"http://localhost:8000/static/graficos/{filename}" + return {"output": f"Gráfico gerado: [Clique aqui para visualizar]({link})", "grafico_url": link} except Exception as e: - return {"output": f"Ocorreu um erro ao processar sua pergunta: {str(e)}"} \ No newline at end of file + return {"output": f"Ocorreu um erro ao processar sua pergunta: {str(e)}"} \ No newline at end of file diff --git a/src/api/endpoints/routes.py b/src/api/endpoints/routes.py index 7c2f7f0..ac4905d 100644 --- a/src/api/endpoints/routes.py +++ b/src/api/endpoints/routes.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, HTTPException -from src.api.models import Question, Response +from fastapi.responses import FileResponse +from src.api.models import Question from src.api.controller.AskController import AskController +import os router = APIRouter() @@ -11,4 +13,14 @@ async def ask_question(question: Question): try: return ask.ask(question) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + raise HTTPException(status_code=500, detail=str(e)) + + +# Endpoint para servir arquivos de gráficos +@router.get("/static/graficos/{filename}") +def serve_grafico(filename: str): + path = os.path.join("src/api/static/graficos", filename) + if os.path.exists(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404, detail="Gráfico não encontrado") \ No newline at end of file diff --git a/src/assets/open_web_ui/pipeline_api.py b/src/assets/open_web_ui/pipeline_api.py index 097e6ef..e2978e5 100644 --- a/src/assets/open_web_ui/pipeline_api.py +++ b/src/assets/open_web_ui/pipeline_api.py @@ -37,9 +37,6 @@ def __init__(self): self.valves = self.Valves() self.last_emit_time = 0.0 # controle de throttle dos status - # -------------------------------------------------------------- - # Utilitário para reportar progresso para o OpenWebUI - # -------------------------------------------------------------- async def _emit_status( self, __event_emitter__: Callable[[dict], Awaitable[None]], @@ -66,19 +63,14 @@ async def _emit_status( ) self.last_emit_time = now - # -------------------------------------------------------------- - # Método principal chamado pelo OpenWebUI - # -------------------------------------------------------------- async def pipe( self, body: dict, __user__: Optional[dict] = None, __event_emitter__: Callable[[dict], Awaitable[None]] = None, - __event_call__: Callable[[dict], Awaitable[dict]] = None, # não usado + __event_call__: Callable[[dict], Awaitable[dict]] = None, ) -> Optional[dict]: - """Envia a última mensagem do usuário à API FastAPI e retorna a resposta.""" - # Recupera a última mensagem do usuário await self._emit_status(__event_emitter__, "info", "Processando entrada...", False) messages = body.get("messages", []) @@ -87,10 +79,8 @@ async def pipe( return {"error": "Nenhuma mensagem encontrada"} last_content = messages[-1]["content"] - # Estrutura de content pode ser: str | list[ {type: text|file} ] question = self._extract_text(last_content) - # Monta request para API FastAPI headers = {"Content-Type": "application/json"} if self.valves.bearer_token: headers["Authorization"] = f"Bearer {self.valves.bearer_token}" @@ -111,28 +101,27 @@ async def pipe( await self._emit_status(__event_emitter__, "error", f"Erro: {exc}", True) return {"error": str(exc)} - # Interpreta resposta da API. try: data = response.json() except ValueError: data = {"output": response.text} - answer = data.get("output") or data + grafico_url = data.get("grafico_url") + if grafico_url: + # link já incluído no output, apenas mostra o texto + answer = data.get("output") + else: + answer = data.get("output") or data - # Acrescenta a resposta ao histórico e devolve ao WebUI body["messages"].append({"role": "assistant", "content": answer}) await self._emit_status(__event_emitter__, "info", "Resposta entregue", True) return answer - # -------------------------------------------------------------- - # Auxiliar: extrair texto da mensagem (lida com arquivos/objetos) - # -------------------------------------------------------------- def _extract_text(self, content) -> str: if isinstance(content, str): return content.replace("Prompt: ", "", 1).strip() - # Caso seja uma lista (arquivos + texto) text_found = "" file_detected = False for item in content: @@ -147,4 +136,4 @@ def _extract_text(self, content) -> str: text_found = f"Recebemos o arquivo {name}, mas ele é muito grande para ser processado." else: text_found = f"Recebemos o arquivo {name}. Ainda não processamos arquivos neste chat." - return text_found or "Arquivo recebido." + return text_found or "Arquivo recebido." \ No newline at end of file