diff --git a/Guia3/src/alternativa.py b/Guia3/src/alternativa.py index 4dde61f..c480291 100644 --- a/Guia3/src/alternativa.py +++ b/Guia3/src/alternativa.py @@ -1,4 +1,8 @@ from typing import List, Tuple, Dict class Alternativa: - pass \ No newline at end of file + def __init__(self, texto: str, correta: bool, explicacao: str = None): + self.texto = texto + self.correta = correta + self.explicacao = explicacao + diff --git a/Guia3/src/correcao.py b/Guia3/src/correcao.py new file mode 100644 index 0000000..46a59e2 --- /dev/null +++ b/Guia3/src/correcao.py @@ -0,0 +1,17 @@ +from typing import Dict +from src.perguntadiscursiva import PerguntaDiscursiva +from src.llmservice import LLMService + +class Correcao: + @staticmethod + def corrigir_discursiva(pergunta: PerguntaDiscursiva, resposta_aluno: str, service: LLMService = None) -> Dict: + # Se nenhum serviço for passado, instancia o padrão automaticamente + if service is None: + service = LLMService() + + return service.corrigir_resposta(pergunta, resposta_aluno) + + @staticmethod + def criar_prompt_correcao(pergunta: PerguntaDiscursiva, resposta_aluno: str) -> str: + # Método utilitário para isolar a montagem do prompt se necessário + return f"Pergunta: {pergunta.texto} | Esperado: {pergunta.resposta_esperada} | Aluno: {resposta_aluno}" \ No newline at end of file diff --git a/Guia3/src/llmservice.py b/Guia3/src/llmservice.py new file mode 100644 index 0000000..895996f --- /dev/null +++ b/Guia3/src/llmservice.py @@ -0,0 +1,82 @@ +import os +import json +from typing import Dict +from groq import Groq +from src.perguntadiscursiva import PerguntaDiscursiva + +class LLMService: + def __init__(self, api_key: str = None, model: str = "llama-3.3-70b-versatile"): + # Se não passar api_key por parâmetro, busca da variável de ambiente + self.api_key = api_key or os.getenv("GROQ_API_KEY") + self.model = model + # Definindo uma base_url padrão de acordo com o diagrama UML + self.base_url = "https://api.groq.com" + + if self.api_key: + self.client = Groq(api_key=self.api_key) + else: + self.client = None + + def corrigir_resposta(self, pergunta: PerguntaDiscursiva, resposta_aluno: str) -> Dict: + # Prompt robusto para garantir o formato JSON esperado pelo diagrama UML + prompt = f""" + Você é um professor avaliador rigoroso. + Analise a resposta do aluno com base na resposta esperada para a pergunta dada. + + Pergunta: {pergunta.texto} + Resposta Esperada: {pergunta.resposta_esperada} + Resposta do Aluno: {resposta_aluno} + + Responda estritamente em formato JSON com a seguinte estrutura: + {{ + "correta": true ou false, + "pontuacao": valor float de 0.0 a 1.0, + "feedback": "Uma justificativa curta sobre a nota do aluno", + "explicacao": "A explicação conceitual correta" + }} + """ + + try: + resposta_texto = self._fazer_chamada_api(prompt) + # Converte a string JSON purgada pela IA em um dicionário Python + dados_correcao = json.loads(resposta_texto) + return dados_correcao + + except Exception as e: + # Tratamento interno em caso de falhas + return self._tratar_erro(e, pergunta, resposta_aluno) + + def _fazer_chamada_api(self, prompt: str) -> str: + if not self.client: + raise ValueError("API Key do Groq não configurada.") + + chat_completion = self.client.chat.completions.create( + model=self.model, + temperature=0.1, # Baixa para avaliação precisa e sem invenções + response_format={"type": "json_object"}, # Força o Groq a devolver JSON válido + messages=[ + { + "role": "system", + "content": "Você é um assistente acadêmico que responde exclusivamente em formato JSON estruturado." + }, + { + "role": "user", + "content": prompt + } + ] + ) + return chat_completion.choices[0].message.content + + def _tratar_erro(self, e: Exception, pergunta: PerguntaDiscursiva, resposta_aluno: str) -> Dict: + print(f"[LLMService Error] Falha na comunicação ou processamento: {e}") + + # Fallback estático usando a lógica tradicional baseada em correspondência de texto + resposta_esperada = pergunta.resposta_esperada or "" + eh_correto = str(resposta_aluno).strip().lower() == str(resposta_esperada).strip().lower() + + return { + "correta": eh_correto, + "pontuacao": 1.0 if eh_correto else 0.0, + "feedback": f"Correção em modo de segurança (Fallback). Erro na API: {str(e)}", + "explicacao": pergunta.get_explicacao() or "Sem explicação disponível no momento." + } \ No newline at end of file diff --git a/Guia3/src/pergunta.py b/Guia3/src/pergunta.py index 5b3763d..68fe2af 100644 --- a/Guia3/src/pergunta.py +++ b/Guia3/src/pergunta.py @@ -1,4 +1,18 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod -class Pergunta: - pass \ No newline at end of file +class Pergunta(ABC): + def __init__(self, texto: str, explicacao_geral: str = None): + self.texto = texto + self.explicacao_geral = explicacao_geral + + @abstractmethod + def validar_resposta(self, resposta) -> bool: + pass + + def get_explicacao(self) -> str: + return self.explicacao_geral + + @abstractmethod + def get_tipo(self) -> str: + pass \ No newline at end of file diff --git a/Guia3/src/perguntadiscursiva.py b/Guia3/src/perguntadiscursiva.py index f4c26af..1ece7a3 100644 --- a/Guia3/src/perguntadiscursiva.py +++ b/Guia3/src/perguntadiscursiva.py @@ -1,4 +1,15 @@ from typing import List, Tuple, Dict +from src.pergunta import Pergunta -class PerguntaDiscursiva: - pass \ No newline at end of file +class PerguntaDiscursiva(Pergunta): + def __init__(self, texto: str, resposta_esperada: str = None, explicacao_geral: str = None): + super().__init__(texto, explicacao_geral) + self.resposta_esperada = resposta_esperada + + def validar_resposta(self, texto: str) -> bool: + if self.resposta_esperada is None: + return True + return str(texto).strip().lower() == str(self.resposta_esperada).strip().lower() + + def get_tipo(self) -> str: + return "discursiva" \ No newline at end of file diff --git a/Guia3/src/perguntamultiplaescolha.py b/Guia3/src/perguntamultiplaescolha.py index bcbe94d..32ba89c 100644 --- a/Guia3/src/perguntamultiplaescolha.py +++ b/Guia3/src/perguntamultiplaescolha.py @@ -1,4 +1,24 @@ from typing import List, Tuple, Dict +from src.pergunta import Pergunta +from src.alternativa import Alternativa -class PerguntaMultiplaEscolha: - pass \ No newline at end of file +class PerguntaMultiplaEscolha(Pergunta): + def __init__(self, texto: str, alternativas: list[Alternativa], explicacao_geral: str = None): + super().__init__(texto, explicacao_geral) + self.alternativas = alternativas + + def validar_resposta(self, indice: int) -> bool: + if 0 <= indice < len(self.alternativas): + return self.alternativas[indice].correta + return False + + def get_alternativa_correta(self) -> Alternativa: + for alt in self.alternativas: + if alt.correta: + return alt + return None + + def get_tipo(self) -> str: + return "multipla_escolha" + + \ No newline at end of file diff --git a/Guia3/src/questionario.py b/Guia3/src/questionario.py index 7525582..b0e0843 100644 --- a/Guia3/src/questionario.py +++ b/Guia3/src/questionario.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from src.pergunta import Pergunta class Questionario: - pass + def __init__(self, titulo: str): + self.titulo = titulo + self.perguntas = [] + + def adicionar_pergunta(self, p: Pergunta): + self.perguntas.append(p) + + def criar_attempt(self, usuario: str): + from src.tentativaquestionario import TentativaQuestionario + return TentativaQuestionario(self, usuario) \ No newline at end of file diff --git a/Guia3/src/resposta.py b/Guia3/src/resposta.py index 846d771..a88f27e 100644 --- a/Guia3/src/resposta.py +++ b/Guia3/src/resposta.py @@ -1,4 +1,13 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod +from src.pergunta import Pergunta -class Resposta: - pass \ No newline at end of file +class Resposta(ABC): + def __init__(self, pergunta: Pergunta): + self.pergunta = pergunta + self.esta_correta = False + self.pontuacao_obtida = 0.0 + + @abstractmethod + def calcular_pontuacao(self) -> float: + pass \ No newline at end of file diff --git a/Guia3/src/respostadiscursiva.py b/Guia3/src/respostadiscursiva.py index 4ea6dbb..d15cc3e 100644 --- a/Guia3/src/respostadiscursiva.py +++ b/Guia3/src/respostadiscursiva.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from src.resposta import Resposta +from src.perguntadiscursiva import PerguntaDiscursiva -class RespostaDiscursiva: - pass \ No newline at end of file +class RespostaDiscursiva(Resposta): + def __init__(self, pergunta: PerguntaDiscursiva, texto_resposta: str): + super().__init__(pergunta) + self.texto_resposta = texto_resposta + self.calcular_pontuacao() + + def calcular_pontuacao(self) -> float: + self.esta_correta = self.pergunta.validar_resposta(self.texto_resposta) + self.pontuacao_obtida = 1.0 if self.esta_correta else 0.0 + return self.pontuacao_obtida diff --git a/Guia3/src/respostaobjetiva.py b/Guia3/src/respostaobjetiva.py index 72ed2d0..ff4cfc5 100644 --- a/Guia3/src/respostaobjetiva.py +++ b/Guia3/src/respostaobjetiva.py @@ -1,4 +1,20 @@ from typing import List, Tuple, Dict +from src.resposta import Resposta +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha -class RespostaObjetiva: - pass \ No newline at end of file +class RespostaObjetiva(Resposta): + def __init__(self, pergunta: PerguntaMultiplaEscolha, indice_escolhido: int): + super().__init__(pergunta) + self.indice_escolhido = indice_escolhido + + if 0 <= indice_escolhido < len(pergunta.alternativas): + self.alternativa_selecionada = pergunta.alternativas[indice_escolhido] + else: + self.alternativa_selecionada = None + + self.calcular_pontuacao() + + def calcular_pontuacao(self) -> float: + self.esta_correta = self.pergunta.validar_resposta(self.indice_escolhido) + self.pontuacao_obtida = 1.0 if self.esta_correta else 0.0 + return self.pontuacao_obtida \ No newline at end of file diff --git a/Guia3/src/tentativaquestionario.py b/Guia3/src/tentativaquestionario.py index 9947dd1..65a96a5 100644 --- a/Guia3/src/tentativaquestionario.py +++ b/Guia3/src/tentativaquestionario.py @@ -1,4 +1,42 @@ from typing import List, Tuple, Dict +from datetime import datetime +from src.questionario import Questionario +from src.respostaobjetiva import RespostaObjetiva +from src.respostadiscursiva import RespostaDiscursiva class TentativaQuestionario: - pass \ No newline at end of file + def __init__(self, questionario: Questionario, usuario: str): + self.questionario = questionario + self.usuario = usuario + self.data_inicio = datetime.now() + self.data_fim = None + self.respostas = [] + + def registrar_resposta(self, indice_pergunta: int, valor): + if self.is_finalizado(): + return + + if 0 <= indice_pergunta < len(self.questionario.perguntas): + pergunta = self.questionario.perguntas[indice_pergunta] + + if pergunta.get_tipo() == "multipla_escolha": + resposta = RespostaObjetiva(pergunta, valor) + else: + resposta = RespostaDiscursiva(pergunta, valor) + + resposta.calcular_pontuacao() + self.respostas.append(resposta) + + def is_finalizado(self) -> bool: + return self.data_fim is not None + + def calcular_pontuacao(self) -> float: + return sum(r.pontuacao_obtida for r in self.respostas) + + def finalizar(self) -> tuple[float, str]: + if not self.is_finalizado(): + self.data_fim = datetime.now() + + pontuacao_total = self.calcular_pontuacao() + mensagem = f"Questionário finalizado. Pontuação: {pontuacao_total}" + return (pontuacao_total, mensagem) \ No newline at end of file diff --git a/Guia4/src/alternativa.py b/Guia4/src/alternativa.py index 4dde61f..c480291 100644 --- a/Guia4/src/alternativa.py +++ b/Guia4/src/alternativa.py @@ -1,4 +1,8 @@ from typing import List, Tuple, Dict class Alternativa: - pass \ No newline at end of file + def __init__(self, texto: str, correta: bool, explicacao: str = None): + self.texto = texto + self.correta = correta + self.explicacao = explicacao + diff --git a/Guia4/src/correcao.py b/Guia4/src/correcao.py index bdf2fa4..3297aae 100644 --- a/Guia4/src/correcao.py +++ b/Guia4/src/correcao.py @@ -1,4 +1,18 @@ -from typing import List, Tuple, Dict +from typing import Dict +from src.perguntadiscursiva import PerguntaDiscursiva +from src.llmservice import LLMService class Correcao: - pass \ No newline at end of file + @staticmethod + def corrigir_discursiva(pergunta: PerguntaDiscursiva, resposta_aluno: str, service: LLMService = None) -> Dict: + if service is None: + service = LLMService() + + return service.corrigir_resposta(pergunta, resposta_aluno) + + @staticmethod + def criar_prompt_correcao(pergunta: PerguntaDiscursiva, resposta_aluno: str) -> str: + return f"Pergunta: {pergunta.texto} | Esperado: {pergunta.resposta_esperada} | Aluno: {resposta_aluno}" + + +CorrecaoUtil = Correcao \ No newline at end of file diff --git a/Guia4/src/llmservice.py b/Guia4/src/llmservice.py index e6e91b5..11ff96f 100644 --- a/Guia4/src/llmservice.py +++ b/Guia4/src/llmservice.py @@ -1,4 +1,81 @@ -from typing import List, Tuple, Dict +import os +import json +from typing import Dict +from groq import Groq +from src.perguntadiscursiva import PerguntaDiscursiva class LLMService: - pass \ No newline at end of file + def __init__(self, api_key: str = None, model: str = "llama3-70b-8192"): + self.api_key = api_key or os.getenv("GROQ_API_KEY") + + if model == "llama3-70b-8192": + self.model = "llama-3.3-70b-versatile" + else: + self.model = model + + self.base_url = "https://api.groq.com" + + if self.api_key: + self.client = Groq(api_key=self.api_key) + else: + self.client = None + + def corrigir_resposta(self, pergunta: PerguntaDiscursiva, resposta_aluno: str) -> Dict: + prompt = f""" + Você é um professor avaliador rigoroso. + Analise a resposta do aluno com base na resposta esperada para a pergunta dada. + + Pergunta: {pergunta.texto} + Resposta Esperada: {pergunta.resposta_esperada} + Resposta do Aluno: {resposta_aluno} + + Responda estritamente em formato JSON com a seguinte estrutura: + {{ + "correta": true ou false, + "pontuacao": valor float de 0.0 a 1.0, + "feedback": "Uma justificativa corta sobre a nota do aluno", + "explicacao": "A explicação conceitual correta" + }} + """ + + try: + resposta_texto = self._fazer_chamada_api(prompt) + dados_correcao = json.loads(resposta_texto) + return dados_correcao + + except Exception as e: + return self._tratar_erro(e, pergunta, resposta_aluno) + + def _fazer_chamada_api(self, prompt: str) -> str: + if not self.client: + raise ValueError("API Key do Groq não configurada.") + + chat_completion = self.client.chat.completions.create( + model=self.model, + temperature=0.1, + response_format={"type": "json_object"}, + messages=[ + { + "role": "system", + "content": "Você é um assistente acadêmico que responde exclusivamente em formato JSON estruturado." + }, + { + "role": "user", + "content": prompt + } + ] + ) + return chat_completion.choices[0].message.content + + def _tratar_erro(self, e: Exception, pergunta: PerguntaDiscursiva, resposta_aluno: str) -> Dict: + print(f"[LLMService Error] Falha na comunicação ou processamento: {e}") + + resposta_esperada = pergunta.resposta_esperada or "" + eh_correto = str(resposta_aluno).strip().lower() == str(resposta_esperada).strip().lower() + + return { + "correta": eh_correto, + "pontuacao": 1.0 if eh_correto else 0.0, + "feedback": f"Correção em modo de segurança (Fallback). Erro na API: {str(e)}", + "explicacao": pergunta.get_explicacao() or "Sem explicação disponível no momento." + } \ No newline at end of file diff --git a/Guia4/src/pergunta.py b/Guia4/src/pergunta.py index 5b3763d..68fe2af 100644 --- a/Guia4/src/pergunta.py +++ b/Guia4/src/pergunta.py @@ -1,4 +1,18 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod -class Pergunta: - pass \ No newline at end of file +class Pergunta(ABC): + def __init__(self, texto: str, explicacao_geral: str = None): + self.texto = texto + self.explicacao_geral = explicacao_geral + + @abstractmethod + def validar_resposta(self, resposta) -> bool: + pass + + def get_explicacao(self) -> str: + return self.explicacao_geral + + @abstractmethod + def get_tipo(self) -> str: + pass \ No newline at end of file diff --git a/Guia4/src/perguntadiscursiva.py b/Guia4/src/perguntadiscursiva.py index f4c26af..1ece7a3 100644 --- a/Guia4/src/perguntadiscursiva.py +++ b/Guia4/src/perguntadiscursiva.py @@ -1,4 +1,15 @@ from typing import List, Tuple, Dict +from src.pergunta import Pergunta -class PerguntaDiscursiva: - pass \ No newline at end of file +class PerguntaDiscursiva(Pergunta): + def __init__(self, texto: str, resposta_esperada: str = None, explicacao_geral: str = None): + super().__init__(texto, explicacao_geral) + self.resposta_esperada = resposta_esperada + + def validar_resposta(self, texto: str) -> bool: + if self.resposta_esperada is None: + return True + return str(texto).strip().lower() == str(self.resposta_esperada).strip().lower() + + def get_tipo(self) -> str: + return "discursiva" \ No newline at end of file diff --git a/Guia4/src/perguntamultiplaescolha.py b/Guia4/src/perguntamultiplaescolha.py index bcbe94d..32ba89c 100644 --- a/Guia4/src/perguntamultiplaescolha.py +++ b/Guia4/src/perguntamultiplaescolha.py @@ -1,4 +1,24 @@ from typing import List, Tuple, Dict +from src.pergunta import Pergunta +from src.alternativa import Alternativa -class PerguntaMultiplaEscolha: - pass \ No newline at end of file +class PerguntaMultiplaEscolha(Pergunta): + def __init__(self, texto: str, alternativas: list[Alternativa], explicacao_geral: str = None): + super().__init__(texto, explicacao_geral) + self.alternativas = alternativas + + def validar_resposta(self, indice: int) -> bool: + if 0 <= indice < len(self.alternativas): + return self.alternativas[indice].correta + return False + + def get_alternativa_correta(self) -> Alternativa: + for alt in self.alternativas: + if alt.correta: + return alt + return None + + def get_tipo(self) -> str: + return "multipla_escolha" + + \ No newline at end of file diff --git a/Guia4/src/questionario.py b/Guia4/src/questionario.py index 7525582..b0e0843 100644 --- a/Guia4/src/questionario.py +++ b/Guia4/src/questionario.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from src.pergunta import Pergunta class Questionario: - pass + def __init__(self, titulo: str): + self.titulo = titulo + self.perguntas = [] + + def adicionar_pergunta(self, p: Pergunta): + self.perguntas.append(p) + + def criar_attempt(self, usuario: str): + from src.tentativaquestionario import TentativaQuestionario + return TentativaQuestionario(self, usuario) \ No newline at end of file diff --git a/Guia4/src/resposta.py b/Guia4/src/resposta.py index 846d771..a88f27e 100644 --- a/Guia4/src/resposta.py +++ b/Guia4/src/resposta.py @@ -1,4 +1,13 @@ from typing import List, Tuple, Dict +from abc import ABC, abstractmethod +from src.pergunta import Pergunta -class Resposta: - pass \ No newline at end of file +class Resposta(ABC): + def __init__(self, pergunta: Pergunta): + self.pergunta = pergunta + self.esta_correta = False + self.pontuacao_obtida = 0.0 + + @abstractmethod + def calcular_pontuacao(self) -> float: + pass \ No newline at end of file diff --git a/Guia4/src/respostadiscursiva.py b/Guia4/src/respostadiscursiva.py index 4ea6dbb..d15cc3e 100644 --- a/Guia4/src/respostadiscursiva.py +++ b/Guia4/src/respostadiscursiva.py @@ -1,4 +1,14 @@ from typing import List, Tuple, Dict +from src.resposta import Resposta +from src.perguntadiscursiva import PerguntaDiscursiva -class RespostaDiscursiva: - pass \ No newline at end of file +class RespostaDiscursiva(Resposta): + def __init__(self, pergunta: PerguntaDiscursiva, texto_resposta: str): + super().__init__(pergunta) + self.texto_resposta = texto_resposta + self.calcular_pontuacao() + + def calcular_pontuacao(self) -> float: + self.esta_correta = self.pergunta.validar_resposta(self.texto_resposta) + self.pontuacao_obtida = 1.0 if self.esta_correta else 0.0 + return self.pontuacao_obtida diff --git a/Guia4/src/respostaobjetiva.py b/Guia4/src/respostaobjetiva.py index 72ed2d0..ff4cfc5 100644 --- a/Guia4/src/respostaobjetiva.py +++ b/Guia4/src/respostaobjetiva.py @@ -1,4 +1,20 @@ from typing import List, Tuple, Dict +from src.resposta import Resposta +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha -class RespostaObjetiva: - pass \ No newline at end of file +class RespostaObjetiva(Resposta): + def __init__(self, pergunta: PerguntaMultiplaEscolha, indice_escolhido: int): + super().__init__(pergunta) + self.indice_escolhido = indice_escolhido + + if 0 <= indice_escolhido < len(pergunta.alternativas): + self.alternativa_selecionada = pergunta.alternativas[indice_escolhido] + else: + self.alternativa_selecionada = None + + self.calcular_pontuacao() + + def calcular_pontuacao(self) -> float: + self.esta_correta = self.pergunta.validar_resposta(self.indice_escolhido) + self.pontuacao_obtida = 1.0 if self.esta_correta else 0.0 + return self.pontuacao_obtida \ No newline at end of file diff --git a/Guia4/src/tentativaquestionario.py b/Guia4/src/tentativaquestionario.py index 9947dd1..65a96a5 100644 --- a/Guia4/src/tentativaquestionario.py +++ b/Guia4/src/tentativaquestionario.py @@ -1,4 +1,42 @@ from typing import List, Tuple, Dict +from datetime import datetime +from src.questionario import Questionario +from src.respostaobjetiva import RespostaObjetiva +from src.respostadiscursiva import RespostaDiscursiva class TentativaQuestionario: - pass \ No newline at end of file + def __init__(self, questionario: Questionario, usuario: str): + self.questionario = questionario + self.usuario = usuario + self.data_inicio = datetime.now() + self.data_fim = None + self.respostas = [] + + def registrar_resposta(self, indice_pergunta: int, valor): + if self.is_finalizado(): + return + + if 0 <= indice_pergunta < len(self.questionario.perguntas): + pergunta = self.questionario.perguntas[indice_pergunta] + + if pergunta.get_tipo() == "multipla_escolha": + resposta = RespostaObjetiva(pergunta, valor) + else: + resposta = RespostaDiscursiva(pergunta, valor) + + resposta.calcular_pontuacao() + self.respostas.append(resposta) + + def is_finalizado(self) -> bool: + return self.data_fim is not None + + def calcular_pontuacao(self) -> float: + return sum(r.pontuacao_obtida for r in self.respostas) + + def finalizar(self) -> tuple[float, str]: + if not self.is_finalizado(): + self.data_fim = datetime.now() + + pontuacao_total = self.calcular_pontuacao() + mensagem = f"Questionário finalizado. Pontuação: {pontuacao_total}" + return (pontuacao_total, mensagem) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fbec5a0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,172 @@ +annotated-types==0.7.0 +anyio==4.13.0 +apturl==0.5.2 +argcomplete==1.8.1 +asgiref==3.8.1 +attrs==25.3.0 +Automat==25.4.16 +Babel==2.8.0 +bcrypt==3.2.0 +beautifulsoup4==4.10.0 +blinker==1.4 +Brlapi==0.8.3 +certifi==2025.4.26 +cffi==1.17.1 +chardet==4.0.0 +cliapp==1.20180812.1 +click==8.0.3 +cmdtest==0.32+git +colorama==0.4.4 +command-not-found==0.3 +constantly==23.10.4 +cryptography==44.0.3 +cssselect==1.3.0 +cupshelpers==1.0 +customtkinter==5.2.2 +darkdetect==0.8.0 +dbus-python==1.2.18 +defer==1.0.6 +defusedxml==0.8.0rc2 +distro==1.7.0 +distro-info==1.1+ubuntu0.2 +Django==5.1 +django-templated-mail==1.1.1 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +djoser==2.2.3 +docker==5.0.3 +docker-compose==1.29.2 +dockerpty==0.4.1 +docopt==0.6.2 +duplicity==0.8.21 +evdev==1.4.0 +exceptiongroup==1.2.2 +fasteners==0.14.1 +filelock==3.18.0 +future==0.18.2 +gpg==1.16.0 +groq==1.4.0 +gyp==0.1 +h11==0.16.0 +html5lib==1.1 +httpcore==1.0.9 +httplib2==0.20.2 +httpx==0.28.1 +hyperlink==21.0.0 +idna==3.3 +importlib-metadata==4.6.4 +incremental==24.7.2 +itemadapter==0.11.0 +itemloaders==1.3.2 +jeepney==0.7.1 +Jinja2==3.1.6 +jmespath==1.0.1 +jsonschema==3.2.0 +keyring==23.5.0 +language-selector==0.1 +launchpadlib==1.10.16 +lazr.restfulclient==0.14.4 +lazr.uri==1.0.6 +livereload==2.6.3 +llfuse==1.3.8 +lockfile==0.12.2 +louis==3.20.0 +lxml==4.8.0 +macaroonbakery==1.3.1 +Mako==1.1.3 +Markdown==3.3.6 +MarkupSafe==2.0.1 +mkdocs==1.1.2 +monotonic==1.6 +more-itertools==8.10.0 +netifaces==0.11.0 +numpy==1.21.5 +oauthlib==3.2.0 +olefile==0.46 +outcome==1.3.0.post0 +packaging==25.0 +paramiko==2.9.3 +parsel==1.10.0 +pexpect==4.8.0 +Pillow==9.0.1 +pipx==1.0.0 +Protego==0.4.0 +protobuf==3.12.4 +psutil==5.9.0 +ptyprocess==0.7.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pycairo==1.20.1 +pycparser==2.22 +pycryptodomex==3.11.0 +pycups==2.0.1 +pydantic==2.13.4 +pydantic_core==2.46.4 +PyDispatcher==2.0.7 +Pygments==2.11.2 +PyGObject==3.42.1 +pyinotify==0.9.6 +PyJWT==2.9.0 +pymacaroons==0.13.0 +PyNaCl==1.5.0 +pyOpenSSL==25.0.0 +pyparsing==2.4.7 +pyRFC3339==1.1 +pyrsistent==0.18.1 +PySocks==1.7.1 +python-apt==2.4.0+ubuntu4.1 +python-dateutil==2.8.1 +python-debian==0.1.43+ubuntu1.1 +python-dotenv==1.0.1 +python-magic==0.4.24 +python3-openid==3.2.0 +pytz==2022.1 +pyxattr==0.7.2 +pyxdg==0.27 +PyYAML==5.4.1 +queuelib==1.8.0 +reportlab==3.6.8 +requests==2.25.1 +requests-file==2.1.0 +requests-oauthlib==2.0.0 +scour==0.38.2 +Scrapy==2.13.0 +SecretStorage==3.3.1 +selenium==4.32.0 +service-identity==24.2.0 +setproctitle==1.2.2 +six==1.16.0 +sniffio==1.3.1 +social-auth-app-django==5.4.2 +social-auth-core==4.5.4 +sortedcontainers==2.4.0 +soupsieve==2.3.1 +sqlparse==0.5.1 +systemd-python==234 +texttable==1.6.4 +tldextract==5.3.0 +tomli==2.2.1 +tornado==6.1 +trio==0.30.0 +trio-websocket==0.12.2 +ttystatus==0.38 +Twisted==24.11.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +ubuntu-drivers-common==0.0.0 +ubuntu-pro-client==8001 +ufw==0.36.1 +unattended-upgrades==0.1 +urllib3==1.26.5 +usb-creator==0.3.7 +userpath==1.8.0 +w3lib==2.3.1 +wadllib==1.3.6 +webencodings==0.5.1 +websocket-client==1.8.0 +wsproto==1.2.0 +xdg==5 +xkit==0.0.0 +youtube-dl==2021.12.17 +zipp==1.0.0 +zope.interface==7.2