Lección 1 · 10 min · Gratis

Introducción a la API de Assistants

La nueva API de Assistants es una evolución con estado de nuestra API de Chat Completions, diseñada para simplificar la creación de experiencias tipo asistente y permitir a los desarrolladores acceder a herramientas potentes como Code Interpreter y File Search.

Assistants API Diagram

API de Chat Completions vs. API de Assistants

Las primitivas de la API de Chat Completions son Messages, sobre las cuales realizas una Completion con un Model (gpt-4o, gpt-4o-mini, etc.). Es ligera y potente, pero inherentemente sin estado, lo que significa que debes gestionar manualmente el estado de la conversación, las definiciones de herramientas, los documentos de recuperación y la ejecución de código.

Las primitivas de la API de Assistants son:

  • Assistants, que encapsulan un modelo base, instrucciones, herramientas y documentos (de contexto),
  • Threads, que representan el estado de una conversación, y
  • Runs, que impulsan la ejecución de un Assistant en un Thread, incluyendo respuestas textuales y el uso de herramientas en varios pasos.

Veremos cómo se pueden usar para crear experiencias potentes y con estado.

Configuración

SDK de Python

Nota Hemos actualizado nuestro SDK de Python para añadir soporte a la API de Assistants, por lo que necesitarás actualizarlo a la última versión (1.59.4 al momento de escribir esto).

!pip install --upgrade openai
Requirement already satisfied: openai in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (1.59.4)
Requirement already satisfied: anyio<5,>=3.5.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (3.7.1)
Requirement already satisfied: distro<2,>=1.7.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (1.9.0)
Requirement already satisfied: httpx<1,>=0.23.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (0.27.0)
Requirement already satisfied: jiter<1,>=0.4.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (0.7.0)
Requirement already satisfied: pydantic<3,>=1.9.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (2.8.2)
Requirement already satisfied: sniffio in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (1.3.1)
Requirement already satisfied: tqdm>4 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (4.66.4)
Requirement already satisfied: typing-extensions<5,>=4.11 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (4.12.2)
Requirement already satisfied: idna>=2.8 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from anyio<5,>=3.5.0->openai) (3.7)
Requirement already satisfied: certifi in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->openai) (2024.7.4)
Requirement already satisfied: httpcore==1.* in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->openai) (1.0.5)
Requirement already satisfied: h11<0.15,>=0.13 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai) (0.14.0)
Requirement already satisfied: annotated-types>=0.4.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from pydantic<3,>=1.9.0->openai) (0.7.0)
Requirement already satisfied: pydantic-core==2.20.1 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from pydantic<3,>=1.9.0->openai) (2.20.1)

Y asegúrate de que esté actualizado ejecutando:

!pip show openai | grep Version
Version: 1.59.4

Ayudante de impresión bonita

import json

def show_json(obj):
    display(json.loads(obj.model_dump_json()))

Ejemplo completo con la API de Assistants

Assistants

La forma más fácil de empezar con la API de Assistants es a través del Assistants Playground.

Assistants Playground

¡Empecemos creando un asistente! Crearemos un tutor de matemáticas como en nuestra documentación.

Creating New Assistant

También puedes crear Assistants directamente a través de la API de Assistants, así:

from openai import OpenAI
import os

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"))


assistant = client.beta.assistants.create(
    name="Math Tutor",
    instructions="You are a personal math tutor. Answer questions briefly, in a sentence or less.",
    model="gpt-4o",
)
show_json(assistant)
{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'created_at': 1736340398,
 'description': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4o',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [],
 'response_format': 'auto',
 'temperature': 1.0,
 'tool_resources': {'code_interpreter': None, 'file_search': None},
 'top_p': 1.0} 'tools': [],
 'response_format': 'auto',
 'temperature': 1.0,
 'tool_resources': {'code_interpreter': None, 'file_search': None},
 'top_p': 1.0}

Independientemente de si creas tu Assistant a través del Dashboard o con la API, querrás mantener un registro del ID del Assistant. Así es como te referirás a tu Assistant a lo largo de los Threads y Runs.

A continuación, crearemos un nuevo Thread y le añadiremos un Message. Esto mantendrá el estado de nuestra conversación, para que no tengamos que volver a enviar todo el historial de mensajes cada vez.

Threads

Crea un nuevo thread:

thread = client.beta.threads.create()
show_json(thread)
{'id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',
 'created_at': 1736340398,
 'metadata': {},
 'object': 'thread',
 'tool_resources': {'code_interpreter': None, 'file_search': None}} 'object': 'thread',
 'tool_resources': {'code_interpreter': None, 'file_search': None}}

Luego añade el Message al thread:

message = client.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content="I need to solve the equation `3x + 11 = 14`. Can you help me?",
)
show_json(message)
{'id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',
 'assistant_id': None,
 'attachments': [],
 'completed_at': None,
 'attachments': [],
 'completed_at': None,
 'content': [{'text': {'annotations': [],
    'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},
   'type': 'text'}],
 'created_at': 1736340400,
 'incomplete_at': None,
 'incomplete_details': None,
 'metadata': {},
 'object': 'thread.message',
 'role': 'user',
 'run_id': None,
 'status': None,
 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}

Nota Aunque ya no envíes todo el historial cada vez, se te seguirá cobrando por los tokens de todo el historial de la conversación con cada Run.

Runs

¡Observa cómo el Thread que creamos no está asociado con el Assistant que creamos anteriormente! Los Threads existen independientemente de los Assistants, lo que puede ser diferente de lo que esperarías si has usado ChatGPT (donde un thread está vinculado a un modelo/GPT).

Para obtener una finalización de un Assistant para un Thread dado, debemos crear un Run. Crear un Run indicará a un Assistant que debe revisar los mensajes en el Thread y tomar acción: ya sea añadiendo una sola respuesta o usando herramientas.

Nota Los Runs son una diferencia clave entre la API de Assistants y la API de Chat Completions. Mientras que en Chat Completions el modelo solo responderá con un único mensaje, en la API de Assistants un Run puede resultar en que un Assistant use una o varias herramientas, y potencialmente añada varios mensajes al Thread.

Para que nuestro Assistant responda al usuario, creemos el Run. Como se mencionó anteriormente, debes especificar tanto el Assistant como el Thread.

run = client.beta.threads.runs.create(
    thread_id=thread.id,
    assistant_id=assistant.id,
)
show_json(run)
{'id': 'run_qVYsWok6OCjHxkajpIrdHuVP',
 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'cancelled_at': None,
 'completed_at': None,
 'created_at': 1736340403,
 'expires_at': 1736341003,
 'failed_at': None,
 'incomplete_details': None,
 'incomplete_details': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'metadata': {},
 'model': 'gpt-4o',
 'object': 'thread.run',
 'parallel_tool_calls': True,
 'parallel_tool_calls': True,
 'required_action': None,
 'response_format': 'auto',
 'response_format': 'auto',
 'started_at': None,
 'status': 'queued',
 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',
 'tool_choice': 'auto',
 'tools': [],
 'truncation_strategy': {'type': 'auto', 'last_messages': None},
 'usage': None,
 'temperature': 1.0,
 'top_p': 1.0,
 'tool_resources': {}}

A diferencia de crear una finalización en la API de Chat Completions, crear un Run es una operación asíncrona. Devolverá inmediatamente los metadatos del Run, que incluyen un status que inicialmente se establecerá en queued. El status se actualizará a medida que el Assistant realice operaciones (como usar herramientas y añadir mensajes).

Para saber cuándo el Assistant ha terminado de procesar, podemos sondear el Run en un bucle. (¡El soporte para streaming estará disponible pronto!) Aunque aquí solo estamos verificando un estado queued o in_progress, en la práctica un Run puede pasar por una variedad de cambios de estado que puedes optar por mostrar al usuario. (Estos se llaman Steps y se cubrirán más adelante).

import time

def wait_on_run(run, thread):
    while run.status == "queued" or run.status == "in_progress":
        run = client.beta.threads.runs.retrieve(
            thread_id=thread.id,
            run_id=run.id,
        )
        time.sleep(0.5)
    return run
run = wait_on_run(run, thread)
show_json(run)
{'id': 'run_qVYsWok6OCjHxkajpIrdHuVP',
 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'cancelled_at': None,
 'completed_at': 1736340406,
 'created_at': 1736340403,
 'expires_at': None,
 'failed_at': None,
 'incomplete_details': None,
 'incomplete_details': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'metadata': {},
 'model': 'gpt-4o',
 'object': 'thread.run',
 'parallel_tool_calls': True,
 'parallel_tool_calls': True,
 'required_action': None,
 'response_format': 'auto',
 'started_at': 1736340405,
 'status': 'completed',
 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',
 'tool_choice': 'auto',
 'tools': [],
 'truncation_strategy': {'type': 'auto', 'last_messages': None},
 'usage': {'completion_tokens': 35,
  'prompt_tokens': 66,
  'total_tokens': 101,
  'prompt_token_details': {'cached_tokens': 0},
  'completion_tokens_details': {'reasoning_tokens': 0}},
 'temperature': 1.0,
 'top_p': 1.0,
 'tool_resources': {}}

Messages

Ahora que el Run ha terminado, podemos listar los Messages en el Thread para ver qué añadió el Assistant.

messages = client.beta.threads.messages.list(thread_id=thread.id)
show_json(messages)
{'data': [{'id': 'msg_A5eAN6ZAJDmFBOYutEm5DFCy',
   'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
   'attachments': [],
   'completed_at': None,
   'content': [{'text': {'annotations': [],
      'value': 'Sure! Subtract 11 from both sides to get \\(3x = 3\\), then divide by 3 to find \\(x = 1\\).'},
     'type': 'text'}],
   'created_at': 1736340405,
   'incomplete_at': None,
   'incomplete_details': None,
   'metadata': {},
   'object': 'thread.message',
   'role': 'assistant',
   'run_id': 'run_qVYsWok6OCjHxkajpIrdHuVP',
   'status': None,
   'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'},
  {'id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',
   'assistant_id': None,
   'attachments': [],
   'completed_at': None,
   'attachments': [],
   'completed_at': None,
   'content': [{'text': {'annotations': [],
      'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},
     'type': 'text'}],
   'created_at': 1736340400,
   'incomplete_at': None,
   'incomplete_details': None,
   'metadata': {},
   'object': 'thread.message',
   'role': 'user',
   'run_id': None,
   'status': None,
   'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}],
 'object': 'list',
 'first_id': 'msg_A5eAN6ZAJDmFBOYutEm5DFCy',
 'last_id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',
 'has_more': False}

Como puedes ver, los Messages están ordenados en orden cronológico inverso; esto se hizo para que los resultados más recientes siempre estén en el primer page (ya que los resultados pueden ser paginados). Ten esto en cuenta, ya que es el orden opuesto a los mensajes en la API de Chat Completions.

¡Pidámosle a nuestro Assistant que explique el resultado un poco más!

# Create a message to append to our thread
message = client.beta.threads.messages.create(
    thread_id=thread.id, role="user", content="Could you explain this to me?"
)

# Execute our run
run = client.beta.threads.runs.create(
    thread_id=thread.id,
    assistant_id=assistant.id,
)

# Wait for completion
wait_on_run(run, thread)

# Retrieve all the messages added after our last user message
messages = client.beta.threads.messages.list(
    thread_id=thread.id, order="asc", after=message.id
)
show_json(messages)
{'data': [{'id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',
   'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
   'attachments': [],
   'completed_at': None,
   'content': [{'text': {'annotations': [],
      'value': 'Certainly! To isolate \\(x\\), first subtract 11 from both sides of the equation \\(3x + 11 = 14\\), resulting in \\(3x = 3\\). Then, divide both sides by 3 to solve for \\(x\\), giving you \\(x = 1\\).'},
     'type': 'text'}],
   'created_at': 1736340414,
   'incomplete_at': None,
   'incomplete_details': None,
   'metadata': {},
   'object': 'thread.message',
   'role': 'assistant',
   'run_id': 'run_lJsumsDtPTmdG3Enx2CfYrrq',
   'status': None,
   'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}],
 'object': 'list',
 'first_id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',
 'last_id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',
 'has_more': False}

Esto puede parecer muchos pasos para obtener una respuesta, especialmente para este ejemplo simple. Sin embargo, pronto verás cómo podemos añadir una funcionalidad muy potente a nuestro Assistant sin cambiar mucho código.

Ejemplo

Veamos cómo podríamos juntar todo esto. A continuación, se muestra todo el código que necesitas para usar un Assistant que hayas creado.

Como ya hemos creado nuestro Math Assistant, he guardado su ID en MATH_ASSISTANT_ID. Luego definí dos funciones:

  • submit_message: crea un Message en un Thread, luego inicia (y devuelve) un nuevo Run
  • get_response: devuelve la lista de Messages en un Thread
from openai import OpenAI

MATH_ASSISTANT_ID = assistant.id  # or a hard-coded ID like "asst-..."

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "<your OpenAI API key if not set as env var>"))

def submit_message(assistant_id, thread, user_message):
    client.beta.threads.messages.create(
        thread_id=thread.id, role="user", content=user_message
    )
    return client.beta.threads.runs.create(
        thread_id=thread.id,
        assistant_id=assistant_id,
    )


def get_response(thread):
    return client.beta.threads.messages.list(thread_id=thread.id, order="asc")

También he definido una función create_thread_and_run que puedo reutilizar (que en realidad es casi idéntica a la función compuesta client.beta.threads.create_and_run en nuestra API ;)). Finalmente, podemos enviar nuestras solicitudes de usuario simuladas a un nuevo Thread.

Observa cómo todas estas llamadas a la API son operaciones asíncronas; ¡esto significa que realmente obtenemos un comportamiento asíncrono en nuestro código sin el uso de bibliotecas asíncronas! (por ejemplo, asyncio)

def create_thread_and_run(user_input):
    thread = client.beta.threads.create()
    run = submit_message(MATH_ASSISTANT_ID, thread, user_input)
    return thread, run


# Emulating concurrent user requests
thread1, run1 = create_thread_and_run(
    "I need to solve the equation `3x + 11 = 14`. Can you help me?"
)
thread2, run2 = create_thread_and_run("Could you explain linear algebra to me?")
thread3, run3 = create_thread_and_run("I don't like math. What can I do?")

# Now all Runs are executing...

Una vez que todos los Runs estén en marcha, podemos esperar a cada uno y obtener las respuestas.

import time

# Pretty printing helper
def pretty_print(messages):
    print("# Messages")
    for m in messages:
        print(f"{m.role}: {m.content[0].text.value}")
    print()


# Waiting in a loop
def wait_on_run(run, thread):
    while run.status == "queued" or run.status == "in_progress":
        run = client.beta.threads.runs.retrieve(
            thread_id=thread.id,
            run_id=run.id,
        )
        time.sleep(0.5)
    return run


# Wait for Run 1
run1 = wait_on_run(run1, thread1)
pretty_print(get_response(thread1))

# Wait for Run 2
run2 = wait_on_run(run2, thread2)
pretty_print(get_response(thread2))

# Wait for Run 3
run3 = wait_on_run(run3, thread3)
pretty_print(get_response(thread3))

# Thank our assistant on Thread 3 :)
run4 = submit_message(MATH_ASSISTANT_ID, thread3, "Thank you!")
run4 = wait_on_run(run4, thread3)
pretty_print(get_response(thread3))
# Messages
user: I need to solve the equation `3x + 11 = 14`. Can you help me?
assistant: Sure! Subtract 11 from both sides to get \(3x = 3\), then divide by 3 to find \(x = 1\).

# Messages
user: Could you explain linear algebra to me?
assistant: Linear algebra is the branch of mathematics concerning vector spaces, linear transformations, and systems of linear equations, often represented with matrices.

# Messages
user: I don't like math. What can I do?
assistant: Try relating math to real-life interests or hobbies, practice with fun games or apps, and gradually build confidence with easier problems.

# Messages
user: I don't like math. What can I do?
assistant: Try relating math to real-life interests or hobbies, practice with fun games or apps, and gradually build confidence with easier problems.
user: Thank you!
assistant: You're welcome! If you have any more questions, feel free to ask!

¡Y listo!

Quizás hayas notado que este código no es específico de nuestro asistente de matemáticas en absoluto... ¡este código funcionará para cualquier nuevo asistente que crees simplemente cambiando el ID del asistente! Ese es el poder de la API de Assistants.

Herramientas

Una característica clave de la API de Assistants es la capacidad de equipar a nuestros Assistants con herramientas, como Code Interpreter, File Search y funciones personalizadas. Echemos un vistazo a cada una.

Code Interpreter

Equipemos a nuestro Math Tutor con la herramienta Code Interpreter, lo cual podemos hacer desde el Dashboard...

Enabling code interpreter

...o la API, usando el ID del Assistant.

assistant = client.beta.assistants.update(
    MATH_ASSISTANT_ID,
    tools=[{"type": "code_interpreter"}],
)
show_json(assistant)
{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'created_at': 1736340398,
 'description': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4o',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [{'type': 'code_interpreter'}],
 'response_format': 'auto',
 'temperature': 1.0,
 'tool_resources': {'code_interpreter': {'file_ids': []}, 'file_search': None},
 'top_p': 1.0} 'tools': [{'type': 'code_interpreter'}],
 'response_format': 'auto',
 'temperature': 1.0,
 'tool_resources': {'code_interpreter': {'file_ids': []}, 'file_search': None},
 'top_p': 1.0}

Ahora, pidámosle al Assistant que use su nueva herramienta.

thread, run = create_thread_and_run(
    "Generate the first 20 fibbonaci numbers with code."
)
run = wait_on_run(run, thread)
pretty_print(get_response(thread))
# Messages
user: Generate the first 20 fibbonaci numbers with code.
assistant: The first 20 Fibonacci numbers are: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181.

¡Y eso es todo! El Assistant usó Code Interpreter en segundo plano y nos dio una respuesta final.

Para algunos casos de uso, esto puede ser suficiente; sin embargo, si queremos más detalles sobre lo que precisamente está haciendo un Assistant, podemos echar un vistazo a los Steps de un Run.

Steps

Un Run se compone de uno o más Steps. Al igual que un Run, cada Step tiene un status que puedes consultar. Esto es útil para mostrar el progreso de un Step a un usuario (por ejemplo, un spinner mientras el Assistant está escribiendo código o realizando una recuperación).

run_steps = client.beta.threads.runs.steps.list(
    thread_id=thread.id, run_id=run.id, order="asc"
)

Echemos un vistazo al step_details de cada Step.

for step in run_steps.data:
    step_details = step.step_details
    print(json.dumps(show_json(step_details), indent=4))
{'tool_calls': [{'id': 'call_E1EE1loDmcWoc7FpkOMKYj6n',
   'code_interpreter': {'input': 'def generate_fibonacci(n):\n    fib_sequence = [0, 1]\n    while len(fib_sequence) < n:\n        next_value = fib_sequence[-1] + fib_sequence[-2]\n        fib_sequence.append(next_value)\n    return fib_sequence\n\n# Generate the first 20 Fibonacci numbers\nfirst_20_fibonacci = generate_fibonacci(20)\nfirst_20_fibonacci',
    'outputs': []},
   'type': 'code_interpreter'}],
 'type': 'tool_calls'}
null
{'message_creation': {'message_id': 'msg_RzTnbBMmzDYHk79a0x9qM5uU'},
 'type': 'message_creation'}
null

Podemos ver el step_details para dos Steps:

  1. tool_calls (plural, ya que podría ser más de uno en un solo Step)
  2. message_creation

El primer Step es un tool_calls, específicamente usando el code_interpreter que contiene:

  • input, que fue el código Python generado antes de que se llamara a la herramienta, y
  • output, que fue el resultado de ejecutar el Code Interpreter.

El segundo Step es un message_creation, que contiene el message que se añadió al Thread para comunicar los resultados al usuario.

Búsqueda de archivos

Otra herramienta potente en la API de Assistants es la búsqueda de archivos. Esto permite subir archivos al Assistant para que los use como base de conocimientos al responder preguntas.

Enabling retrieval

# Upload the file
file = client.files.create(
    file=open(
        "data/language_models_are_unsupervised_multitask_learners.pdf",
        "rb",
    ),
    purpose="assistants",
)

# Create a vector store
vector_store = client.beta.vector_stores.create(
    name="language_models_are_unsupervised_multitask_learners",
)

# Add the file to the vector store
vector_store_file = client.beta.vector_stores.files.create_and_poll(
    vector_store_id=vector_store.id,
    file_id=file.id,
)

# Confirm the file was added
while vector_store_file.status == "in_progress":
    time.sleep(1)
if vector_store_file.status == "completed":
    print("File added to vector store")
elif vector_store_file.status == "failed":
    raise Exception("Failed to add file to vector store")

# Update Assistant
assistant = client.beta.assistants.update(
    MATH_ASSISTANT_ID,
    tools=[{"type": "code_interpreter"}, {"type": "file_search"}],
    tool_resources={
        "file_search":{
            "vector_store_ids": [vector_store.id]
        },
        "code_interpreter": {
            "file_ids": [file.id]
        }
    },
)
show_json(assistant)
File added to vector store
{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'created_at': 1736340398,
 'description': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4o',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'file_search',
   'file_search': {'max_num_results': None,
    'ranking_options': {'score_threshold': 0.0,
     'ranker': 'default_2024_08_21'}}}],
 'response_format': 'auto',
 'temperature': 1.0,
 'tool_resources': {'code_interpreter': {'file_ids': ['file-GQFm2i7N8LrAQatefWKEsE']},
  'file_search': {'vector_store_ids': ['vs_dEArILZSJh7J799QACi3QhuU']}},
 'top_p': 1.0}
thread, run = create_thread_and_run(
    "What are some cool math concepts behind this ML paper pdf? Explain in two sentences."
)
run = wait_on_run(run, thread)
pretty_print(get_response(thread))
# Messages
user: What are some cool math concepts behind this ML paper pdf? Explain in two sentences.
assistant: The paper explores the concept of multitask learning where a single model is used to perform various tasks, modeling the conditional distribution \( p(\text{output} | \text{input, task}) \), inspired by probabilistic approaches【6:10†source】. It also discusses the use of Transformer-based architectures and parallel corpus substitution in language models, enhancing their ability to generalize across domain tasks without explicit task-specific supervision【6:2†source】【6:5†source】.

Nota Hay más complejidades en la búsqueda de archivos, como las Annotations, que podrían cubrirse en otro cuaderno.

# Delete the vector store
client.beta.vector_stores.delete(vector_store.id)
VectorStoreDeleted(id='vs_dEArILZSJh7J799QACi3QhuU', deleted=True, object='vector_store.deleted')

Funciones

Como última herramienta potente para tu Assistant, puedes especificar Funciones personalizadas (muy parecido a la llamada a funciones en la API de Chat Completions). Durante un Run, el Assistant puede indicar que quiere llamar a una o más funciones que especificaste. Luego eres responsable de llamar a la Función y proporcionar la salida de vuelta al Assistant.

Veamos un ejemplo definiendo una Función display_quiz() para nuestro Math Tutor.

Esta función tomará un title y un array de questions, mostrará el cuestionario y obtendrá la entrada del usuario para cada uno:

  • title
  • questions
    • question_text
    • question_type: [MULTIPLE_CHOICE, FREE_RESPONSE]
    • choices: ["opción 1", "opción 2", ...]

Simularé las respuestas con get_mock_response.... Aquí es donde obtendrías la entrada real del usuario.

def get_mock_response_from_user_multiple_choice():
    return "a"


def get_mock_response_from_user_free_response():
    return "I don't know."


def display_quiz(title, questions):
    print("Quiz:", title)
    print()
    responses = []

    for q in questions:
        print(q["question_text"])
        response = ""

        # If multiple choice, print options
        if q["question_type"] == "MULTIPLE_CHOICE":
            for i, choice in enumerate(q["choices"]):
                print(f"{i}. {choice}")
            response = get_mock_response_from_user_multiple_choice()

        # Otherwise, just get response
        elif q["question_type"] == "FREE_RESPONSE":
            response = get_mock_response_from_user_free_response()

        responses.append(response)
        print()

    return responses

Así se vería un cuestionario de ejemplo:

responses = display_quiz(
    "Sample Quiz",
    [
        {"question_text": "What is your name?", "question_type": "FREE_RESPONSE"},
        {
            "question_text": "What is your favorite color?",
            "question_type": "MULTIPLE_CHOICE",
            "choices": ["Red", "Blue", "Green", "Yellow"],
        },
    ],
)
print("Responses:", responses)
Quiz: Sample Quiz

What is your name?

What is your favorite color?
0. Red
1. Blue
2. Green
3. Yellow

Responses: ["I don't know.", 'a']

Ahora, definamos la interfaz de esta función en formato JSON, para que nuestro Assistant pueda llamarla:

function_json = {
    "name": "display_quiz",
    "description": "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    "parameters": {
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "questions": {
                "type": "array",
                "description": "An array of questions, each with a title and potentially options (if multiple choice).",
                "items": {
                    "type": "object",
                    "properties": {
                        "question_text": {"type": "string"},
                        "question_type": {
                            "type": "string",
                            "enum": ["MULTIPLE_CHOICE", "FREE_RESPONSE"]
                        },
                        "choices": {"type": "array", "items": {"type": "string"}}
                    },
                    "required": ["question_text"]
                }
            }
        },
        "required": ["title", "questions"]
    }
}

Una vez más, actualicemos nuestro Assistant ya sea a través del Dashboard o de la API.

Enabling custom function

Nota Pegar el JSON de la función en el Dashboard fue un poco complicado debido a la indentación, etc. Simplemente le pedí a ChatGPT que formateara mi función igual que uno de los ejemplos en el Dashboard :)

assistant = client.beta.assistants.update(
    MATH_ASSISTANT_ID,
    tools=[
        {"type": "code_interpreter"},
        {"type": "file_search"},
        {"type": "function", "function": function_json},
    ],
)
show_json(assistant)
{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'created_at': 1736340398,
 'description': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4o',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'file_search',
   'file_search': {'max_num_results': None,
    'ranking_options': {'score_threshold': 0.0,
     'ranker': 'default_2024_08_21'}}},
  {'function': {'name': 'display_quiz',
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    'parameters': {'type': 'object',
     'properties': {'title': {'type': 'string'},
      'questions': {'type': 'array',
       'description': 'An array of questions, each with a title and potentially options (if multiple choice).',
       'items': {'type': 'object',
        'properties': {'question_text': {'type': 'string'},
         'question_type': {'type': 'string',
          'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},
         'choices': {'type': 'array', 'items': {'type': 'string'}}},
        'required': ['question_text']}}},
     'required': ['title', 'questions']},
    'strict': False},
   'type': 'function'}],
 'response_format': 'auto',
 'temperature': 1.0,
 'tool_resources': {'code_interpreter': {'file_ids': ['file-GQFm2i7N8LrAQatefWKEsE']},
  'file_search': {'vector_store_ids': []}},
 'top_p': 1.0}

Y ahora, pedimos un cuestionario.

thread, run = create_thread_and_run(
    "Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses."
)
run = wait_on_run(run, thread)
run.status
'requires_action'

Sin embargo, ahora, cuando verificamos el status del Run, ¡vemos requires_action! Echemos un vistazo más de cerca.

show_json(run)
{'id': 'run_ekMRSI2h35asEzKirRf4BTwZ',
 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'cancelled_at': None,
 'completed_at': None,
 'created_at': 1736341020,
 'expires_at': 1736341620,
 'failed_at': None,
 'incomplete_details': None,
 'incomplete_details': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'metadata': {},
 'model': 'gpt-4o',
 'object': 'thread.run',
 'parallel_tool_calls': True,
 'required_action': {'submit_tool_outputs': {'tool_calls': [{'id': 'call_uvJEn0fxM4sgmzek8wahBGLi',
     'function': {'arguments': '{"title":"Math Quiz","questions":[{"question_text":"What is the derivative of the function f(x) = 3x^2 + 2x - 5?","question_type":"FREE_RESPONSE"},{"question_text":"What is the value of \\\\( \\\\int_{0}^{1} 2x \\\\, dx \\\\)?","question_type":"MULTIPLE_CHOICE","choices":["0","1","2","3"]}]}',
      'name': 'display_quiz'},
     'type': 'function'}]},
  'type': 'submit_tool_outputs'},
 'response_format': 'auto',
 'started_at': 1736341022,
 'status': 'requires_action',
 'thread_id': 'thread_8bK2PXfoeijEHBVEzYuJXt17',
 'tool_choice': 'auto',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'file_search',
   'file_search': {'max_num_results': None,
    'ranking_options': {'score_threshold': 0.0,
     'ranker': 'default_2024_08_21'}}},
  {'function': {'name': 'display_quiz',
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    'parameters': {'type': 'object',
     'properties': {'title': {'type': 'string'},
      'questions': {'type': 'array',
       'description': 'An array of questions, each with a title and potentially options (if multiple choice).',
       'items': {'type': 'object',
        'properties': {'question_text': {'type': 'string'},
         'question_type': {'type': 'string',
          'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},
         'choices': {'type': 'array', 'items': {'type': 'string'}}},
        'required': ['question_text']}}},
     'required': ['title', 'questions']},
    'strict': False},
   'type': 'function'}],
 'truncation_strategy': {'type': 'auto', 'last_messages': None},
 'usage': None,
 'temperature': 1.0,
 'top_p': 1.0,
 'tool_resources': {}}    'strict': False},
   'type': 'function'}],
 'truncation_strategy': {'type': 'auto', 'last_messages': None},
 'usage': None,
 'temperature': 1.0,
 'top_p': 1.0,
 'tool_resources': {}}

El campo required_action indica que una herramienta está esperando que la ejecutemos y enviemos su salida de vuelta al Assistant. ¡Específicamente, la función display_quiz! Empecemos analizando el name y el arguments.

Nota Aunque en este caso sabemos que solo hay una llamada a la herramienta, en la práctica el Assistant puede optar por llamar a varias herramientas.

# Extract single tool call
tool_call = run.required_action.submit_tool_outputs.tool_calls[0]
name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)

print("Function Name:", name)
print("Function Arguments:")
arguments
Function Name: display_quiz
Function Arguments:
{'title': 'Math Quiz',
 'questions': [{'question_text': 'What is the derivative of the function f(x) = 3x^2 + 2x - 5?',
   'question_type': 'FREE_RESPONSE'},
  {'question_text': 'What is the value of \\( \\int_{0}^{1} 2x \\, dx \\)?',
   'question_type': 'MULTIPLE_CHOICE',
   'choices': ['0', '1', '2', '3']}]}

Ahora, llamemos a nuestra función display_quiz con los argumentos proporcionados por el Assistant:

responses = display_quiz(arguments["title"], arguments["questions"])
print("Responses:", responses)
Quiz: Math Quiz
Quiz: Math Quiz

What is the derivative of the function f(x) = 3x^2 + 2x - 5?

What is the value of \( \int_{0}^{1} 2x \, dx \)?
0. 0
1. 1
2. 2
3. 3

Responses: ["I don't know.", 'a']

¡Genial! (Recuerda que estas respuestas son las que simulamos antes. En realidad, estaríamos obteniendo la entrada de vuelta de esta llamada a la función).

