使用 Elasticsearch 调用 OpenAI 函数

1,168 阅读14分钟

作者:来自 Elastic Ashish Tiwari

介绍

OpenAI 中的函数调用是指 AI 模型与外部函数或 API 交互的能力,使它们能够执行文本生成之外的任务。此功能使模型能够通过调用预定义函数来执行代码、从数据库检索信息、与外部服务交互等。

该模型根据用户提示智能识别需要调用哪个函数,并使用适当的参数调用该函数。参数也可以由模型动态生成。

可能的用例包括:

  • 数据检索:从数据库或 API 访问实时数据。 (例如天气信息、股票价格)
  • 增强交互:执行需要逻辑和计算的复杂操作(例如,预订航班、安排会议)。
  • 与外部系统集成:与外部系统和工具交互(例如,执行脚本、发送电子邮件)。

在本博客中,我们将创建两个函数:

  1. fetch_from_elasticsearch() - 使用自然语言查询从 Elasticsearch 获取数据。
  2. weather_report() - 获取特定位置的天气预报。

我们将集成函数调用,以根据用户的查询动态确定要调用哪个函数,并相应地生成必要的参数。

先决条件

Elastic

创建 Elastic Cloud 部署以获取所有 Elastic 凭证。

  • ES_API_KEY:创建 API 密钥。
  • ES_ENDPOINT:复制 Elasticsearch 的端点。

OpenAI

  • OPENAI_API_KEY:设置 Open AI 帐户并创建密钥。
  • GPT 模型:我们将使用 gpt-4o 模型,但你可以在此处检查函数调用支持哪个模型。

Open-Meteo API

我们将使用 Open-Meteo API。 Open-Meteo 是一个开源天气 API,为非商业用途提供免费访问。无需 API 密钥。

  • OPEN_METEO_ENDPOINThttps://api.open-meteo.com/v1/forecast

样本数据

创建 Elastic 云部署后,我们在 Kibana 上添加示例飞行数据。样本数据将存储到 kibana_sample_data_flights 索引中。

Python notebook

我们将为整个流程创建一个快速的 Python notebook。安装以下依赖项并创建 Python 脚本/笔记本。

pip install openai 

导入包



1.  from openai import OpenAI
2.  from getpass import getpass
3.  import json
4.  import requests


接受凭证



1.  OPENAI_API_KEY = getpass("OpenAI API Key:")
2.  client = OpenAI(
3.      api_key=OPENAI_API_KEY,
4.  )
5.  GPT_MODEL = "gpt-4o"

7.  ES_API_KEY = getpass("Elastic API Key:")
8.  ES_ENDPOINT = input("Elasticsearch Endpoint:")
9.  ES_INDEX = "kibana_sample_data_flights"

11.  OPEN_METEO_ENDPOINT = "https://api.open-meteo.com/v1/forecast"


Function1: fetch_from_elasticsearch()

def fetch_from_elasticsearch(nl_query):

此函数将接受 nl_query 参数作为自然语言(英语)的字符串,并以字符串形式返回 json elasticsearch 响应。它将对 kibana_sample_data_flights 索引执行所有查询,该索引保存所有航班相关数据。

它将由 3 个步骤/子功能组成。

  1. get_index_mapping() - 它将返回索引的映射。
  2. get_ref_document() - 它将返回一个示例文档以供参考。
  3. build_query() - 这里我们将利用 GPT 模型 (gpt-4o) 和一些镜头提示将用户问题(文本)转换为 Elasticsearch Query DSL

通过将所有功能添加在一起来继续笔记本。

Get Index Mapping

1.  def get_index_mapping():

3.      url = f"""{ES_ENDPOINT}/{ES_INDEX}/_mappings"""

5.      headers = {
6.          "Content-type": "application/json",
7.          "Authorization": f"""ApiKey {ES_API_KEY}""",
8.      } 

Get reference document

1.  def get_index_mapping():

3.      url = f"""{ES_ENDPOINT}/{ES_INDEX}/_mappings"""

5.      headers = {
6.          "Content-type": "application/json",
7.          "Authorization": f"""ApiKey {ES_API_KEY}""",
8.      }

10.      resp = requests.request("GET", url, headers=headers)
11.      resp = json.loads(resp.text)
12.      mapping = json.dumps(resp, indent=4)

14.      return mapping 

注意:你还可以缓存索引映射和参考文档,以避免频繁查询 Elasticsearch。

根据用户查询生成 Elasticsearch Query DSL



1.  def build_query(nl_query):

3.      index_mapping = get_index_mapping()
4.      ref_document = get_ref_document()

6.      few_shots_prompt = """
7.      1. User Query - Average delay time of flights going to India
8.          Elasticsearch Query DSL:
9.           {
10.            "size": 0,
11.            "query": {
12.              "bool": {
13.                "filter": {
14.                  "term": {
15.                    "DestCountry": "IN"
16.                  }
17.                }
18.              }
19.            },
20.            "aggs": {
21.              "average_delay": {
22.                "avg": {
23.                  "field": "FlightDelayMin"
24.                }
25.              }
26.            }
27.          }

29.          2. User Query - airlines with the highest delays
30.          Elasticsearch Query DSL:
31.           {
32.            "size": 0,
33.            "aggs": {
34.              "airlines_with_highest_delays": {
35.                "terms": {
36.                  "field": "Carrier",
37.                  "order": {
38.                    "average_delay": "desc"
39.                  }
40.                },
41.                "aggs": {
42.                  "average_delay": {
43.                    "avg": {
44.                      "field": "FlightDelayMin"
45.                    }
46.                  }
47.                }
48.              }
49.            }
50.          }

52.          3. User Query - Which was the last flight that got delayed for Bangalore
53.          Elasticsearch Query DSL:
54.          {
55.            "query": {
56.              "bool": {
57.                "must": [
58.                  { "match": { "DestCityName": "Bangalore" } },
59.                  { "term": { "FlightDelay": true } }
60.                ]
61.              }
62.            },
63.            "sort": [
64.              { "timestamp": { "order": "desc" } }
65.            ],
66.            "size": 1
67.          }
68.      """

70.      prompt = f"""
71.          Use below index mapping and reference document to build Elasticsearch query:

73.          Index mapping:
74.          {index_mapping}

76.          Reference elasticsearch document:
77.          {ref_document}

79.          Return single line Elasticsearch Query DSL according to index mapping for the below search query related to flights.:

81.          {nl_query}

83.          If any field has a `keyword` type, Just use field name instead of field.keyword.

85.          Just return Query DSL without REST specification (e.g. GET, POST etc.) and json markdown format (e.g. ```json)

87.          few example of Query DSL

89.          {few_shots_prompt}

91.      """

93.      resp = client.chat.completions.create(
94.          model=GPT_MODEL,
95.          messages=[
96.              {
97.                  "role": "user",
98.                  "content": prompt,
99.              }
100.          ],
101.          temperature=0,
102.      )

104.      return resp.choices[0].message.content


注意:有时,可能需要修改提示以获得更准确的响应(查询 DSL)或一致的报告。虽然我们依靠模型自身的知识来生成查询,但可以通过对更复杂查询的少量提示来提高可靠性。少量提示涉及提供你希望其返回的查询类型的示例,这有助于提高一致性。

Execute Query on Elasticsearch



1.  def fetch_from_elasticsearch(nl_query):

3.      query_dsl = build_query(nl_query)
4.      print(f"""Query DSL: ==== \n\n {query_dsl}""")

6.      url = f"""{ES_ENDPOINT}/{ES_INDEX}/_search"""

8.      payload = query_dsl

10.      headers = {
11.          "Content-type": "application/json",
12.          "Authorization": f"""ApiKey {ES_API_KEY}""",
13.      }

15.      resp = requests.request("GET", url, headers=headers, data=payload)
16.      resp = json.loads(resp.text)
17.      json_resp = json.dumps(resp, indent=4)

19.      print(f"""\n\nElasticsearch response: ==== \n\n {json_resp}""")
20.      return json_resp


文本到 Elasticsearch 查询

让我们带着一些问题/查询调用 fetch_from_elasticsearch()。

Query1



1.  fetch_from_elasticsearch("Average delay time of flights going to India")




响应:



1.  Query DSL: ==== 

3.   {
4.    "size": 0,
5.    "query": {
6.      "bool": {
7.        "filter": {
8.          "term": {
9.            "DestCountry": "IN"
10.          }
11.        }
12.      }
13.    },
14.    "aggs": {
15.      "average_delay": {
16.        "avg": {
17.          "field": "FlightDelayMin"
18.        }
19.      }
20.    }
21.  }

24.  Elasticsearch response: ==== 

26.   {
27.      "took": 1,
28.      "timed_out": false,
29.      "_shards": {
30.          "total": 1,
31.          "successful": 1,
32.          "skipped": 0,
33.          "failed": 0
34.      },
35.      "hits": {
36.          "total": {
37.              "value": 372,
38.              "relation": "eq"
39.          },
40.          "max_score": null,
41.          "hits": []
42.      },
43.      "aggregations": {
44.          "average_delay": {
45.              "value": 48.346774193548384
46.          }
47.      }
48.  }


Query2



1.  fetch_from_elasticsearch("airlines with the highest delays")




响应:



1.  Query DSL: ==== 

3.   {
4.    "size": 0,
5.    "aggs": {
6.      "airlines_with_highest_delays": {
7.        "terms": {
8.          "field": "Carrier",
9.          "order": {
10.            "average_delay": "desc"
11.          }
12.        },
13.        "aggs": {
14.          "average_delay": {
15.            "avg": {
16.              "field": "FlightDelayMin"
17.            }
18.          }
19.        }
20.      }
21.    }
22.  }

25.  Elasticsearch response: ==== 

27.   {
28.      "took": 3,
29.      "timed_out": false,
30.      "_shards": {
31.          "total": 1,
32.          "successful": 1,
33.          "skipped": 0,
34.          "failed": 0
35.      },
36.      "hits": {
37.          "total": {
38.              "value": 10000,
39.              "relation": "gte"
40.          },
41.          "max_score": null,
42.          "hits": []
43.      },
44.      "aggregations": {
45.          "airlines_with_highest_delays": {
46.              "doc_count_error_upper_bound": 0,
47.              "sum_other_doc_count": 0,
48.              "buckets": [
49.                  {
50.                      "key": "Logstash Airways",
51.                      "doc_count": 3323,
52.                      "average_delay": {
53.                          "value": 49.59524526030695
54.                      }
55.                  },
56.                  {
57.                      "key": "ES-Air",
58.                      "doc_count": 3211,
59.                      "average_delay": {
60.                          "value": 47.45250700716288
61.                      }
62.                  },
63.                  {
64.                      "key": "Kibana Airlines",
65.                      "doc_count": 3219,
66.                      "average_delay": {
67.                          "value": 46.38397017707363
68.                      }
69.                  },
70.                  {
71.                      "key": "JetBeats",
72.                      "doc_count": 3261,
73.                      "average_delay": {
74.                          "value": 45.910763569457224
75.                      }
76.                  }
77.              ]
78.          }
79.      }
80.  }


尝试其中一些查询,看看会得到什么结果 -



1.  fetch_from_elasticsearch("top 10 reasons for flight cancellation")

3.  fetch_from_elasticsearch("top 5 flights with expensive ticket")

5.  fetch_from_elasticsearch("flights got delay for Bangalore")




完成测试后,你可以从上面的代码中注释掉我们出于调试目的而添加的 print 语句。

Function2: weather_report()

def weather_report(latitude, longitude):

该函数将接受参数纬度和经度作为字符串。它将调用 Open-Meteo API 来获取指定坐标的报告。

在 notebook 中添加函数



1.  def weather_report(latitude, longitude):

3.      url = f"""{OPEN_METEO_ENDPOINT}?latitude={latitude}&longitude={longitude}&current=temperature_2m,precipitation,cloud_cover,visibility,wind_speed_10m"""

5.      resp = requests.request("GET", url)
6.      resp = json.loads(resp.text)
7.      json_resp = json.dumps(resp, indent=4)

9.      print(f"""\n\nOpen-Meteo response: ==== \n\n {json_resp}""")
10.      return json_resp


Test function

让我们调用 weather_report() 函数:

检 Whitefield,Bangalore



1.  weather_report("12.96","77.75")




响应:



1.  {
2.      "latitude": 19.125,
3.      "longitude": 72.875,
4.      "generationtime_ms": 0.06604194641113281,
5.      "utc_offset_seconds": 0,
6.      "timezone": "GMT",
7.      "timezone_abbreviation": "GMT",
8.      "elevation": 6.0,
9.      "current_units": {
10.          "time": "iso8601",
11.          "interval": "seconds",
12.          "temperature_2m": "\u00b0C",
13.          "precipitation": "mm",
14.          "cloud_cover": "%",
15.          "visibility": "m",
16.          "wind_speed_10m": "km/h"
17.      },
18.      "current": {
19.          "time": "2024-05-30T21:00",
20.          "interval": 900,
21.          "temperature_2m": 29.7,
22.          "precipitation": 0.0,
23.          "cloud_cover": 36,
24.          "visibility": 24140.0,
25.          "wind_speed_10m": 2.9
26.      }
27.  }


Function 调用

在本部分中,我们将看到 OpenAI 模型如何根据用户查询检测需要调用哪个函数并生成所需的参数。

定义函数

让我们在一个对象数组中定义这两个函数。我们将创建一个新函数 run_conversation()。



1.  def run_conversation(query):

3.      all_functions = [
4.          {
5.              "type": "function",
6.              "function": {
7.                  "name": "fetch_from_elasticsearch",
8.                  "description": "All flights/airline related data is stored into Elasticsearch. Call this function if receiving any query around airlines/flights.",
9.                  "parameters": {
10.                      "type": "object",
11.                      "properties": {
12.                          "query": {
13.                              "type": "string",
14.                              "description": "Exact query string which is asked by user.",
15.                          }
16.                      },
17.                      "required": ["query"],
18.                  },
19.              },
20.          },
21.          {
22.              "type": "function",
23.              "function": {
24.                  "name": "weather_report",
25.                  "description": "It will return weather report in json format for given location co-ordinates.",
26.                  "parameters": {
27.                      "type": "object",
28.                      "properties": {
29.                          "latitude": {
30.                              "type": "string",
31.                              "description": "The latitude of a location with 0.01 degree",
32.                          },
33.                          "longitude": {
34.                              "type": "string",
35.                              "description": "The longitude of a location with 0.01 degree",
36.                          },
37.                      },
38.                      "required": ["latitude", "longitude"],
39.                  },
40.              },
41.          },
42.      ]


在每个对象中,我们需要设置属性。

  • type:function
  • name:要调用的函数的名称
  • description:函数功能的描述,模型使用它来选择何时以及如何调用该函数。
  • parameters:函数接受的参数,以 JSON Schema 对象的形式描述。

查看工具参考以了解有关属性的更多信息。

调用 OpenAI Chat Completion API

让我们在 Chat Completion API 中设置上述 all_functions。在 run_conversation() 中添加以下代码片段:

 1.      messages = []
2.      messages.append(
3.          {
4.              "role": "system",
5.              "content": "If no data received from any function. Just say there is issue fetching details from function(function_name).",
6.          }
7.      )

9.      messages.append(
10.          {
11.              "role": "user",
12.              "content": query,
13.          }
14.      )

16.      response = client.chat.completions.create(
17.          model=GPT_MODEL,
18.          messages=messages,
19.          tools=all_functions,
20.          tool_choice="auto",
21.      )

23.      response_message = response.choices[0].message
24.      tool_calls = response_message.tool_calls

26.      print(tool_calls)

tools:所有函数的集合。tool_choice = "auto":这让模型决定是否调用函数,如果调用,则调用哪些函数。但我们可以通过为 tool_choice 设置适当的值来强制模型使用一个或多个函数。

  • 设置 tool_choice:“required” 以确保模型始终调用一个或多个函数。
  • 使用 tool_choice:{“type”:“function”, “function”:“name”:“my_function”}} 强制模型调用特定函数。
  • 设置 tool_choice:“none” 以禁用函数调用并使模型仅生成面向用户的消息。

让我们运行聊天完成 API,看看它是否选择了正确的函数。

run_conversation(“how many flights got delay”) 

响应:

[ChatCompletionMessageToolCall(id='call_0WcSIBFj3Ekg2tijS5yJJOYu', function=Function(arguments='{"query":"flights delayed"}', name='fetch_from_elasticsearch'), type='function')] 

如果你注意到,它检测到了 name='fetch_from_elasticsearch',因为我们已经询问了航班相关查询,并且 Elasticsearch 具有航班相关数据。让我们尝试其他查询。



1.  run_conversation("hows weather in delhi")




响应:

[ChatCompletionMessageToolCall(id='call_MKROQ3VnmxK7XOgiEJ6fFXaW', function=Function(arguments='{"latitude":"28.7041","longitude":"77.1025"}', name='weather_report'), type='function')] 

函数检测到 name='weather_report()' 和由模型arguments='{"latitude":"28.7041","longitude":"77.1025"}' 生成的参数。我们刚刚传递了城市名称(德里),模型生成了适当的参数,即纬度和经度。

执行选定的函数

让我们使用生成的参数执行检测到的函数。在此部分中,我们将简单地运行由模型确定的函数并传递生成的参数。

在 run_conversation() 中添加以下代码片段。

 1.      if tool_calls:

3.          available_functions = {
4.              "fetch_from_elasticsearch": fetch_from_elasticsearch,
5.              "weather_report": weather_report,
6.          }
7.          messages.append(response_message)

9.          for tool_call in tool_calls:

11.              function_name = tool_call.function.name
12.              function_to_call = available_functions[function_name]
13.              function_args = json.loads(tool_call.function.arguments)

15.              if function_name == "fetch_from_elasticsearch":
16.                  function_response = function_to_call(
17.                      nl_query=function_args.get("query"),
18.                  )

20.              if function_name == "weather_report":
21.                  function_response = function_to_call(
22.                      latitude=function_args.get("latitude"),
23.                      longitude=function_args.get("longitude"),
24.                  )

26.              print(function_response)

让我们测试一下这部分:



1.  run_conversation("hows weather in whitefield, bangalore")




响应:



1.  [ChatCompletionMessageToolCall(id='call_BCfdhkRtwmkjqmf2A1jP5k6U', function=Function(arguments='{"latitude":"12.97","longitude":"77.75"}', name='weather_report'), type='function')]

3.   {
4.      "latitude": 13.0,
5.      "longitude": 77.75,
6.      "generationtime_ms": 0.06604194641113281,
7.      "utc_offset_seconds": 0,
8.      "timezone": "GMT",
9.      "timezone_abbreviation": "GMT",
10.      "elevation": 873.0,
11.      "current_units": {
12.          "time": "iso8601",
13.          "interval": "seconds",
14.          "temperature_2m": "\u00b0C",
15.          "precipitation": "mm",
16.          "cloud_cover": "%",
17.          "visibility": "m",
18.          "wind_speed_10m": "km/h"
19.      },
20.      "current": {
21.          "time": "2024-05-30T21:00",
22.          "interval": 900,
23.          "temperature_2m": 24.0,
24.          "precipitation": 0.0,
25.          "cloud_cover": 42,
26.          "visibility": 24140.0,
27.          "wind_speed_10m": 11.7
28.      }
29.  }


它检测到函数 weather_report() 并使用适当的参数执行它。

让我们尝试一些与航班相关的查询,我们希望从 Elasticsearch 获取数据。



1.  run_conversation("Average delay for Bangalore flights")




响应:



1.  {
2.      "took": 1,
3.      "timed_out": false,
4.      "_shards": {
5.          "total": 1,
6.          "successful": 1,
7.          "skipped": 0,
8.          "failed": 0
9.      },
10.      "hits": {
11.          "total": {
12.              "value": 78,
13.              "relation": "eq"
14.          },
15.          "max_score": null,
16.          "hits": []
17.      },
18.      "aggregations": {
19.          "average_delay": {
20.              "value": 48.65384615384615
21.          }
22.      }
23.  }


延长对话

我们以 JSON 格式获取所有响应。这实际上不是人类可读的。让我们使用 GPT 模型将此响应转换为自然语言。我们将函数响应传递给 Chat Completion API 以延长对话。

在 run_conversation() 中添加以下代码片段。

 1.              messages.append(
2.                  {
3.                      "tool_call_id": tool_call.id,
4.                      "role": "tool",
5.                      "name": function_name,
6.                      "content": function_response,
7.                  }
8.              )

10.          second_response = client.chat.completions.create(
11.              model=GPT_MODEL,
12.              messages=messages,
13.          )

15.          return second_response.choices[0].message.content

让我们测试端到端流程。我建议注释掉所有打印语句,除非你想保留它们用于调试目的。



1.  i = input("Ask:")
2.  answer = run_conversation(i)
3.  print(answer)


Q1: Average delay for Bangalore flights

The average delay for Bangalore flights is approximately 48.65 minutes.

Q2: last 10 flight delay to Bangalore, show in table

以上数据来自 Elasticsearch 和模型将 json 响应转换为表。

Q3: How is the climate in Whitefield, Bangalore, and what precautions should I take?

模型调用 weather_report() 函数来获取班加罗尔怀特菲尔德的信息,并添加了需要采取的预防措施。

执行的一些问答:

Q4: How's the weather in BKC Mumbai?

e current weather in BKC Mumbai is as follows:

  • Temperature: 31.09°C
  • Humidity: 74.5%
  • Wind Speed: 0.61 m/s, coming from the west-northwest (256.5°)
  • No rain intensity or accumulation reported at the moment.

Q5: Which day of the week do flights experience the most delays?

Here is a table showing the count of flight cancellations by country:

CountryCount of Cancellations
IT (Italy)315
US (United States)253
JP (Japan)118
CN (China)97
CA (Canada)67
DE (Germany)66
IN (India)63
GB (United Kingdom)72
AU (Australia)56
KR (South Korea)55

并行函数调用

较新的模型(例如 gpt-4o 或 gpt-3.5-turbo)可以一次调用多个函数。例如,如果我们询问 “details of last 10 delayed flights for Bangalore in tabular format and describe the current climate there.”,则我们需要来自这两个函数的信息。

Python 笔记本

Elasticsearch Labs 中查找完整的 Python notebook

结论

使用 GPT-4 或其他模型将函数调用合并到你的应用程序中可以显著增强其功能和灵活性。通过策略性地配置 tool_choice 参数,你可以决定模型何时以及如何与外部函数交互。

它还为你的响应添加了一层智能。在上面的例子中,我要求以表格格式显示数据,它会自动将 json 转换为表格格式。它还根据国家代码添加了国家名称。

因此,函数调用不仅简化了复杂的工作流程,还为集成各种数据源和 API 开辟了新的可能性,使你的应用程序更智能、更能响应用户需求。

准备好自己尝试一下了吗?开始免费试用
想要将 RAG 构建到你的应用程序中吗?想尝试使用向量数据库的不同 LLM 吗?
在 Github 上查看我们针对 LangChain、Cohere 等的示例笔记本,并立即加入 Elasticsearch Relevance Engine 培训。

原文:OpenAI function calling with Elasticsearch — Elastic Search Labs