paint-brush
Cómo crear su propio AnythingGPT: un bot que responde de la manera que usted quierepor@balakhonoff
1,538 lecturas
1,538 lecturas

Cómo crear su propio AnythingGPT: un bot que responde de la manera que usted quiere

por Kirill Balakhonov24m2023/07/25
Read on Terminal Reader

Demasiado Largo; Para Leer

Hablaremos sobre la creación de una versión personalizada de ChatGPT que responda preguntas, teniendo en cuenta una gran base de conocimiento. Usaremos incrustaciones contextuales de OpenAI (para una búsqueda verdaderamente de alta calidad de preguntas relevantes de la base de conocimiento). Daremos formato a las respuestas en lenguaje humano natural.
featured image - Cómo crear su propio AnythingGPT: un bot que responde de la manera que usted quiere
Kirill Balakhonov HackerNoon profile picture
0-item
1-item
2-item


¡Hola a todos! Recientemente, apliqué una solución interesante durante mi práctica que quería probar desde hace mucho tiempo, y ahora estoy listo para explicar cómo puedes crear algo similar para cualquier otra tarea. Hablaremos sobre la creación de una versión personalizada de ChatGPT que responda preguntas, teniendo en cuenta una gran base de conocimientos que no está limitada en longitud por el tamaño de la indicación (lo que significa que no podrá simplemente agregar toda la información antes de cada pregunta a ChatGPT).


Para lograr esto, utilizaremos incrustaciones contextuales de OpenAI (para una búsqueda verdaderamente de alta calidad de preguntas relevantes de la base de conocimientos) y la propia API de ChatGPT (para dar formato a las respuestas en lenguaje humano natural).


Además, se supone que el asistente puede responder no solo las preguntas de preguntas y respuestas explícitas , sino también las preguntas que podría responder una persona familiarizada con las preguntas y respuestas. Si está interesado en aprender a crear bots simples que respondan utilizando una gran base de conocimientos, bienvenido a los detalles.


Me gustaría señalar que hay algunos proyectos de biblioteca que intentan resolver esta tarea en forma de marco, por ejemplo, LangChain , y también probé usarlo. Sin embargo, como cualquier marco que se encuentra en una etapa temprana de desarrollo, en algunos casos, tiende a limitar en lugar de simplificar las cosas. En particular, desde el comienzo de la resolución de esta tarea, entendí lo que quería hacer con los datos y supe cómo hacerlo por mí mismo (incluida la búsqueda basada en el contexto, establecer el contexto correcto en las indicaciones, combinar fuentes de información).


Pero no pude configurar el marco para hacer exactamente eso con un nivel de calidad aceptable, y depurar el marco parecía una exageración para esta tarea. Al final, creé mi propio código repetitivo y quedé satisfecho con este enfoque.

Tarea

Permítame describir brevemente la tarea en la que estaba trabajando, y puede usar el mismo código en sus propias tareas, reemplazando las fuentes de datos y las indicaciones con las que más le convengan . Todavía tendrás control total sobre la lógica del bot.


Cuando escribo código, a menudo uso ChatGPT (y no me avergüenzo de ello🙂). Sin embargo, debido a la falta de datos para el año 2022+, a veces hay problemas con tecnologías relativamente nuevas.


En particular, al desarrollar subgrafos para el protocolo The Graph (la forma más popular de construir ETL para recuperar datos indexados de cadenas de bloques compatibles con EVM, puede leer más sobre esto en mis artículos anteriores [ 1 ] y [ 2 ]), las bibliotecas mismas han sufrido varios cambios de compatibilidad importantes. Las respuestas "antiguas" de ChatGPT ya no son útiles, y tengo que buscar las respuestas correctas en la escasa documentación o, en el peor de los casos, en el Discord de los desarrolladores, lo cual no es muy conveniente (no es como StackOverflow).


La segunda parte del problema es que cada vez que necesita proporcionar el contexto de conversación correctamente porque ChatGPT a menudo se desvía del tema de los subgráficos, saltando a GraphQL, SQL o matemáticas superiores ("El gráfico", "subgráficos", etc. no son términos únicos y tienen muchas interpretaciones y temas diferentes).


Por lo tanto, después de un breve período de lucha con ChatGPT para corregir errores en el código del subgráfico, decidí crear mi propio bot SubgraphGPT , que siempre estará en el contexto correcto y tratará de responder, teniendo en cuenta la base de conocimiento y los mensajes de discordia de los desarrolladores.


PD. Trabajo como gerente principal de productos en chainstack.com , un proveedor de infraestructura Web3, y soy responsable del desarrollo del servicio de alojamiento de subgráficos . Así que tengo que trabajar mucho con subgráficos, ayudando a los usuarios a entender esta tecnología relativamente nueva.

Solución de primer nivel

Al final, para solucionar este problema, decidí usar dos fuentes:

  1. Una base de conocimiento compilada manualmente de preguntas y respuestas, seleccionada en modo semi ciego (a menudo tomaba el título del tema de la documentación como la pregunta y el párrafo completo de información como la respuesta).

  2. Mensajes exportados de los desarrolladores del protocolo Discord de los últimos 2 años (para cubrir el período faltante desde finales de 2021).


A continuación, se utilizaron diferentes enfoques para que cada fuente redactara una solicitud a la API de ChatGPT, específicamente:


Para las preguntas y respuestas compiladas manualmente,

  1. para cada pregunta se genera un embedding contextual (un vector que describe esta pregunta en un estado multidimensional), obtenido a través del modelo text-embedding-ada-002 ,

  2. luego, usando una función de búsqueda de distancia coseno, se encuentran las 3 preguntas más similares de la base de conocimientos (en lugar de 3, se puede usar el número más adecuado para su conjunto de datos),

  3. las respuestas a estas 3 preguntas se agregan al mensaje final con una descripción aproximada de " Use este fragmento de preguntas y respuestas solo si es relevante para la pregunta dada ".


    Para los mensajes exportados desde Discord, se utilizó el siguiente algoritmo:

  4. para cada mensaje que contiene un signo de interrogación, también se genera una incrustación contextual (usando el mismo modelo),

  5. luego, de manera similar, se seleccionan las 5 preguntas más similares,

  6. y como contexto para la respuesta, se agregan los 20 mensajes que siguen a esa pregunta, los cuales se supone que tienen cierta probabilidad de contener la respuesta a la pregunta,

  7. y esta información se agregó al aviso final aproximadamente así: "Si no encontró una respuesta explícita a la pregunta en el fragmento de preguntas y respuestas adjunto, los siguientes fragmentos de chat del desarrollador pueden serle útiles para responder la pregunta original..."


Además, si el tema no se proporciona explícitamente, la presencia de fragmentos de preguntas y respuestas y chats puede generar ambigüedad en las respuestas, que pueden verse, por ejemplo, de la siguiente manera:



Entonces, entiende que la pregunta fue desvinculada del contexto y la respuesta también fue aceptada desvinculada del contexto. Luego se dijo que tales datos pueden ser utilizados, y lo resume de la siguiente manera:

  1. En realidad, la respuesta puede ser así...
  2. Y si consideramos el contexto, entonces será así...


Para evitar esto, introducimos el concepto de un tema, que se define explícitamente y se inserta al comienzo del indicador como:

"Necesito obtener una respuesta a una pregunta relacionada con el tema 'El desarrollo del subgrafo del gráfico': {{{¿qué es un subgrafo?}}}"


Además, en la última oración, también agrego esto:

Finalmente, solo si la información anterior no es suficiente, puede usar su conocimiento en el tema 'El desarrollo del subgrafo del gráfico' para responder la pregunta.


Al final, el indicador completo (excluyendo la parte obtenida de los chats) queda de la siguiente manera:

 ==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.==


La respuesta a la solicitud anterior con este mensaje generado semiautomáticamente en la entrada parece correcta desde el principio:



En este caso, el bot responde de inmediato con la clave correcta y agrega información más relevante, por lo que la respuesta no parece tan sencilla como en las preguntas y respuestas (recuerdo que esta pregunta está exactamente en la lista de preguntas y respuestas), pero con explicaciones razonables que abordan en parte las siguientes preguntas.

Código fuente

Debo señalar de inmediato que habrá un enlace al repositorio al final , por lo que puede ejecutar el bot tal como está, reemplazando el "tema" con el suyo propio, el archivo de la base de conocimientos de preguntas y respuestas con el suyo propio y proporcionando sus propias claves API para OpenAI y el bot de Telegram. Por lo tanto, la descripción aquí no pretende corresponder completamente con el código fuente en GitHub, sino resaltar los aspectos principales del código.

1 - preparar el entorno virtual

Vamos a crear un nuevo entorno virtual e instalar las dependencias desde requirements.txt:


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

2 - Base de conocimiento, recopilada manualmente

Como se mencionó anteriormente, se supone que existe una lista de preguntas y respuestas, en este caso en formato de archivo Excel del siguiente tipo:



Para encontrar la pregunta más similar a la dada, necesitamos agregar una incrustación de la pregunta (un vector multidimensional en el espacio de estado) a cada línea de este archivo. Usaremos el archivo add_embeddings.py para esto. El guión consta de varias partes simples.

Importación de bibliotecas y lectura de argumentos de línea 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


A continuación, lea el archivo en un marco de datos de pandas y filtre las preguntas en función de la presencia de un signo de interrogación. Este fragmento de código es común para manejar una base de conocimientos, así como flujos de mensajes sin procesar de Discord, por lo que suponiendo que las preguntas a menudo se dupliquen, decidí mantener un método tan simple de filtrado aproximado sin preguntas.


 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('\?')]


Y finalmente, una función para generar una incrustación llamando a la API del modelo text-embedding-ada-002 , un par de solicitudes repetidas ya que la API puede sobrecargarse ocasionalmente y puede responder con un error, y aplicar esta función a cada fila del marco de datos.


 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)


Al final, este script se puede llamar con el siguiente comando:


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


configurando la clave API de OpenAI, el archivo con la base de conocimiento y el nombre de la columna donde se encuentra el texto de la pregunta. El archivo final creado, subgraphs_faq._question_embed.csv, contiene las columnas "Pregunta", "Respuesta" y "ada_embedding ".

3 - Recopilación de datos de Discord (opcional)

Si está interesado en un bot simple que responda solo en base a la base de conocimiento recopilada manualmente, puede omitir esta y la siguiente sección. Sin embargo, proporcionaré brevemente ejemplos de código aquí para recopilar datos tanto de un canal de Discord como de un grupo de Telegram. El archivo discord-channel-data-collection.py consta de dos partes. La primera parte incluye la importación de bibliotecas y la inicialización de argumentos de la línea de comandos:


 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


La segunda es la función para recuperar datos del canal y guardarlos en un marco de datos de pandas, así como su llamada con parámetros específicos.


 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)


De la información útil aquí, hay un detalle que no puedo encontrar cada vez que lo necesito: obtener una clave de autorización. Teniendo en cuenta que channel_id se puede obtener de la URL del canal Discord abierto en el navegador (el último número largo en el enlace), la autorización_key solo se puede encontrar comenzando a escribir un mensaje en el canal, luego usando las herramientas de desarrollo para encontrar el evento llamado " escribir " en la sección Red y extraer el parámetro del encabezado.



Después de recibir estos parámetros, puede ejecutar el siguiente comando para recopilar todos los mensajes del canal (sustituya sus propios valores):


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


4 - Recopilación de datos de Telegram

Dado que a menudo descargo varios datos de chats/canales en Telegram, también decidí proporcionar un código para esto, que genera un archivo CSV de formato similar (compatible en términos del script add_embeddings.py ). Entonces, el script telegram-group-data-collection.py se ve de la siguiente manera. Importación de bibliotecas e inicialización de argumentos desde la línea de comandos:


 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 puede ver, no puede simplemente descargar todos los mensajes del chat sin autorizarse como primera persona. En otras palabras, además de crear una aplicación a través de https://my.telegram.org/apps (obteniendo APP_ID y APP_HASH), también deberá usar su número de teléfono y contraseña para crear una instancia de la clase TelegramClient de la biblioteca de Telethon.


Además, necesitará el nombre de grupo público del chat de Telegram y especificar explícitamente la cantidad de mensajes más recientes que se recuperarán. En general, he realizado este procedimiento muchas veces con cualquier cantidad de mensajes exportados sin recibir prohibiciones temporales o permanentes de la API de Telegram, a diferencia de cuando uno envía mensajes con demasiada frecuencia desde una cuenta.


La segunda parte del script contiene la función real para exportar mensajes y su ejecución (con el filtrado necesario para evitar errores críticos que detendrían la recopilación a la mitad):


 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())


Al final, este script se puede ejecutar con el siguiente comando (reemplace los valores con los suyos):


 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 de Telegram que realmente responde preguntas

La mayoría de las veces, envuelvo mis proyectos favoritos en bots de Telegram porque requiere un esfuerzo mínimo para lanzarlos y muestra potencial de inmediato. En este caso, hice lo mismo. Debo decir que el código del bot no contiene todos los casos de esquina que uso en la versión de producción del bot SubgraphGPT , ya que tiene bastante lógica heredada de otro proyecto favorito mío. En su lugar, dejé la cantidad mínima de código básico que debería ser fácil de modificar según sus necesidades.

El script telegram-bot.py consta de varias partes. Primero, como antes, se importan las bibliotecas y se inicializan los argumentos de la línea de comandos.


 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


Tenga en cuenta que, en este caso, también necesitará una clave API de OpenAI, ya que para encontrar la pregunta más similar a la que acaba de ingresar el usuario desde la base de conocimientos, primero debe obtener la incrustación de esa pregunta llamando a la API como lo hicimos para la propia base de conocimientos.


Además, necesitarás:


  • telegram_bot_token : un token para el bot de Telegram de BotFather
  • archivo : una ruta al archivo de la base de conocimientos (salto intencionalmente el caso con mensajes de Discord aquí, ya que asumo que es una tarea de nicho, pero se pueden integrar fácilmente en el código si es necesario)
  • tema : la formulación textual del tema (mencionado al principio del artículo) en el que operará el bot
  • start_message : el mensaje que verá el usuario que hizo clic en /start (de forma predeterminada, "¡Hola, mundo!")
  • modelo - la elección del modelo (establecido por defecto)
  • num_top_qa : la cantidad de preguntas y respuestas más similares de la base de conocimientos que se usarán como contexto para la solicitud de ChatGPT


Luego sigue la carga del archivo de la base de conocimientos y la inicialización de las incrustaciones de preguntas.


 # 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 realizar una solicitud a la API de ChatGPT, sabiendo que a veces responde con un error debido a una sobrecarga, utilizo una función con reintento automático de solicitud en caso de error.


 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)


De acuerdo con la recomendación de OpenAI, antes de convertir el texto en incrustaciones, las líneas nuevas deben reemplazarse con espacios.


 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 las preguntas más similares, calculamos la distancia del coseno entre las incrustaciones de dos preguntas, tomadas directamente de la 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


Después de recibir una lista de los pares de preguntas y respuestas más similares al dado, puede compilarlos en un texto, marcándolo de manera que ChatGPT pueda determinar sin ambigüedades qué es 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


Después de eso, ya es necesario reunir las "piezas" del mensaje descrito al comienzo del artículo en un 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


En este caso, eliminé la parte usando mensajes de Discord, pero aún puedes seguir la lógica si chat_prompt != None.


Además, necesitaremos una función que divida la respuesta recibida de la API de ChatGPT en mensajes de Telegram (no más de 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]


El bot comienza con una secuencia típica de pasos, asignando dos funciones que se activarán con el comando /start y recibiendo un mensaje personal del usuario:


 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()


El código para responder a /start es sencillo:


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


Y para responder a un mensaje de forma libre, no está del todo claro.


En primer lugar , para evitar el bloqueo de subprocesos de diferentes usuarios, "sepárelos" inmediatamente en procesos independientes utilizando la biblioteca de subprocesos .


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


En segundo lugar , toda la lógica ocurrirá dentro de la función long_running_task . Envolví intencionalmente los fragmentos principales en try/except para localizar fácilmente los errores al modificar el código del bot.


  • Primero, recuperamos el mensaje y manejamos el error si el usuario envía un archivo o una imagen en lugar de un mensaje.
  • Luego, buscamos las preguntas-respuestas más similares usando search_similar .
  • Después de eso, recopilamos todas las preguntas y respuestas en un solo texto usando collect_text_qa .
  • Y generamos el aviso final para la API de 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


Dado que puede haber errores al reemplazar la base de conocimientos y el tema con los suyos, por ejemplo, debido al formato, se muestra un error legible por humanos.


A continuación, la solicitud se envía a la API de ChatGPT con un mensaje principal del sistema que ya ha demostrado su eficacia: " Eres un asistente útil". El resultado resultante se divide en varios mensajes si es necesario y se envía de vuelta al usuario.


 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)



Eso concluye la parte con el código.

Prototipo

Ahora, un prototipo de dicho bot está disponible en un formato limitado en el siguiente enlace . Como la API es paga, puedes hacer hasta 3 solicitudes por día, pero no creo que limite a nadie, ya que lo más interesante no es un bot especializado y enfocado en un tema limitado, sino el código del proyecto AnythingGPT, que está disponible en GitHub con una breve instrucción sobre cómo crear tu propio bot para resolver tu tarea específica con tu base de conocimiento basada en este ejemplo. Si has leído hasta el final, espero que este artículo te haya sido de ayuda.



Captura de pantalla de la comunicación diaria con un bot