paint-brush
Como criar seu próprio AnythingGPT - um bot que responde da maneira que você desejapor@balakhonoff
1,545 leituras
1,545 leituras

Como criar seu próprio AnythingGPT - um bot que responde da maneira que você deseja

por Kirill Balakhonov24m2023/07/25
Read on Terminal Reader

Muito longo; Para ler

Estaremos falando sobre a criação de uma versão personalizada do ChatGPT que responda a perguntas, levando em consideração uma grande base de conhecimento. Usaremos incorporações contextuais do OpenAI (para uma pesquisa verdadeiramente de alta qualidade de perguntas relevantes da base de conhecimento). Formataremos as respostas em linguagem humana natural.
featured image - Como criar seu próprio AnythingGPT - um bot que responde da maneira que você deseja
Kirill Balakhonov HackerNoon profile picture
0-item
1-item
2-item


Olá pessoal! Recentemente, apliquei uma solução interessante durante minha prática que queria experimentar há muito tempo e agora estou pronto para explicar como você pode criar algo semelhante para qualquer outra tarefa. Estaremos falando sobre a criação de uma versão personalizada do ChatGPT que responda a perguntas, levando em consideração uma grande base de conhecimento que não é limitada em comprimento pelo tamanho do prompt (o que significa que você não seria capaz de simplesmente adicionar todas as informações antes de cada pergunta ao ChatGPT).


Para conseguir isso, usaremos embeddings contextuais do OpenAI (para uma pesquisa verdadeiramente de alta qualidade de perguntas relevantes da base de conhecimento) e da própria API do ChatGPT (para formatar as respostas em linguagem humana natural).


Além disso, supõe-se que o assistente possa responder não apenas às perguntas de Q&A explicitamente declaradas , mas também às perguntas que uma pessoa familiarizada com as perguntas e respostas poderia responder. Se você estiver interessado em aprender como criar bots simples que respondem usando uma grande base de conhecimento, seja bem-vindo aos detalhes.


Gostaria de ressaltar que existem alguns projetos de bibliotecas que tentam resolver essa tarefa na forma de um framework, por exemplo, LangChain , e também tentei utilizá-lo. No entanto, como qualquer framework que está em um estágio inicial de desenvolvimento, em alguns casos, ele tende a limitar ao invés de simplificar as coisas. Em particular, desde o início da resolução desta tarefa, entendi o que queria fazer com os dados e sabia como fazê-lo sozinho (incluindo pesquisa baseada em contexto, definição do contexto correto em prompts, combinação de fontes de informação).


Mas não consegui configurar a estrutura para fazer exatamente isso com um nível de qualidade aceitável, e depurar a estrutura parecia um exagero para essa tarefa. No final, criei meu próprio código clichê e fiquei satisfeito com essa abordagem.

Tarefa

Deixe-me descrever brevemente a tarefa em que estava trabalhando e você pode usar o mesmo código em suas próprias tarefas, substituindo as fontes de dados e os prompts pelos que mais lhe convierem . Você ainda terá controle total sobre a lógica do bot.


Ao escrever código, costumo usar o ChatGPT (e não tenho vergonha disso🙂). No entanto, devido à falta de dados para o ano 2022+, às vezes há problemas com tecnologias relativamente novas.


Em particular, ao desenvolver subgráficos para o protocolo The Graph (a maneira mais popular de construir ETL para recuperar dados indexados de blockchains compatíveis com EVM, você pode ler mais sobre isso em meus artigos anteriores [ 1 ] e [ 2 ]), as próprias bibliotecas passaram por várias alterações de compatibilidade de quebra. As respostas "antigas" do ChatGPT não são mais úteis e tenho que procurar as respostas corretas na documentação escassa ou, na pior das hipóteses, no Discord dos desenvolvedores, o que não é muito conveniente (não é como o StackOverflow).


A segunda parte do problema é que toda vez que você precisa fornecer o contexto da conversa corretamente, porque o ChatGPT geralmente desvia do tópico de subgráficos, pulando para GraphQL, SQL ou matemática avançada (“O gráfico”, “subgráficos” etc. não são termos exclusivos e têm muitas interpretações e tópicos diferentes).


Portanto, após um curto período de luta com o ChatGPT para corrigir erros no código do subgráfico, decidi criar meu próprio bot SubgraphGPT , que estará sempre no contexto certo e tentará responder, levando em consideração a base de conhecimento e as mensagens dos desenvolvedores discórdia.


PS. Trabalho como gerente de produto líder em chainstack.com , um provedor de infraestrutura Web3, e sou responsável pelo desenvolvimento do serviço de hospedagem de subgrafos . Portanto, tenho que trabalhar bastante com subgráficos, ajudando os usuários a entender essa tecnologia relativamente nova.

Solução de nível superior

No final, para resolver esse problema, decidi usar duas fontes:

  1. Uma base de conhecimento de perguntas e respostas compilada manualmente, selecionada no modo semi-cego (muitas vezes eu peguei o título do tópico da documentação como a pergunta e todo o parágrafo de informação como a resposta).

  2. Mensagens exportadas dos desenvolvedores do protocolo Discord dos últimos 2 anos (para cobrir o período ausente do final de 2021).


Em seguida, diferentes abordagens foram utilizadas para cada fonte compor uma requisição à API ChatGPT, especificamente:


Para as perguntas e respostas compiladas manualmente,

  1. para cada questão é gerado um embedding contextual (um vetor que descreve esta questão em um estado multidimensional), obtido através do modelo text-embedding-ada-002 ,

  2. então, usando uma função de pesquisa de distância de cosseno, as 3 perguntas mais semelhantes da base de conhecimento são encontradas (em vez de 3, o número mais adequado para o seu conjunto de dados pode ser usado),

  3. as respostas a essas 3 perguntas são adicionadas ao prompt final com uma descrição aproximada de " Use este trecho de Q&A somente se for relevante para a pergunta em questão ."


    Para as mensagens exportadas do Discord, foi utilizado o seguinte algoritmo:

  4. para cada mensagem que contém um ponto de interrogação, também é gerado um embedding contextual (usando o mesmo modelo),

  5. então, de maneira semelhante, as 5 perguntas mais semelhantes são selecionadas,

  6. e como contexto para a resposta, são adicionadas as 20 mensagens seguintes a essa pergunta, que se supõe ter uma certa probabilidade de conter a resposta à pergunta,

  7. e esta informação foi adicionada ao prompt final aproximadamente assim: " Se você não encontrou uma resposta explícita para a pergunta no trecho de perguntas e respostas em anexo, os seguintes fragmentos de bate-papo do desenvolvedor podem ser úteis para responder à pergunta original ..."


Além disso, se o assunto não for dado explicitamente, a presença de trechos de perguntas e respostas e chats pode levar a ambiguidade nas respostas, que podem parecer, por exemplo, da seguinte forma:



Assim, entende que a pergunta foi descolada do contexto e a resposta também foi aceita descolada do contexto. Então foi dito que tais dados podem ser usados, e os resume da seguinte forma:

  1. Na verdade, a resposta pode ser assim...
  2. E se considerarmos o contexto, então será assim...


Para evitar isso, introduzimos o conceito de tópico, que é explicitamente definido e inserido no início do prompt como:

"Preciso obter uma resposta para uma pergunta relacionada ao tópico 'Desenvolvimento do subgrafo do gráfico': {{{o que é um subgrafo?}}}"


Além disso, na última frase, acrescento também o seguinte:

Finalmente, somente se as informações acima não forem suficientes, você pode usar seu conhecimento no tópico 'Desenvolvimento do subgrafo do gráfico' para responder à pergunta.


No final, o prompt completo (excluindo a parte obtida dos chats) fica assim:

 ==I need to get an answer to the question related to the topic of "The Graph subgraph development": {{{what is a subgraph?}}}.== ==Possibly, you might find an answer in these Q&As \[use the information only if it is actually relevant and useful for the question answering\]:== ==Q: <What is a subgraph?>== ==A: <A subgraph is a custom API built on blockchain data. Subgraphs are queried using the GraphQL query language and are deployed to a Graph Node using the Graph CLI. Once deployed and published to The Graph's decentralized network, Indexers process subgraphs and make them available to be queried by subgraph consumers.>== ==Q: <Am I still able to create a subgraph if my smart contracts don't have events?>== ==A: <It is highly recommended that you structure your smart contracts to have events associated with data you are interested in querying. Event handlers in the subgraph are triggered by contract events and are by far the fastest way to retrieve useful data. If the contracts you are working with do not contain events, your subgraph can use call and block handlers to trigger indexing. Although this is not recommended, as performance will be significantly slower.>== ==Q: <How do I call a contract function or access a public state variable from my subgraph mappings?>== ==A: <Take a look at Access to smart contract state inside the section AssemblyScript API. https://thegraph.com/docs/en/developing/assemblyscript-api/>== ==Finally, only if the information above was not enough you can use your knowledge in the topic of "The Graph subgraph development" to answer the question.==


A resposta à solicitação acima com este prompt semi-gerado automaticamente na entrada parece correta desde o início:



Nesse caso, o bot responde imediatamente com a chave correta e adiciona informações mais relevantes, de modo que a resposta não pareça tão direta quanto no Q&A (lembro que essa pergunta está exatamente na lista de perguntas e respostas), mas com explicações razoáveis que abordam parcialmente as perguntas a seguir.

Código fonte

Devo observar imediatamente que haverá um link para o repositório no final , para que você possa executar o bot como está, substituindo "tópico" pelo seu, o arquivo da base de conhecimento de perguntas e respostas pelo seu e fornecendo suas próprias chaves de API para OpenAI e o bot do Telegram. Portanto, a descrição aqui não pretende corresponder totalmente ao código-fonte no GitHub, mas sim destacar os principais aspectos do código.

1 - preparando o ambiente virtual

Vamos criar um novo ambiente virtual e instalar as dependências de requirements.txt:


 virtualenv -p python3.8 .venv source .venv/bin/activate pip install -r requirements.txt

2 - Base de conhecimento, coletada manualmente

Conforme mencionado acima, assume-se que existe uma lista de perguntas e respostas, neste caso no formato de um arquivo Excel do seguinte tipo:



Para encontrar a questão mais semelhante à dada, precisamos adicionar uma incorporação da questão (um vetor multidimensional no espaço de estado) a cada linha deste arquivo. Usaremos o arquivo add_embeddings.py para isso. O script consiste em várias partes simples.

Importando bibliotecas e lendo argumentos de linha de comando:


 import pandas as pd import openai import argparse # Create an Argument Parser object parser = argparse.ArgumentParser(description='Adding embeddings for each line of csv file') # Add the arguments parser.add_argument('--openai_api_key', type=str, help='API KEY of OpenAI API to create contextual embeddings for each line') parser.add_argument('--file', type=str, help='A source CSV file with the text data') parser.add_argument('--colname', type=str, help='Column name with the texts') # Parse the command-line arguments args = parser.parse_args() # Access the argument values openai.api_key = args.openai_api_key file = args.file colname = args.colname


Em seguida, lendo o arquivo em um dataframe do pandas e filtrando as perguntas com base na presença de um ponto de interrogação. Este trecho de código é comum para lidar com uma base de conhecimento, bem como fluxos de mensagens brutas do Discord, portanto, supondo que as perguntas sejam frequentemente duplicadas, decidi manter um método tão simples de filtragem aproximada de não perguntas.


 if file[-4:] == '.csv': df = pd.read_csv(file) else: df = pd.read_excel(file) # filter NAs df = df[~df[colname].isna()] # Keep only questions df = df[df[colname].str.contains('\?')]


E finalmente - uma função para gerar uma incorporação chamando a API do modelo text-embedding-ada-002 , algumas solicitações repetidas, pois a API pode ocasionalmente ser sobrecarregada e responder com um erro e aplicar esta função a cada linha do dataframe.


 def get_embedding(text, model="text-embedding-ada-002"): i = 0 max_try = 3 # to avoid random OpenAI API fails: while i < max_try: try: text = text.replace("\n", " ") result = openai.Embedding.create(input=[text], model=model)['data'][0]['embedding'] return result except: i += 1 def process_row(x): return get_embedding(x, model='text-embedding-ada-002') df['ada_embedding'] = df[colname].apply(process_row) df.to_csv(file[:-4]+'_question_embed.csv', index=False)


No final, este script pode ser chamado com o seguinte comando:


 python add_embeddings.py \ --openai_api_key="xxx" \ --file="./subgraphs_faq.xlsx" \ --colname="Question"


configurar a chave da API OpenAI, o arquivo com a base de conhecimento e o nome da coluna onde o texto da pergunta está localizado. O arquivo final criado, subgraphs_faq._question_embed.csv, contém as colunas "Pergunta", "Resposta" e "ada_embedding ".

3 - Coleta de dados do Discord (opcional)

Se você estiver interessado em um bot simples que responda com base apenas na base de conhecimento coletada manualmente, você pode pular esta e a próxima seção. No entanto, fornecerei brevemente exemplos de código aqui para coletar dados de um canal do Discord e de um grupo do Telegram. O arquivo discord-channel-data-collection.py consiste em duas partes. A primeira parte inclui importar bibliotecas e inicializar argumentos de linha de comando:


 import requests import json import pandas as pd import argparse # Create an Argument Parser object parser = argparse.ArgumentParser(description='Discord Channel Data Collection Script') # Add the arguments parser.add_argument('--channel_id', type=str, help='Channel ID from the URL of a channel in browser https://discord.com/channels/xxx/{CHANNEL_ID}') parser.add_argument('--authorization_key', type=str, help='Authorization Key. Being on the discord channel page, start typing anything, then open developer tools -> Network -> Find "typing" -> Headers -> Authorization.') # Parse the command-line arguments args = parser.parse_args() # Access the argument values channel_id = args.channel_id authorization_key = args.authorization_key


A segunda é a função para recuperar dados do canal e salvá-los em um dataframe do pandas, bem como sua chamada com parâmetros especificados.


 def retrieve_messages(channel_id, authorization_key): num = 0 limit = 100 headers = { 'authorization': authorization_key } last_message_id = None # Create a pandas DataFrame df = pd.DataFrame(columns=['id', 'dt', 'text', 'author_id', 'author_username', 'is_bot', 'is_reply', 'id_reply']) while True: query_parameters = f'limit={limit}' if last_message_id is not None: query_parameters += f'&before={last_message_id}' r = requests.get( f'https://discord.com/api/v9/channels/{channel_id}/messages?{query_parameters}', headers=headers ) jsonn = json.loads(r.text) if len(jsonn) == 0: break for value in jsonn: is_reply = False id_reply = '0' if 'message_reference' in value and value['message_reference'] is not None: if 'message_id' in value['message_reference'].keys(): is_reply = True id_reply = value['message_reference']['message_id'] text = value['content'] if 'embeds' in value.keys(): if len(value['embeds'])>0: for x in value['embeds']: if 'description' in x.keys(): if text != '': text += ' ' + x['description'] else: text = x['description'] df_t = pd.DataFrame({ 'id': value['id'], 'dt': value['timestamp'], 'text': text, 'author_id': value['author']['id'], 'author_username': value['author']['username'], 'is_bot': value['author']['bot'] if 'bot' in value['author'].keys() else False, 'is_reply': is_reply, 'id_reply': id_reply, }, index=[0]) if len(df) == 0: df = df_t.copy() else: df = pd.concat([df, df_t], ignore_index=True) last_message_id = value['id'] num = num + 1 print('number of messages we collected is', num) # Save DataFrame to a CSV file df.to_csv(f'../discord_messages_{channel_id}.csv', index=False) if __name__ == '__main__': retrieve_messages(channel_id, authorization_key)


Das informações úteis aqui, há um detalhe que não consigo encontrar sempre que preciso - obter uma chave de autorização. Considerando que o channel_id pode ser obtido a partir da URL do canal do Discord aberto no navegador (o último número longo no link), a authorization_key só pode ser encontrada começando a digitar uma mensagem no canal e, em seguida, usando as ferramentas do desenvolvedor para encontrar o evento chamado " digitando " na seção Rede e extraindo o parâmetro do cabeçalho.



Depois de receber esses parâmetros, você pode executar o seguinte comando para coletar todas as mensagens do canal (substitua seus próprios valores):


 python discord-channel-data-collection.py \ --channel_id=123456 \ --authorization_key="123456qwerty"


4 - Coletando dados do Telegram

Como costumo baixar vários dados de chats/canais no Telegram, também decidi fornecer um código para isso, que gera um arquivo CSV de formato semelhante (compatível em termos do script add_embeddings.py ). Portanto, o script telegram-group-data-collection.py tem a seguinte aparência. Importando bibliotecas e inicializando argumentos da linha de comando:


 import pandas as pd import argparse from telethon import TelegramClient # Create an Argument Parser object parser = argparse.ArgumentParser(description='Telegram Group Data Collection Script') # Add the arguments parser.add_argument('--app_id', type=int, help='Telegram APP id from https://my.telegram.org/apps') parser.add_argument('--app_hash', type=str, help='Telegram APP hash from https://my.telegram.org/apps') parser.add_argument('--phone_number', type=str, help='Telegram user phone number with the leading "+"') parser.add_argument('--password', type=str, help='Telegram user password') parser.add_argument('--group_name', type=str, help='Telegram group public name without "@"') parser.add_argument('--limit_messages', type=int, help='Number of last messages to download') # Parse the command-line arguments args = parser.parse_args() # Access the argument values app_id = args.app_id app_hash = args.app_hash phone_number = args.phone_number password = args.password group_name = args.group_name limit_messages = args.limit_messages


Como você pode ver, você não pode simplesmente baixar todas as mensagens do chat sem se autorizar como primeira pessoa. Ou seja, além de criar um aplicativo através de https://my.telegram.org/apps (obtendo APP_ID e APP_HASH), você também precisará usar seu número de telefone e senha para criar uma instância da classe TelegramClient da biblioteca Telethon.


Além disso, você precisará do group_name público do bate-papo do Telegram e especificará explicitamente o número de mensagens mais recentes a serem recuperadas. No geral, fiz esse procedimento muitas vezes com qualquer número de mensagens exportadas sem receber nenhum banimento temporário ou permanente da API do Telegram, ao contrário de quando alguém envia mensagens com muita frequência de uma conta.


A segunda parte do script contém a própria função de exportação de mensagens e sua execução (com filtragem necessária para evitar erros críticos que interromperiam a coleta no meio do caminho):


 async def main(): messages = await client.get_messages(group_name, limit=limit_messages) df = pd.DataFrame(columns=['date', 'user_id', 'raw_text', 'views', 'forwards', 'text', 'chan', 'id']) for m in messages: if m is not None: if 'from_id' in m.__dict__.keys(): if m.from_id is not None: if 'user_id' in m.from_id.__dict__.keys(): df = pd.concat([df, pd.DataFrame([{'date': m.date, 'user_id': m.from_id.user_id, 'raw_text': m.raw_text, 'views': m.views, 'forwards': m.forwards, 'text': m.text, 'chan': group_name, 'id': m.id}])], ignore_index=True) df = df[~df['user_id'].isna()] df = df[~df['text'].isna()] df['date'] = pd.to_datetime(df['date']) df = df.sort_values('date').reset_index(drop=True) df.to_csv(f'../telegram_messages_{group_name}.csv', index=False) client = TelegramClient('session', app_id, app_hash) client.start(phone=phone_number, password=password) with client: client.loop.run_until_complete(main())


No final, este script pode ser executado com o seguinte comando (substitua os valores pelos seus):


 python telegram-group-data-collection.py \ --app_id=123456 --app_hash="123456qwerty" \ --phone_number="+xxxxxx" --password="qwerty123" \ --group_name="xxx" --limit_messages=10000


5 - Script de bot do Telegram que realmente responde a perguntas

Na maioria das vezes, envolvo meus projetos de estimação em bots do Telegram porque requer um esforço mínimo para iniciar e mostra potencial imediatamente. Neste caso, fiz o mesmo. Devo dizer que o código do bot não contém todos os casos de canto que uso na versão de produção do bot SubgraphGPT , pois tem muita lógica herdada de outro projeto meu de estimação. Em vez disso, deixei a quantidade mínima de código básico que deve ser fácil de modificar para suas necessidades.

O script telegram-bot.py consiste em várias partes. Primeiro, como antes, as bibliotecas são importadas e os argumentos da linha de comando são inicializados.


 import threading import telegram from telegram.ext import Updater, CommandHandler, MessageHandler, Filters import openai from openai.embeddings_utils import cosine_similarity import numpy as np import pandas as pd import argparse import functools # Create an Argument Parser object parser = argparse.ArgumentParser(description='Run the bot which uses prepared knowledge base enriched with contextual embeddings') # Add the arguments parser.add_argument('--openai_api_key', type=str, help='API KEY of OpenAI API to create contextual embeddings for each line') parser.add_argument('--telegram_bot_token', type=str, help='A telegram bot token obtained via @BotFather') parser.add_argument('--file', type=str, help='A source CSV file with the questions, answers and embeddings') parser.add_argument('--topic', type=str, help='Write the topic to add a default context for the bot') parser.add_argument('--start_message', type=str, help="The text that will be shown to the users after they click /start button/command", default="Hello, World!") parser.add_argument('--model', type=str, help='A model of ChatGPT which will be used', default='gpt-3.5-turbo-16k') parser.add_argument('--num_top_qa', type=str, help="The number of top similar questions' answers as a context", default=3) # Parse the command-line arguments args = parser.parse_args() # Access the argument values openai.api_key = args.openai_api_key token = args.telegram_bot_token file = args.file topic = args.topic model = args.model num_top_qa = args.num_top_qa start_message = args.start_message


Observe que, neste caso, você também precisará de uma chave de API OpenAI, pois para encontrar a pergunta mais semelhante à que acabou de ser inserida pelo usuário na base de conhecimento, primeiro você precisa obter a incorporação dessa pergunta chamando a API, como fizemos para a própria base de conhecimento.


Além disso, você vai precisar de:


  • telegram_bot_token - um token para o bot Telegram do BotFather
  • arquivo - um caminho para o arquivo da base de conhecimento (eu pulo intencionalmente o caso com mensagens do Discord aqui, pois presumo que seja uma tarefa de nicho, mas elas podem ser facilmente integradas ao código, se necessário)
  • topic - a formulação textual do tópico (mencionado no início do artigo) no qual o bot irá operar
  • start_message - a mensagem que o usuário que clicou em /start verá (por padrão, "Hello, World!")
  • modelo - a escolha do modelo (definido por padrão)
  • num_top_qa - o número de perguntas-respostas mais semelhantes da base de conhecimento que serão usadas como contexto para a solicitação ChatGPT


Em seguida, segue-se o carregamento do arquivo da base de conhecimento e a inicialização dos embeddings de perguntas.


 # reading QA file with embeddings df_qa = pd.read_csv(file) df_qa['ada_embedding'] = df_qa.ada_embedding.apply(eval).apply(np.array)


Para fazer uma requisição à API do ChatGPT, sabendo que às vezes ela responde com erro por sobrecarga, utilizo uma função com repetição automática da requisição em caso de erro.


 def retry_on_error(func): @functools.wraps(func) def wrapper(*args, **kwargs): max_retries = 3 for i in range(max_retries): try: return func(*args, **kwargs) except Exception as e: print(f"Error occurred, retrying ({i+1}/{max_retries} attempts)...") # If all retries failed, raise the last exception raise e return wrapper @retry_on_error def call_chatgpt(*args, **kwargs): return openai.ChatCompletion.create(*args, **kwargs)


Segundo recomendação da OpenAI, antes de converter o texto em embeddings, as novas linhas devem ser substituídas por espaços.


 def get_embedding(text, model="text-embedding-ada-002"): text = text.replace("\n", " ") return openai.Embedding.create(input=[text], model=model)['data'][0]['embedding']


Para buscar as questões mais semelhantes, calculamos a distância do cosseno entre os embeddings de duas questões, retiradas diretamente da biblioteca openai.


 def search_similar(df, question, n=3, pprint=True): embedding = get_embedding(question, model='text-embedding-ada-002') df['similarities'] = df.ada_embedding.apply(lambda x: cosine_similarity(x, embedding)) res = df.sort_values('similarities', ascending=False).head(n) return res


Depois de receber uma lista dos pares de perguntas e respostas mais semelhantes ao dado, você pode compilá-los em um texto, marcando-o de forma que o ChatGPT possa determinar inequivocamente o que é o quê.


 def collect_text_qa(df): text = '' for i, row in df.iterrows(): text += f'Q: <'+row['Question'] + '>\nA: <'+ row['Answer'] +'>\n\n' print('len qa', len(text.split(' '))) return text


Depois disso, já é necessário reunir os "pedaços" do prompt descrito no início do artigo em um todo.


 def collect_full_prompt(question, qa_prompt, chat_prompt=None): prompt = f'I need to get an answer to the question related to the topic of "{topic}": ' + "{{{"+ question +"}}}. " prompt += '\n\nPossibly, you might find an answer in these Q&As [use the information only if it is actually relevant and useful for the question answering]: \n\n' + qa_prompt # edit if you need to use this also if chat_prompt is not None: prompt += "---------\nIf you didn't find a clear answer in the Q&As, possibly, these talks from chats might be helpful to answer properly [use the information only if it is actually relevant and useful for the question answering]: \n\n" + chat_prompt prompt += f'\nFinally, only if the information above was not enough you can use your knowledge in the topic of "{topic}" to answer the question.' return prompt


