فراخوانی توابع توسط مدل

فراخوانی توابع توسط مدل

function-call
preview

این notebook نحوه استفاده از API Chat Completions را در ترکیب با توابع خارجی برای گسترش قابلیت های مدل های GPT را نشان می دهد.

پارامتر tools یک پارامتر اختیاری در API Chat Completion است که می تواند برای ارائه مشخصات تابع استفاده شود. هدف از این امر فراهم کردن امکان تولید آرگومان های تابعی است که با مشخصات ارائه شده مطابقت دارند.

توجه داشته باشید که API هیچ تابعی را اجرا نمی کند٬ بلکه مشخصات تابعی که متناسب با متن ورودی است را تعیین می‌کند و این بر عهده توسعه دهندگان است که با استفاده از خروجی های مدل توابع را اجرا کنند.

در پارامتر tools اگر پارامتر functions ارائه شود، به طور پیش فرض مدل تصمیم می گیرد که کی مناسب است که یکی از توابع را استفاده کند. همچنین میشود مدل را به استفاده از یک تابع خاص مجبور کرد . اینکار را با تنظیم پارامتر tool_choice به {"type": "function", "function": {"name": "my_function"} انجام می‌دهیم.

همچنین API می تواند مجبور شود که از هیچ تابعی استفاده نکند با تنظیم پارامتر tool_choice به مقدار"none".

اگر تابعی استفاده شود، خروجی حاوی "finish_reason": "tool_calls" به علاوه یک شیء tool_calls که نام تابع و آرگومان های تابع تولید شده را دارد خواهد بود.

توجه: ورودی‌های داده شده و خروجی‌های تولید شده توسط مدل در این مثال به زبان انگلیسی هستند. برای تولید خروجی به زبان فارسی٬ کافی‌ست از مدل بخواهید که خروجی را به زبان فارسی تولید کند.

این notebooks شامل 2 بخش زیر است:

  • چگونه آرگومان های تابع را تولید کنیم: مجموعه ای از توابع را مشخص کنید و از API برای تولید آرگومان های تابع استفاده کنید.

  • چگومه توابع را با آرگومان های تولید شده توسط مدل فراخوانی کنیم: اجرای واقعی توابع با آرگومان های تولید شده توسط مدل.

تولید آرگومان های تابع توسط مدل #

برای اجرای کدهای زیر ابتدا باید یک کلید API را از طریق پنل کاربری گیلاس تولید کنید. برای این کار ابتدا یک حساب کاربری جدید بسازید یا اگر صاحب حساب کاربری هستید وارد پنل کاربری خود شوید. سپس، به صفحه کلید API بروید و با کلیک روی دکمه “ساخت کلید API” یک کلید جدید برای دسترسی به Gilas API بسازید.
1!pip install scipy --quiet
2!pip install tenacity --quiet
3!pip install tiktoken --quiet
4!pip install termcolor --quiet
5!pip install openai --quiet
1import json
2from openai import OpenAI
3from tenacity import retry, wait_random_exponential, stop_after_attempt
4from termcolor import colored 
5
6client = OpenAI(
7    api_key=os.environ.get(("GILAS_API_KEY", "<کلید API خود را اینجا بسازید https://dashboard.gilas.io/apiKey>")), 
8    base_url="https://api.gilas.io/v1/" # Gilas APIs
9)

Utilities

اول چند ابزار (utility function) را برای ارتباط با API Chat Completions و حفظ و ردیابی وضعیت مکالمه تعریف کنیم.

تابع زیر وظیفه ارسال درخواست API به مدل را دارد.

 1@retry(wait=wait_random_exponential(multiplier=1, max=40), stop=stop_after_attempt(3))
 2def chat_completion_request(messages, tools=None, tool_choice=None, model=GPT_MODEL):
 3    try:
 4        response = client.chat.completions.create(
 5            model=model,
 6            messages=messages,
 7            tools=tools,
 8            tool_choice=tool_choice,
 9        )
10        return response
11    except Exception as e:
12        print("Unable to generate ChatCompletion response")
13        print(f"Exception: {e}")
14        return e

و این تابع مسیول چاپ کردن پیام‌ها است.

 1def pretty_print_conversation(messages):
 2    role_to_color = {
 3        "system": "red",
 4        "user": "green",
 5        "assistant": "blue",
 6        "function": "magenta",
 7    }
 8    
 9    for message in messages:
10        if message["role"] == "system":
11            print(colored(f"system: {message['content']}\n", role_to_color[message["role"]]))
12        elif message["role"] == "user":
13            print(colored(f"user: {message['content']}\n", role_to_color[message["role"]]))
14        elif message["role"] == "assistant" and message.get("function_call"):
15            print(colored(f"assistant: {message['function_call']}\n", role_to_color[message["role"]]))
16        elif message["role"] == "assistant" and not message.get("function_call"):
17            print(colored(f"assistant: {message['content']}\n", role_to_color[message["role"]]))
18        elif message["role"] == "function":
19            print(colored(f"function ({message['name']}): {message['content']}\n", role_to_color[message["role"]]))

مفاهیم اولیه #

می‌خواهیم برخی از مشخصات تابع را برای ارتباط با یک API آب و هوای فرضی ایجاد کنیم. ما این مشخصات تابع را به API Chat Completions می دهیم تا آرگومان های تابعی را تولید کنیم که با مشخصات مطابقت دارند.

پیشنهاد می‌کنیم مشخصات توابع توصیف شده در زیر را بررسی کنید تا متوجه شوید هر تابع چه آرگومان‌های ورودی رو دریافت می‌کند و چه وظیفه‌ای را بر عهده دارد.

 1tools = [
 2    {
 3        "type": "function",
 4        "function": {
 5            "name": "get_current_weather",
 6            "description": "Get the current weather",
 7            "parameters": {
 8                "type": "object",
 9                "properties": {
10                    "location": {
11                        "type": "string",
12                        "description": "The city and state, e.g. San Francisco, CA",
13                    },
14                    "format": {
15                        "type": "string",
16                        "enum": ["celsius", "fahrenheit"],
17                        "description": "The temperature unit to use. Infer this from the users location.",
18                    },
19                },
20                "required": ["location", "format"],
21            },
22        }
23    },
24    {
25        "type": "function",
26        "function": {
27            "name": "get_n_day_weather_forecast",
28            "description": "Get an N-day weather forecast",
29            "parameters": {
30                "type": "object",
31                "properties": {
32                    "location": {
33                        "type": "string",
34                        "description": "The city and state, e.g. San Francisco, CA",
35                    },
36                    "format": {
37                        "type": "string",
38                        "enum": ["celsius", "fahrenheit"],
39                        "description": "The temperature unit to use. Infer this from the users location.",
40                    },
41                    "num_days": {
42                        "type": "integer",
43                        "description": "The number of days to forecast",
44                    }
45                },
46                "required": ["location", "format", "num_days"]
47            },
48        }
49    },
50]

با استفاده از پیغام system زیر از مدل می‌خواهیم که در صورتی که کاربر در مورد آب و هوای فعلی سوال کرد، چند سوال تکمیلی برای تکمیل اطلاعات خود از کاربر بپرسد.

1messages = []
2messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
3messages.append({"role": "user", "content": "What's the weather like today"})
4chat_response = chat_completion_request(
5    messages, tools=tools
6)
7assistant_message = chat_response.choices[0].message
8messages.append(assistant_message)
9assistant_message

خروجی:

ChatCompletionMessage(content='Sure, could you please tell me the location for which you would like to know the weather?', role='assistant', function_call=None, tool_calls=None)

هنگامی که کاربر اطلاعات تکمیلی را ارائه دهد، مدل آرگومان های مناسب تابع را برای ما تولید می کند.

1messages.append({"role": "user", "content": "I'm in Glasgow, Scotland."})
2chat_response = chat_completion_request(
3    messages, tools=tools
4)
5assistant_message = chat_response.choices[0].message
6messages.append(assistant_message)
7assistant_message

خروجی مدل همراه با اسم تابعی که باید فراخوانی شود و آرگومان‌های ورودی آن:

ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_2PArU89L2uf4uIzRqnph4SrN', function=Function(arguments='{\n  "location": "Glasgow, Scotland",\n  "format": "celsius"\n}', name='get_current_weather'), type='function')])

با تغییر متنی که به مدل می دهیم، می توانیم آن را به تابع دیگری که برای آن تعریف کرده‌ایم، متمایل کنیم.

1messages = []
2messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
3messages.append({"role": "user", "content": "what is the weather going to be like in Glasgow, Scotland over the next x days"})
4chat_response = chat_completion_request(
5    messages, tools=tools
6)
7assistant_message = chat_response.choices[0].message
8messages.append(assistant_message)
9assistant_message

خروجی:

ChatCompletionMessage(content='Sure, I can help you with that. How many days would you like to get the weather forecast for?', role='assistant', function_call=None, tool_calls=None)

دوباره، مدل از ما برای توضیحات بیشتر سوال می کند زیرا هنوز اطلاعات کافی را برای تولید آرکومان‌های لازم برای تابع ندارد. در این مورد مدل از مکانی که کاربر بیان کرده مطلع است، اما نمی‌داند پیش‌بینی آب و هوا را برای چند روز باید انجام دهد.

1messages.append({"role": "user", "content": "5 days"})
2chat_response = chat_completion_request(
3    messages, tools=tools
4)
5chat_response.choices[0]
Choice(finish_reason='tool_calls', index=0, logprobs=None, message=ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_ujD1NwPxzeOSCbgw2NOabOin', function=Function(arguments='{\n  "location": "Glasgow, Scotland",\n  "format": "celsius",\n  "num_days": 5\n}', name='get_n_day_weather_forecast'), type='function')]), internal_metrics=[{'cached_prompt_tokens': 128, 'total_accepted_tokens': 0, 'total_batched_tokens': 273, 'total_predicted_tokens': 0, 'total_rejected_tokens': 0, 'total_tokens_in_completion': 274, 'cached_embeddings_bytes': 0, 'cached_embeddings_n': 0, 'uncached_embeddings_bytes': 0, 'uncached_embeddings_n': 0, 'fetched_embeddings_bytes': 0, 'fetched_embeddings_n': 0, 'n_evictions': 0, 'sampling_steps': 40, 'sampling_steps_with_predictions': 0, 'batcher_ttft': 0.035738229751586914, 'batcher_initial_queue_time': 0.0007979869842529297}])

اجبار مدل به استفاده از توابعی خاص یا هیچ تابعی #

ما می توانیم مدل را مجبور کنیم که از یک تابع خاص استفاده کند، به عنوان مثال get_n_day_weather_forecast با استفاده از آرگومان function_call. با این کار، ما مدل را مجبور می کنیم که خودش فرضیاتی را در مورد نحوه استفاده از آن بکند.

1# in this cell we force the model to use get_n_day_weather_forecast
2messages = []
3messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
4messages.append({"role": "user", "content": "Give me a weather report for Toronto, Canada."})
5chat_response = chat_completion_request(
6   messages, tools=tools, tool_choice={"type": "function", "function": {"name": "get_n_day_weather_forecast"}}
7)
8chat_response.choices[0].message
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_MapM0kaNZBR046H4tAB2UGVu', function=Function(arguments='{\n  "location": "Toronto, Canada",\n  "format": "celsius",\n  "num_days": 1\n}', name='get_n_day_weather_forecast'), type='function')])
1# if we don't force the model to use get_n_day_weather_forecast it may not
2messages = []
3messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
4messages.append({"role": "user", "content": "Give me a weather report for Toronto, Canada."})
5chat_response = chat_completion_request(
6    messages, tools=tools
7)
8chat_response.choices[0].message
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_z8ijGSoMLS7xcaU7MjLmpRL8', function=Function(arguments='{\n  "location": "Toronto, Canada",\n  "format": "celsius"\n}', name='get_current_weather'), type='function')])

ما همچنین می توانیم مدل را مجبور کنیم که از هیچ تابعی استفاده نکند.

1messages = []
2messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
3messages.append({"role": "user", "content": "Give me the current weather (use Celcius) for Toronto, Canada."})
4chat_response = chat_completion_request(
5    messages, tools=tools, tool_choice="none"
6)
7chat_response.choices[0].message
ChatCompletionMessage(content='{\n  "location": "Toronto, Canada",\n  "format": "celsius"\n}', role='assistant', function_call=None, tool_calls=None)

فراخوانی موازی توابع #

مدل های جدیدتر مانند gpt-4-turbo یا gpt-3.5-turbo می توانند در یک نوبت چندین تابع را فراخوانی کنند.

1messages = []
2messages.append({"role": "system", "content": "Don't make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous."})
3messages.append({"role": "user", "content": "what is the weather going to be like in San Francisco and Glasgow over the next 4 days"})
4chat_response = chat_completion_request(
5    messages, tools=tools, model='gpt-3.5-turbo'
6)
7
8assistant_message = chat_response.choices[0].message.tool_calls
9assistant_message
[
    ChatCompletionMessageToolCall(id='call_8BlkS2yvbkkpL3V1Yxc6zR6u', function=Function(arguments='{"location": "San Francisco, CA", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function'),

    ChatCompletionMessageToolCall(id='call_vSZMy3f24wb3vtNXucpFfAbG', function=Function(arguments='{"location": "Glasgow", "format": "celsius", "num_days": 4}', name='get_n_day_weather_forecast'), type='function')
]

فراخوانی توابع با آرگومان های تولید شده توسط مدل #

حال نشان خواهیم داد چگونه توابعی را اجرا کنیم که ورودی های آنها توسط مدل تولید شده است، و از این برای پیاده‌سازی یک چت‌بات که می تواند سوالات ما را در مورد یک پایگاه داده پاسخ دهد.

برای سادگی ما از پایگاه داده نمونه Chinook استفاده خواهیم کرد.

توجه: تولید SQL در محیط production می‌تواند ریسک بالایی داشته باشد زیرا مدل ها در تولید SQL صحیح کاملاً قابل اعتماد نیستند.

تعیین یک تابع برای اجرای پرس و جوهای SQL #

اول بیایید چند تابع کمکی مفید را برای استخراج داده ها از یک پایگاه داده SQLite تعریف کنیم.

 1import sqlite3
 2
 3conn = sqlite3.connect("data/Chinook.db")
 4print("Opened database successfully")
 5
 6def get_table_names(conn):
 7    """Return a list of table names."""
 8    table_names = []
 9    tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table';")
10    for table in tables.fetchall():
11        table_names.append(table[0])
12    return table_names
13
14
15def get_column_names(conn, table_name):
16    """Return a list of column names."""
17    column_names = []
18    columns = conn.execute(f"PRAGMA table_info('{table_name}');").fetchall()
19    for col in columns:
20        column_names.append(col[1])
21    return column_names
22
23
24def get_database_info(conn):
25    """Return a list of dicts containing the table name and columns for each table in the database."""
26    table_dicts = []
27    for table_name in get_table_names(conn):
28        columns_names = get_column_names(conn, table_name)
29        table_dicts.append({"table_name": table_name, "column_names": columns_names})
30    return table_dicts

حالا می توانیم از این توابع کمکی برای استخراج نمایشی از ساختار پایگاه داده استفاده کنیم.#

1database_schema_dict = get_database_info(conn)
2database_schema_string = "\n".join(
3    [
4        f"Table: {table['table_name']}\nColumns: {', '.join(table['column_names'])}"
5        for table in database_schema_dict
6    ]
7)

مانند قبل، مشخصات تابع را برای تابعی که می خواهیم API برای آن آرگومان ایجاد کند، تعریف می کنیم. توجه کنید که ما ساختار پایگاه داده را در مشخصات تابع وارد می کنیم چون این برای اطلاعات برای تصمیم‌گیری مدل مهم خواهد بود.

 1tools = [
 2    {
 3        "type": "function",
 4        "function": {
 5            "name": "ask_database",
 6            "description": "Use this function to answer user questions about music. Input should be a fully formed SQL query.",
 7            "parameters": {
 8                "type": "object",
 9                "properties": {
10                    "query": {
11                        "type": "string",
12                        "description": f"""
13                                SQL query extracting info to answer the user's question.
14                                SQL should be written using this database schema:
15                                {database_schema_string}
16                                The query should be returned in plain text, not in JSON.
17                                """,
18                    }
19                },
20                "required": ["query"],
21            },
22        }
23    }
24]

اجرای پرس و جوهای SQL #

حالا بیایید تابعی را پیاده سازی کنیم که واقعاً پرس و جوها را در برابر پایگاه داده اجرا می کند.

 1def ask_database(conn, query):
 2    """Function to query SQLite database with a provided SQL query."""
 3    try:
 4        results = str(conn.execute(query).fetchall())
 5    except Exception as e:
 6        results = f"query failed with error: {e}"
 7    return results
 8
 9def execute_function_call(message):
10    if message.tool_calls[0].function.name == "ask_database":
11        query = json.loads(message.tool_calls[0].function.arguments)["query"]
12        results = ask_database(conn, query)
13    else:
14        results = f"Error: function {message.tool_calls[0].function.name} does not exist"
15    return results

و حالا از مدل سوالی در مورد دیتا‌های داخل دیتابیس می‌پرسیم و انتظار داریم که مدل اسم تابع ask_database را به همراه آرگومان‌های لازم برای فراخوانی آن را تولید کند.

 1messages = []
 2messages.append({"role": "system", "content": "Answer user questions by generating SQL queries against the Chinook Music Database."})
 3messages.append({"role": "user", "content": "Hi, who are the top 5 artists by number of tracks?"})
 4chat_response = chat_completion_request(messages, tools)
 5assistant_message = chat_response.choices[0].message
 6assistant_message.content = str(assistant_message.tool_calls[0].function)
 7messages.append({"role": assistant_message.role, "content": assistant_message.content})
 8if assistant_message.tool_calls:
 9    results = execute_function_call(assistant_message)
10    messages.append({"role": "function", "tool_call_id": assistant_message.tool_calls[0].id, "name": assistant_message.tool_calls[0].function.name, "content": results})
11pretty_print_conversation(messages)
system: Answer user questions by generating SQL queries against the Chinook Music Database.

user: Hi, who are the top 5 artists by number of tracks?

assistant: Function(arguments='{\n  "query": "SELECT Artist.Name, COUNT(Track.TrackId) AS TrackCount FROM Artist JOIN Album ON Artist.ArtistId = Album.ArtistId JOIN Track ON Album.AlbumId = Track.AlbumId GROUP BY Artist.ArtistId ORDER BY TrackCount DESC LIMIT 5;"\n}', name='ask_database')

function (ask_database): [('Iron Maiden', 213), ('U2', 135), ('Led Zeppelin', 114), ('Metallica', 112), ('Lost', 92)]

مثالی دیگر:

1messages.append({"role": "user", "content": "What is the name of the album with the most tracks?"})
2chat_response = chat_completion_request(messages, tools)
3assistant_message = chat_response.choices[0].message
4assistant_message.content = str(assistant_message.tool_calls[0].function)
5messages.append({"role": assistant_message.role, "content": assistant_message.content})
6if assistant_message.tool_calls:
7    results = execute_function_call(assistant_message)
8    messages.append({"role": "function", "tool_call_id": assistant_message.tool_calls[0].id, "name": assistant_message.tool_calls[0].function.name, "content": results})
9pretty_print_conversation(messages)
system: Answer user questions by generating SQL queries against the Chinook Music Database.

user: Hi, who are the top 5 artists by number of tracks?

assistant: Function(arguments='{\n  "query": "SELECT Artist.Name, COUNT(Track.TrackId) AS TrackCount FROM Artist JOIN Album ON Artist.ArtistId = Album.ArtistId JOIN Track ON Album.AlbumId = Track.AlbumId GROUP BY Artist.ArtistId ORDER BY TrackCount DESC LIMIT 5;"\n}', name='ask_database')

function (ask_database): [('Iron Maiden', 213), ('U2', 135), ('Led Zeppelin', 114), ('Metallica', 112), ('Lost', 92)]

user: What is the name of the album with the most tracks?

assistant: Function(arguments='{\n  "query": "SELECT Album.Title, COUNT(Track.TrackId) AS TrackCount FROM Album JOIN Track ON Album.AlbumId = Track.AlbumId GROUP BY Album.AlbumId ORDER BY TrackCount DESC LIMIT 1;"\n}', name='ask_database')

function (ask_database): [('Greatest Hits', 57)]