diff --git a/README.md b/README.md index f62776c..04d00e5 100644 --- a/README.md +++ b/README.md @@ -1,164 +1,240 @@ -# CodeWise +# CodeWise -* Ferramenta instalável via pip que usa IA para analisar o código e automatizar a documentação de Pull Requests através de hooks do Git. - -## Funcionalidades Principais -- **Geração de Título:** Cria títulos de PR claros e concisos seguindo o padrão *Conventional Commits*. -- **Geração de Descrição:** Escreve descrições detalhadas baseadas nas alterações do código. -- **Análise Técnica:** Posta um comentário no PR com um resumo executivo de melhorias de arquitetura, aderência a princípios S.O.L.I.D. e outros pontos de qualidade. -- **Automação com hooks:** Integra-se ao seu fluxo de trabalho Git para rodar automaticamente a cada `git commit` e `git push`. +Ferramenta instalavel via `pip` que utiliza IA para analisar codigo e automatizar a documentacao de Pull Requests atraves de hooks do Git. --- -## Guia de Instalação -Siga estes passos para instalar e configurar o CodeWise em qualquer um dos seus repositórios. +## Funcionalidades + +- **Geracao de Titulo:** Cria titulos de PR claros e concisos seguindo o padrao Conventional Commits. +- **Geracao de Descricao:** Escreve descricoes detalhadas baseadas nas alteracoes do codigo. +- **Analise Tecnica:** Posta um comentario no PR com resumo executivo de melhorias de arquitetura, aderencia a principios S.O.L.I.D. e outros pontos de qualidade. +- **Automacao com Hooks:** Integra-se ao fluxo de trabalho Git para rodar automaticamente a cada `git commit` e `git push`. +- **Flexibilidade de IA:** Escolha qual provedor de IA usar (`Cohere`, `Google Gemini`, `Groq`, `OpenAI`) atraves de configuracao. +- **Verificacao de Privacidade (LGPD):** Analisa automaticamente a politica de coleta de dados do provedor de IA antes de enviar o codigo. +- **Avaliacao de Codigo:** Gera relatorios de avaliacao com nota e justificativa detalhada. +- **Notificacao via Telegram:** Envia avaliacoes automaticamente para gestores via Telegram Bot API. --- -### Passo 1: Pré-requisitos (ter no PC antes de tudo) +## Pre-requisitos -Antes de começar, garanta que você tenha as seguintes ferramentas instaladas em seu sistema: +Antes de comecar, garanta que voce tenha instaladas as seguintes ferramentas: -1. **Python** (versão 3.11 ou superior). -2. **Git**. -3. **GitHub CLI (`gh`)**: Após instalar em (https://cli.github.com), logue com sua conta do GitHub executando `gh auth login` no seu terminal (só precisa fazer isso uma vez por PC). ---- +1. **Python** (versao 3.11 ou superior) +2. **Git** +3. **GitHub CLI (`gh`)** -### Passo 2: Configurando Seu Repositório +Apos instalar a CLI do GitHub (https://cli.github.com), execute: -**Para cada novo Repositório em que você desejar usar o CodeWise, siga os passos abaixo.** - -"*O ideal é sempre criar um ambiente virtual na pasta raiz do novo repositório para evitar conflitos das dependências.*" +```bash +gh auth login +``` + +Faca login na sua conta. Este passo e necessario apenas uma vez por computador. --- -#### 2.1 Crie e Utilize um Ambiente Virtual -Para evitar conflitos com outros projetos Python, use um ambiente virtual (`venv`). +## Instalacao -* **Para Criar o Ambiente:** +### 1. Criar e Ativar o Ambiente Virtual - * Este comando cria uma pasta `.venv` com uma instalação limpa do Python. Faça isso uma única vez por repositório, - *Lembrando que o ".venv" é o nome da pasta que foi criada, voce pode escolher qualquer outro nome pra ela.* - +Crie o ambiente virtual na raiz do repositorio onde esta a pasta `.git`: -(**dentro da raíz do repositório onde está a pasta .git**) +```bash +# Windows +py -m venv .venv - ```bash - # No Windows - py -m venv .venv - - # No Linux/WSL - python3 -m venv .venv - ``` +# Linux/WSL +python3 -m venv .venv +``` -* **Para Ativar o Ambiente:** +Ative o ambiente: - * Sempre que for trabalhar no projeto, você precisa ativar o ambiente. +```bash +# Windows (PowerShell) +.\.venv\Scripts\activate - * **Dica para Windows/PowerShell:** Se o comando de ativação der um erro de política de execução, rode este comando primeiro: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` +# Linux/WSL +source .venv/bin/activate +``` - ```bash - # No Windows (PowerShell) - .\.venv\Scripts\activate - - # No Linux/WSL - source .venv/bin/activate - ``` - *Você saberá que funcionou porque o nome `(.venv)` aparecerá no início da linha do seu terminal.* ---- -#### 2.2 Instale a Ferramenta CodeWise -Com o ambiente virtual ativo, instale a biblioteca com o `pip`. +Se ocorrer erro de politica de execucao no PowerShell, rode: + +```bash +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +### 2. Instalar o CodeWise + +Com o ambiente virtual ativo, instale o pacote: ```bash pip install codewise-lib ``` - **Pode demorar um pouco pra instalar todas as dependências na primeira vez.** +Apos concluir, confirme se esta tudo certo com: -*Após instalar a lib, você pode confirmar se está tudo certo com o comando `codewise-help`* +```bash +codewise-help +``` --- -#### 2.3 Configure a Chave da API (.env) -Para que a IA funcione, você precisa configurar sua chave da API do Google Gemini. +## Configuracao do Arquivo .env + +Na raiz do projeto, crie um arquivo `.env` com as seguintes variaveis: + +```ini +# PROVEDOR DE IA +# Opcoes disponiveis: "COHERE", "GROQ", "GEMINI", "OPENAI" +AI_PROVIDER="GEMINI" + +# MODELO ESPECIFICO +# Exemplos: "gemini-2.0-flash", "gpt-4o-mini", "command-r-plus", "llama-3.1-70b-versatile" +AI_MODEL=gemini-2.0-flash + +# CHAVES DE API DOS PROVEDORES +# Configure a chave correspondente ao AI_PROVIDER escolhido +COHERE_API_KEY=sua_chave_cohere_aqui +GROQ_API_KEY=sua_chave_groq_aqui +GEMINI_API_KEY=sua_chave_gemini_aqui +OPENAI_API_KEY=sua_chave_openai_aqui + +# TELEGRAM (opcional - para notificacoes de avaliacao) +TELEGRAM_BOT_TOKEN=seu_token_do_bot_telegram +TELEGRAM_CHAT_ID=seu_chat_id_telegram +``` + +**Importante:** Adicione o arquivo `.env` ao `.gitignore` para evitar expor suas chaves secretas. -1. **Na raiz do seu projeto**, crie um arquivo chamado `.env`. Você pode usar os seguintes comandos no terminal: +--- + +## Chave OpenAI para Embedding (Obrigatorio) - * **Windows** - ```bash - notepad .env - ``` - * **Linux/WSL:** - ```bash - touch .env && nano .env - ``` +O CodeWise utiliza o CrewAI com ferramentas que dependem de embedding para busca semantica. Por isso, **a chave `OPENAI_API_KEY` e obrigatoria** no arquivo `.env`, mesmo que voce utilize outro provedor de IA (Gemini, Groq, Cohere) como modelo principal. -2. Dentro do arquivo `.env`, cole o seguinte conteúdo, adicione sua chave e salve: - ``` - GEMINI_API_KEY=SUA_CHAVE_AQUI - MODEL_NAME=gemini-2.0-flash - ``` - ⚠️ **Importante:** Lembre-se de adicionar o arquivo `.env` ao seu `.gitignore` para não enviar sua chave secreta para o GitHub ao dar push e que ele deve ser do tipo "arquivo ENV" e não .txt ou coisa do tipo. +A OpenAI e utilizada internamente pelo CrewAI Tools para realizar operacoes de embedding. Sem essa chave configurada, as ferramentas de analise nao funcionarao corretamente. --- -## Nota Importante: A ferramenta CodeWise espera que seus remotes sigam a convenção padrão do GitHub: +## Configuracao do Telegram (Opcional) + +Para receber notificacoes de avaliacao de codigo via Telegram: + +1. **Criar um Bot no Telegram:** + - Abra o Telegram e busque por `@BotFather` + - Envie o comando `/newbot` e siga as instrucoes + - Copie o token gerado para `TELEGRAM_BOT_TOKEN` -origin: Deve apontar para o seu fork pessoal do repositório. +2. **Obter o Chat ID:** + - Inicie uma conversa com seu bot + - Acesse `https://api.telegram.org/bot/getUpdates` + - Localize o campo `chat.id` na resposta JSON + - Copie o valor para `TELEGRAM_CHAT_ID` -upstream: (caso você adicione ao repositório)Deve apontar para o repositório principal do qual você fez o fork. +3. **Adicionar ao .env:** -**Caso você comece um repositório novo totalmente zerado, você tem que dar um push inicial com "git push --no-verify" antes de usar a ferramenta para que o GH CLI funcione corretamente ao criar os Pull Requests** +```ini +TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz +TELEGRAM_CHAT_ID=987654321 +``` + +As notificacoes incluem: desenvolvedor avaliado, repositorio, nota, resumo da avaliacao e data. + +--- -#### 2.4 Agora apenas uma vez > Ative a Automação no Repositório com um comando. -Na raiz do projeto onde também está a pasta .git use: +## Ativar a Automacao no Repositorio + +Na raiz do projeto (onde esta a pasta `.git`), execute uma unica vez: ```bash codewise-init --all ``` -**Use esse comando sempre que você quiser mudar para onde o PULL REQUEST SERÁ CRIADO nos hooks de pre push, pois se você adicionar um remoto upstream você tem que alternar entre qual o PR será gerado.** -Aqui está a configuração do Alvo do Pull Request: +Esse comando adicionara automaticamente os hooks `pre-commit` e `pre-push`. + +Se o seu repositorio tiver um `upstream`, o instalador perguntara qual deve ser o comportamento padrao do `git push` para criacao de Pull Requests. + +--- + +## Comandos Disponiveis + +| Comando | Descricao | +|---------|-----------| +| `codewise-init --all` | Instala os hooks pre-commit e pre-push | +| `codewise-init --commit` | Instala apenas o hook pre-commit | +| `codewise-init --push` | Instala apenas o hook pre-push | +| `codewise-pr` | Analisa commits e cria/atualiza PR com IA | +| `codewise-pr-origin` | Cria PR no remote origin | +| `codewise-pr-upstream` | Cria PR no remote upstream | +| `codewise-lint` | Analisa arquivos staged antes do commit | +| `codewise-help` | Exibe ajuda e comandos disponiveis | + +--- + +## Fluxo de Uso + +1. **Adicione suas alteracoes:** + +```bash +git add . +``` + +2. **Faca o commit:** + +```bash +git commit -m "implementa novo recurso" +``` + +O hook `pre-commit` sera ativado e executara o `codewise-lint` automaticamente. + +3. **Envie para o GitHub:** + +```bash +git push +``` + +O hook `pre-push` ativara o `codewise-pr`, que criara ou atualizara o Pull Request com titulo, descricao e analise tecnica gerados pela IA. + +--- + +## Nota sobre Remotes -Se o seu repositório tiver um remote upstream configurado, o instalador fará uma pergunta depois que você usou o comando "codewise-init --all" -para definir o comportamento padrão do hook pre-push: +A ferramenta CodeWise espera que seus remotes sigam a convencao padrao do GitHub: - Um remote 'upstream' foi detectado. -Qual deve ser o comportamento padrão do 'git push' para este repositório? -1: Criar Pull Request no 'origin' (seu fork) -2: Criar Pull Request no 'upstream' (projeto principal) -Escolha o padrão (1 ou 2): +- **origin:** aponta para o seu fork pessoal do repositorio +- **upstream:** (opcional) aponta para o repositorio principal -Sua escolha será salva no hook, e você não precisará mais se preocupar com isso. Se não houver upstream, ele será configurado para origin por padrão. +Se o repositorio for novo, execute um push inicial com: -Você verá uma mensagem de sucesso confirmando que a automação está ativa. +```bash +git push --no-verify +``` -Com esse comando os arquivos de pre-commit e pre-push já terão sido adicionados ao seu hooks do repositório. +Isso garante que o `gh` funcione corretamente na criacao dos Pull Requests. --- -Tudo está funcionando agora no repositório que você configurou. -Caso queira instalar em um novo repositório basta repetir os passos. +## Verificacao de Privacidade e LGPD -# Usando o CodeWise -Com a configuração concluída, você já tem acesso aos comandos **codewise-lint** e **codewise-pr** de forma manual e automatizada após instalar os hooks. +Antes de qualquer envio de codigo, o CodeWise realiza uma verificacao de privacidade automatica. O objetivo e garantir que o provedor de IA configurado no `.env` possua politicas compativeis com a LGPD, assegurando a protecao dos seus dados e da sua base de codigo. -1. **Adicione suas alterações** +--- + +## Dependencias - * Após modificar seus arquivos, adicione-os à "staging area": - ```bash - git add . - ``` - * Aqui você já pode usar o comando `codewise-lint` para analisar os arquivos e você poder fazer ajustes antes de commitar. +- crewai >= 0.201.1 +- crewai-tools >= 0.76.0 +- python-dotenv >= 1.1.1 +- PyYAML >= 6.0.3 +- litellm >= 1.74.9 +- qdrant-client >= 1.15.1 +- requests >= 2.32.3 + +--- -2. **Faça o commit** - ```bash - git commit -m "implementa novo recurso " - ``` - * Neste momento, o **hook `pre-commit` será ativado**, e o `codewise-lint` fará a análise rápida no seu terminal. +✅ Tudo pronto! -3. **Envie para o GitHub** - ```bash - git push - ``` - * Agora, o **hook `pre-push` será ativado**. O `codewise-pr` vai perguntar para qual remote você quer enviar caso haja um upstream além do seu origin em seguida irá criar um novo/atualizar seu Pull Request com título, descrição e análise técnica gerados pela IA. \ No newline at end of file +Seu repositório já está com o CodeWise ativo. +Para usar em outro repositório, basta repetir os passos acima. diff --git a/codewise_lib/code_reviewer.py b/codewise_lib/code_reviewer.py new file mode 100644 index 0000000..0a16bb8 --- /dev/null +++ b/codewise_lib/code_reviewer.py @@ -0,0 +1,75 @@ +import os +import subprocess + + +def coletar_dados_git(repo_path: str, commits_limit: int = 3) -> str: + """ + Coleta informações detalhadas do repositório Git para análise de código. + + Args: + repo_path: Caminho para o repositório Git local + commits_limit: Número máximo de commits a analisar (padrão: 3) + + Returns: + str: Texto formatado com informações dos commits e diffs para análise + """ + try: + try: + result = subprocess.run( + ['git', '-C', repo_path, 'config', 'user.email'], + capture_output=True, + text=True, + check=True + ) + user_email = result.stdout.strip() + except: + user_email = "Desenvolvedor" + + #coleta de logs dos commits anteriores + git_log_cmd = [ + 'git', '-C', repo_path, 'log', + f'-{commits_limit}', + '--pretty=format:%H|%an|%ae|%ad|%s', + '--date=iso', + '--numstat', + 'HEAD' + ] + + log_output = subprocess.check_output( + git_log_cmd, + stderr=subprocess.STDOUT, + text=True + ) + + #formatação do resultado final + resultado = [] + resultado.append("=" * 80) + resultado.append(f"ANÁLISE DE CÓDIGO - {user_email}") + resultado.append("=" * 80) + resultado.append("") + resultado.append(f"Últimos {commits_limit} commits:") + resultado.append("") + resultado.append(log_output) + resultado.append("") + + #coleta de diffs dos commits anteriores + for i in range(commits_limit): + try: + diff_cmd = ['git', '-C', repo_path, 'show', f'HEAD~{i}', '--unified=3'] + diff_output = subprocess.check_output( + diff_cmd, + stderr=subprocess.STDOUT, + text=True + ) + resultado.append(f"--- Mudanças do commit HEAD~{i} ---") + resultado.append(diff_output[:3000]) # Limita tamanho + resultado.append("") + except: + pass + + return "\n".join(resultado) + + except subprocess.CalledProcessError as e: + return f"Erro ao coletar dados Git: {e.output}" + except Exception as e: + return f"Erro: {str(e)}" diff --git a/codewise_lib/config/agents.yaml b/codewise_lib/config/agents.yaml index 6f97f6e..f4b2d37 100644 --- a/codewise_lib/config/agents.yaml +++ b/codewise_lib/config/agents.yaml @@ -25,3 +25,22 @@ summary_specialist: goal: Gerar um resumo técnico claro e objetivo do que foi implementado ou modificado no commit analisado. backstory: Um engenheiro de software experiente em revisão de código e documentação, responsável por redigir resumos de alterações para pull requests sem julgamentos ou sugestões. +code_mentor: + role: Mentor de Sugestões Educacionais + goal: Identificar oportunidades de aprendizado baseadas nas análises técnicas e sugerir artigos, cursos, ou videos para ajudar a desenvolver habilidades de desenvolvedor. + backstory: Um educador experiente em engenharia de software com vasto conhecimento em recursos de aprendizado, cursos online e materiais educativos. Especialista em Identificar dificuldades e transformar críticas técnicas em oportunidades de crescimento profissional. + +dataCollect_policy_analytics: + role: Especialista em análise de políticas de coletas de dados + goal: Encontrar, a partir de buscas na web, e Analisar documentações sobre a política de coleta de dados de modelos de api keys fornecido por um provedor para uma futura comparação com as leis LGPD. + backstory: Analista de segurança de dados com foco na política de coleta de dados de um modelo de api key fornecido por um determinado provedor e que não desiste até efetuar a análise completa. + +lgpd_judge: + role: Especialista em leis LGPD + goal: Comparar o resumo da política de coleta de dados de um provedor de api key com as leis LGPD e determinar se tal provedor está respeitando as leis LGPD. + backstory: Um especialista em leis LGPD que julga se há o cumprimento dessas leis por parte do provedor da api key. + +code_reviewer: + role: Revisor Sênior de Código e Avaliador de Qualidade + goal: Analisar mudanças de código realizadas por desenvolvedores e atribuir uma nota objetiva baseada em critérios técnicos rigorosos de qualidade, arquitetura, boas práticas e impacto. + backstory: Um engenheiro de software altamente experiente com mais de 15 anos de experiência em revisão de código, arquitetura de sistemas e mentoria técnica. Especialista em identificar problemas sutis de design, violações de princípios SOLID, code smells, problemas de performance e segurança. Conhecido por avaliações justas, objetivas e fundamentadas tecnicamente. Utiliza uma abordagem holística considerando não apenas o código em si, mas também o contexto, complexidade, impacto e aderência às melhores práticas da indústria. \ No newline at end of file diff --git a/codewise_lib/config/tasks.yaml b/codewise_lib/config/tasks.yaml index 1b4ec7c..449acac 100644 --- a/codewise_lib/config/tasks.yaml +++ b/codewise_lib/config/tasks.yaml @@ -37,4 +37,67 @@ summarize_analysis: escreva um resumo objetivo e claro descrevendo o que foi implementado no código analisado com base nas boas práticas de Pull Request. expected_output: > Um parágrafo bem estruturado com as alterações realizadas, pronto para ser usado como descrição de um Pull Request em (pt-br). + agent: summary_specialist +mentoring_task: + description: > + Com base nas análises técnicas realizadas, + identifique oportunidades de aprendizado e sugira recursos educacionais personalizados para ajudar o desenvolvedor a melhorar suas habilidades técnicas. + expected_output: > + Documento sugestoes_aprendizado.md com recursos educacionais específicos (artigos, cursos, vídeos) organizados por tópico e conectados aos problemas identificados. + agent: code_mentor + +policy_analytics: + description: > + Crie um relatório sobre a política de coleta de dados do "{IA_PROVIDER}" para o modelo "{IA_MODEL}" + com base, e apenas, na documentação oficial. Lembre-se de usar a ferramenta para buscar a documentação oficial. + expected_output: > + Relatório analise_politica_coleta_de_dados.md com um resumo dos principais pontos REAIS sobre a política de coleta de dados do provedor a partir da documentação oficial, com as referências (links dos sites utilizados) no fim do arquivo e duas linhas logo ao inicio, reservadas informando APENAS E DIRETAMENTE o provedor e o modelo da api key, da seguinte forma: **providermodeloapikey**. + agent: dataCollect_policy_analytics + +lgpd_judging: + description: > + Com base nas leis LGPD e no relatório da analise da documentação fornecida pela tarefa anterior, + determine se o provedor da api key está respeitando ou não as leis LGPD e justifique o julgamento apontando as provas, por meio de um relatório, onde a primeira linha **exatamente na linha 1** do arquivo é a resposta antecipada (sim ou não). + Lembre-se de usar a ferramenta para buscar as leis LGPD do site oficial (gov.br) e de utilizar o contexto (análise) da tarefa anterior. Além disso, coloque a conclusão sempre no final do arquivo. + Monta o relatório de maneira CLEAN, e bem separado por tópicos (Breve introdução --> Análise da Conformidade com a LGPD --> Conclusão --> artigos LGPD utilizados --> referências). + Após os tópicos: Referências, coloque em uma linha única, *fim referências*, para indicar o fim da conclusão. + expected_output: > + Documento julgamento_lgpd.md com as provas ou artigos utilizados para determinar a decisão tomada. + agent: lgpd_judge + +code_review_scoring: + description: > + Com base nas mudanças de código fornecidas: "{input}", + + Avalie criteriosamente os seguintes aspectos: + 1. Qualidade do Código (0-2.5 pontos): Legibilidade, clareza, nomenclatura, organização + 2. Arquitetura e Design (0-2.5 pontos): Aderência a SOLID, padrões de projeto, separação de responsabilidades + 3. Boas Práticas (0-2.5 pontos): Tratamento de erros, segurança, performance, testes + 4. Impacto e Complexidade (0-2.5 pontos): Tamanho da mudança, complexidade ciclomática, risco de bugs + + Considere também: + - Consistência com o código existente + - Documentação e comentários quando necessários + - Potenciais problemas de manutenibilidade + - Code smells e anti-patterns + - Eficiência e otimização + + Seja rigoroso mas justo. Uma nota 10.0 deve ser excepcional, não comum. + Notas entre 7.0-8.5 são boas, 8.5-9.5 são muito boas, 9.5-10.0 são excepcionais. + Abaixo de 7.0 indica necessidade de melhorias significativas. + + A resposta deve ser **obrigatoriamente em Português do Brasil**. + + No tópico Justificativa Detalhada, informe o fim do mesmo em uma linha única após todo seu conteúdo (Fim Justificativa Detalhada). + + expected_output: > + Relatório de avaliação em markdown contendo: + - Nome do desenvolvedor e email + - Nota final (0-10.0) + - Breakdown de pontos por categoria + - **Justificativa Detalhada** e técnica para a nota + - Pontos fortes identificados + - Pontos de melhoria com exemplos específicos + - Recomendações práticas e acionáveis + agent: code_reviewer \ No newline at end of file diff --git a/codewise_lib/crew.py b/codewise_lib/crew.py index a9c0083..535a9cb 100644 --- a/codewise_lib/crew.py +++ b/codewise_lib/crew.py @@ -5,25 +5,34 @@ from dotenv import load_dotenv from crewai import Agent, Crew, Process, Task, LLM from crewai.project import CrewBase, agent, crew, task +from .select_llm import create_llm + +from crewai_tools import ( + WebsiteSearchTool +) + @CrewBase class Codewise: """Classe principal da crew Codewise""" def __init__(self, commit_message: str = ""): + """ + Inicializa a crew Codewise com agentes e configurações. + + Args: + commit_message: Mensagem de commit ou contexto para análise (opcional) + """ load_dotenv() + #configurações iniciais da llm e agentes self.commit_message = commit_message - if not os.getenv("GEMINI_API_KEY"): - print("Erro: A variável de ambiente GEMINI_API_KEY não foi definida.") - sys.exit(1) - try: - self.llm = LLM( - model="gemini/gemini-2.0-flash", - temperature=0.7 - ) - except Exception as e: - print(f"Erro ao inicializar o LLM. Verifique sua chave de API e dependências. Erro: {e}") - sys.exit(1) + self.provider = os.getenv("AI_PROVIDER").upper() + self.model = os.getenv("AI_MODEL") + self.llm = create_llm(self.provider,self.model) + + #tools iniciais + self.web_search_tool = WebsiteSearchTool() + #carregamento de configurações de agentes e tarefas base_dir = os.path.dirname(os.path.abspath(__file__)) config_path = os.path.join(base_dir, "config") agents_path = os.path.join(config_path, "agents.yaml") @@ -38,6 +47,7 @@ def __init__(self, commit_message: str = ""): print(f"Erro: Arquivo de configuração não encontrado: {e}") sys.exit(1) + #definição dos agentes disponíveis @agent def senior_architect(self) -> Agent: return Agent(config=self.agents_config['senior_architect'], llm=self.llm, verbose=False) @agent @@ -48,7 +58,19 @@ def quality_consultant(self) -> Agent: return Agent(config=self.agents_config['q def quality_control_manager(self) -> Agent: return Agent(config=self.agents_config['quality_control_manager'], llm=self.llm, verbose=False) @agent def summary_specialist(self) -> Agent: return Agent(config=self.agents_config['summary_specialist'], llm=self.llm, verbose=False) + @agent + def code_mentor(self) -> Agent: return Agent(config=self.agents_config['code_mentor'], llm=self.llm, verbose=False) + + @agent + def dataCollect_policy_analytics(self) -> Agent: return Agent(config=self.agents_config['dataCollect_policy_analytics'], llm=self.llm, tools=[self.web_search_tool], verbose=False) + + @agent + def lgpd_judge(self) -> Agent: return Agent(config=self.agents_config['lgpd_judge'], llm=self.llm, tools=[self.web_search_tool], verbose = False) + @agent + def code_reviewer(self) -> Agent: return Agent(config=self.agents_config['code_reviewer'], llm=self.llm, verbose=False) + + #definição das tarefas disponíveis para cada agente @task def task_estrutura(self) -> Task: cfg = self.tasks_config['analise_estrutura'] @@ -69,18 +91,78 @@ def task_padroes(self) -> Task: def task_summarize(self) -> Task: cfg = self.tasks_config['summarize_analysis'] return Task(description=cfg['description'], expected_output=cfg['expected_output'], agent=self.summary_specialist()) + + @task + def task_mentoring(self) -> Task: + cfg = self.tasks_config['mentoring_task'] + return Task(description=cfg['description'], expected_output=cfg['expected_output'], agent=self.code_mentor()) + + @task + def task_policy(self) -> Task: + cfg = self.tasks_config['policy_analytics'] + formatted_description = cfg['description'].format( + IA_PROVIDER=self.provider, + IA_MODEL=self.model + ) + + return Task(description=formatted_description, expected_output=cfg['expected_output'], agent=self.dataCollect_policy_analytics()) + + @task + def task_judging(self) -> Task: + cfg = self.tasks_config['lgpd_judging'] + return Task(description=cfg['description'], expected_output=cfg['expected_output'], agent=self.lgpd_judge(), context=[self.task_policy()]) + + @task + def task_code_review(self) -> Task: + cfg = self.tasks_config['code_review_scoring'] + return Task(description=cfg['description'], expected_output=cfg['expected_output'], agent=self.code_reviewer()) + + + #definição da crew principal @crew def crew(self) -> Crew: return Crew( - agents=[self.senior_architect(), self.senior_analytics(), self.quality_consultant(), self.quality_control_manager()], - tasks=[self.task_estrutura(), self.task_heuristicas(), self.task_solid(), self.task_padroes()], + agents=[self.senior_architect(), self.senior_analytics(), self.quality_consultant(), self.quality_control_manager(),self.code_mentor()], + tasks=[self.task_estrutura(), self.task_heuristicas(), self.task_solid(), self.task_padroes(),self.task_mentoring()], process=Process.sequential ) def summary_crew(self) -> Crew: + """ + Cria uma crew especializada em gerar resumos executivos. + + Returns: + Crew: Instância da crew de resumo + """ return Crew( agents=[self.summary_specialist()], tasks=[self.task_summarize()], process=Process.sequential + ) + + def lgpd_crew(self) -> Crew: + """ + Cria uma crew especializada em análise de conformidade com a LGPD. + + Returns: + Crew: Instância da crew de análise LGPD + """ + return Crew( + agents=[self.dataCollect_policy_analytics(), self.lgpd_judge()], + tasks=[self.task_policy(), self.task_judging()], + process=Process.sequential + ) + + def code_review_crew(self) -> Crew: + """ + Cria uma crew especializada em revisão e avaliação de código. + + Returns: + Crew: Instância da crew de code review + """ + return Crew( + agents=[self.code_reviewer()], + tasks=[self.task_code_review()], + process=Process.sequential ) \ No newline at end of file diff --git a/codewise_lib/cw_runner.py b/codewise_lib/cw_runner.py index 68b5cd0..e18be91 100644 --- a/codewise_lib/cw_runner.py +++ b/codewise_lib/cw_runner.py @@ -1,22 +1,53 @@ import os import sys +import re from .crew import Codewise from .entradagit import gerar_entrada_automatica, obter_mudancas_staged from crewai import Task, Crew +from .lgpd import * +from .code_reviewer import coletar_dados_git +from .notificacao_gestor import processar_avaliacao_e_notificar + class CodewiseRunner: + """ + Classe responsável por organizar a execução das análises do CodeWise. + Gerencia diferentes modos de operação (lint, titulo, descricao, analise, lgpd_verify). + """ def __init__(self): + """ + Inicializa o CodewiseRunner com os caminhos necessários. + """ self.BASE_DIR = os.path.dirname(os.path.abspath(__file__)) self.caminho_entrada = os.path.join(self.BASE_DIR, ".entrada_temp.txt") def executar(self, caminho_repo: str, nome_branch: str, modo: str): - contexto_para_ia = "" + """ + Executa a análise de código no modo especificado. + Args: + caminho_repo: Caminho para o repositório Git + nome_branch: Nome da branch a ser analisada + modo: Modo de operação ('lint', 'titulo', 'descricao', 'analise', 'lgpd_verify') + """ + + caminho_dir_lgpd = os.path.join(caminho_repo, "analises-julgamento-lgpd") + policy_file_path = os.path.join(caminho_dir_lgpd, "analise_politica_coleta_de_dados.md") + lgpd_judge_file_path = os.path.join(caminho_dir_lgpd, "julgamento_lgpd.md") + + + if(modo == 'lgpd_verify'): + if not(verifica_se_existe_analise_lgpd(policy_file_path, lgpd_judge_file_path)): + print("Iniciando análise e julgamento LGPD...",file=sys.stderr) + verify_lgpd(caminho_dir_lgpd, policy_file_path, lgpd_judge_file_path) + return 0 + + contexto_para_ia = "" if modo == 'lint': resultado_git = obter_mudancas_staged(caminho_repo) if resultado_git is None: - print("Nenhum problema aparente detectado.") + print("Nenhum problema aparente detectado.",file=sys.stderr) sys.exit(0) if resultado_git.startswith("AVISO:") or resultado_git.startswith("FALHA:"): @@ -32,16 +63,19 @@ def executar(self, caminho_repo: str, nome_branch: str, modo: str): codewise_instance = Codewise(commit_message=contexto_para_ia) resultado_final = "" + if modo == 'titulo': agent = codewise_instance.summary_specialist() task = Task(description=f"Crie um título de PR conciso no padrão Conventional Commits para as seguintes mudanças. A resposta deve ser APENAS o título, **obrigatoriamente em Português do Brasil**, sem aspas, acentos graves ou qualquer outro texto:\n{contexto_para_ia}", expected_output="Um único título de PR.", agent=agent) resultado_final = Crew(agents=[agent], tasks=[task]).kickoff() + elif modo == 'descricao': agent = codewise_instance.summary_specialist() task = Task(description=f"Crie uma descrição de um parágrafo **obrigatoriamente em Português do Brasil** para um Pull Request para as seguintes mudanças:\n{contexto_para_ia}", expected_output="Um único parágrafo de texto.", agent=agent) resultado_final = Crew(agents=[agent], tasks=[task]).kickoff() + elif modo == 'analise': analysis_crew = codewise_instance.crew() analysis_crew.kickoff(inputs={'input': contexto_para_ia}) @@ -83,6 +117,62 @@ def executar(self, caminho_repo: str, nome_branch: str, modo: str): ) resultado_final = Crew(agents=[resumo_agent], tasks=[resumo_task]).kickoff() + mentor_agent = codewise_instance.code_mentor() + mentor_task = Task( + description="Com base nas análises técnicas realizadas, comente e sugira recursos educacionais personalizados com base nas mudanças **obrigatoriamente em Português do Brasil**, bem formatado em markdown, com links que possuam conteúdo para melhorar o código.", + expected_output="Sugestões de melhoria.", + agent=mentor_agent, + context=analysis_crew.tasks + ) + resultado_mentor = Crew(agents=[mentor_agent],tasks=[mentor_task]).kickoff() + mentor_file_path = os.path.join(output_dir_path, "sugestoes_aprendizado.md") + try: + with open(mentor_file_path, "w", encoding="utf-8") as f: + f.write(str(resultado_mentor)) + print(f" - Arquivo 'sugestoes_aprendizado.md' salvo com sucesso em '{output_dir_path}'.", file=sys.stderr) + except Exception as e: + print(f" - ERRO ao salvar o arquivo 'sugestoes_aprendizado.md': {e}", file=sys.stderr) + + + #avaliação de código e notificação para o gestor + print("\n🔍 Gerando avaliação de código...", file=sys.stderr) + #coleta de dados git para análise + try: + dados_git = coletar_dados_git(caminho_repo, commits_limit=3) + + if "Erro" not in dados_git: + code_review_crew = codewise_instance.code_review_crew() + + code_review_crew.kickoff(inputs={'input': dados_git}) + + resultado_review = code_review_crew.tasks[0].output + review_file_path = os.path.join(output_dir_path, "avaliacao_codigo.md") + + with open(review_file_path, "w", encoding="utf-8") as f: + f.write(str(resultado_review)) + + print(f" - Arquivo 'avaliacao_codigo.md' salvo com sucesso.", file=sys.stderr) + + #obtenção do email do desenvolvedor + try: + import subprocess + email_dev = subprocess.check_output( + ['git', '-C', caminho_repo, 'config', 'user.email'], + text=True + ).strip() + except: + email_dev = "desconhecido" + + print("\n📤 Enviando avaliação para o gestor...", file=sys.stderr) + processar_avaliacao_e_notificar(review_file_path, email_dev, caminho_repo) + + else: + print(f" - Aviso: {dados_git}", file=sys.stderr) + + except Exception as e: + print(f" - Aviso: Não foi possível gerar avaliação de código: {str(e)}", file=sys.stderr) + + elif modo == 'lint': agent = codewise_instance.quality_consultant() task = Task(description=f"Analise rapidamente as seguintes mudanças de código ('git diff') e aponte APENAS problemas óbvios ou code smells. A resposta deve ser **obrigatoriamente em Português do Brasil**. Seja conciso. Se não houver problemas, retorne 'Nenhum problema aparente detectado.'.\n\nCódigo a ser analisado:\n{contexto_para_ia}", expected_output="Uma lista curta em bullet points com sugestões, ou uma mensagem de que está tudo ok.", agent=agent) @@ -93,7 +183,17 @@ def executar(self, caminho_repo: str, nome_branch: str, modo: str): print(str(resultado_final).strip().replace('`', '')) + def _ler_arquivo(self, file_path: str) -> str: + """ + Lê o conteúdo de um arquivo de texto. + + Args: + file_path: Caminho do arquivo a ser lido + + Returns: + str: Conteúdo do arquivo ou string vazia se não encontrado + """ try: with open(file_path, "r", encoding="utf-8") as f: return f.read() - except FileNotFoundError: return "" \ No newline at end of file + except FileNotFoundError: return "" diff --git a/codewise_lib/entradagit.py b/codewise_lib/entradagit.py index d2a7bd2..024efbf 100644 --- a/codewise_lib/entradagit.py +++ b/codewise_lib/entradagit.py @@ -3,42 +3,61 @@ import sys def run_git_command(command, repo_path): - """Função auxiliar para executar comandos Git e capturar a saída.""" + """ + Função auxiliar para executar comandos Git e capturar a saída. + + Args: + command: Lista com o comando Git e seus argumentos + repo_path: Caminho do repositório onde executar o comando + + Returns: + str ou None: Saída do comando se tiver ok, string vazia para erros não fatais, None para erros fatais + """ try: - # Usamos repo_path como o diretório de trabalho para o comando + #se não houver erro, retorna a saída do comando normal result = subprocess.check_output(command, cwd=repo_path, text=True, encoding='utf-8', stderr=subprocess.PIPE) return result.strip() except subprocess.CalledProcessError as e: - # Se o comando falhar, mas o erro não for fatal, imprime no stderr e continua + #se for um erro não fatal, como branch inexistente, retorna string vazia para tratar depois e não quebrar o fluxo if e.stderr: print(f"Aviso do Git: {e.stderr.strip()}", file=sys.stderr) - return "" # Retorna vazio para erros não fatais (ex: branch não encontrada no remote) + return "" except FileNotFoundError: + #erros fatais, como o git não estar instalado ou coisas do tipo print("ERRO: O executável 'git' não foi encontrado. Verifique se o Git está instalado e no PATH.", file=sys.stderr) - return None # Retorna None para erros fatais + return None def gerar_entrada_automatica(caminho_repo, caminho_saida, nome_branch): + """ + Gera automaticamente o arquivo de entrada com commits e diffs para análise. + + Args: + caminho_repo: Caminho para o repositório Git + caminho_saida: Caminho onde salvar o arquivo de entrada gerado + nome_branch: Nome da branch a ser analisada + + Returns: + bool: True se gerado com sucesso, False caso contrário + """ try: - # 1. Busca atualizações do repositório remoto + #busca alguma alteração que tiver na branch remota print("🔄 Buscando atualizações do repositório remoto...", file=sys.stderr) run_git_command(["git", "fetch", "origin", "--prune"], caminho_repo) - # 2. Define a branch base para comparação + #define a branch remota a ser comparada já verificando se existe branch_remota_str = f'origin/{nome_branch}' - # Verifica se a branch existe no remote remote_branch_exists = run_git_command(["git", "show-ref", "--verify", f"refs/remotes/{branch_remota_str}"], caminho_repo) - default_branch_name = "main" # Assume 'main' como padrão + default_branch_name = "main" base_ref_str = f'origin/{default_branch_name}' if remote_branch_exists: - # Se a branch já existe, a base é ela mesma no remote base_ref_str = branch_remota_str print(f"✅ Branch '{nome_branch}' já existe no remote. Analisando novos commits desde o último push.", file=sys.stderr) else: print(f"✅ Branch '{nome_branch}' é nova. Comparando com a branch principal remota ('{default_branch_name}').", file=sys.stderr) - # 3. Pega a lista de mensagens de commit + #pega a lista de commits range_commits = f"{base_ref_str}..{nome_branch}" log_commits = run_git_command(["git", "log", "--pretty=format:- %s", range_commits], caminho_repo) @@ -48,10 +67,10 @@ def gerar_entrada_automatica(caminho_repo, caminho_saida, nome_branch): commits_pendentes = log_commits.splitlines() - # 4. Gera o diff incremental + #pega o diff completo dos commits diff_completo = run_git_command(["git", "diff", f"{base_ref_str}..{nome_branch}"], caminho_repo) - # 5. Monta o texto final + #monta o texto final para o arquivo de entrada entrada = [f"Analisando {len(commits_pendentes)} novo(s) commit(s).\n\nMensagens de commit:\n"] entrada.extend(commits_pendentes) entrada.append(f"\n{'='*80}\nDiferenças de código consolidadas a serem analisadas:\n{diff_completo}") @@ -64,19 +83,27 @@ def gerar_entrada_automatica(caminho_repo, caminho_saida, nome_branch): return False def obter_mudancas_staged(repo_path="."): - """Verifica o estado do repositório para o modo lint usando subprocess.""" + """ + Verifica o estado do repositório para o modo lint. + + Args: + repo_path: Caminho para o repositório Git (padrão: diretório atual) + + Returns: + str ou None: Diff das mudanças staged, mensagem de aviso, ou None se não houver mudanças + """ try: - # 1. Verifica a 'staging area' + #verifica a área de stage diff_staged = run_git_command(["git", "diff", "--cached"], repo_path) if diff_staged: return diff_staged - # 2. Se o stage está limpo, verifica o 'working directory' + #se não tiver nada na staging area, verifica se tem mudanças no working dir diff_working_dir = run_git_command(["git", "diff"], repo_path) if diff_working_dir: return "AVISO: Nenhuma mudança na 'staging area', mas existem modificações não adicionadas.\nUse 'git add ' para prepará-las para a análise." - # 3. Se ambos estiverem limpos, retorna None + #se ambos estiverem limpos, retorna None return None except Exception as e: print(f"Erro em 'entradagit.py' ao obter staged changes: {e}", file=sys.stderr) diff --git a/codewise_lib/lgpd.py b/codewise_lib/lgpd.py new file mode 100644 index 0000000..6cd0dfd --- /dev/null +++ b/codewise_lib/lgpd.py @@ -0,0 +1,125 @@ +import os +import sys +import re +from dotenv import load_dotenv +from .crew import Codewise + +def verify_lgpd(caminho_dir_lgpd: str, policy_file_path: str, lgpd_judge_file_path: str) -> bool: + """ + Executa a verificação de conformidade LGPD do provedor de IA definido no .env. + + Args: + caminho_dir_lgpd: Diretório para salvar as análises LGPD + policy_file_path: Caminho do arquivo de análise de política + lgpd_judge_file_path: Caminho do arquivo de julgamento LGPD + + Returns: + bool: True se aprovado, False caso contrário + """ + # Tentativa de colocar a analise lgpd para rodar antes do envio dos dados sensiveis + # instancia sem passar o commit como contexto + codewise_instance = Codewise() + + print(f"Verificando a política de coleta de dados do provedor com base neste modelo de api key...") + + #lgpd crew + lgpd_check_crew = codewise_instance.lgpd_crew() + + # roda a analise e o julgamento lgpd (todo o time) + lgpd_check_crew.kickoff() + + # cria directory + os.makedirs(caminho_dir_lgpd, exist_ok=True) + + # salvando o resultado da analise da politica de coleta de dados + resultado_policy = lgpd_check_crew.tasks[0].output + + try: + with open(policy_file_path, "w", encoding="utf-8") as f: + f.write(str(resultado_policy)) + print(f"Arquivo 'analise_politica_coleta_de_dados.md' salvo com sucesso em '{caminho_dir_lgpd}'.", file=sys.stderr) + except FileNotFoundError as e: + print(f"❌ ERRO - Arquivo 'analise_politica_coleta_de_dados.md' não existe: {e}", file=sys.stderr) + except Exception as e: + print(f"❌ ERRO - Ao salvar o arquivo 'analise_politica_coleta_de_dados.md': {e}", file=sys.stderr) + + resultado_lgpd = lgpd_check_crew.tasks[1].output + + try: + with open(lgpd_judge_file_path, "w", encoding="utf-8") as fj: + fj.write(str(resultado_lgpd)) + print(f"✅ Arquivo 'julgamento_lgpd.md' salvo com sucesso em '{caminho_dir_lgpd}'.", file=sys.stderr) + except FileNotFoundError as e: + print(f"❌ ERRO - Arquivo 'julgamento_lgpd.md' não existe: {e}", file=sys.stderr) + except Exception as e: + print(f"❌ ERRO - Ao salvar o arquivo 'julgamento_lgpd.md': {e}", file=sys.stderr) + + #retorna o resultado do julgamento + return verify_result_judgement(lgpd_judge_file_path) + +def verify_result_judgement(lgpd_judge_file_path) -> bool: + """ + Verifica o resultado do julgamento LGPD no arquivo gerado. + + Args: + lgpd_judge_file_path: Caminho do arquivo de julgamento LGPD + + Returns: + bool: True se aprovado ('sim'), False se reprovado ('não') + """ + try: + with open(lgpd_judge_file_path, "r", encoding="utf-8") as julgamento: + + for linha in julgamento: + linha_clean = linha.strip().lower() + + linha_clean = re.sub(r'[*_#>`~]', '', linha_clean).strip() + + linha_clean = linha_clean.strip() + + if(linha_clean == "sim"): + return True + if (linha_clean == "não"): + return False + except FileNotFoundError as e: + print(f"❌ ERRO - Arquivo 'julgamento_lgpd.md' não existe: {e}", file=sys.stderr) + except Exception as e: + print(f"❌ ERRO - ao ler o arquivo 'julgamento_lgpd.md': {e}", file=sys.stderr) + +def verifica_se_existe_analise_lgpd(policy_file_path, lgpd_judge_file_path) -> bool: + """ + Verifica se já existe uma análise LGPD para o provedor e modelo atuais. + + Args: + policy_file_path: Caminho do arquivo de análise de política + lgpd_judge_file_path: Caminho do arquivo de julgamento LGPD + + Returns: + bool: True se análise já existe para o provedor/modelo atual, False caso contrário + """ + + provider = os.getenv("AI_PROVIDER").lower() + model = os.getenv("AI_MODEL").lower() + model = re.sub(r'[*_#>`~]', '', model) + + provider_model = provider + model + + if(os.path.exists(policy_file_path) and os.path.exists(lgpd_judge_file_path)): + try: + with open(policy_file_path, "r", encoding="utf-8") as f: + for linha in f: + linha_clean = linha.strip().lower() + + linha_clean = re.sub(r'[*_#>`~]', '', linha_clean).strip() + + linha_clean = linha_clean.strip() + if(linha_clean == provider_model): + return True + if(not linha): + return False + except FileNotFoundError as e: + print(f"❌ ERRO - Arquivo 'analise_politica_coleta_de_dados.md' não existe: {e}", file=sys.stderr) + except Exception as e: + print(f"❌ ERRO - ao ler o arquivo 'analise_politica_coleta_de_dados.md': {e}", file=sys.stderr) + else: + return False \ No newline at end of file diff --git a/codewise_lib/main.py b/codewise_lib/main.py index 3df954b..8178db0 100644 --- a/codewise_lib/main.py +++ b/codewise_lib/main.py @@ -1,6 +1,5 @@ -import argparse -# CORREÇÃO: Adicionado o '.' para indicar uma importação relativa dentro do pacote. from .cw_runner import CodewiseRunner +import argparse def main(): parser = argparse.ArgumentParser(description="Code Wise - Ferramenta de Análise de Código com IA.") @@ -10,11 +9,11 @@ def main(): "--mode", type=str, required=True, - choices=['descricao', 'analise', 'titulo', 'lint'], + choices=['descricao', 'analise', 'titulo', 'lint', 'lgpd_verify'], help="Modo de operação." ) args = parser.parse_args() - + runner = CodewiseRunner() runner.executar( caminho_repo=args.repo, diff --git a/codewise_lib/notificacao_gestor.py b/codewise_lib/notificacao_gestor.py new file mode 100644 index 0000000..58f15d6 --- /dev/null +++ b/codewise_lib/notificacao_gestor.py @@ -0,0 +1,140 @@ +import os +import sys +import re +import requests +from datetime import datetime +from dotenv import load_dotenv + +load_dotenv() + + +def enviar_telegram(mensagem: str) -> bool: + """ + Envia uma mensagem via Telegram Bot API. + + Args: + mensagem: Texto da mensagem que será enviada + + Returns: + bool: True se enviado, False caso contrário + """ + telegram_token = os.getenv("TELEGRAM_BOT_TOKEN") + telegram_chat_id = os.getenv("TELEGRAM_CHAT_ID") + + if not telegram_token or not telegram_chat_id: + print("⚠️ TELEGRAM_BOT_TOKEN ou TELEGRAM_CHAT_ID não configurados no .env", file=sys.stderr) + return False + + try: + url = f"https://api.telegram.org/bot{telegram_token}/sendMessage" + + payload = { + "chat_id": telegram_chat_id, + "text": mensagem, + "parse_mode": "Markdown" + } + + response = requests.post(url, json=payload, timeout=10) + + if response.status_code == 200: + print("✅ Notificação enviada via Telegram", file=sys.stderr) + return True + else: + print(f"⚠️ Erro ao enviar Telegram: {response.status_code}", file=sys.stderr) + return False + + except Exception as e: + print(f"⚠️ Erro ao conectar com Telegram: {str(e)}", file=sys.stderr) + return False + + +def processar_avaliacao_e_notificar(caminho_arquivo: str, email_dev: str, repo_path: str) -> bool: + """ + Processa o arquivo de avaliação de código e envia notificação ao gestor. + + Args: + caminho_arquivo: Caminho do arquivo de avaliação gerado + email_dev: Email do desenvolvedor avaliado + repo_path: Caminho do repositório Git + + Returns: + bool: True se notificação for enviada com sucesso, False caso contrário + """ + try: + nota = 0.0 + justificativa_linhas = [] + capturando_breakdown = False + + with open(caminho_arquivo, 'r', encoding='utf-8') as f: + for linha in f: + linha_clean = linha.strip() + + #varre o arquivo gerado procurando a nota final + if 'nota final' in linha_clean.lower(): + linha_limpa = re.sub(r'[*_#>`~]', '', linha_clean) + linha_limpa = linha_limpa.strip() + + #pega a nota + nota_match = re.search(r'(\d+\.?\d*)', linha_limpa) + if nota_match: + nota = float(nota_match.group(1)) + print(f" ✓ Nota encontrada: {nota}/10", file=sys.stderr) + + #varre o arquivo procurando o breakdown de pontos + if 'breakdown de pontos' in linha_clean.lower(): + capturando_breakdown = True + continue + + # Para de capturar quando encontrar "Justificativa detalhada" + if capturando_breakdown and 'fim justificativa' in linha_clean.lower(): + break + + if capturando_breakdown: + # Remove caracteres especiais + linha_limpa = re.sub(r'[*_#>`~]', '', linha_clean) + linha_limpa = linha_limpa.strip() + + if linha_limpa: + justificativa_linhas.append(linha_limpa) + + #justificativa final da nota + justificativa = '\n'.join(justificativa_linhas) + + if len(justificativa) > 4000: + justificativa = justificativa[:3997] + "..." + + if justificativa: + print(f" ✓ Justificativa extraída ({len(justificativa)} chars)", file=sys.stderr) + else: + print(f" ⚠️ Justificativa não encontrada", file=sys.stderr) + justificativa = "Avaliação concluída." + + #nome do repositório + repo_nome = os.path.basename(repo_path) + + #visual para a nota + emoji_nota = "🟢" if nota >= 8.5 else "🟡" if nota >= 7.0 else "🔴" + + justificativa_escaped = justificativa.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace('`', '\\`') + + mensagem = f""" +{emoji_nota} *Nova Avaliação de Código* + +👤 *Desenvolvedor:* {email_dev} +📦 *Repositório:* {repo_nome} +📊 *Nota:* {nota}/10 + +📝 *Resumo:* +{justificativa_escaped} + +📅 *Data:* {datetime.now().strftime("%d/%m/%Y %H:%M")} +""" + + #faz o envio pro telegram já formatado + telegram_ok = enviar_telegram(mensagem) + + return telegram_ok + + except Exception as e: + print(f"⚠️ Erro ao processar avaliação: {str(e)}", file=sys.stderr) + return False diff --git a/codewise_lib/select_llm.py b/codewise_lib/select_llm.py new file mode 100644 index 0000000..a1b8c44 --- /dev/null +++ b/codewise_lib/select_llm.py @@ -0,0 +1,66 @@ +import os +from crewai import LLM +import sys + +def create_llm(provider:str, model:str)-> LLM: + """ + Cria e configura uma instância de LLM baseada no provedor especificado. + + Args: + provider: Nome do provedor ('GEMINI', 'OPENAI', 'GROQ', 'COHERE') + model: Nome do modelo a ser utilizado + + Returns: + LLM: Instância configurada do modelo de linguagem + + Raises: + SystemExit: Se a API key não estiver configurada ou houver erro na inicialização + """ + if provider == "GEMINI": + if not os.getenv("GEMINI_API_KEY"): + print("Erro: A variável de ambiente GEMINI_API_KEY não foi definida.") + sys.exit(1) + try: + return LLM( + model= "gemini/" + model, + temperature=0.7 + ) + except Exception as e: + print(f"Erro ao inicializar o LLM. Verifique sua chave de API e dependências. Erro: {e}") + sys.exit(1) + elif provider == "OPENAI": + if not os.getenv("OPENAI_API_KEY"): + print("Erro: A variável de ambiente OPENAI_API_KEY não foi definida.") + sys.exit(1) + try: + return LLM( + model= "openai/" + model, + temperature=0.7, + ) + except Exception as e: + print(f"Erro ao inicializar o LLM. Verifique sua chave de API e dependências. Erro: {e}") + sys.exit(1) + elif provider == "GROQ": + if not os.getenv("GROQ_API_KEY"): + print("Erro: A variável de ambiente OPENAI_API_KEY não foi definida.") + sys.exit(1) + try: + return LLM( + model= "groq/" + model, + temperature=0.7, + ) + except Exception as e: + print(f"Erro ao inicializar o LLM. Verifique sua chave de API e dependências. Erro: {e}") + sys.exit(1) + elif provider == "COHERE": + if not os.getenv("COHERE_API_KEY"): + print("Erro: A variável de ambiente COHERE_API_KEY não foi definida.") + sys.exit(1) + try: + return LLM( + model= "cohere_chat/" + model, + temperature=0.7, + ) + except Exception as e: + print(f"Erro ao inicializar o LLM. Verifique sua chave de API e dependências. Erro: {e}") + sys.exit(1) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 23565c2..66d22b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -crewai -python-dotenv -pyyaml -google-generativeai -langchain-google-genai \ No newline at end of file +crewai==0.201.1 +crewai-tools==0.76.0 +python-dotenv==1.1.1 +PyYAML==6.0.3 +litellm==1.74.9 +qdrant-client==1.15.1 +requests==2.32.3 diff --git a/scripts/codewise_review_win.py b/scripts/codewise_review_win.py index 85f4096..ff6ac63 100644 --- a/scripts/codewise_review_win.py +++ b/scripts/codewise_review_win.py @@ -36,6 +36,10 @@ def run_codewise_mode(mode, repo_path, branch_name): env=env, stdin=subprocess.DEVNULL ) + + #if result.stderr: + #print(result.stderr, file=sys.stderr) + return result.stdout.strip() except subprocess.CalledProcessError as e: error_output = e.stderr or "" @@ -43,14 +47,14 @@ def run_codewise_mode(mode, repo_path, branch_name): # Imprime a mensagem amigável e formatada print(""" ================================================================ - ❌ ERRO: Limite de Uso da API do Google Atingido (Erro 429) + ❌ ERRO: Limite de Uso da API Atingido (Erro 429) ================================================================ A sua chave de API atingiu o limite máximo de requisições - permitido pelo plano gratuito do Google Gemini. + permitido pelo plano selecionado do modelo. Isso não é um bug no CodeWise, mas uma limitação da sua conta - na plataforma do Google. + na plataforma do provedor. . Aguarde: A cota do plano gratuito geralmente é renovada a cada 24 horas. Você pode tentar novamente amanhã. @@ -64,6 +68,15 @@ def run_codewise_mode(mode, repo_path, branch_name): return None def obter_branch_padrao_remota(repo_path): + """ + Obtém o nome da branch padrão do repositório remoto no GitHub. + + Args: + repo_path: Caminho para o repositório Git local + + Returns: + str: Nome da branch padrão (ex: 'main', 'master') ou 'main' como fallback + """ try: remote_url_result = subprocess.check_output(["git", "config", "--get", "remote.origin.url"], cwd=repo_path, text=True, encoding='utf-8').strip() match = re.search(r'github\.com/([^/]+/[^/]+?)(\.git)?$', remote_url_result) @@ -80,11 +93,31 @@ def obter_branch_padrao_remota(repo_path): return "main" def extrair_titulo_valido(texto): + """ + Extrai um título válido no formato Conventional Commits de um texto. + + Args: + texto: Texto contendo o título do commit + + Returns: + str ou None: Título extraído se encontrado (str), None caso contrário + """ match = re.search(r"(feat|fix|refactor|docs):\s.+", texto, re.IGNORECASE) if match: return match.group(0).strip() return None def obter_pr_aberto_para_branch(branch, repo_dir, repo_slug): + """ + Verifica se existe um Pull Request aberto para uma branch específica. + + Args: + branch: Nome da branch a verificar + repo_dir: Diretório do repositório local + repo_slug: Identificador do repositório no formato 'usuario/repo' + + Returns: + int ou None: Número do PR se encontrado (int), None caso contrário + """ try: comando_list = [ "gh", "pr", "list", @@ -102,7 +135,16 @@ def obter_pr_aberto_para_branch(branch, repo_dir, repo_slug): return None def obter_repo_slug(remote_name, repo_path): - """Obtém o slug 'usuario/repo' de um remote específico ('origin' ou 'upstream').""" + """ + Obtém o slug 'usuario/repo' de um remote específico do GitHub. + + Args: + remote_name: Nome do remote ('origin' ou 'upstream') + repo_path: Caminho para o repositório Git local + + Returns: + str ou None: Slug no formato 'usuario/repo' se encontrado (str), None caso contrário + """ try: remote_url = subprocess.check_output( ["git", "config", "--get", f"remote.{remote_name}.url"], @@ -116,7 +158,16 @@ def obter_repo_slug(remote_name, repo_path): return None def verificar_remote_existe(remote_name, repo_path): - """Verifica se um remote com o nome especificado existe.""" + """ + Verifica se um remote com o nome especificado existe no repositório. + + Args: + remote_name: Nome do remote a verificar (ex: 'origin', 'upstream') + repo_path: Caminho para o repositório Git local + + Returns: + bool: True se o remote existe, False caso contrário + """ try: remotes = subprocess.check_output(["git", "remote"], cwd=repo_path, text=True, encoding='utf-8') return remote_name in remotes.split() @@ -127,12 +178,18 @@ def verificar_remote_existe(remote_name, repo_path): # LÓGICA DO COMANDO 'codewise-lint' (PARA PRE-COMMIT) # =================================================================== def main_lint(): + """ + Executa análise rápida pré-commit do código staged. + """ os.environ['PYTHONIOENCODING'] = 'utf-8' repo_path = os.getcwd() try: current_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], encoding='utf-8', cwd=repo_path).strip() except Exception: current_branch = "" + + # Chamando função que pergunta ao usuário se ele gostaria de continuar (enviar os dados para provedor ou não) + lgpd_check_user_choice(repo_path, current_branch) print("--- 🔍 Executando análise rápida pré-commit do CodeWise ---", file=sys.stderr) sugestoes = run_codewise_mode("lint", repo_path, current_branch) @@ -161,8 +218,13 @@ def main_lint(): # =================================================================== def run_pr_logic(target_selecionado, pushed_branch): - """Função principal que contém toda a lógica de criação de PR.""" - + """ + Função principal que contém toda a lógica de criação de Pull Request. + + Args: + target_selecionado: Nome do remote alvo ('origin' ou 'upstream') + pushed_branch: Nome da branch que está sendo enviada + """ current_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True).strip() @@ -178,6 +240,9 @@ def run_pr_logic(target_selecionado, pushed_branch): os.environ['PYTHONIOENCODING'] = 'utf-8' repo_path = os.getcwd() + # Chamando função que pergunta ao usuário se ele gostaria de continuar (enviar os dados para provedor ou não) + lgpd_check_user_choice(repo_path, current_branch) + upstream_existe = verificar_remote_existe('upstream', repo_path) upstream_renomeado = False @@ -301,7 +366,9 @@ def run_pr_logic(target_selecionado, pushed_branch): def main_pr_origin(): - """Ponto de entrada para criar um PR no 'origin'.""" + """ + Ponto de entrada para criar um PR no 'origin'. + """ parser = argparse.ArgumentParser() parser.add_argument("--pushed-branch", required=False, type=str, help="A branch que está sendo enviada.") args = parser.parse_args() @@ -319,7 +386,9 @@ def main_pr_origin(): def main_pr_upstream(): - """Ponto de entrada para criar um PR no 'upstream'.""" + """ + Ponto de entrada para criar um PR no 'upstream'. + """ parser = argparse.ArgumentParser() parser.add_argument("--pushed-branch", required=False, type=str, help="A branch que está sendo enviada.") args = parser.parse_args() @@ -338,7 +407,9 @@ def main_pr_upstream(): def main_pr_interactive(): - """Função interativa para ser chamada manualmente pelo comando 'codewise-pr'.""" + """ + Função interativa para ser chamada manualmente pelo comando 'codewise-pr'. + """ repo_path = os.getcwd() try: @@ -372,3 +443,90 @@ def main_pr_interactive(): sys.exit(f"❌ Erro ao detectar a branch Git atual: {e}") run_pr_logic(target_selecionado=target_selecionado, pushed_branch=current_branch) + + +# =================================================================== +# LÓGICA DO MODO INTERNO LGPD_VERIFY QUE CAPTURA INPUT DO USUÁRIO +# =================================================================== + + +def lgpd_check_user_choice(repo_path:str, branch_atual:str): + """ + Verifica a conformidade LGPD e solicita autorização do usuário para envio de dados. + + Args: + repo_path: Caminho para o repositório Git local + branch_atual: Nome da branch atual sendo analisada + + Returns: + bool: True se o usuário autorizou, encerra o programa caso contrário + """ + caminho_dir_lgpd = os.path.join(repo_path, "analises-julgamento-lgpd") + lgpd_judge_file_path = os.path.join(caminho_dir_lgpd, "julgamento_lgpd.md") + + run_codewise_mode("lgpd_verify", repo_path, branch_atual) + + if not os.path.exists(caminho_dir_lgpd): + sys.exit("Erro: A pasta analises-julgamento-lgpd não existe! Execute novamente para efetuar a verificação LGPD.") + + try: + with open(lgpd_judge_file_path, "r", encoding="utf-8") as f: + content_resume = f.read() + + print("-" * 40) + print() + print("Resumo sobre a análise da política de uso de dados: ") + print("") + + # Pegar apenas a conclusao do arquivo e mostrar ao usuario no cmd + conclusao = re.search(r"(?i)^#+\s*Conclusão\s*\n+(.*?)\s*fim referência", content_resume, re.MULTILINE | re.DOTALL) + + if(conclusao): + conclusao_content = conclusao.group(1).strip() + + print(conclusao_content) + print("") + + while True: + print("-" * 40) + print("\n⚠️ AVISO: Esta ação requer o envio de dados, como por exemplo, o código-fonte, para o provedor da API key fornecida.", file=sys.stderr) + print() + + # Trecho utilizando biblioteca para receber o input direto do SO! + # Se estiver no Windows + if sys.platform == 'win32': + import msvcrt + + print("\nCom base na verificação apresentada acima, você gostaria de continuar com o envio de seus dados para o provedor e modelo de api key escolhido? [S/N]:") + char = msvcrt.getwche() + choice = char.upper() + print() + # Se estiver no Linux/Mac + elif(sys.platform == 'linux' or sys.platform == 'darwin'): + with open("/dev/tty", "r") as tty: + choice = tty.readline().strip().upper() + else: + choice = input("* Com base na verificação apresentada acima, você gostaria de continuar com o envio de seus dados para o provedor e modelo de api key escolhido? [S/N]: ").strip().upper() + + print() + if(choice == "S"): + print("-" * 40) + print("\nVocê ✅ AUTORIZOU ✅ o envio de dados necessários para o provedor da API key escolhida!", file=sys.stderr) + print("\nContinuando as análises...") + print() + print("-" * 40) + print() + return True + elif(choice == "N"): + print("-" * 40) + print("\nVocê ❌ NÃO AUTORIZOU ❌ o envio de dados necessários para o provedor da API key escolhida. Execute novamente com outro modelo ou provedor.", file=sys.stderr) + print("\nDados ❌ NÃO ENVIADOS! ❌ Interrompendo programa...", file=sys.stderr) + print() + print("-" * 40) + sys.exit(0) + except FileNotFoundError as e: + print(f"❌ ERRO - Arquivo 'julgamento_lgpd.md' não existe: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Erro em obter a autorização do usuário: {e}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/help.py b/scripts/help.py index 7ad2ead..72e416f 100644 --- a/scripts/help.py +++ b/scripts/help.py @@ -1,4 +1,7 @@ def main(): + """ + Exibe a mensagem de ajuda do CodeWise com comandos disponíveis e instruções de uso. + """ print(""" CodeWise CLI diff --git a/scripts/install_hook.py b/scripts/install_hook.py index 1928e11..cace7b6 100644 --- a/scripts/install_hook.py +++ b/scripts/install_hook.py @@ -21,6 +21,15 @@ """ def verificar_remote_existe(remote_name): + """ + Verifica se um remote Git com o nome especificado existe no repositório atual. + + Args: + remote_name: Nome do remote a verificar (ex: 'origin', 'upstream') + + Returns: + bool: True se o remote existe, False caso contrário + """ try: repo_path = os.getcwd() remotes = subprocess.check_output(["git", "remote"], cwd=repo_path, text=True, encoding='utf-8') @@ -29,6 +38,17 @@ def verificar_remote_existe(remote_name): return False def install_hook(hook_name, hook_content, repo_root): + """ + Instala um hook Git no repositório especificado. + + Args: + hook_name: Nome do hook (ex: 'pre-commit', 'pre-push') + hook_content: Conteúdo do script do hook + repo_root: Caminho raiz do repositório + + Returns: + bool: True se instalado com sucesso, False caso contrário + """ hooks_dir = os.path.join(repo_root, '.git', 'hooks') if not os.path.isdir(hooks_dir): print(f"❌ Erro: Diretório de hooks do Git não encontrado em '{hooks_dir}'.", file=sys.stderr) @@ -47,6 +67,10 @@ def install_hook(hook_name, hook_content, repo_root): return False def main(): + """ + Ponto de entrada principal para instalação de hooks Git do CodeWise. + Permite instalar hooks de pre-commit e/ou pre-push, --all para instalar ambos juntos. + """ parser = argparse.ArgumentParser(description="Instalador de hooks do CodeWise.") parser.add_argument('--commit', action='store_true', help='Instala o hook pre-commit.') parser.add_argument('--push', action='store_true', help='Instala o hook pre-push.')