Skip to content

Conversation

@jtrecenti
Copy link
Collaborator

@jtrecenti jtrecenti commented Apr 22, 2025

Adiciona feature de auto label. Por enquanto apenas com OpenAI, mas seria legal usar alguma ferramenta mais geral para lidar com outros modelos

Por exemplo, usando o llm do simonw, que o guilherme recomendou:

import llm, json
from pydantic import BaseModel

class Dog(BaseModel):
    name: str
    age: int

model = llm.get_model("gpt-4o-mini")
response = model.prompt("Describe a nice dog", schema=Dog)
dog = json.loads(response.text())
print(dog)
# {"name":"Buddy","age":3}

Obs: não precisa dar merge ainda, vamos conversar antes

@jtrecenti jtrecenti requested review from bdcdo and Copilot April 22, 2025 02:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces an automatic labeling feature leveraging the OpenAI API to generate and refine cluster labels. Key changes include:

  • The addition of utility functions for generating and refining cluster labels in utils_auto_label.py.
  • Integration of the auto-labeling feature into the cluster module.
  • An update to the Python version requirement and dependency revisions in pyproject.toml.

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

File Description
src/cluster_facil/utils_auto_label.py New utilities for generating and refining cluster labels using the OpenAI API.
src/cluster_facil/cluster.py Added an auto_label_cluster method to integrate automatic labeling into the clustering workflow.
pyproject.toml Updated the python version requirement and added/updated dependencies including openai and python-dotenv.
Files not reviewed (1)
  • .env.example: Language not supported

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@bdcdo
Copy link
Owner

bdcdo commented Apr 25, 2025

Acho que uma boa ideia seria usar o langchain, porque ele tem muito mais suporte e funções que o llm do simonw.
A função que encaixaria melhor pra gente seria essa: https://python.langchain.com/docs/how_to/chat_models_universal_init/

Única coisa meio chatinha é que ela envolveria incluir uma dependência a mais para cada provedor, o que acrescenta uma dificuldade adicional para quem não programa, mas quer utilizar a biblioteca. No entanto, como eles já vão ter que ir atrás de uma chave de API de qualquer jeito, não vejo grande problema nisso.

Copy link
Owner

@bdcdo bdcdo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Achei que a implementação ficou ótima. Vou depois testar pra ver como ficou. Estou bastante curioso pra ver se com esses prompts simples já funciona, ou se pra ficar algo menor precisamos construir um agente.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tava na minha lista de tarefas pensar em que versão de python vamos colocar como mínima. Imagino que trocou para 3.10 por conta do uso do list. O ideal é só identificar qual o menor valor que não quebra o nosso código, certo? Se sim, tudo certo.

logging.info(f"Analisando características de {len(df_para_preparar)} textos (TF-IDF)...")
# Define parâmetros padrão que podem ser sobrescritos pelos kwargs
default_tfidf_params = {'stop_words': STOPWORDS_PT}
default_tfidf_params = {'stop_words': list(STOPWORDS_PT)}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tive também problema com isso e acabei solucionando trocando no arquivo utils como o STOPWORDS_PT está definido. Troquei de tupla pra lista e já fiz o push pra main. Acho que podemos deixar essa redundância. O que acha?

logging.info(f"Contagem de textos por classificação manual na coluna '{self.nome_coluna_classificacao}':\n{contagem}")
return None

def auto_label_cluster(self, rodada: int = None, model: str = "gpt-4.1-nano", api_key: str = None, temperature: float = 0.0, cut_limit: int = 30, random_state: int = None, final_refine: bool = True, n_examples_final: int = 10) -> dict:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O random_state padrão tá como 42 em outros lugares. Acho que vale padronizar em um só valor.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considerando que esse é um método de um objeto da classe ClusterFacil, acho que não precisa incluir "cluster" no nome.
Podemos também abrasileirar para "auto_classificar" ou algo do tipo.

model (str): Nome do modelo OpenAI (default: 'gpt-4.1-nano').
api_key (str, opcional): Chave da API OpenAI. Se não fornecida, busca em OPENAI_API_KEY.
temperature (float): Temperatura do modelo.
cut_limit (int, opcional): Número máximo de textos a serem enviados para o LLM por cluster. Se None, usa todos. Default=30.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acho que o default pode ser menor para economizar tokens. Estamos usando 10 para analisar manualmente e tem funcionado bem.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Estou me referindo ao cut_limit

