{"id":3339,"date":"2023-07-13T12:57:04","date_gmt":"2023-07-13T19:57:04","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/surface-duo\/?p=3339"},"modified":"2024-01-03T16:23:55","modified_gmt":"2024-01-04T00:23:55","slug":"android-openai-chatgpt-9-functions","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/surface-duo\/android-openai-chatgpt-9-functions\/","title":{"rendered":"OpenAI chat functions on Android"},"content":{"rendered":"<p>\n  Hello prompt engineers,\n<\/p>\n<p>\n  OpenAI recently announced a new feature \u2013 <a href=\"https:\/\/openai.com\/blog\/function-calling-and-other-api-updates\">function calling<\/a> \u2013 that makes it easier to extend the <a href=\"https:\/\/platform.openai.com\/docs\/guides\/gpt\/chat-completions-api\">chat API<\/a> with external data and functionality. This post will walk through the code to implement a \u201cchat function\u201d in the <a href=\"https:\/\/github.com\/conceptdev\/droidcon-sf-23\/tree\/main\/Jetchat\">JetchatAI sample app<\/a> (discussed in <a href=\"https:\/\/devblogs.microsoft.com\/surface-duo\/android-openai-chatgpt-6\/\">earlier posts<\/a>).\n<\/p>\n<p>\n  Following the <a href=\"https:\/\/platform.openai.com\/docs\/guides\/gpt\/function-calling\">function calling documentation<\/a> and the example provided by the <a href=\"https:\/\/github.com\/aallam\/openai-kotlin\/blob\/main\/guides\/ChatFunctionCall.md\">OpenAI kotlin client-library<\/a>, a real-time \u201cweather\u201d data source will be added to the chat. Figures 1 and 2 below show how the chat response before and after implementing the function:\n<\/p>\n<p>\n  <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-1.png\" class=\"wp-image-3340\" width=\"450\" alt=\"Chat conversation asking for a weather report but the AI responding that it cannot as it does not have current data\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-1.png 938w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-1-300x149.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-1-768x382.png 768w\" sizes=\"(max-width: 938px) 100vw, 938px\" \/><br\/><em>Figure 1: without a function to provide real-time data, chat can\u2019t provide weather updates<\/em>\n<\/p>\n<p>\n  <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-2.png\" class=\"wp-image-3341\" width=\"450\" alt=\"Chat conversation asking for a weather report and providing a forecast\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-2.png 826w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-2-300x138.png 300w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-2-768x353.png 768w\" sizes=\"(max-width: 826px) 100vw, 826px\" \/><br\/><em>Figure 2: with a function added to the prompt, chat recognizes when to call it (and what parameters are required) to get real-time data<\/em>\n<\/p>\n<h2>How chat functions work<\/h2>\n<p>\n  A \u201cchat function\u201d isn\u2019t actually code running in the model or on the server. Rather, as part of the chat completion request, you tell that model that you can execute code on its behalf, and describe (in both plain text and with a JSON structure) what parameters your code can accept.\n<\/p>\n<p>\n  The model will then decide, based on the user\u2019s input and the capabilities you described, whether your function might be able to help with the completion. If so, the model response indicates \u201cplease execute your code with these parameters, and send back the result\u201d. Rather than show this response to the user, your code extracts the parameters, runs the code, and sends the result to the model.\n<\/p>\n<p>\n  The model now reconsiders how to respond to the user input, using all the context from earlier in the chat <em>plus<\/em> the result of your function. It may use all, some, or none of the function result, depending on how well it matches the user\u2019s initial request. The model may also re-phrase, paraphrase, or otherwise interpret your function\u2019s results to best fit the completion.\n<\/p>\n<p>\n  If you have provided multiple functions, the model will choose the best one (according to its capabilities), and if none seem to be applicable then it will respond with a completion without calling any functions.\n<\/p>\n<h2>How to implement a function<\/h2>\n<p>\n  The code added to the <a href=\"https:\/\/github.com\/conceptdev\/droidcon-sf-23\/tree\/main\/Jetchat\">JetchatAI sample<\/a> is a variation of the example provided in the <a href=\"https:\/\/github.com\/aallam\/openai-kotlin\/blob\/main\/guides\/ChatFunctionCall.md\">OpenAI Kotlin client library docs<\/a>. Before adding code make sure to update your <strong>build.gradle<\/strong> file to use the latest version of the client library (v3.3.1) which includes function calling support:\n<\/p>\n<pre>com.aallam.openai:openai-client:3.3.1<\/pre>\n<p>\n  Also make sure you\u2019re referencing a chat model that supports the function calling feature, either <code>gpt-3.5-turbo-0613<\/code> or <code>gpt-4<\/code> (see <strong>Constants.kt<\/strong> in the sample project). Don\u2019t forget that you\u2019ll need to add your OpenAI developer key to the constants class as well.\n<\/p>\n<h3>Add the function call to the chat<\/h3>\n<p>\n  The basic code for making chat completion requests includes just the model name and the serialized conversation (including the most recent entry by the user). You can find this in the <strong>OpenAIWrapper.kt<\/strong> file in the JetchatAI sample:\n<\/p>\n<pre>val chatCompletionRequest = chatCompletionRequest {\r\n    model = ModelId(Constants.OPENAI_CHAT_MODEL)\r\n    messages = conversation\r\n}<\/pre>\n<p><em>Figure 3: executing a chat completion without functions<\/em>\n<\/p>\n<p>\n  To provide the model with a function (or functions), the completion request needs these additional parameters:\n<\/p>\n<ul>\n<li>\n    <code>functions<\/code> \u2013 a collection of <code>function<\/code>, each of which has:<\/p>\n<ul>\n<li>\n        <code>name<\/code> \u2013 key used to track which function is called\n      <\/li>\n<li>\n        <code>description<\/code> \u2013 explanation of what the function does \u2013 the model will use this to determine if\/when to call the function\n      <\/li>\n<li>\n        <code>parameters<\/code> \u2013 a JSON representation of the function\u2019s parameters following the OpenAPI specification (shown in Figure 5)\n      <\/li>\n<\/ul>\n<\/li>\n<li>\n    <code>functionCall<\/code> \u2013 the ability to control how functions are called. <code>Auto<\/code> means the model will decide, but you can also turn the functions off (<code>FunctionMode.None<\/code>) OR force a specific function to be called with <code>FunctionMode.Named<\/code> followed by the function\u2019s name.\n  <\/li>\n<\/ul>\n<p>\n  The updated chat completion request is shown below:\n<\/p>\n<pre>val chatCompletionRequest = chatCompletionRequest {\r\n    model = ModelId(Constants.OPENAI_CHAT_MODEL)\r\n    messages = conversation\r\n    functions {\r\n        function {\r\n            name = \"currentWeather\"\r\n            description = \"Get the current weather in a given location\"\r\n            parameters = OpenAIFunctions.currentWeatherParams()\r\n        }\r\n    }\r\n    functionCall = FunctionMode.Auto\r\n}<\/pre>\n<p><em>Figure 4: a function for getting the current weather added to the chat completion request<\/em>\n<\/p>\n<p>\n  To keep the code readable the function parameters (and implementation) are in the <strong>OpenAIFunctions.kt<\/strong> file. The parameters are set in the <code>currentWeatherParams<\/code> function, which returns the attributes of all the function parameters, such as:\n<\/p>\n<ul>\n<li>\n    <code>name<\/code> &#8211; each parameter has a name, such as <code>\"latitude\"<\/code> and <code>\"longitude\"<\/code>\n  <\/li>\n<li>\n    <code>type<\/code> &#8211; data type, such as <code>\"string\"<\/code>. This can also be an <code>enum<\/code> as shown in Figure 5.\n  <\/li>\n<li>\n    <code>description<\/code> \u2013 an explanation of the parameter\u2019s purpose \u2013 the model will use this to understand what data should be provided.\n  <\/li>\n<\/ul>\n<p>\n  There is also an element to specify which parameters are <code>required<\/code>, which will also help the model collect information from the user. The parameters for the weather function are described like this:\n<\/p>\n<pre>fun currentWeatherParams(): Parameters {\r\n    val params = Parameters.buildJsonObject {\r\n        put(\"type\", \"object\")\r\n        putJsonObject(\"properties\") {\r\n            putJsonObject(\"latitude\") {\r\n                put(\"type\", \"string\")\r\n                put(\"description\", \"The latitude of the requested location, e.g. 37.773972 for San Francisco, CA\")\r\n            }\r\n            putJsonObject(\"longitude\") {\r\n                put(\"type\", \"string\")\r\n                put(\"description\", \"The longitude of the requested location, e.g. -122.431297 for San Francisco, CA\")\r\n            }\r\n            putJsonObject(\"unit\") {\r\n                put(\"type\", \"string\")\r\n                putJsonArray(\"enum\") {\r\n                    add(\"celsius\")\r\n                    add(\"fahrenheit\")\r\n                }\r\n            }\r\n        }\r\n        putJsonArray(\"required\") {\r\n            add(\"latitude\")\r\n            add(\"longitude\")\r\n        }\r\n    }\r\n    return params\r\n}<\/pre>\n<p><em>Figure 5: describe the shape of the function parameters using JSON<\/em>\n<\/p>\n<p>\n  With just the above changes the model will detect that you have declared a function and attempt to use it if appropriate (according to the user\u2019s input). To actually respond to the model\u2019s request, we need to add more code handling the chat completion.\n<\/p>\n<h3>Implement the function call behaviour<\/h3>\n<p>\n  The basic code for handling a chat response is very simple \u2013 it extracts the most recent message content (from the model), adds it to the conversation history and renders it on screen:\n<\/p>\n<pre>\/\/ this value is added to the UI\r\nval chatResponse = completion.choices[0].message?.content ?: \"\"\r\n\/\/ add the response to the conversation history for subsequent requests\r\nconversation.add(\r\n    ChatMessage(\r\n        role = ChatRole.Assistant,\r\n        content = chatResponse\r\n    )\r\n)<\/pre>\n<p><em>Figure 6: handling a chat response without functions<\/em>\n<\/p>\n<p>\n  Because we have added a function to the response, it\u2019s possible that the model decided to \u201ccall\u201d it, so we need to handle that case after the chat completion by checking the <code>functionCall<\/code> property of the response. If a function name is provided, this indicates the model wishes to \u201cexecute\u201d that function, so we need to extract the parameters, perform the function, and send the function\u2019s output back to the model for consideration.\n<\/p>\n<p>\n  This additional round-trip to the model is hidden from the user \u2013 they do not see the response with the function call, nor do they have any direct input into the request that is sent back with the function\u2019s results. The <em>subsequent<\/em> response \u2013 where the model has considered the function\u2019s results and incorporated them into a final response \u2013 is what is rendered in the UI.\n<\/p>\n<pre>if (completionMessage.functionCall != null) {\r\n    \/\/ handle function\r\n    val function = completionMessage.functionCall\r\n    if (function!!.name == \"currentWeather\")\r\n    {\r\n        val functionArgs = function.argumentsAsJson() ?: error(\"arguments field is missing\")\r\n        \/\/ CALL THE FUNCTION with the parameters sent back by the model\r\n        val functionResponse = OpenAIFunctions.currentWeather( \/\/ implementation to come\u2026\r\n            functionArgs.getValue(\"latitude\").jsonPrimitive.content,\r\n            functionArgs.getValue(\"longitude\").jsonPrimitive.content,\r\n            functionArgs[\"unit\"]?.jsonPrimitive?.content ?: \"fahrenheit\"\r\n        )\r\n        \/\/ add the model\u2019s \"call a function\" response to the history \u2013 this doesn\u2019t show in the UI\r\n        conversation.add(\r\n            ChatMessage(\r\n                role = completionMessage.role,\r\n                content = completionMessage.content ?: \"\", \/\/ required to not be empty\r\n                functionCall = completionMessage.functionCall\r\n            )\r\n        )\r\n        \/\/ add the response to the \"function call\" to the history \u2013 this doesn\u2019t show in the UI\r\n        conversation.add(\r\n            ChatMessage(\r\n                role = ChatRole.Function, \/\/ this is a new role type\r\n                name = function.name,\r\n                content = functionResponse\r\n            )\r\n        )\r\n        \/\/ send the function request\/response back to the model\r\n        val functionCompletionRequest = chatCompletionRequest {\r\n            model = ModelId(Constants.OPENAI_CHAT_MODEL)\r\n            messages = conversation }\r\n        val functionCompletion: ChatCompletion = openAI.chatCompletion(functionCompletionRequest)\r\n        \/\/ show the interpreted function response as chat completion in the UI\r\n        chatResponse = functionCompletion.choices.first().message?.content!!\r\n        conversation.add(\r\n            ChatMessage(\r\n                role = ChatRole.Assistant,\r\n                content = chatResponse\r\n            )\r\n        )\r\n    }\r\n}<\/pre>\n<p><em>Figure 7: Code to handle a function call request from the model<\/em>\n<\/p>\n<p>\n  The above code will successfully define a function for OpenAI and handle the case where the model wishes to call the function, but the actual implementation of the <code>currentWeather()<\/code> code has not yet been discussed. The next section explains how a basic web service client for <a href=\"https:\/\/weather.gov\">weather.gov<\/a> can be implemented, although the OpanAI model does not care about the local implementation of the function call.\n<\/p>\n<h2>Wire up a web service<\/h2>\n<p>\n  To make this demo work without too many dependencies, it uses the free <a href=\"https:\/\/www.weather.gov\/documentation\/services-web-api\">weather data API from weather.gov<\/a>. Note that this only works for locations within the United States, and the provider requests a unique user-agent be sent with each request (which can be set in <strong>Constants.kt<\/strong>) \u2013 failure to add a user-agent string may result in failed requests.\n<\/p>\n<p>\n  The full implementation of <code>currentWeather<\/code> is in <strong>OpenAIFunctions.kt<\/strong> in the JetchatAI sample. There are three steps to using this API:\n<\/p>\n<ol>\n<li><strong>Determine the \u2018grid location\u2019 for the weather query<\/strong>\n  <br \/>\n  This is a web service call with latitude and longitude parameters. It will return <b>404<\/b> for locations outside the USA. Parse the JSON result to determine the grid location (<code>office<\/code>, <code>gridX<\/code>, <code>gridY<\/code>).\n<\/li>\n<li><strong>Get the weather forecast<\/strong>\n  <br \/>\n  Create another web service call to get the weather forecast for the given \u2018grid location\u2019. Parse the JSON result to get the forecast information (<code>name<\/code>, <code>temperature<\/code>, <code>temperatureUnit<\/code>, <code>detailedForecast<\/code>).\n<\/li>\n<li><strong>Format response<\/strong>\n  <br \/>\n  Although there is no strict requirement for formatting, in this case the code sends JSON back to the model using the data class <code>weatherInfo<\/code>. The model will use the keys and values to interpret the data and use it in the completion shown to the user.\n<\/li>\n<\/ol>\n<h3>1. Get grid location<\/h3>\n<p>\n  This particular API cannot work directly with latitude and longitude, but instead requires the location be converted to a grid reference.\n<\/p>\n<pre>val gridUrl = \"https:\/\/api.weather.gov\/points\/$latitude,$longitude\"\r\nval gridResponse = httpClient.get(gridUrl) {\r\n    contentType(ContentType.Application.Json)\r\n}\r\n\/\/\u2026 then extract grid info\r\nval responseText = gridResponse.bodyAsText()\r\nval responseJson =Json.parseToJsonElement(responseText).jsonObject[\"properties\"]!!\r\nvar office = responseJson.jsonObject[\"gridId\"]?.jsonPrimitive?.content<\/pre>\n<p><em>Figure 8: code to convert a lat\/long location into a grid reference<\/em>\n<\/p>\n<h3>2. Get weather forecast<\/h3>\n<p>\n  Using the grid reference another web service call will return the weather forecast. The response includes multiple days of information, but the code retrieves just the most current data.\n<\/p>\n<pre>val forecastUrl =\r\n    \"https:\/\/api.weather.gov\/gridpoints\/$office\/$gridX,$gridY\/forecast\"\r\nval forecastResponse = httpClient.get(forecastUrl) {\r\n    contentType(ContentType.Application.Json)\r\n}\r\n\/\/\u2026 then extract forecast info\r\nJson.parseToJsonElement(responseText).jsonObject[\"properties\"]!!\r\nval periods = responseJson.jsonObject[\"periods\"]!!\r\nval period1 = periods.jsonArray[0]!!\r\nvar name = period1.jsonObject[\"name\"]?.jsonPrimitive?.content!!\r\nvar temperature = period1.jsonObject[\"temperature\"]?.jsonPrimitive?.content!!<\/pre>\n<p><em>Figure 9: code to get a weather forecast for a grid location<\/em>\n<\/p>\n<h3>3. Format result<\/h3>\n<p>\n  Once the current weather forecast data has been extracted, use the <code>WeatherInfo<\/code> class to format as JSON to include in the chat response:\n<\/p>\n<pre>val weatherInfo = WeatherInfo(\r\n    latitude,\r\n    longitude,\r\n    temperature,\r\n    unit,\r\n    listOf(name, detailedForecast)\r\n)\r\nreturn weatherInfo.toJson()<\/pre>\n<p><em>Figure 10: a simple data class is used to format the output as JSON<\/em>\n<\/p>\n<p>\n  As mentioned above, the model doesn\u2019t require a specific\/declared response format \u2013 it will use all the context you provide (e.g., the key names as well as the data values) to help it understand the data and incorporate it in the response to the user.\n<\/p>\n<h2>One last thing \u2013 grounding<\/h2>\n<p>\n  The model cannot know your location by default, so with the implementation already shown if the user asks \u201cwhat\u2019s the weather\u201d the model will respond with a question about their location. To help make the chat seem smarter we could use the location services APIs to determine the device\u2019s location and ground each message with that information \u2013 but for now the demo uses a hardcoded default location that you can set in <strong>Constants.kt<\/strong>.\n<\/p>\n<pre>val groundedMessage = \"Current location is ${Constants.TEST_LOCATION}.\\n\\n$message\"<\/pre>\n<p><em>Figure 11: simple grounding data added to the prompt<\/em>\n<\/p>\n<p>\n  With this added to the prompt (but not shown in the UI), it\u2019s easy to ask for local weather or to give another location:\n<\/p>\n<p>\n  <img decoding=\"async\" width=\"519\" height=\"800\" src=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-3.png\" class=\"wp-image-3342\" alt=\"Android device running an AI chat application asking about weather in different places (SF, New York, Seattle) and replying with a weather report.\" srcset=\"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-3.png 519w, https:\/\/devblogs.microsoft.com\/surface-duo\/wp-content\/uploads\/sites\/53\/2023\/07\/word-image-3339-3-195x300.png 195w\" sizes=\"(max-width: 519px) 100vw, 519px\" \/><br\/><em>Figure 12: screenshot of the weather function in action<\/em>\n<\/p>\n<h2>Feedback and resources<\/h2>\n<p>\n  You can find the code for this post (plus more) in this <a href=\"https:\/\/github.com\/conceptdev\/droidcon-sf-23\/pull\/7\/files\">pull request<\/a> on the <a href=\"https:\/\/github.com\/conceptdev\/droidcon-sf-23\/\">JetchatAI repo<\/a>.\n<\/p>\n<p>\n  If you have any questions, use the\u00a0<a href=\"http:\/\/aka.ms\/SurfaceDuoSDK-Feedback\" target=\"_blank\" rel=\"noopener\">feedback forum<\/a>\u00a0or message us on\u00a0<a href=\"https:\/\/twitter.com\/surfaceduodev\" target=\"_blank\" rel=\"noopener\">Twitter @surfaceduodev<\/a>.\n<\/p>\n<p>\n  There will be no livestream this week, but you can check out the\u00a0<a href=\"https:\/\/youtube.com\/c\/surfaceduodev\" target=\"_blank\" rel=\"noopener\">archives on YouTube<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hello prompt engineers, OpenAI recently announced a new feature \u2013 function calling \u2013 that makes it easier to extend the chat API with external data and functionality. This post will walk through the code to implement a \u201cchat function\u201d in the JetchatAI sample app (discussed in earlier posts). Following the function calling documentation and the [&hellip;]<\/p>\n","protected":false},"author":570,"featured_media":3341,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[741],"tags":[734,733],"class_list":["post-3339","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-chatgpt","tag-openai"],"acf":[],"blog_post_summary":"<p>Hello prompt engineers, OpenAI recently announced a new feature \u2013 function calling \u2013 that makes it easier to extend the chat API with external data and functionality. This post will walk through the code to implement a \u201cchat function\u201d in the JetchatAI sample app (discussed in earlier posts). Following the function calling documentation and the [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/3339","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/users\/570"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/comments?post=3339"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/posts\/3339\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media\/3341"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/media?parent=3339"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/categories?post=3339"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/surface-duo\/wp-json\/wp\/v2\/tags?post=3339"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}