Ahora que tenemos nuestras respuestas, enviémoslas de vuelta al Assistant. Necesitaremos el ID tool_call, que se encuentra en el tool_call que analizamos anteriormente. También necesitaremos codificar nuestro list de respuestas en un str.

run = client.beta.threads.runs.submit_tool_outputs(
    thread_id=thread.id,
    run_id=run.id,
    tool_outputs=tool_outputs
)
show_json(run)
{'id': 'run_ekMRSI2h35asEzKirRf4BTwZ',
 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',
 'cancelled_at': None,
 'completed_at': None,
 'created_at': 1736341020,
 'expires_at': 1736341620,
 'failed_at': None,
 'incomplete_details': None,
 'incomplete_details': None,
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'max_completion_tokens': None,
 'max_prompt_tokens': None,
 'metadata': {},
 'model': 'gpt-4o',
 'object': 'thread.run',
 'parallel_tool_calls': True,
 'parallel_tool_calls': True,
 'required_action': None,
 'response_format': 'auto',
 'started_at': 1736341022,
 'status': 'queued',
 'thread_id': 'thread_8bK2PXfoeijEHBVEzYuJXt17',
 'tool_choice': 'auto',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'file_search',
   'file_search': {'max_num_results': None,
    'ranking_options': {'score_threshold': 0.0,
     'ranker': 'default_2024_08_21'}}},
  {'function': {'name': 'display_quiz',
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.",
    'parameters': {'type': 'object',
     'properties': {'title': {'type': 'string'},
      'questions': {'type': 'array',
       'description': 'An array of questions, each with a title and potentially options (if multiple choice).',
       'items': {'type': 'object',
        'properties': {'question_text': {'type': 'string'},
         'question_type': {'type': 'string',
          'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},
         'choices': {'type': 'array', 'items': {'type': 'string'}}},
        'required': ['question_text']}}},
     'required': ['title', 'questions']},
    'strict': False},
   'type': 'function'}],
 'truncation_strategy': {'type': 'auto', 'last_messages': None},
 'usage': None,
 'temperature': 1.0,
 'top_p': 1.0,
 'tool_resources': {}}    'strict': False},
   'type': 'function'}],
 'truncation_strategy': {'type': 'auto', 'last_messages': None},
 'usage': None,
 'temperature': 1.0,
 'top_p': 1.0,
 'tool_resources': {}}

¡Ahora podemos esperar a que el Run se complete de nuevo y revisar nuestro Thread!

run = wait_on_run(run, thread)
pretty_print(get_response(thread))
# Messages
user: Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses.
assistant: Since no specific information was found in the uploaded file, I'll create a general math quiz for you:

1. **Open-ended Question**: What is the derivative of the function \( f(x) = 3x^2 + 2x - 5 \)?

2. **Multiple Choice Question**: What is the value of \( \int_{0}^{1} 2x \, dx \)?
    - A) 0
    - B) 1
    - C) 2
    - D) 3

I will now present the quiz to you for response.
assistant: Here is the feedback for your responses:

1. **Derivative Question**: 
   - Your Response: "I don't know."
   - Feedback: The derivative of \( f(x) = 3x^2 + 2x - 5 \) is \( f'(x) = 6x + 2 \).

2. **Integration Question**: 
   - Your Response: A) 0
   - Feedback: The correct answer is B) 1. The integration \(\int_{0}^{1} 2x \, dx \) evaluates to 1.

¡Woohoo 🎉

Conclusión

Cubrimos mucho terreno en este cuaderno, ¡felicítate! Esperamos que ahora tengas una base sólida para construir experiencias potentes y con estado utilizando herramientas como Code Interpreter, Retrieval y Functions.

Hay algunas secciones que no cubrimos por brevedad, así que aquí tienes algunos recursos para explorar más a fondo:

  • Annotations: análisis de citas de archivos
  • Files: con alcance de Thread vs. con alcance de Assistant
  • Parallel Function Calls: llamar a múltiples herramientas en un solo Step
  • Multi-Assistant Thread Runs: un solo Thread con Messages de múltiples Assistants
  • Streaming: ¡próximamente!

¡Ahora ve y construye algo asombroso!

Lección del curso «OpenAI Cookbook» de OpenAI, publicado con licencia MIT. Traducción y adaptación al español de IA con Clase. IA con Clase no está afiliado a OpenAI. Ver el original · Licencia
Esta lección es gratuita. El resto del curso se abre con la Membresía de IA con Clase, que incluye todos los cursos del catálogo. Ver precios