Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: fix: array query parameters with single entries #9771

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

baywet
Copy link
Member

@baywet baywet commented Nov 20, 2024

fixes an issue where OpenAPI query parameters of type array with a single value would break the request generation

@baywet baywet requested a review from a team as a code owner November 20, 2024 17:37
@markwallace-microsoft markwallace-microsoft added the .NET Issue or Pull requests regarding .NET code label Nov 20, 2024
@github-actions github-actions bot changed the title fix: array query parameters with single entries .Net: fix: array query parameters with single entries Nov 20, 2024
Signed-off-by: Vincent Biret <[email protected]>
#else
string s when s.Trim().StartsWith("[", StringComparison.OrdinalIgnoreCase) => JsonArray.Parse(s) as JsonArray,
#endif
string s => [JsonValue.Create(s)],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder which scenario requires the string -> JSONArray conversion. Is it when the LLM provides a string argument to a parameter of an array type? I'm not sure SK should perform this conversion, and I would argue that it's the caller's responsibility to provide a valid JSON array as an argument for the parameter of array type. If the LLM is returning the string, the error details should be sent back to it so it can make another call with the correct argument type - this is already implemented in SK.

My concern is that if we start doing the conversion because the LLM sometimes provides invalid JSON values, sooner or later we will find ourselves in a tricky situation when we are asked, for example, to create an array element for each word in the string - "v1 v2 v3," "v1, v2, v3," or "v1;v2;v3." How are we going to support that without introducing any behavioral change? We were creating an array with one element from the string, but now we would have an array with an element per word in the string. Will we add an extra option/execution parameter that allows us to tweak the conversion behavior? What if different string -> array conversion flavors are needed within the same operation or in different operations imported from the same document?

I think it's safer not to perform that conversion at the SK level and allow the LLM to self-correct based on function invocation errors. If an extra request to the LLM is not desirable, then consumers can easily decorate each operation with a delegate - How to transform a KernelFunction that would handle the conversion of arguments provided by the LLM based on the target type from the operation metadata and call original function with valid arguments.

Copy link
Member Author

@baywet baywet Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is a matter of the LLM generating "the wrong JSON" but rather how the response is being parsed, hence the fix.

For the following prompt using only the subject, recap my last 5 emails it eventually builds createdDateTime desc as the string value here for the $orderBy query parameter, which is correct.

       - name: $orderby
          in: query
          description: Order items by property values
          style: form
          explode: false
          schema:
            uniqueItems: true
            type: array
            items:
              type: string
              enum:
                - id
                - id desc
                - createdDateTime
                - createdDateTime desc

However, I do agree this could be a slippery slope, especially for request and response bodies, so maybe we limit those graceful upgrades to query/paths parameters and headers?

What's probably causing the mis-alignment here is the style from and the transformation that already gets applied post parsing. We could alternatively accept the LLMs response as is since it's correct and remove the parsing all together, but this might impact reliability in other cases?

I don't think letting the LLM brute force through adding extra square brackets is a productive use of resources here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baywet The example you show is serializing of URL parameters, as per 6570 rules. I don't think we can mix serializing of URL parameters and JSON. They follow very different rules. With 6570 whether the parameter value is a simple string, or an array of strings it would serialize correctly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which begs the question: why does the pipeline to generate query parameters value go through a JSON parsing in the first place?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the following prompt using only the subject, recap my last 5 emails it eventually builds createdDateTime desc as the string value here for the $orderBy query parameter, which is correct.

Who builds it, LLM or SK? If it's the LLM that provides "createdDate desc" string for a parameter of an array type, then it's incorrect because the "createdDate desc" string is not a valid JSON array (see https://datatracker.ietf.org/doc/html/rfc8259#section-5).

What's probably causing the mis-alignment here is the style from and the transformation that already gets applied post parsing. We could alternatively accept the LLMs response as is since it's correct and remove the parsing all together, but this might impact reliability in other cases?

To consider the LLM response as correct, it should conform to the parameter specification that requires the parameter to be of array type; every response that deviates from this is incorrect, and accepting it only pushes the issue further in the invocation chain -> parameters serialization failing to serialize the parameter value in accordance with the specified format or the REST API service itself receiving a wrongly formatted query string if the serialization part is dropped or produces the wrong result.

Parsing is required because SK supports non-LLM scenarios as well, where parameter arguments can be provided as a JSON string or .NET type. For example, the argument for the orderby parameter can either be a JSON array - "["id desc", "createdDateTime asc"]" or an instance of a .NET List type, such as new List<string>() { "id desc" }.

I don't think letting the LLM brute force through adding extra square brackets is a productive use of resources here.

... If an extra request to the LLM is not desirable, then consumers can easily decorate each operation with a delegate - How to transform a KernelFunction that would handle the conversion of arguments provided by the LLM based on the target type from the operation metadata and call original function with valid arguments.

Which begs the question: why does the pipeline to generate query parameters value go through a JSON parsing in the first place?

To simplify the parameters serialization functionality that would otherwise be cluttered with the parsing and validation logic required to format the parameters, as well as the if/else logic to determine whether the argument is JSON or an instance of a .NET type.

BTW: There's no need to specify "$" in the "$orderby" parameter name, SK will do it for you.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the additional information.

As far as I understand kernel functions transformations, they are deeply specific to a given function, which would not be suitable in our case since we'll want a solution that works for any function, generated from any OpenAPI/Api Manifest/Copilot Plugin.

Who builds it, LLM or SK?

I'm not sure, whatever ends up calling this method. As far as I understand, SK calls the LLM to get the query parameters values generated, but maybe I understand this wrong?

In your previous answer, you seem to be eluding to the fact that we might be able to "force the hand" of the LLM and tell it "no, really, even if you come up with a single result, it needs to be returned as a valid JSON array with a single entry". Or maybe this is me extrapolating. Would you have pointers for that?

@baywet
Copy link
Member Author

baywet commented Nov 21, 2024

Sharing some context from debugging

Here is the returned plan from the LLM (azure OpenAI GPT4)

Plan:

1. Use the `MessagesPlugin-me_ListMessages` function with the `_top` parameter set to 5 and `_orderby` parameter set to "receivedDateTime desc". This will retrieve the last 5 messages received, sorted by the most recent. Also, set the `_select` parameter to "subject" to only retrieve the subject of the emails.
2. For each message retrieved, extract the 'subject' field.
3. Use the `UserInteraction-SendFinalAnswer` function to send the subjects of the last 5 emails to the user.

As we can see, the LLM is returning a string, not an array of strings

Here is what we currently send to the LLM to generate the plan

[
	{
		"name": "MessagesPlugin-me_CreateMessages",
		"description": "Create an open extension (openTypeExtension object) and add custom properties in a new or existing instance of a resource. You can create an open extension in a resource instance and store custom data to it all in the same operation, except for specific resources. The table in the Permissions section lists the resources that support open extensions.",
		"parameters": {
			"type": "object",
			"required": [],
			"properties": {
				"id": {
					"type": "string",
					"description": "The unique identifier for an entity. Read-only."
				},
				"categories": {
					"type": "array",
					"items": {
						"type": "string",
						"nullable": true
					},
					"description": "The categories associated with the item"
				},
				"changeKey": {
					"type": "string",
					"description": "Identifies the version of the item. Every time the item is changed, changeKey changes as well. This allows Exchange to apply changes to the correct version of the object. Read-only.",
					"nullable": true
				},
				"createdDateTime": {
					"pattern": "^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[\u002B-][0-9][0-9]:[0-9][0-9])$",
					"type": "string",
					"description": "The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z",
					"format": "date-time",
					"nullable": true
				},
				"lastModifiedDateTime": {
					"pattern": "^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[\u002B-][0-9][0-9]:[0-9][0-9])$",
					"type": "string",
					"description": "The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z",
					"format": "date-time",
					"nullable": true
				},
				"bccRecipients": {
					"type": "array",
					"items": {
						"title": "recipient",
						"type": "object",
						"properties": {
							"emailAddress": {
								"anyOf": [
									{
										"title": "emailAddress",
										"type": "object",
										"properties": {
											"address": {
												"type": "string",
												"description": "The email address of the person or entity.",
												"nullable": true
											},
											"name": {
												"type": "string",
												"description": "The display name of the person or entity.",
												"nullable": true
											}
										}
									},
									{
										"type": "object",
										"nullable": true
									}
								],
								"description": "The recipient\u0027s email address."
							}
						}
					},
					"description": "The Bcc: recipients for the message."
				},
				"body": {
					"anyOf": [
						{
							"title": "itemBody",
							"type": "object",
							"properties": {
								"content": {
									"type": "string",
									"description": "The content of the item.",
									"nullable": true
								},
								"contentType": {
									"anyOf": [
										{
											"title": "bodyType",
											"enum": [
												"text",
												"html"
											],
											"type": "string"
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The type of the content. Possible values are text and html."
								}
							}
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The body of the message. It can be in HTML or text format. Find out about safe HTML in a message body."
				},
				"bodyPreview": {
					"type": "string",
					"description": "The first 255 characters of the message body. It is in text format.",
					"nullable": true
				},
				"ccRecipients": {
					"type": "array",
					"items": {
						"title": "recipient",
						"type": "object",
						"properties": {
							"emailAddress": {
								"anyOf": [
									{
										"title": "emailAddress",
										"type": "object",
										"properties": {
											"address": {
												"type": "string",
												"description": "The email address of the person or entity.",
												"nullable": true
											},
											"name": {
												"type": "string",
												"description": "The display name of the person or entity.",
												"nullable": true
											}
										}
									},
									{
										"type": "object",
										"nullable": true
									}
								],
								"description": "The recipient\u0027s email address."
							}
						}
					},
					"description": "The Cc: recipients for the message."
				},
				"conversationId": {
					"type": "string",
					"description": "The ID of the conversation the email belongs to.",
					"nullable": true
				},
				"conversationIndex": {
					"type": "string",
					"description": "Indicates the position of the message within the conversation.",
					"format": "base64url",
					"nullable": true
				},
				"flag": {
					"anyOf": [
						{
							"title": "followupFlag",
							"type": "object",
							"properties": {
								"completedDateTime": {
									"anyOf": [
										{
											"title": "dateTimeTimeZone",
											"type": "object",
											"properties": {
												"dateTime": {
													"type": "string",
													"description": "A single point of time in a combined date and time representation ({date}T{time}; for example, 2017-08-29T04:00:00.0000000)."
												},
												"timeZone": {
													"type": "string",
													"description": "Represents a time zone, for example, \u0027Pacific Standard Time\u0027. See below for more possible values.",
													"nullable": true
												}
											}
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The date and time that the follow-up was finished."
								},
								"dueDateTime": {
									"anyOf": [
										{
											"title": "dateTimeTimeZone",
											"type": "object",
											"properties": {
												"dateTime": {
													"type": "string",
													"description": "A single point of time in a combined date and time representation ({date}T{time}; for example, 2017-08-29T04:00:00.0000000)."
												},
												"timeZone": {
													"type": "string",
													"description": "Represents a time zone, for example, \u0027Pacific Standard Time\u0027. See below for more possible values.",
													"nullable": true
												}
											}
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The date and time that the follow-up is to be finished. Note: To set the due date, you must also specify the startDateTime; otherwise, you get a 400 Bad Request response."
								},
								"flagStatus": {
									"anyOf": [
										{
											"title": "followupFlagStatus",
											"enum": [
												"notFlagged",
												"complete",
												"flagged"
											],
											"type": "string"
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The status for follow-up for an item. Possible values are notFlagged, complete, and flagged."
								},
								"startDateTime": {
									"anyOf": [
										{
											"title": "dateTimeTimeZone",
											"type": "object",
											"properties": {
												"dateTime": {
													"type": "string",
													"description": "A single point of time in a combined date and time representation ({date}T{time}; for example, 2017-08-29T04:00:00.0000000)."
												},
												"timeZone": {
													"type": "string",
													"description": "Represents a time zone, for example, \u0027Pacific Standard Time\u0027. See below for more possible values.",
													"nullable": true
												}
											}
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The date and time that the follow-up is to begin."
								}
							}
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The flag value that indicates the status, start date, due date, or completion date for the message."
				},
				"from": {
					"anyOf": [
						{
							"title": "recipient",
							"type": "object",
							"properties": {
								"emailAddress": {
									"anyOf": [
										{
											"title": "emailAddress",
											"type": "object",
											"properties": {
												"address": {
													"type": "string",
													"description": "The email address of the person or entity.",
													"nullable": true
												},
												"name": {
													"type": "string",
													"description": "The display name of the person or entity.",
													"nullable": true
												}
											}
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The recipient\u0027s email address."
								}
							}
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The owner of the mailbox from which the message is sent. In most cases, this value is the same as the sender property, except for sharing or delegation scenarios. The value must correspond to the actual mailbox used. Find out more about setting the from and sender properties of a message."
				},
				"hasAttachments": {
					"type": "boolean",
					"description": "Indicates whether the message has attachments. This property doesn\u0027t include inline attachments, so if a message contains only inline attachments, this property is false. To verify the existence of inline attachments, parse the body property to look for a src attribute, such as \u003CIMG src=\u0027cid:[email protected]\u0027\u003E.",
					"nullable": true
				},
				"importance": {
					"anyOf": [
						{
							"title": "importance",
							"enum": [
								"low",
								"normal",
								"high"
							],
							"type": "string"
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The importance of the message. The possible values are: low, normal, and high."
				},
				"inferenceClassification": {
					"anyOf": [
						{
							"title": "inferenceClassificationType",
							"enum": [
								"focused",
								"other"
							],
							"type": "string"
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The classification of the message for the user, based on inferred relevance or importance, or on an explicit override. The possible values are: focused or other."
				},
				"internetMessageHeaders": {
					"type": "array",
					"items": {
						"title": "internetMessageHeader",
						"type": "object",
						"properties": {
							"name": {
								"type": "string",
								"description": "Represents the key in a key-value pair.",
								"nullable": true
							},
							"value": {
								"type": "string",
								"description": "The value in a key-value pair.",
								"nullable": true
							}
						}
					},
					"description": "A collection of message headers defined by RFC5322. The set includes message headers indicating the network path taken by a message from the sender to the recipient. It can also contain custom message headers that hold app data for the message.  Returned only on applying a $select query option. Read-only."
				},
				"internetMessageId": {
					"type": "string",
					"description": "The message ID in the format specified by RFC2822.",
					"nullable": true
				},
				"isDeliveryReceiptRequested": {
					"type": "boolean",
					"description": "Indicates whether a read receipt is requested for the message.",
					"nullable": true
				},
				"isDraft": {
					"type": "boolean",
					"description": "Indicates whether the message is a draft. A message is a draft if it hasn\u0027t been sent yet.",
					"nullable": true
				},
				"isRead": {
					"type": "boolean",
					"description": "Indicates whether the message has been read.",
					"nullable": true
				},
				"isReadReceiptRequested": {
					"type": "boolean",
					"description": "Indicates whether a read receipt is requested for the message.",
					"nullable": true
				},
				"parentFolderId": {
					"type": "string",
					"description": "The unique identifier for the message\u0027s parent mailFolder.",
					"nullable": true
				},
				"receivedDateTime": {
					"pattern": "^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[\u002B-][0-9][0-9]:[0-9][0-9])$",
					"type": "string",
					"description": "The date and time the message was received.  The date and time information uses ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z.",
					"format": "date-time",
					"nullable": true
				},
				"replyTo": {
					"type": "array",
					"items": {
						"title": "recipient",
						"type": "object",
						"properties": {
							"emailAddress": {
								"anyOf": [
									{
										"title": "emailAddress",
										"type": "object",
										"properties": {
											"address": {
												"type": "string",
												"description": "The email address of the person or entity.",
												"nullable": true
											},
											"name": {
												"type": "string",
												"description": "The display name of the person or entity.",
												"nullable": true
											}
										}
									},
									{
										"type": "object",
										"nullable": true
									}
								],
								"description": "The recipient\u0027s email address."
							}
						}
					},
					"description": "The email addresses to use when replying."
				},
				"sender": {
					"anyOf": [
						{
							"title": "recipient",
							"type": "object",
							"properties": {
								"emailAddress": {
									"anyOf": [
										{
											"title": "emailAddress",
											"type": "object",
											"properties": {
												"address": {
													"type": "string",
													"description": "The email address of the person or entity.",
													"nullable": true
												},
												"name": {
													"type": "string",
													"description": "The display name of the person or entity.",
													"nullable": true
												}
											}
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The recipient\u0027s email address."
								}
							}
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The account that is actually used to generate the message. In most cases, this value is the same as the from property. You can set this property to a different value when sending a message from a shared mailbox, for a shared calendar, or as a delegate. In any case, the value must correspond to the actual mailbox used. Find out more about setting the from and sender properties of a message."
				},
				"sentDateTime": {
					"pattern": "^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[\u002B-][0-9][0-9]:[0-9][0-9])$",
					"type": "string",
					"description": "The date and time the message was sent.  The date and time information uses ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z.",
					"format": "date-time",
					"nullable": true
				},
				"subject": {
					"type": "string",
					"description": "The subject of the message.",
					"nullable": true
				},
				"toRecipients": {
					"type": "array",
					"items": {
						"title": "recipient",
						"type": "object",
						"properties": {
							"emailAddress": {
								"anyOf": [
									{
										"title": "emailAddress",
										"type": "object",
										"properties": {
											"address": {
												"type": "string",
												"description": "The email address of the person or entity.",
												"nullable": true
											},
											"name": {
												"type": "string",
												"description": "The display name of the person or entity.",
												"nullable": true
											}
										}
									},
									{
										"type": "object",
										"nullable": true
									}
								],
								"description": "The recipient\u0027s email address."
							}
						}
					},
					"description": "The To: recipients for the message."
				},
				"uniqueBody": {
					"anyOf": [
						{
							"title": "itemBody",
							"type": "object",
							"properties": {
								"content": {
									"type": "string",
									"description": "The content of the item.",
									"nullable": true
								},
								"contentType": {
									"anyOf": [
										{
											"title": "bodyType",
											"enum": [
												"text",
												"html"
											],
											"type": "string"
										},
										{
											"type": "object",
											"nullable": true
										}
									],
									"description": "The type of the content. Possible values are text and html."
								}
							}
						},
						{
							"type": "object",
							"nullable": true
						}
					],
					"description": "The part of the body of the message that is unique to the current message. uniqueBody is not returned by default but can be retrieved for a given message by use of the ?$select=uniqueBody query. It can be in HTML or text format."
				},
				"webLink": {
					"type": "string",
					"description": "The URL to open the message in Outlook on the web.You can append an ispopout argument to the end of the URL to change how the message is displayed. If ispopout is not present or if it is set to 1, then the message is shown in a popout window. If ispopout is set to 0, the browser shows the message in the Outlook on the web review pane.The message opens in the browser if you are signed in to your mailbox via Outlook on the web. You are prompted to sign in if you are not already signed in with the browser.This URL cannot be accessed from within an iFrame.",
					"nullable": true
				},
				"attachments": {
					"type": "array",
					"items": {
						"allOf": [
							{
								"title": "entity",
								"type": "object",
								"properties": {
									"id": {
										"type": "string",
										"description": "The unique identifier for an entity. Read-only."
									}
								}
							},
							{
								"title": "attachment",
								"type": "object",
								"properties": {
									"contentType": {
										"type": "string",
										"description": "The MIME type.",
										"nullable": true
									},
									"isInline": {
										"type": "boolean",
										"description": "true if the attachment is an inline attachment; otherwise, false."
									},
									"lastModifiedDateTime": {
										"pattern": "^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[\u002B-][0-9][0-9]:[0-9][0-9])$",
										"type": "string",
										"description": "The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z",
										"format": "date-time",
										"nullable": true
									},
									"name": {
										"type": "string",
										"description": "The attachment\u0027s file name.",
										"nullable": true
									},
									"size": {
										"maximum": 2147483647,
										"minimum": -2147483648,
										"type": "number",
										"description": "The length of the attachment in bytes.",
										"format": "int32"
									}
								}
							}
						]
					},
					"description": "The fileAttachment and itemAttachment attachments for the message."
				},
				"extensions": {
					"type": "array",
					"items": {
						"allOf": [
							{
								"title": "entity",
								"type": "object",
								"properties": {
									"id": {
										"type": "string",
										"description": "The unique identifier for an entity. Read-only."
									}
								}
							},
							{
								"title": "extension",
								"type": "object"
							}
						]
					},
					"description": "The collection of open extensions defined for the message. Nullable."
				},
				"multiValueExtendedProperties": {
					"type": "array",
					"items": {
						"allOf": [
							{
								"title": "entity",
								"type": "object",
								"properties": {
									"id": {
										"type": "string",
										"description": "The unique identifier for an entity. Read-only."
									}
								}
							},
							{
								"title": "multiValueLegacyExtendedProperty",
								"type": "object",
								"properties": {
									"value": {
										"type": "array",
										"items": {
											"type": "string",
											"nullable": true
										},
										"description": "A collection of property values."
									}
								}
							}
						]
					},
					"description": "The collection of multi-value extended properties defined for the message. Nullable."
				},
				"singleValueExtendedProperties": {
					"type": "array",
					"items": {
						"allOf": [
							{
								"title": "entity",
								"type": "object",
								"properties": {
									"id": {
										"type": "string",
										"description": "The unique identifier for an entity. Read-only."
									}
								}
							},
							{
								"title": "singleValueLegacyExtendedProperty",
								"type": "object",
								"properties": {
									"value": {
										"type": "string",
										"description": "A property value.",
										"nullable": true
									}
								}
							}
						]
					},
					"description": "The collection of single-value extended properties defined for the message. Nullable."
				}
			}
		},
		"responses": {}
	},
	{
		"name": "MessagesPlugin-me_ListMessages",
		"description": "Get an open extension (openTypeExtension object) identified by name or fully qualified name. The table in the Permissions section lists the resources that support open extensions. The following table lists the three scenarios where you can get an open extension from a supported resource instance.",
		"parameters": {
			"type": "object",
			"required": [],
			"properties": {
				"includeHiddenMessages": {
					"type": "string"
				},
				"_top": {
					"minimum": 0,
					"type": "integer"
				},
				"_skip": {
					"minimum": 0,
					"type": "integer"
				},
				"_search": {
					"type": "string"
				},
				"_filter": {
					"type": "string"
				},
				"_count": {
					"type": "boolean"
				},
				"_orderby": {
					"uniqueItems": true,
					"type": "array",
					"items": {
						"enum": [
							"id",
							"id desc",
							"categories",
							"categories desc",
							"changeKey",
							"changeKey desc",
							"createdDateTime",
							"createdDateTime desc",
							"lastModifiedDateTime",
							"lastModifiedDateTime desc",
							"bccRecipients",
							"bccRecipients desc",
							"body",
							"body desc",
							"bodyPreview",
							"bodyPreview desc",
							"ccRecipients",
							"ccRecipients desc",
							"conversationId",
							"conversationId desc",
							"conversationIndex",
							"conversationIndex desc",
							"flag",
							"flag desc",
							"from",
							"from desc",
							"hasAttachments",
							"hasAttachments desc",
							"importance",
							"importance desc",
							"inferenceClassification",
							"inferenceClassification desc",
							"internetMessageHeaders",
							"internetMessageHeaders desc",
							"internetMessageId",
							"internetMessageId desc",
							"isDeliveryReceiptRequested",
							"isDeliveryReceiptRequested desc",
							"isDraft",
							"isDraft desc",
							"isRead",
							"isRead desc",
							"isReadReceiptRequested",
							"isReadReceiptRequested desc",
							"parentFolderId",
							"parentFolderId desc",
							"receivedDateTime",
							"receivedDateTime desc",
							"replyTo",
							"replyTo desc",
							"sender",
							"sender desc",
							"sentDateTime",
							"sentDateTime desc",
							"subject",
							"subject desc",
							"toRecipients",
							"toRecipients desc",
							"uniqueBody",
							"uniqueBody desc",
							"webLink",
							"webLink desc"
						],
						"type": "string"
					}
				},
				"_select": {
					"uniqueItems": true,
					"type": "array",
					"items": {
						"enum": [
							"id",
							"categories",
							"changeKey",
							"createdDateTime",
							"lastModifiedDateTime",
							"bccRecipients",
							"body",
							"bodyPreview",
							"ccRecipients",
							"conversationId",
							"conversationIndex",
							"flag",
							"from",
							"hasAttachments",
							"importance",
							"inferenceClassification",
							"internetMessageHeaders",
							"internetMessageId",
							"isDeliveryReceiptRequested",
							"isDraft",
							"isRead",
							"isReadReceiptRequested",
							"parentFolderId",
							"receivedDateTime",
							"replyTo",
							"sender",
							"sentDateTime",
							"subject",
							"toRecipients",
							"uniqueBody",
							"webLink",
							"attachments",
							"extensions",
							"multiValueExtendedProperties",
							"singleValueExtendedProperties"
						],
						"type": "string"
					}
				},
				"_expand": {
					"uniqueItems": true,
					"type": "array",
					"items": {
						"enum": [
							"*",
							"attachments",
							"extensions",
							"multiValueExtendedProperties",
							"singleValueExtendedProperties"
						],
						"type": "string"
					}
				}
			}
		},
		"responses": {}
	},
	{
		"name": "UserInteraction-SendFinalAnswer",
		"description": "This function is used to send the final answer of a plan to the user.",
		"parameters": {
			"type": "object",
			"required": [
				"answer"
			],
			"properties": {
				"answer": {
					"type": "string",
					"description": "The final answer"
				}
			}
		},
		"responses": {}
	}
]

@baywet
Copy link
Member Author

baywet commented Nov 21, 2024

Note: gpt4o generates the correct array structure, and even gpt4 generates the correct structure for the subject query parameter.

From discussions with @SergeyMenshykh we could apply that transformation at a different place. Wrapping the function calls with a decorator. This way people could choose to implement variations based on their scenarios (no splitting, csv, scsv, space sv, ...)

@baywet
Copy link
Member Author

baywet commented Nov 21, 2024

Another option still with the decorator pattern would be to reinforce the prompt, either at the schema level, or during the prompt for the planner saying something like "if the schema is an array and you only want to generate a single value, make sure you format it as an array"

// Invoke the kernel with a chat prompt and display the result
string chatPrompt = """
<message role="system">Respond with JSON.</message>
    """;
has context menu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
.NET Issue or Pull requests regarding .NET code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants