5️⃣Assistants API

OpenAI의 Assistant APIChat Completions API를 발전시킨 것으로, 개발자가 어시스턴트와 유사한 경험을 간편하게 만들고 코드 해석기 및 검색과 같은 강력한 도구에 액세스할 수 있도록 하기 위한 것입니다.

Chat Completions API vs Assistants API

**Chat Completions API의 기본 요소는 Message로, Model(gpt-3.5-turbo, gpt-4 등)을 사용하여 Completion를 수행합니다. 가볍고 강력하지만 본질적으로 상태가 없으므로 대화 상태, 도구 정의, 검색 문서 및 코드 실행을 수동으로 관리해야 합니다.

어시스턴트 API**의 기본 요소는 다음과 같습니다.

  • Assistantc기본 모델, 지침, 도구 및 (컨텍스트) 문서를 캡슐화

  • Threads: 대화의 상태를 나타냄

  • Runs: 텍스트 응답 및 다단계 도구 사용 등 '스레드'에서 '어시스턴트'의 실행을 구동

이를 사용하여 강력하고 상태 저장된 경험을 만드는 방법을 살펴보겠습니다.

Setup

Python SDK

[!note] 어시스턴트 API에 대한 지원을 추가하기 위해 Python SDK를 업데이트했으므로 최신 버전(작성 시점 기준 1.2.3)으로 업데이트해야 합니다.

!pip install --upgrade openai
!pip show openai | grep Version

Version: 1.2.3

Printing Helper 함수 정의

import json

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

Complete Example with Assistants API

Assistants

어시스턴트 API를 시작하는 가장 쉬운 방법은 어시스턴트 플레이그라운드를 이용하는 것입니다.

어시스턴트를 만드는 것부터 시작하겠습니다! 문서에서와 마찬가지로 수학 튜터를 만들겠습니다.

다음과 같이 어시스턴트 API를 통해 직접 어시스턴트를 만들 수도 있습니다:

import os
from dotenv import load_dotenv

# 토큰 정보로드
api_key = os.getenv("OPENAI_API_KEY")
load_dotenv()
from openai import OpenAI
import os

client = OpenAI(api_key=api_key)

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-4-1106-preview",
)
show_json(assistant)
{'id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'created_at': 1699828331,
 'description': None,
 'file_ids': [],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': []}

대시보드를 통해 어시스턴트를 만들든 API를 통해 만들든 관계없이 어시스턴트 ID를 추적하고 싶을 것입니다. 이렇게 하면 스레드와 런에서 어시스턴트를 참조할 수 있습니다.

다음으로 새 스레드를 만들고 여기에 메시지를 추가하겠습니다. 여기에는 대화의 상태가 유지되므로 매번 전체 메시지 기록을 다시 보낼 필요가 없습니다.

Threads

새로운 thread 만들기:

thread = client.beta.threads.create()
show_json(thread)
{'id': 'thread_bw42vPoQtYBMQE84WubNcJXG',
 'created_at': 1699828331,
 'metadata': {},
 'object': '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_IBiZDAWHhWPewxzN0EfTYNew',
 'assistant_id': None,
 'content': [{'text': {'annotations': [],
    'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},
   'type': 'text'}],
 'created_at': 1699828332,
 'file_ids': [],
 'metadata': {},
 'object': 'thread.message',
 'role': 'user',
 'run_id': None,
 'thread_id': 'thread_bw42vPoQtYBMQE84WubNcJXG'}

[!note] 더 이상 전체 대화 내역을 매번 전송하지 않더라도, 실행할 때마다 전체 대화 내역의 토큰에 대한 요금은 계속 청구됩니다.

Runs

우리가 만든 스레드가 앞서 만든 어시스턴트와 어떻게 연결되지 않는지 주목하세요! 스레드는 어시스턴트와 독립적으로 존재하며, 이는 ChatGPT(스레드가 모델/GPT에 연결되는 방식)를 사용했을 때 예상했던 것과는 다를 수 있습니다.

특정 스레드에 대해 어시스턴트로부터 Completion를 받으려면 실행을 만들어야 합니다. 실행을 만들면 어시스턴트가 스레드의 메시지를 살펴보고 단일 응답을 추가하거나 도구를 사용하여 조치를 취해야 함을 나타냅니다.

[!note] RunsAssistants APIChat Completion API의 주요 차이점입니다. Chat Completion에서는 모델이 단일 메시지로만 응답하지만, Assistants API에서는 실행을 통해 어시스턴트가 하나 또는 여러 도구를 사용하고 잠재적으로 여러 메시지를 스레드에 추가할 수 있습니다.

어시스턴트가 사용자에게 응답하도록 하려면 실행을 만들어 보겠습니다. 앞서 언급했듯이 어시스턴트와 스레드를 둘 다 지정해야 합니다.

run = client.beta.threads.runs.create(
    thread_id=thread.id,
    assistant_id=assistant.id,
)
show_json(run)
{'id': 'run_LA08RjouV3RemQ78UZXuyzv6',
 'assistant_id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'cancelled_at': None,
 'completed_at': None,
 'created_at': 1699828332,
 'expires_at': 1699828932,
 'failed_at': None,
 'file_ids': [],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'object': 'thread.run',
 'required_action': None,
 'started_at': None,
 'status': 'queued',
 'thread_id': 'thread_bw42vPoQtYBMQE84WubNcJXG',
 'tools': []}

Chat Completions API에서 completion를 만드는 것과 달리 Run 만들기는 비동기 작업**입니다. 실행의 메타데이터와 함께 즉시 반환되며, 여기에는 처음에는 queued으로 설정되는 status가 포함됩니다. status는 어시스턴트가 작업을 수행(예: 도구 사용 및 메시지 추가)할 때 업데이트됩니다.

어시스턴트가 언제 처리를 완료 했는지 알기 위해 Run을 반복적으로 폴링할 수 있습니다. 여기서는 queued 또는 in_progress 상태만 확인하지만, 실제로는 Run다양한 상태 변경을 겪을 수 있으며, 이를 사용자에게 표시하도록 선택할 수 있습니다. (이를 Steps고 하며 나중에 다룰 예정입니다.

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_LA08RjouV3RemQ78UZXuyzv6',
 'assistant_id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'cancelled_at': None,
 'completed_at': 1699828333,
 'created_at': 1699828332,
 'expires_at': None,
 'failed_at': None,
 'file_ids': [],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'object': 'thread.run',
 'required_action': None,
 'started_at': 1699828332,
 'status': 'completed',
 'thread_id': 'thread_bw42vPoQtYBMQE84WubNcJXG',
 'tools': []}

Messages

이제 실행이 완료되었으므로 스레드에 메시지를 나열하여 어시스턴트가 추가한 내용을 확인할 수 있습니다.

messages = client.beta.threads.messages.list(thread_id=thread.id)
show_json(messages)
{'data': [{'id': 'msg_S0ZtKIWjyWtbIW9JNUocPdUS',
   'assistant_id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
   'content': [{'text': {'annotations': [],
      'value': 'Yes. Subtract 11 from both sides to get `3x = 3`, then divide by 3 to find `x = 1`.'},
     'type': 'text'}],
   'created_at': 1699828333,
   'file_ids': [],
   'metadata': {},
   'object': 'thread.message',
   'role': 'assistant',
   'run_id': 'run_LA08RjouV3RemQ78UZXuyzv6',
   'thread_id': 'thread_bw42vPoQtYBMQE84WubNcJXG'},
  {'id': 'msg_IBiZDAWHhWPewxzN0EfTYNew',
   'assistant_id': None,
   'content': [{'text': {'annotations': [],
      'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},
     'type': 'text'}],
   'created_at': 1699828332,
   'file_ids': [],
   'metadata': {},
   'object': 'thread.message',
   'role': 'user',
   'run_id': None,
   'thread_id': 'thread_bw42vPoQtYBMQE84WubNcJXG'}],
 'object': 'list',
 'first_id': 'msg_S0ZtKIWjyWtbIW9JNUocPdUS',
 'last_id': 'msg_IBiZDAWHhWPewxzN0EfTYNew',
 'has_more': False}

As you can see, Messages are ordered in reverse-chronological order – this was done so the most recent results are always on the first page (since results can be paginated). Do keep a look out for this, since this is the opposite order to messages in the Chat Completions API.

Let's ask our Assistant to explain the result a bit further!

보시다시피 메시지는 시간 역순으로 정렬되며, 이는 가장 최근의 결과가 항상 첫 번째 page에 표시되도록 하기 위함입니다. (결과 페이지 매김이 가능하기 때문에). 이는 Chat Completions API의 메시지와 반대 순서이므로 이 점에 유의하세요.

어시스턴트에게 결과에 대해 좀 더 자세히 설명해 달라고 요청해 보겠습니다!

# 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_9MAeOrGriHcImeQnAzvYyJbs',
   'assistant_id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
   'content': [{'text': {'annotations': [],
      'value': 'Certainly. To solve for x in the equation `3x + 11 = 14`:\n\n1. Subtract 11 from both sides: `3x + 11 - 11 = 14 - 11` simplifies to `3x = 3`.\n2. Divide both sides by 3: `3x / 3 = 3 / 3` simplifies to `x = 1`.\n\nSo, the solution is `x = 1`.'},
     'type': 'text'}],
   'created_at': 1699828335,
   'file_ids': [],
   'metadata': {},
   'object': 'thread.message',
   'role': 'assistant',
   'run_id': 'run_IFHfsubkJv7RSUbDZpNVs4PG',
   'thread_id': 'thread_bw42vPoQtYBMQE84WubNcJXG'}],
 'object': 'list',
 'first_id': 'msg_9MAeOrGriHcImeQnAzvYyJbs',
 'last_id': 'msg_9MAeOrGriHcImeQnAzvYyJbs',
 'has_more': False}

특히 이 간단한 예제에서는 응답을 받기 위해 많은 단계를 거쳐야 하는 것처럼 느껴질 수 있습니다. 하지만 곧 코드를 전혀 변경하지 않고도 어시스턴트에 매우 강력한 기능을 추가할 수 있는 방법을 알게 될 것입니다!

Example

이 모든 것을 어떻게 조합할 수 있는지 살펴봅시다. 다음은 직접 만든 어시스턴트를 사용하는 데 필요한 모든 코드입니다.

이미 Math Assistants를 만들었으므로 그 ID를 MATH_ASSISTANT_ID에 저장했습니다. 그런 다음 두 개의 함수를 정의했습니다:

  • submit_message: 스레드에 메시지를 만든 다음 새 실행을 Run(및 returns)합니다.

  • get_response: 스레드 내 메시지 목록을 반환합니다.

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

또한 재사용할 수 있는 create_thread_and_run 함수도 정의했습니다(실제로는 API의 client.beta.threads.create_and_run 복합 함수와 거의 동일합니다). 마지막으로 모의 사용자 요청을 각각 새 스레드에 제출할 수 있습니다.

이 모든 API 호출이 비동기 작업이라는 점에 주목하세요. 즉, 비동기 라이브러리를 사용하지 않고도 코드에서 실제로 비동기 동작을 얻을 수 있습니다! (예: 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...

모든 Runs이 진행되면 각 실행을 기다렸다가 응답을 받을 수 있습니다.

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: Yes, subtract 11 from both sides to get `3x = 3`, then divide both sides by 3 to find `x = 1`.

# Messages
user: Could you explain linear algebra to me?
assistant: Linear algebra is the branch of mathematics that deals with vector spaces, linear equations, and matrices, focusing on the properties and operations that can be applied to vectors and linear transformations.

# Messages
user: I don't like math. What can I do?
assistant: Try finding aspects of math that relate to your interests or daily life, and consider a tutor or interactive resources to make learning more enjoyable.

# Messages
user: I don't like math. What can I do?
assistant: Try finding aspects of math that relate to your interests or daily life, and consider a tutor or interactive resources to make learning more enjoyable.
user: Thank you!
assistant: You're welcome! If you have any more questions, feel free to ask.

이 코드는 실제로 수학 어시스턴트에만 국한된 것이 아니라, Assistants ID만 변경하면 새 Assistants를 생성할 때 모두 사용할 수 있습니다! 이것이 바로 Assistants API의 힘입니다.

Tools

어시스턴트 API의 핵심 기능은 어시스턴트에 Code Interpreter, Retrieval, 사용자 지정 함수 등의 도구를 장착할 수 있는 기능입니다. 각각에 대해 살펴보겠습니다.

Code Interpreter

Let's equip our Math Tutor with the Code Interpreter tool, which we can do from the Dashboard...

만들어진 수학 튜터에게 대시보드에서 사용할 수 있는 Code Interpreter 도구를 장착해 봅시다...

...또는 Assistant ID를 사용하여 API에 접속합니다.

assistant = client.beta.assistants.update(
    MATH_ASSISTANT_ID,
    tools=[{"type": "code_interpreter"}],
)
show_json(assistant)
{'id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'created_at': 1699828331,
 'description': None,
 'file_ids': [],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [{'type': 'code_interpreter'}]}

이제 어시스턴트에게 새 도구를 사용하도록 요청해 보겠습니다.

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, and 4181.

And that's it! The Assistant used Code Interpreter in the background, and gave us a final response.

For some use cases this may be enough – however, if we want more details on what precisely an Assistant is doing we can take a look at a Run's Steps.

Steps

A Run is composed of one or more Steps. Like a Run, each Step has a status that you can query. This is useful for surfacing the progress of a Step to a user (e.g. a spinner while the Assistant is writing code or performing retrieval).

Run은 하나 이상의 스텝으로 구성됩니다. Run과 마찬가지로 각 단계에는 쿼리할 수 있는 status가 있습니다. 이는 사용자에게 Step의 진행 상황을 표시하는 데 유용합니다(예: 어시스턴트가 코드를 작성하거나 retrieval을 수행하는 동안 스피너가 작동하는 경우).

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

각 단계의 내용을 살펴보자면 step_details메서드를 사용

for step in run_steps.data:
    step_details = step.step_details
    print(json.dumps(show_json(step_details), indent=4))
{'tool_calls': [{'id': 'call_WMNqd63PtX8vZzTwaA6eWpBg',
   'code_interpreter': {'input': '# Python function to generate the first 20 Fibonacci numbers\ndef fibonacci(n):\n    fib_sequence = [0, 1]\n    while len(fib_sequence) < n:\n        fib_sequence.append(fib_sequence[-1] + fib_sequence[-2])\n    return fib_sequence\n\n# Generate the first 20 Fibonacci numbers\nfirst_20_fibonacci = fibonacci(20)\nfirst_20_fibonacci',
    'outputs': [{'logs': '[0,\n 1,\n 1,\n 2,\n 3,\n 5,\n 8,\n 13,\n 21,\n 34,\n 55,\n 89,\n 144,\n 233,\n 377,\n 610,\n 987,\n 1597,\n 2584,\n 4181]',
      'type': 'logs'}]},
   'type': 'code_interpreter'}],
 'type': 'tool_calls'}

null

{'message_creation': {'message_id': 'msg_z593lE5bvcD6BngeDFHDxzwm'},
 'type': 'message_creation'}

null

두 단계에 대한 step_details를 볼 수 있습니다:

  1. tool_calls (단일 스텝에 둘 이상 있을 수 있으므로 복수형)

  2. message_creation

첫 번째 단계는 tool_calls이며, 특히 code_interpreter를 사용합니다:

  • input: 도구가 호출되기 전에 생성된 파이썬 코드

  • output: 코드 인터프리터를 실행한 결과를 포함

두 번째 단계는 결과를 사용자에게 전달하기 위해 스레드에 추가한 message를 포함하는 message_creation입니다.

Retrieval

어시스턴트 API의 또 다른 강력한 도구는 Retrieval으로, 어시스턴트가 질문에 답변할 때 지식창고로 사용할 파일을 업로드할 수 있는 기능입니다. 이 기능은 대시보드 또는 API에서 사용하려는 파일을 업로드할 수 있습니다.

# Upload the file
file = client.files.create(
    file=open(
        "data/language_models_are_unsupervised_multitask_learners.pdf",
        "rb",
    ),
    purpose="assistants",
)
# Update Assistant
assistant = client.beta.assistants.update(
    MATH_ASSISTANT_ID,
    tools=[{"type": "code_interpreter"}, {"type": "retrieval"}],
    file_ids=[file.id],
)
show_json(assistant)
{'id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'created_at': 1699828331,
 'description': None,
 'file_ids': ['file-MdXcQI8OdPp76wukWI4dpLwW'],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [{'type': 'code_interpreter'}, {'type': 'retrieval'}]}
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: I am unable to find specific sections referring to "cool math concepts" directly in the paper using the available tools. I will now read the beginning of the paper to identify any mathematical concepts that are fundamental to the paper.
assistant: The paper discusses leveraging large language models as a framework for unsupervised multitask learning, where tasks are implicitly defined by the context within sequences of text. It explores the zero-shot learning capabilities of such models by showing that when a language model is trained on a vast dataset, it begins to generalize and perform tasks without explicit supervision, achieving competitive results across various natural language processing tasks using a probabilistic framework based on sequential modeling and conditional probabilities.

[!note] Retrieval에는 Annotations과 같은 더 복잡한 기능이 있으며, 이는 다른 설명서에서 다룰 수 있습니다.

Functions

As a final powerful tool for your Assistant, you can specify custom Functions (much like the Function Calling in the Chat Completions API). During a Run, the Assistant can then indicate it wants to call one or more functions you specified. You are then responsible for calling the Function, and providing the output back to the Assistant.

Let's take a look at an example by defining a display_quiz() Function for our Math Tutor.

This function will take a title and an array of questions, display the quiz, and get input from the user for each:

어시스턴트를 위한 마지막 강력한 도구로 사용자 지정 Functions를 지정할 수 있습니다(Chat Completions API의 Function Calling, 함수 호출과 매우 유사). 그러면 실행 중에 어시스턴트가 사용자가 지정한 하나 이상의 함수를 호출하고 싶다고 표시할 수 있습니다. 그러면 사용자는 함수를 호출하고 그 결과를 다시 어시스턴트에게 제공할 책임이 있습니다.

수학 튜터에 대한 display_quiz() 함수를 정의하여 예를 살펴 보겠습니다.

이 함수는 titlequestions'의 배열을 받아 퀴즈를 표시하고 각각에 대해 사용자로부터 입력을 받습니다:

  • title

  • questions

    • question_text

    • question_type: [MULTIPLE_CHOICE, FREE_RESPONSE]

    • choices: ["choice 1", "choice 2", ...]

안타깝게도 파이썬 노트북 내에서 사용자 입력을 가져오는 방법을 모르기 때문에 get_mock_response...로 응답을 모의해 보겠습니다. 여기에서 사용자의 실제 입력을 얻을 수 있습니다.

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

다음은 샘플 퀴즈의 모습입니다:

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']

이제 어시스턴트가 호출할 수 있도록 이 함수의 인터페이스를 JSON 형식으로 정의해 보겠습니다:

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"],
    },
}

다시 한 번 대시보드 또는 API를 통해 어시스턴트를 업데이트해 보겠습니다.

[!note] 함수 JSON을 대시보드에 붙여넣는 것은 들여쓰기 등으로 인해 약간 까다로웠습니다. 저는 대시보드에 있는 예제 중 하나와 동일하게 함수의 형식을 지정해 달라고 ChatGPT에 요청했습니다.

assistant = client.beta.assistants.update(
    MATH_ASSISTANT_ID,
    tools=[
        {"type": "code_interpreter"},
        {"type": "retrieval"},
        {"type": "function", "function": function_json},
    ],
)
show_json(assistant)
{'id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'created_at': 1699828331,
 'description': None,
 'file_ids': ['file-MdXcQI8OdPp76wukWI4dpLwW'],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'name': 'Math Tutor',
 'object': 'assistant',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'retrieval'},
  {'function': {'name': 'display_quiz',
    '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']},
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions."},
   'type': 'function'}]}

이제 퀴즈를 출제합니다.

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'

하지만 이제 실행의 상태를 확인하면 requires_action이 표시됩니다! 자세히 살펴봅시다.

show_json(run)
{'id': 'run_98PGE3qGtHoaWaCLoytyRUBf',
 'assistant_id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'cancelled_at': None,
 'completed_at': None,
 'created_at': 1699828370,
 'expires_at': 1699828970,
 'failed_at': None,
 'file_ids': ['file-MdXcQI8OdPp76wukWI4dpLwW'],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'object': 'thread.run',
 'required_action': {'submit_tool_outputs': {'tool_calls': [{'id': 'call_Zf650sWT1wW4Uwbf5YeDS0VG',
     'function': {'arguments': '{\n  "title": "Mathematics Quiz",\n  "questions": [\n    {\n      "question_text": "Explain why the square root of a negative number is not a real number.",\n      "question_type": "FREE_RESPONSE"\n    },\n    {\n      "question_text": "What is the value of an angle in a regular pentagon?",\n      "choices": [\n        "72 degrees",\n        "90 degrees",\n        "108 degrees",\n        "120 degrees"\n      ],\n      "question_type": "MULTIPLE_CHOICE"\n    }\n  ]\n}',
      'name': 'display_quiz'},
     'type': 'function'}]},
  'type': 'submit_tool_outputs'},
 'started_at': 1699828370,
 'status': 'requires_action',
 'thread_id': 'thread_bICTESFvWoRdj0O0SzsosLCS',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'retrieval'},
  {'function': {'name': 'display_quiz',
    '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']},
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions."},
   'type': 'function'}]}

required_action 필드는 Tool이 실행되고 그 출력을 다시 어시스턴트에게 제출하기를 기다리고 있음을 나타냅니다. 구체적으로는 display_quiz 함수입니다! namearguments를 파싱하는 것부터 시작해 보겠습니다.

[!note] 이 경우에는 Tool 호출이 하나만 있지만, 실제로는 어시스턴트가 여러 도구를 호출하도록 선택할 수 있습니다.

# 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': 'Mathematics Quiz',
 'questions': [{'question_text': 'Explain why the square root of a negative number is not a real number.',
   'question_type': 'FREE_RESPONSE'},
  {'question_text': 'What is the value of an angle in a regular pentagon?',
   'choices': ['72 degrees', '90 degrees', '108 degrees', '120 degrees'],
   'question_type': 'MULTIPLE_CHOICE'}]}

이제 어시스턴트가 제공한 인수를 사용하여 display_quiz 함수를 실제로 호출해 보겠습니다:

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

Explain why the square root of a negative number is not a real number.

What is the value of an angle in a regular pentagon?
0. 72 degrees
1. 90 degrees
2. 108 degrees
3. 120 degrees

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

이 응답은 앞서 mocked respones라는 것을 기억하세요. 실제로는 이 함수 호출을 통해 뒤쪽에서 입력을 받을 것입니다).

이제 응답을 얻었으니 어시스턴트에게 다시 제출해 봅시다. 여기에는 앞서 파싱한 tool_call에서 찾은 tool_call ID가 필요합니다. 또한 응답의 liststr으로 인코딩해야 합니다.

run = client.beta.threads.runs.submit_tool_outputs(
    thread_id=thread.id,
    run_id=run.id,
    tool_outputs=[
        {
            "tool_call_id": tool_call.id,
            "output": json.dumps(responses),
        }
    ],
)
show_json(run)
{'id': 'run_98PGE3qGtHoaWaCLoytyRUBf',
 'assistant_id': 'asst_9HAjl9y41ufsViNcThW1EXUS',
 'cancelled_at': None,
 'completed_at': None,
 'created_at': 1699828370,
 'expires_at': 1699828970,
 'failed_at': None,
 'file_ids': ['file-MdXcQI8OdPp76wukWI4dpLwW'],
 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',
 'last_error': None,
 'metadata': {},
 'model': 'gpt-4-1106-preview',
 'object': 'thread.run',
 'required_action': None,
 'started_at': 1699828370,
 'status': 'queued',
 'thread_id': 'thread_bICTESFvWoRdj0O0SzsosLCS',
 'tools': [{'type': 'code_interpreter'},
  {'type': 'retrieval'},
  {'function': {'name': 'display_quiz',
    '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']},
    'description': "Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions."},
   'type': 'function'}]}

이제 다시 한 번 Run이 완료되기를 기다렸다가 스레드를 확인할 수 있습니다!

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: Thank you for attempting the quiz.

For the first question, it's important to know that the square root of a negative number is not a real number because real numbers consist of all the numbers on the number line, and that includes all positive numbers, zero, and negative numbers. However, the square root of a negative number is not on this number line; instead, it is what we call an imaginary number. When we want to take the square root of a negative number, we typically use the imaginary unit \(i\), where \(i\) is defined as \(\sqrt{-1}\).

For the second question, the correct answer is "108 degrees." In a regular pentagon, which is a five-sided polygon with equal sides and angles, each interior angle is \(108\) degrees. This is because the sum of the interior angles of a pentagon is \(540\) degrees, and when divided by \(5\) (the number of angles), it gives \(108\) degrees per angle. The choice you selected, "72 degrees," actually refers to the external angle of a regular pentagon, not the internal angle.

Last updated