Created
November 1, 2024 13:35
-
-
Save isaacabraham/2f60d5eb51b17f73e671822a94e6b9b4 to your computer and use it in GitHub Desktop.
F# OpenAI FunctionTools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#r "nuget: OpenAI, 2.0.0" | |
open OpenAI.Chat | |
open System.Text.Json | |
open System.Collections.Generic | |
open System.IO | |
let client = ChatClient("gpt-4o", "KEY GOES HERE") | |
module FsOpenAi = | |
type FunctionTool = Map<string, obj> -> string | |
open System.Text.Json.Serialization | |
open System | |
let jsonOptions = | |
JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault) | |
type PropertyType = | |
| Enum of string list | |
| String | |
| Number | |
type Property = { | |
Name: string | |
Description: string | |
Type: PropertyType | |
} with | |
static member Create name description type_ = { | |
Name = name | |
Description = description | |
Type = type_ | |
} | |
type FunctionToolConfig = { | |
Name: string | |
Description: string | |
Parameters: Property list | |
Func: FunctionTool | |
} with | |
static member Create name description parameters func = { | |
Name = name | |
Description = description | |
Parameters = parameters | |
Func = func | |
} | |
let makeTool config = | |
let asKeyValuePair (property: Property) = | |
property.Name, | |
{| | |
``type`` = | |
match property.Type with | |
| Enum _ | |
| String -> "string" | |
| Number -> "number" | |
description = property.Description | |
``enum`` = | |
match property.Type with | |
| Enum values -> box values | |
| _ -> null | |
|} | |
let parameters = | |
match config.Parameters with | |
| [] -> null | |
| _ -> | |
box {| | |
``type`` = "object" | |
required = config.Parameters |> List.map _.Name | |
properties = config.Parameters |> List.map asKeyValuePair |> Map | |
|} | |
ChatTool.CreateFunctionTool( | |
config.Name, | |
config.Description, | |
JsonSerializer.Serialize(parameters, jsonOptions) |> BinaryData.FromString | |
) | |
type Conversation = { | |
Messages: ResizeArray<ChatMessage> | |
Tools: Map<string, FunctionTool> | |
Options: ChatCompletionOptions | |
} with | |
/// Starts a brand new conversation | |
static member StartNew(?tools) = | |
let tools = defaultArg tools [] | |
{ | |
Messages = ResizeArray() | |
Tools = | |
tools | |
|> List.map (fun (config: FunctionToolConfig) -> config.Name, config.Func) | |
|> Map | |
Options = | |
let options = ChatCompletionOptions() | |
for tool in tools do | |
options.Tools.Add(makeTool tool) | |
options | |
} | |
/// Sends a message to the bot and returns the response, storing the response in the conversation value. | |
member this.SendMessage(message: string) = | |
this.Messages.Add(UserChatMessage message) | |
let response = client.CompleteChat(this.Messages, this.Options) | |
this.Messages.Add(AssistantChatMessage response.Value) | |
let rec handleToolCalls (toolCalls: ChatToolCall IReadOnlyList) = | |
match toolCalls.Count with | |
| 0 -> () | |
| _ -> | |
for toolCall in toolCalls do | |
let tool = this.Tools.[toolCall.FunctionName] | |
let args = JsonSerializer.Deserialize<Map<string, obj>> toolCall.FunctionArguments | |
let toolResponse = tool args | |
this.Messages.Add(ToolChatMessage(toolCall.Id, toolResponse)) | |
let response = client.CompleteChat(this.Messages, this.Options) | |
this.Messages.Add(AssistantChatMessage response.Value) | |
handleToolCalls response.Value.ToolCalls | |
handleToolCalls response.Value.ToolCalls | |
this.History() |> Seq.last |> snd | |
/// Returns the history of the conversation, as a tuple array of message type and content. | |
member this.History() = | |
this.Messages | |
|> Seq.map (fun message -> | |
let description = | |
match message with | |
| :? UserChatMessage as m -> m.Content[0].Text | |
| :? AssistantChatMessage as m -> | |
match m.ToolCalls.Count with | |
| 0 -> m.Content[0].Text | |
| _ -> | |
m.ToolCalls | |
|> Seq.map (fun call -> | |
$"Calling {call.FunctionName} with arguments %A{call.FunctionArguments | |
|> JsonSerializer.Deserialize<Map<string, obj>> | |
|> Seq.toArray}") | |
|> String.concat ", " | |
| :? ToolChatMessage as m -> $"{m.Content[0].Text}" | |
| _ -> failwith "Unknown message type" | |
message.GetType().Name, description) | |
|> Seq.toArray | |
open FsOpenAi | |
//1. Basic call | |
let response = | |
client.CompleteChat([ UserChatMessage "Please send a funny greeting back to me!" :> ChatMessage ]) | |
//2. Chat history | |
let responseOfConversation = | |
client.CompleteChat [ | |
UserChatMessage "Please send a funny greeting back to me!" :> ChatMessage | |
AssistantChatMessage | |
"Sure thing! Here you go: Why did the computer go to therapy? Because it had too many bytes of emotional baggage! Hope your day is full of laughter and zero glitches!" | |
UserChatMessage "Now send me a short one." | |
] | |
responseOfConversation.Value.Content[0].Text | |
//3. Stateful conversation | |
let conversation = Conversation.StartNew() | |
conversation.SendMessage "Please send a funny greeting back to me!" | |
conversation.SendMessage "Now send a short, unfunny one." | |
conversation.SendMessage "Yes, I'm having a great day thanks! How about you?" | |
conversation.History() | |
let fsConversation = Conversation.StartNew() | |
fsConversation.SendMessage "What is the F# programming language?" | |
fsConversation.SendMessage | |
"Give me those points as some JSON that I can parse, with properties Name and Description. Don't include any other information or headings - just plain JSON please that can be directly parsed by a machine." | |
fsConversation.SendMessage "No markdown formatting. PLAIN JSON ONLY." | |
let data = | |
JsonSerializer.Deserialize<{| Name: string; Description: string |} array>( | |
fsConversation.History() |> Seq.item 5 |> snd | |
) | |
fsConversation.SendMessage | |
"Now return that as a fully-featured HTML page with styles built on top of the Bulma CSS framework." | |
// Working with Function Tools | |
let tools = [ | |
FunctionToolConfig.Create | |
"ReadFile" | |
"Loads the contents of a file" | |
[ | |
Property.Create "Path" "The relative path (from the current directory) to the file" String | |
] | |
(fun args -> File.ReadAllText(System.IO.Path.Combine(__SOURCE_DIRECTORY__, args.["Path"] |> string))) | |
{ | |
Name = "Greet" | |
Description = "Greets the user" | |
Parameters = [ | |
Property.Create "Name" "The name of the person to greet" String | |
Property.Create "Friendliness" "How friendly the greeting should be, on the scale of 1-5" Number | |
] | |
Func = | |
fun args -> | |
let name = string args.["Name"] | |
let friendliness = int (string args.["Friendliness"]) | |
if friendliness < 3 then | |
$"Hi, {name}." | |
else | |
$"Hello, {name}!" | |
} | |
] | |
let toolConversation = Conversation.StartNew(tools) | |
toolConversation.SendMessage "Send greetings to all the people listed in the GuestList.txt file." | |
toolConversation.History() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment