Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
183 changes: 130 additions & 53 deletions src/api/controller/AskController.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,81 +18,151 @@
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
})

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)}"}
return {"output": f"Ocorreu um erro ao processar sua pergunta: {str(e)}"}
16 changes: 14 additions & 2 deletions src/api/endpoints/routes.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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))
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")
27 changes: 8 additions & 19 deletions src/assets/open_web_ui/pipeline_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
Expand All @@ -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", [])
Expand All @@ -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}"
Expand All @@ -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:
Expand All @@ -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."
Loading