api_key (str, opcional): Chave da API OpenAI. Se não fornecida, busca em OPENAI_API_KEY.
temperature (float): Temperatura do modelo.
cut_limit (int, opcional): Número máximo de textos a serem enviados para o LLM por cluster. Se None, usa todos. Default=30.
random_state (int, opcional): Semente para amostragem aleatória dos textos. Default=None (não controla aleatoriedade).
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mencionei acima a sugestão de padronizar em 42.
Numa nota mais de desenho da biblioteca, acho que o ideal seria controlar todas as aleatoriedades possíveis para tornar os resultados de cada pesquisador reprodutíveis.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E aqui ao random_state

Comment on lines +40 to +58
# Utiliza a Responses API mais recente com formato estruturado
resp_format = {
"format": {
"type": "json_schema",
"name": "rotulo",
"schema": {
"type": "object",
"properties": {
"rotulo": {
"type": "string",
"description": "Rótulo curto, claro e descritivo para o cluster de textos."
}
},
"required": ["rotulo"],
"additionalProperties": False
},
"strict": True
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Langchain nos ajudaria também a garantir que todos os provedores nos forneçam respostas parseadas em json

Comment on lines +85 to +89
if not api_key:
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("É necessário fornecer uma chave de API OpenAI via argumento ou variável de ambiente OPENAI_API_KEY.")

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Se isso vai se repetir, acho que, na hora de generalizar para poder receber outras APIs, podemos criar uma função que lida com as chaves e a armazena junto ao cluster_facil.

Comment on lines +94 to +104
prompt = (
"Você receberá exemplos de clusters, cada um com um rótulo sugerido e algumas amostras de textos.\n"
"Sua tarefa é:\n"
"- Unificar rótulos semelhantes se fizer sentido,\n"
"- Sugerir nomes mais claros e concisos para cada grupo,\n"
"- Retornar um dicionário JSON com o id do cluster e o novo rótulo.\n\n"
"Exemplo de entrada:\n"
"Cluster 0 - Rótulo inicial: 'Esportes'\n<sample1>Texto exemplo</sample1>\n<sample2>Texto exemplo</sample2>\n\n"
"Cluster 1 - Rótulo inicial: 'Futebol'\n<sample1>Texto exemplo</sample1>\n<sample2>Texto exemplo</sample2>\n\n"
"Agora, siga o mesmo padrão para os clusters abaixo:\n"
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tenho a impressão de que para essa revisão um llm de raciocínio funcionaria melhor. Podemos testar ter como padrão o o4-mini-low.

Comment on lines +36 to +39
prompt = (
"Dado o seguinte conjunto de textos, gere um rótulo curto (tema) que represente o cluster. "
"O rótulo deve ser claro, conciso e descritivo."
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
prompt = (
"Dado o seguinte conjunto de textos, gere um rótulo curto (tema) que represente o cluster. "
"O rótulo deve ser claro, conciso e descritivo."
)
prompt = (
"Dado o seguinte conjunto de textos, gere um rótulo curto (tema) que represente o cluster. "
"O rótulo deve ser claro, conciso e descritivo.\n"
"Se não for possível identificar um padrão claro entre as decisões, responda "falta_coesão".
)

Uma parte importante das rodadas de clusterização é entender quais clusters estão coesos e quais não estão. Acho que precisamos indicar isso explicitamente como uma possibilidade, para que esses casos possam ser novamente clusterizados.

Nesse sentido, seria interessante eventualmente incluir a possibilidade de que uma segunda rodada de auto_label seja automaticamente aplicada apenas nos casos considerados não coesos. E, a médio prazo, poderiamos automatizar seguidas rodadas de reclusterização.

Ligado a isso, registro aqui algum ceticismo em relação a se modelos menores vão ser capazes de fazer essa decisão de falta de unidade. Acho que os modelos menores vão se sentir mais pressionados a dizer que há algo em comum, pelo que já tive de experiência em outros casos.

for cid, info in cluster_samples.items():
label = info["label"]
examples = "\n\n".join(f"<sample{i+1}>\n{t}\n</sample{i+1}>" for i, t in enumerate(info["examples"]))
clusters_str += f"Cluster {cid} - Rótulo inicial: '{label}'\n{examples}\n\n"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eu me sentiria confortável até em reduzir para metade os clusters que aparecem aqui.

Acho que vale incluir também algum tipo de conferência de tamanho dos tokens nessa chamada, para evitar passar do limite em casos limite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants