Assistant의 함수 호출 (function call) 기능은 사용자가 정의한 함수를 Assistant가 사용할 수 있도록 해주는 기능입니다. Assistant가 직접 함수를 호출하는 것은 아니고, 사용자가 함수 사용법을 알려주면 Assistant가 필요할 때 함수를 호출해달라고 사용자에게 요청합니다. 사용자가 요청대로 함수를 호출하고 그 결과를 Assistant에게 전달해주면, 그 결과를 바탕으로 Assistant가 응답을 생성합니다.
예제
이번에는 Assistant에게 현재 유가를 원화로 알려달라고 해보겠습니다. Assistant가 질문에 답할 수 있도록 현재 석유 가격을 달러로 알려주는 함수와 현재 원/달러 환율을 알려주는 함수를 준비했습니다. 11월에 업데이트된 API에서 Assistant는 한 번에 여러 개의 함수를 호출할 수 있게 되었는데, 한 번에 두 함수를 호출해 답하는지 확인해보겠습니다.
함수, 함수 설명 준비
현재 석유 가격을 반환하는 함수와 현재 원/달러 환율을 알려주는 함수는 BeautifulSoup을 이용한 Web Scraping으로 구현했습니다.
from bs4 import BeautifulSoup
import requests
def oil_price():
url = "https://www.knoc.co.kr/"
r = requests.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
price=[]
for tag in soup.find_all("dl",class_="money_list")[3:]:
price.append(tag.find('li').text)
return {"Dubai":price[0], "WTI":price[1], "Brent":price[2]}
def usd_krw():
url = "https://finance.naver.com/marketindex/"
r = requests.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
usdkrw = soup.find("span",class_='value').text
return usdkrw
Assistant에게 함수 사용법을 알려주기 위해 함수 설명도 준비했습니다.
tools = [
{
"type":"function",
"function": {
"name":"oil_price",
"description":"Get current oil prices - Dubai, WTI, and Brent",
"parameters": {
"type":"object",
"properties": {}
},
"required": []
}
},
{
"type":"function",
"function": {
"name":"usd_krw",
"description":"Get current exchange rate from USD to KRW",
"parameters": {
"type":"object",
"properties": {}
},
"required": []
}
}
]
Assistant, 질문 준비
from openai import OpenAI
client = OpenAI(api_key="sk-")
assistant = client.beta.assistants.create(
instructions = "You are an oil expert.",
model="gpt-4-1106-preview",
tools = tools
)
thread = client.beta.threads.create(
messages=[
{
"role":"user",
"content": "현재 WTI 석유 가격을 원화로 알려줄래?"
}
]
)
Run 실행
Assistant의 응답을 얻기 위해서는 Run 객체를 이용합니다. 함수 호출을 포함하는 경우 Assistant의 Run 상태는 다음과 같은 순서로 변합니다.
- queued: 사용자의 입력을 받으면 바로 queued 상태가 됩니다.
- in_progress: Assistant가 입력을 분석하는 상태입니다.
- requires_action: 함수 호출이 필요하다고 판단해서 사용자에게 특정 함수를 호출해달라고 요청하는 상태입니다. 사용자는 해당 함수를 호출하고 결과를 전달해주게 됩니다.
- queued: 함수 호출 결과를 받았습니다.
- in_progress: 질문과 함수 호출 결과를 이용해 응답을 생성합니다.
- completed: 응답 생성이 끝났습니다. 사용자는 Thread의 Message를 가져와 응답을 확인할 수 있습니다.
Run 객체의 실행 상태가 queued, in_progress 상태인 동안은 기다렸다가 다른 상태로 바뀌면 진행하도록 하는 함수를 정의하겠습니다.
import time
def wait_run(client, run, thread):
while True:
run_check = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id
)
print(run_check.status)
if run_check.status in ['queued','in_progress']:
time.sleep(2)
else:
break
return run_check
Run 객체를 생성하고 결과를 기다리겠습니다.
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id
)
run_check = wait_run(client, run, thread)
run_check.status
# 결과: requires_action
함수 호출 요청
Run 상태가 required_action으로 바뀌었습니다. 사용자에게 함수를 호출해달라고 요청하는 상태입니다. 어떤 함수를 어떻게 호출해야하는지에 관한 정보는 다음과 같이 확인할 수 있습니다.
run_check.required_action.submit_tool_outputs.tool_calls
## 결과
[RequiredActionFunctionToolCall(id='call_M5haSVtL6eox816h1ibHKdTv', function=Function(arguments='{}', name='oil_price'), type='function'),
RequiredActionFunctionToolCall(id='call_PmZ6Fp8bh12sb7qi0jeGGZj1', function=Function(arguments='{}', name='usd_krw'), type='function')]
위 결과를 보면, 두 개의 RequiredActionFunctionToolCall 객체로 구성된 리스트가 보입니다. 각각의 객체가 함수 호출에 관한 정보를 담고 있습니다. 두 개의 객체가 있으므로 두 개의 함수를 함께 호출해달라는 의미입니다. 각 객체는 id, arguments, name 정보를 가지고 있습니다. id는 여러 개의 함수 호출 결과를 전달할 때 어떤 함수의 결과인지 알려주기 위함이고, name과 arguments는 사용자가 어떤 함수를 어떤 인자를 사용해 호출해야하는지에 관한 정보입니다. 이 정보를 이용해 함수를 호출하고 결과를 알려주면 됩니다.
함수 호출
tool_calls = run_check.required_action.submit_tool_outputs.tool_calls
tool_outputs = []
for tool in tool_calls:
func_name = tool.function.name
kwargs = json.loads(tool.function.arguments)
output = locals()[func_name](**kwargs)
tool_outputs.append(
{
"tool_call_id":tool.id,
"output":str(output)
}
)
tool_outputs
## 결과 (2023-11-29)
[{'tool_call_id': 'call_M5haSVtL6eox816h1ibHKdTv',
'output': "{'Dubai': '82.14', 'WTI': '76.41', 'Brent': '81.68'}"},
{'tool_call_id': 'call_PmZ6Fp8bh12sb7qi0jeGGZj1', 'output': '1,289.00'}]
함수 호출 결과에서 석유 가격과 환율을 확인할 수 있습니다.
여기서는 locals()를 이용해 함수 이름 문자열로 함수를 호출하고 결과를 tool_outputs에 id와 함께 문자열로 저장했습니다. 함수가 다른 모듈에 정의되어 있다면 getattr을 이용할 수도 있습니다. 물론, 함수의 수가 많지 않다면 if 문을 사용하는 것도 괜찮습니다.
함수 호출 결과 제출
이제 위에서 저장한 함수 호출 결과를 다시 Assistant에게 알려주면 Assistant가 응답을 생성하게 됩니다. 함수 호출 결과를 알려주기 위해서는 runs.submit_tool_outputs를 이용합니다.
run = client.beta.threads.runs.submit_tool_outputs(
thread_id=thread.id,
run_id=run.id,
tool_outputs=tool_outputs
)
run_check = wait_run(client,run,thread)
run_check.status
# 결과: completed
Assistant의 응답
응답이 끝났으니 응답을 확인해보겠습니다.
thread_messages = client.beta.threads.messages.list(thread.id)
for msg in reversed(thread_messages.data):
print(f"{msg.role}: {msg.content[0].text.value}")
## 결과
user: 현재 WTI 석유 가격을 원화로 알려줄래?
assistant: 현재 WTI 원유 가격은 배럴당 $76.41 이며, USD/KRW 환율은 1,289.00원입니다. 따라서 WTI 원유 가격을 원화로 환산하면 배럴당 약 98,436.89원입니다.
76.41 x 1289 = 98,492.49로 Assistant의 응답에 약간의 오차가 있긴 하지만 가깝게 맞췄습니다. 수학 계산 함수를 추가하거나 code interpreter를 사용하면 더 정확한 결과를 얻을 수 있을 것입니다.
Run steps
어떤 과정을 거쳐서 위의 결과를 얻었는지 정확히 알고 싶다면 run steps를 확인하면 됩니다.
run_steps = client.beta.threads.runs.steps.list(
thread_id=thread.id,
run_id=run.id
)
for i,run_step in enumerate(run_steps.data):
print(i, run_step.step_details)
## 결과
0 MessageCreationStepDetails(message_creation=MessageCreation(message_id='msg_40MzHjYXvYvpqYKf1059yrRb'), type='message_creation')
1 ToolCallsStepDetails(tool_calls=[FunctionToolCall(id='call_M5haSVtL6eox816h1ibHKdTv', function=Function(arguments='{}', name='oil_price', output="{'Dubai': '82.14', 'WTI': '76.41', 'Brent': '81.68'}"), type='function'), FunctionToolCall(id='call_PmZ6Fp8bh12sb7qi0jeGGZj1', function=Function(arguments='{}', name='usd_krw', output='1,289.00'), type='function')], type='tool_calls')
위 결과는 역순으로, 먼저 oil_price와 usd_krw두 개의 함수를 호출해서 결과를 얻었고, 그 다음 응답 메시지를 생성했다는 것을 알 수 있습니다.
모든 작업이 끝났다면 Thread와 Assistant를 삭제합니다.
response = client.beta.threads.delete(thread.id)
response = client.beta.assistants.delete(assistant.id)
앞에서 살펴본 Retrieval, Code interpreter와 함께 오늘 살펴본 함수 호출 기능을 이용하면 Assistant의 능력을 다양하게 확장할 수 있습니다.