Nesse caso, removi a parte usando mensagens do Discord, mas você ainda pode seguir a lógica se chat_prompt != None.


Além disso, precisaremos de uma função que divida a resposta recebida da API ChatGPT em mensagens do Telegram (no máximo 4096 caracteres):


 def telegram_message_format(text): max_message_length = 4096 if len(text) > max_message_length: parts = [] while len(text) > max_message_length: parts.append(text[:max_message_length]) text = text[max_message_length:] parts.append(text) return parts else: return [text]


O bot inicia com uma sequência típica de passos, atribuindo duas funções a serem acionadas pelo comando /start e recebendo uma mensagem pessoal do usuário:


 bot = telegram.Bot(token=token) updater = Updater(token=token, use_context=True) dispatcher = updater.dispatcher dispatcher.add_handler(CommandHandler("start", start, filters=Filters.chat_type.private)) dispatcher.add_handler(MessageHandler(~Filters.command & Filters.text, message_handler)) updater.start_polling()


O código para responder a /start é direto:


 def start(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text=start_message)


E para responder a uma mensagem de formato livre, não é muito claro.


Em primeiro lugar , para evitar o bloqueio de threads de usuários diferentes, vamos "separá-los" imediatamente em processos independentes usando a biblioteca de threading .


 def message_handler(update, context): thread = threading.Thread(target=long_running_task, args=(update, context)) thread.start()


Em segundo lugar , toda a lógica acontecerá dentro da função long_running_task . Eu envolvi intencionalmente os fragmentos principais em try/except para localizar erros facilmente ao modificar o código do bot.


  • Primeiro, recuperamos a mensagem e tratamos do erro se o usuário enviar um arquivo ou imagem em vez de uma mensagem.
  • Em seguida, procuramos as perguntas-respostas mais semelhantes usando search_similar .
  • Depois disso, coletamos todas as perguntas-respostas em um texto usando collect_text_qa .
  • E geramos o prompt final para a API ChatGPT usando collect_full_prompt .


 def long_running_task(update, context): user = update.effective_user context.bot.send_message(chat_id=user.id, text='🕰️⏰🕙⏱️⏳...') try: question = update.message.text.strip() except Exception as e: context.bot.send_message(chat_id=user.id, text=f"🤔It seems like you're sending not text to the bot. Currently, the bot can only work with text requests.") return try: qa_found = search_similar(df_qa, question, n=num_top_qa) qa_prompt = collect_text_qa(qa_found) full_prompt = collect_full_prompt(question, qa_prompt) except Exception as e: context.bot.send_message(chat_id=user.id, text=f"Search failed. Debug needed.") return


Como pode haver erros ao substituir a base de conhecimento e o tópico pelos seus, por exemplo, devido à formatação, um erro legível por humanos é exibido.


Em seguida, a solicitação é enviada para a API do ChatGPT com uma mensagem de sistema líder que já se provou: " Você é um assistente útil". A saída resultante é dividida em várias mensagens, se necessário, e enviada de volta ao usuário.


 try: print(full_prompt) completion = call_chatgpt( model=model, n=1, messages=[{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": full_prompt}] ) result = completion['choices'][0]['message']['content'] except Exception as e: context.bot.send_message(chat_id=user.id, text=f'It seems like the OpenAI service is responding with errors. Try sending the request again.') return parts = telegram_message_format(result) for part in parts: update.message.reply_text(part, reply_to_message_id=update.message.message_id)



Isso conclui a parte com o código.

Protótipo

Agora, um protótipo desse bot está disponível em um formato limitado no link a seguir . Como a API é paga, você pode fazer até 3 requisições por dia, mas acho que não vai limitar ninguém, pois o mais interessante não é um bot especializado focado em um tema restrito, mas sim o código do projeto AnythingGPT, que está disponível no GitHub com uma breve instrução de como criar seu próprio bot para resolver sua tarefa específica com sua base de conhecimento baseada neste exemplo. Se você leu até o final, espero que este artigo tenha sido útil para você.



Captura de tela da comunicação diária com um bot