From 8b1dcce530c2a6d87ba7600c3d104a4e1a8d5f4d Mon Sep 17 00:00:00 2001 From: aliciacapo Date: Thu, 16 Oct 2025 14:59:00 -0300 Subject: [PATCH 1/5] retirando item desnecessario, adicionando treinamento --- src/api/controller/AskController.py | 16 +-- src/api/database/MyVanna.py | 185 +++++++++++++++++++++++++--- 2 files changed, 174 insertions(+), 27 deletions(-) diff --git a/src/api/controller/AskController.py b/src/api/controller/AskController.py index 2bc33a9..154aefc 100644 --- a/src/api/controller/AskController.py +++ b/src/api/controller/AskController.py @@ -23,22 +23,16 @@ def __init__(self): self.vn.prepare() def ask(self, question: Question): - client = genai.Client(api_key=GEMINI_API_KEY) - - vn = MyVanna(config={ - 'print_prompt': False, - 'print_sql': False, - 'api_key': GEMINI_API_KEY, - 'model_name': GEMINI_MODEL_NAME - }) try: - sql_gerado = vn.generate_sql(question.question) + + + sql_gerado = self.vn.generate_sql(question.question) 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 = vn.run_sql(sql_gerado) + resultado = self.vn.run_sql(sql_gerado) if not resultado: return {"output": "A consulta foi feita, mas não há dados correspondentes no banco."} @@ -54,7 +48,7 @@ def ask(self, question: Question): Gere uma resposta clara e útil para o usuário, explicando o que o resultado significa. """ - response = client.models.generate_content( + response = self.client.models.generate_content( model=GEMINI_MODEL_NAME, contents=prompt, config={ diff --git a/src/api/database/MyVanna.py b/src/api/database/MyVanna.py index 84c2e70..a21ab42 100644 --- a/src/api/database/MyVanna.py +++ b/src/api/database/MyVanna.py @@ -15,12 +15,61 @@ DB_PASSWORD = env["DB_PASSWORD"] DB_URL = env["DB_URL"] -class MyVanna(ChromaDB_VectorStore, GoogleGeminiChat): +class ChromaDB_VectorStoreReset(ChromaDB_VectorStore): def __init__(self, config=None): if config is None: config = {} - ChromaDB_VectorStore.__init__(self, config=config) + # Força o reset na inicialização + config["reset_on_init"] = config.get("reset_on_init", True) + + super().__init__(config=config) + + # Limpa as coleções após a inicialização padrão + if config["reset_on_init"]: + self._reset_collections() + + # Recria as coleções vazias + collection_metadata = config.get("collection_metadata", None) + self.documentation_collection = self.chroma_client.get_or_create_collection( + name="documentation", + embedding_function=self.embedding_function, + metadata=collection_metadata, + ) + self.ddl_collection = self.chroma_client.get_or_create_collection( + name="ddl", + embedding_function=self.embedding_function, + metadata=collection_metadata, + ) + self.sql_collection = self.chroma_client.get_or_create_collection( + name="sql", + embedding_function=self.embedding_function, + metadata=collection_metadata, + ) + + def _reset_collections(self): + """Limpa todas as coleções existentes""" + try: + self.chroma_client.delete_collection("documentation") + except Exception: + pass + + try: + self.chroma_client.delete_collection("ddl") + except Exception: + pass + + try: + self.chroma_client.delete_collection("sql") + except Exception: + pass + +class MyVanna(ChromaDB_VectorStoreReset, GoogleGeminiChat): + def __init__(self, config=None): + if config is None: + config = {} + + ChromaDB_VectorStoreReset.__init__(self, config=config) GEMINI_API_KEY = config.get('api_key') GEMINI_MODEL_NAME = config.get('model_name') @@ -123,32 +172,134 @@ def prepare(self): password = DB_PASSWORD ) - schema = self.get_schema() - - self.train(ddl=self.schema) + self.train(ddl=self.get_schema()) self.train(documentation=""" - A tabela repository armazena os repositórios, identificados por um ID único e nome. +Table: user_info + + id: Bigint primary key with default value from sequence + + login: Required username field (character varying) + + html_url: Required profile URL field (text) + +Table: milestone + + id: Bigint primary key with default value from sequence + + repository_id: Associated repository ID (integer, required) + + title: Milestone title (text, required) + + description: Milestone description (text, optional) + + number: Milestone number (integer, required) + + state: Milestone state (character varying, required) + + created_at: Creation timestamp with time zone + + updated_at: Update timestamp with time zone + + creator: Creator user ID (bigint, required) + +Table: repository + + id: Integer primary key with default value from sequence + + name: Repository name (character varying, required) + +Table: branch + + id: Bigint primary key with default value from sequence + + name: Branch name (character varying, required) + + repository_id: Associated repository ID (integer, required) + +Table: issue - A tabela user contém informações dos usuários, como login e URL de perfil. É usada como referência em outras tabelas, como quem criou issues, pull requests e milestones. + id: Bigint primary key with default value from sequence - A tabela milestone representa marcos definidos nos repositórios, contendo título, descrição, número, estado (aberta ou fechada), datas de criação e atualização, o repositório ao qual pertence e o usuário criador. + title: Issue title (text, required) - A tabela issue armazena tarefas ou bugs reportados. Contém título, corpo, número, autor, repositório, milestone relacionada, datas e URL. As atribuições de usuários a uma issue são registradas em issue_assignees. + body: Issue body/description (text, optional) - A tabela pull_requests armazena os pull requests criados nos repositórios. Inclui título, corpo, número, estado, criador, repositório, milestone (opcional), datas e URL. Os responsáveis são registrados em pull_request_assignees. + number: Issue number (integer, required) - A tabela branch armazena os nomes de branches de cada repositório. + html_url: Issue URL (text, optional) - A tabela commits armazena cada commit feito. Cada registro contém o SHA, mensagem, autor (usuário), repositório, branch (opcional), data de criação e URL. Também há referência à tabela de usuários. + created_at: Creation timestamp with time zone - A tabela parents_commits representa a relação entre commits e seus pais (para commits com múltiplos ancestrais, como em merges). Usa o SHA do commit pai e o ID do commit filho. + updated_at: Update timestamp with time zone - A tabela issue_assignees relaciona múltiplos usuários a uma mesma issue, representando atribuições de tarefas. É uma tabela de junção entre issues e usuários. + created_by: Creator user ID (bigint, required) - A tabela pull_request_assignees relaciona múltiplos usuários a um pull request, permitindo registrar quem é responsável por revisar ou aprovar um PR. + repository_id: Associated repository ID (bigint, required) - O modelo garante integridade por meio de chaves estrangeiras, e unicidade de registros por restrições compostas (como número + repositório para issues, pull requests e milestones). + milestone_id: Associated milestone ID (bigint, optional) + +Table: pull_requests + + id: Bigint primary key with default value from sequence + + created_by: Creator user ID (bigint, required) + + repository_id: Associated repository ID (integer, required) + + number: Pull request number (integer, required) + + state: Pull request state (character varying, required) + + title: Pull request title (text, required) + + body: Pull request body/description (text, optional) + + html_url: Pull request URL (text, required) + + created_at: Creation timestamp with time zone + + updated_at: Update timestamp with time zone + + milestone_id: Associated milestone ID (bigint, optional) + +Table: commits + + id: Bigint primary key with default value from sequence + + user_id: Author user ID (bigint, required) + + branch_id: Associated branch ID (integer, optional) + + pull_request_id: Associated pull request ID (bigint, optional) + + created_at: Creation timestamp with time zone + + message: Commit message (text, required) + + sha: Commit SHA hash (character varying, required) + + html_url: Commit URL (text, optional) + +Table: parents_commits + + id: Integer primary key with default value from sequence + + parent_sha: Parent commit SHA hash (character varying, required) + + commit_id: Child commit ID (integer, required) + +Table: issue_assignees + + issue_id: Issue ID (bigint, required, part of primary key) + + user_id: Assigned user ID (bigint, required, part of primary key) + +Table: pull_request_assignees + + pull_request_id: Pull request ID (bigint, required, part of primary key) + + user_id: Assigned user ID (bigint, required, part of primary key) """) self.train(sql=""" @@ -232,3 +383,5 @@ def prepare(self): ORDER BY total_commits DESC; """) + + From a93c2ab67e3795e95b56243c70c82480adfa07ad Mon Sep 17 00:00:00 2001 From: aliciacapo Date: Fri, 24 Oct 2025 19:54:28 -0300 Subject: [PATCH 2/5] ajustando requirements e adicionando lengchain basico --- requirements.txt | 4 +++- src/api/controller/AskController.py | 31 +++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 271ff40..27ad852 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,9 +5,11 @@ psycopg2-binary==2.9.10 python-dotenv==1.1.0 vanna==0.7.9 chromadb -google-generativeai==0.8.5 +google-genai google-cloud-aiplatform==1.96.0 onnxruntime==1.22.0 +langchain-google-genai<1.0.0 +langchain pytest==7.4.0 pytest-asyncio==0.21.1 diff --git a/src/api/controller/AskController.py b/src/api/controller/AskController.py index 154aefc..ba55acc 100644 --- a/src/api/controller/AskController.py +++ b/src/api/controller/AskController.py @@ -1,6 +1,8 @@ from src.assets.pattern.singleton import SingletonMeta from src.api.models import Question, Response +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain.schema import SystemMessage, HumanMessage from google import genai from src.api.database.MyVanna import MyVanna @@ -13,6 +15,15 @@ class AskController(metaclass=SingletonMeta): def __init__(self): self.client = genai.Client(api_key=GEMINI_API_KEY) + 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_sql': False, @@ -26,8 +37,23 @@ def ask(self, question: Question): try: - - sql_gerado = self.vn.generate_sql(question.question) + 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."} @@ -48,6 +74,7 @@ def ask(self, question: Question): 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, From 1d2919c57ca55da5a925b59a65ec3443033d996c Mon Sep 17 00:00:00 2001 From: Vitor Ramos Date: Wed, 29 Oct 2025 10:34:55 -0300 Subject: [PATCH 3/5] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20de=20contexto=20c?= =?UTF-8?q?om=20langchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example.env | 6 +- src/api/controller/AskController.py | 282 ++++++++++++++++++++++++---- 2 files changed, 249 insertions(+), 39 deletions(-) diff --git a/example.env b/example.env index bcee7a6..7476625 100644 --- a/example.env +++ b/example.env @@ -1,5 +1,5 @@ # Airbyte -GITHUB_TOKEN= +GITHUB_TOKEN= "" # Database credentials DB_HOST=db @@ -12,5 +12,5 @@ DB_PASSWORD=postgres DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} # Google cloud gemini -GEMINI_API_KEY= -GEMINI_MODEL_NAME=gemini-2.0-flash \ No newline at end of file +GEMINI_API_KEY="" +GEMINI_MODEL_NAME=gemini-2.0-flash diff --git a/src/api/controller/AskController.py b/src/api/controller/AskController.py index ba55acc..0e63ce0 100644 --- a/src/api/controller/AskController.py +++ b/src/api/controller/AskController.py @@ -2,79 +2,178 @@ from src.api.models import Question, Response from langchain_google_genai import ChatGoogleGenerativeAI -from langchain.schema import SystemMessage, HumanMessage +from langchain.schema import SystemMessage, HumanMessage, AIMessage +from langchain.memory import ConversationBufferWindowMemory +from langchain.chains import LLMChain +from langchain.prompts import ( + ChatPromptTemplate, + MessagesPlaceholder, + SystemMessagePromptTemplate, + HumanMessagePromptTemplate, +) from google import genai from src.api.database.MyVanna import MyVanna +import json +from typing import Dict, Optional +import hashlib from src.assets.aux.env import env + # Gemini env vars GEMINI_API_KEY = env["GEMINI_API_KEY"] GEMINI_MODEL_NAME = env["GEMINI_MODEL_NAME"] + class AskController(metaclass=SingletonMeta): def __init__(self): self.client = genai.Client(api_key=GEMINI_API_KEY) - self.gen = ChatGoogleGenerativeAI(model=GEMINI_MODEL_NAME, + # LLM principal para geração de respostas + self.llm = 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 - ) + ) + + # Memória conversacional (mantém últimas 5 interações) + self.memory = ConversationBufferWindowMemory( + k=5, # Número de interações a manter + memory_key="chat_history", + return_messages=True, + input_key="question", + output_key="answer" + ) + + # Chain para pré-processamento com contexto + self.preprocessing_chain = self._create_preprocessing_chain() + # instancia do vanna 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): + # Cache simples para queries SQL (em memória) + self.sql_cache: Dict[str, str] = {} + self.result_cache: Dict[str, any] = {} - try: + def _create_preprocessing_chain(self) -> LLMChain: + """Cria chain para pré-processar perguntas com contexto da conversa""" + + system_template = """Você é um assistente especializado em processar perguntas sobre dados do GitHub. + +Sua função é: +1. Analisar o histórico da conversa para entender o contexto +2. Resolver referências contextuais (ex: "e no mês passado?", "mostre mais detalhes", "e o outro repositório?") +3. Normalizar expressões temporais: + - "3 meses" → "90 dias" + - "1 ano e 2 meses" → "425 dias" + - Meses separados = 30 dias cada +4. Normalizar terminologia: + - "mudança" → "commit" + - "alteração" → "commit" +5. Expandir a pergunta com contexto necessário do histórico + +REGRAS CRÍTICAS: +- Se a pergunta fizer referência a algo anterior ("e aquele", "o outro", "também"), inclua o contexto explícito +- Se não houver referência contextual, retorne a pergunta apenas normalizada +- NÃO explique, NÃO confirme, NÃO dê exemplos +- Retorne APENAS a pergunta processada e expandida - 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)] +Histórico da conversa está disponível abaixo.""" - ai_mensagem = self.gen.invoke(mensagem) + prompt = ChatPromptTemplate.from_messages([ + SystemMessagePromptTemplate.from_template(system_template), + MessagesPlaceholder(variable_name="chat_history"), + HumanMessagePromptTemplate.from_template("{question}") + ]) - print(ai_mensagem.content) + return LLMChain( + llm=self.llm, + prompt=prompt, + memory=self.memory, + verbose=False + ) - sql_gerado = self.vn.generate_sql(ai_mensagem.content) + def _get_cache_key(self, text: str) -> str: + """Gera chave de cache baseada no hash da pergunta normalizada""" + normalized = text.lower().strip() + return hashlib.md5(normalized.encode()).hexdigest() - 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."} + def _validate_sql(self, sql: str) -> tuple[bool, Optional[str]]: + """Valida SQL gerado para segurança""" + sql_upper = sql.upper().strip() + + # Whitelist: apenas SELECT permitido + if not sql_upper.startswith("SELECT"): + return False, "Apenas queries SELECT são permitidas" + + # Blacklist: operações perigosas + dangerous_keywords = [ + "DELETE", "DROP", "TRUNCATE", "INSERT", + "UPDATE", "ALTER", "CREATE", "GRANT", "REVOKE" + ] + + for keyword in dangerous_keywords: + if keyword in sql_upper: + return False, f"Operação '{keyword}' não é permitida" + + # Limite de complexidade (número de JOINs) + join_count = sql_upper.count("JOIN") + if join_count > 10: + return False, "Query muito complexa (máximo 10 JOINs)" + + return True, None - resultado = self.vn.run_sql(sql_gerado) + def _format_response_with_context(self, question: str, sql: str, result: any) -> str: + """Formata resposta final usando LLM com contexto conversacional""" + + # Recupera histórico da memória + memory_vars = self.memory.load_memory_variables({}) + chat_history = memory_vars.get("chat_history", []) + + # Monta contexto do histórico + history_context = "" + if chat_history: + history_context = "\n\nContexto da conversa anterior:\n" + for msg in chat_history[-3:]: # Últimas 3 mensagens + if isinstance(msg, HumanMessage): + history_context += f"Usuário: {msg.content}\n" + elif isinstance(msg, AIMessage): + history_context += f"Assistente: {msg.content}\n" - if not resultado: - return {"output": "A consulta foi feita, mas não há dados correspondentes no banco."} + prompt = f""" +Você é um assistente especializado em análise de dados do GitHub. - # Prompt mais informativo - prompt = f""" - Você é um assistente que responde perguntas sobre dados extraídos do GitHub. +{history_context} - Pergunta do usuário: "{question.question}" +Pergunta atual: "{question}" - Resultado da consulta SQL: {resultado} +SQL gerado e executado: +```sql +{sql} +``` - Gere uma resposta clara e útil para o usuário, explicando o que o resultado significa. - """ +Resultado da consulta: {result} +Com base no contexto da conversa e nos resultados, gere uma resposta: +1. Clara e direta +2. Em linguagem natural +3. Destacando insights relevantes +4. Relacionando com perguntas anteriores se aplicável +5. Formato estruturado se houver múltiplos dados +Responda de forma conversacional e útil. +""" + + try: response = self.client.models.generate_content( model=GEMINI_MODEL_NAME, contents=prompt, @@ -83,9 +182,120 @@ def ask(self, question: Question): "response_schema": list[Response], } ) + return response.parsed[0].texto + except Exception as e: + # Fallback para resposta simples + return f"Resultado: {result}" + + def ask(self, question: Question, session_id: Optional[str] = None) -> dict: + """ + Processa pergunta com contexto conversacional + + Args: + question: Objeto Question com a pergunta do usuário + session_id: ID da sessão para memória multi-usuário (futuro) + """ + + # --- Dentro de ask() --- + try: + original_question = question.question + + # Etapa 1: Pré-processar com contexto (usando chain) + processed_question = self.preprocessing_chain.invoke({ + "question": original_question + }) + if isinstance(processed_question, dict) and "text" in processed_question: + processed_question = processed_question["text"] + elif not isinstance(processed_question, str): + processed_question = str(processed_question) + + print(f"[Preprocessed] {processed_question}") + + # Etapa 2: Verificar cache de SQL + cache_key = self._get_cache_key(processed_question) + + if cache_key in self.sql_cache: + print(f"[Cache Hit] SQL encontrado no cache") + sql_gerado = self.sql_cache[cache_key] + else: + sql_gerado = self.vn.generate_sql(processed_question) + + is_valid, error_msg = self._validate_sql(sql_gerado) + if not is_valid: + return { + "output": f"Query inválida: {error_msg}", + "error": True + } + + self.sql_cache[cache_key] = sql_gerado + print(f"[Cache Miss] SQL gerado e armazenado") + + print(f"[SQL] {sql_gerado}") + + result_cache_key = hashlib.md5(sql_gerado.encode()).hexdigest() + + if result_cache_key in self.result_cache: + print(f"[Cache Hit] Resultado encontrado no cache") + resultado = self.result_cache[result_cache_key] + else: + resultado = self.vn.run_sql(sql_gerado) + if not resultado: + self.memory.save_context( + inputs={"question": original_question}, + outputs={"answer": "Não há dados correspondentes no banco."} + ) + return { + "output": "A consulta foi executada, mas não há dados correspondentes.", + "sql": sql_gerado + } + + self.result_cache[result_cache_key] = resultado + print(f"[Cache Miss] Resultado obtido e armazenado") + + resposta_formatada = self._format_response_with_context( + question=original_question, + sql=sql_gerado, + result=resultado + ) - texto = response.parsed[0].texto - return {"output": texto} + self.memory.save_context( + inputs={"question": original_question}, + outputs={"answer": resposta_formatada} + ) + + return { + "output": resposta_formatada, + "sql": sql_gerado, + "cached": result_cache_key in self.result_cache + } except Exception as e: - return {"output": f"Ocorreu um erro ao processar sua pergunta: {str(e)}"} \ No newline at end of file + error_msg = f"Erro ao processar pergunta: {str(e)}" + print(f"[Error] {error_msg}") + + self.memory.save_context( + inputs={"question": question.question}, + outputs={"answer": error_msg} + ) + + return { + "output": error_msg, + "error": True + } + + + def clear_memory(self): + """Limpa o histórico da conversa""" + self.memory.clear() + print("[Memory] Histórico limpo") + + def get_conversation_history(self) -> list: + """Retorna o histórico da conversa""" + memory_vars = self.memory.load_memory_variables({}) + return memory_vars.get("chat_history", []) + + def clear_cache(self): + """Limpa os caches de SQL e resultados""" + self.sql_cache.clear() + self.result_cache.clear() + print("[Cache] Caches limpos") \ No newline at end of file From ef6619ab4207a93e0096abdfd076d65a67053366 Mon Sep 17 00:00:00 2001 From: Vitor Ramos Date: Wed, 29 Oct 2025 16:37:14 -0300 Subject: [PATCH 4/5] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20de=20contexto=20e?= =?UTF-8?q?=20gerenciamento=20de=20token=20com=20langchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/requirements.txt b/requirements.txt index 27ad852..00101b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,14 @@ -airbyte==0.31.3 -FastAPI==0.115.9 -uvicorn==0.34.3 -psycopg2-binary==2.9.10 -python-dotenv==1.1.0 -vanna==0.7.9 -chromadb -google-genai -google-cloud-aiplatform==1.96.0 -onnxruntime==1.22.0 -langchain-google-genai<1.0.0 -langchain - -pytest==7.4.0 -pytest-asyncio==0.21.1 -pytest-mock==3.12.0 -pytest-cov==4.1.0 -httpx -requests-mock==1.11.0 - -torch>=2.2.0,<2.8.0 -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 +annotated-types==0.7.0 +anyio==4.11.0 +certifi==2025.10.5 +exceptiongroup==1.3.0 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.11 +ollama==0.6.0 +pydantic==2.12.3 +pydantic_core==2.41.4 +sniffio==1.3.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 From 76e735577579368906316bc8578cf06a12f51f69 Mon Sep 17 00:00:00 2001 From: Vitor Ramos Date: Wed, 29 Oct 2025 21:01:38 -0300 Subject: [PATCH 5/5] =?UTF-8?q?corre=C3=A7=C3=A3o=20de=20implementa=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20contexto=20e=20gerenciamento=20de=20token=20com?= =?UTF-8?q?=20langchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example.env | 16 ---- src/api/controller/AskController.py | 127 ++++++++++++++++++---------- 2 files changed, 80 insertions(+), 63 deletions(-) delete mode 100644 example.env diff --git a/example.env b/example.env deleted file mode 100644 index 7476625..0000000 --- a/example.env +++ /dev/null @@ -1,16 +0,0 @@ -# Airbyte -GITHUB_TOKEN= "" - -# Database credentials -DB_HOST=db -DB_AB_DESTINATION_HOST=host.docker.internal -DB_PORT=5432 -EXPOSED_DB_PORT=5432 -DB_NAME=databasex -DB_USER=postgres -DB_PASSWORD=postgres -DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} - -# Google cloud gemini -GEMINI_API_KEY="" -GEMINI_MODEL_NAME=gemini-2.0-flash diff --git a/src/api/controller/AskController.py b/src/api/controller/AskController.py index 0e63ce0..cd320a4 100644 --- a/src/api/controller/AskController.py +++ b/src/api/controller/AskController.py @@ -4,13 +4,6 @@ from langchain_google_genai import ChatGoogleGenerativeAI from langchain.schema import SystemMessage, HumanMessage, AIMessage from langchain.memory import ConversationBufferWindowMemory -from langchain.chains import LLMChain -from langchain.prompts import ( - ChatPromptTemplate, - MessagesPlaceholder, - SystemMessagePromptTemplate, - HumanMessagePromptTemplate, -) from google import genai from src.api.database.MyVanna import MyVanna import json @@ -48,10 +41,7 @@ def __init__(self): output_key="answer" ) - # Chain para pré-processamento com contexto - self.preprocessing_chain = self._create_preprocessing_chain() - - # instancia do vanna + # Instância do vanna self.vn = MyVanna(config={ 'print_prompt': False, 'print_sql': False, @@ -64,10 +54,29 @@ def __init__(self): self.sql_cache: Dict[str, str] = {} self.result_cache: Dict[str, any] = {} - def _create_preprocessing_chain(self) -> LLMChain: - """Cria chain para pré-processar perguntas com contexto da conversa""" - - system_template = """Você é um assistente especializado em processar perguntas sobre dados do GitHub. + def _preprocess_question(self, question: str) -> str: + """ + Pré-processa a pergunta usando LLM com contexto da memória + Versão simplificada sem LLMChain para evitar problemas de parsing + """ + try: + # Recupera histórico da memória + memory_vars = self.memory.load_memory_variables({}) + chat_history = memory_vars.get("chat_history", []) + + # Monta contexto do histórico + history_text = "" + if chat_history: + history_text = "Histórico da conversa:\n" + for msg in chat_history[-5:]: # Últimas 5 mensagens + if isinstance(msg, HumanMessage): + history_text += f"Usuário: {msg.content}\n" + elif isinstance(msg, AIMessage): + history_text += f"Assistente: {msg.content}\n" + history_text += "\n" + + # Monta o prompt + system_prompt = """Você é um assistente especializado em processar perguntas sobre dados do GitHub. Sua função é: 1. Analisar o histórico da conversa para entender o contexto @@ -85,22 +94,37 @@ def _create_preprocessing_chain(self) -> LLMChain: - Se a pergunta fizer referência a algo anterior ("e aquele", "o outro", "também"), inclua o contexto explícito - Se não houver referência contextual, retorne a pergunta apenas normalizada - NÃO explique, NÃO confirme, NÃO dê exemplos -- Retorne APENAS a pergunta processada e expandida +- Retorne APENAS a pergunta processada e expandida em uma única linha""" -Histórico da conversa está disponível abaixo.""" - - prompt = ChatPromptTemplate.from_messages([ - SystemMessagePromptTemplate.from_template(system_template), - MessagesPlaceholder(variable_name="chat_history"), - HumanMessagePromptTemplate.from_template("{question}") - ]) - - return LLMChain( - llm=self.llm, - prompt=prompt, - memory=self.memory, - verbose=False - ) + # Monta mensagens + messages = [ + SystemMessage(content=system_prompt) + ] + + # Adiciona histórico se existir + if history_text: + messages.append(HumanMessage(content=history_text)) + + # Adiciona pergunta atual + messages.append(HumanMessage(content=f"Pergunta a processar: {question}")) + + # Chama LLM + response = self.llm.invoke(messages) + + # Extrai conteúdo da resposta + if hasattr(response, 'content'): + processed = response.content.strip() + else: + processed = str(response).strip() + + # Remove qualquer explicação extra (pega só a primeira linha) + processed = processed.split('\n')[0].strip() + + return processed + + except Exception as e: + print(f"[Warning] Erro no preprocessing: {e}. Usando pergunta original.") + return question def _get_cache_key(self, text: str) -> str: """Gera chave de cache baseada no hash da pergunta normalizada""" @@ -184,8 +208,9 @@ def _format_response_with_context(self, question: str, sql: str, result: any) -> ) return response.parsed[0].texto except Exception as e: + print(f"[Error] Erro ao formatar resposta: {e}") # Fallback para resposta simples - return f"Resultado: {result}" + return f"Consulta executada com sucesso. Resultado: {result}" def ask(self, question: Question, session_id: Optional[str] = None) -> dict: """ @@ -196,19 +221,12 @@ def ask(self, question: Question, session_id: Optional[str] = None) -> dict: session_id: ID da sessão para memória multi-usuário (futuro) """ - # --- Dentro de ask() --- try: original_question = question.question + print(f"[Original] {original_question}") - # Etapa 1: Pré-processar com contexto (usando chain) - processed_question = self.preprocessing_chain.invoke({ - "question": original_question - }) - if isinstance(processed_question, dict) and "text" in processed_question: - processed_question = processed_question["text"] - elif not isinstance(processed_question, str): - processed_question = str(processed_question) - + # Etapa 1: Pré-processar com contexto + processed_question = self._preprocess_question(original_question) print(f"[Preprocessed] {processed_question}") # Etapa 2: Verificar cache de SQL @@ -218,8 +236,10 @@ def ask(self, question: Question, session_id: Optional[str] = None) -> dict: print(f"[Cache Hit] SQL encontrado no cache") sql_gerado = self.sql_cache[cache_key] else: + # Gerar SQL com Vanna sql_gerado = self.vn.generate_sql(processed_question) + # Validar SQL is_valid, error_msg = self._validate_sql(sql_gerado) if not is_valid: return { @@ -227,19 +247,24 @@ def ask(self, question: Question, session_id: Optional[str] = None) -> dict: "error": True } + # Armazenar no cache self.sql_cache[cache_key] = sql_gerado print(f"[Cache Miss] SQL gerado e armazenado") print(f"[SQL] {sql_gerado}") + # Etapa 3: Verificar cache de resultados result_cache_key = hashlib.md5(sql_gerado.encode()).hexdigest() if result_cache_key in self.result_cache: print(f"[Cache Hit] Resultado encontrado no cache") resultado = self.result_cache[result_cache_key] else: + # Executar SQL resultado = self.vn.run_sql(sql_gerado) + if not resultado: + # Salvar na memória mesmo sem resultado self.memory.save_context( inputs={"question": original_question}, outputs={"answer": "Não há dados correspondentes no banco."} @@ -248,16 +273,19 @@ def ask(self, question: Question, session_id: Optional[str] = None) -> dict: "output": "A consulta foi executada, mas não há dados correspondentes.", "sql": sql_gerado } - + + # Armazenar resultado no cache self.result_cache[result_cache_key] = resultado print(f"[Cache Miss] Resultado obtido e armazenado") + # Etapa 4: Formatar resposta com contexto resposta_formatada = self._format_response_with_context( question=original_question, sql=sql_gerado, result=resultado ) + # Etapa 5: Salvar na memória self.memory.save_context( inputs={"question": original_question}, outputs={"answer": resposta_formatada} @@ -270,20 +298,25 @@ def ask(self, question: Question, session_id: Optional[str] = None) -> dict: } except Exception as e: + import traceback error_msg = f"Erro ao processar pergunta: {str(e)}" print(f"[Error] {error_msg}") + print(f"[Error] Traceback: {traceback.format_exc()}") - self.memory.save_context( - inputs={"question": question.question}, - outputs={"answer": error_msg} - ) + # Salvar erro na memória + try: + self.memory.save_context( + inputs={"question": question.question}, + outputs={"answer": error_msg} + ) + except: + pass return { "output": error_msg, "error": True } - def clear_memory(self): """Limpa o histórico da conversa""" self.memory.clear()