Merge pull request #436 from hchen2020/master

Anthropic AI
This commit is contained in:
C. Oceania 2024-05-03 11:42:59 -05:00 committed by GitHub
commit d0144a686e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 437 additions and 23 deletions

View file

@ -42,6 +42,9 @@ public class RoleDialogModel : ITrackableMessage
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? FunctionName { get; set; } public string? FunctionName { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ToolCallId { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? PostbackFunctionName { get; set; } public string? PostbackFunctionName { get; set; }
@ -108,6 +111,7 @@ public class RoleDialogModel : ITrackableMessage
MessageId = source.MessageId, MessageId = source.MessageId,
FunctionArgs = source.FunctionArgs, FunctionArgs = source.FunctionArgs,
FunctionName = source.FunctionName, FunctionName = source.FunctionName,
ToolCallId = source.ToolCallId,
PostbackFunctionName = source.PostbackFunctionName, PostbackFunctionName = source.PostbackFunctionName,
RichContent = source.RichContent, RichContent = source.RichContent,
StopCompletion = source.StopCompletion, StopCompletion = source.StopCompletion,

View file

@ -13,7 +13,7 @@ public class FunctionCallingResponse
[JsonPropertyName("content")] [JsonPropertyName("content")]
public string? Content { get; set; } public string? Content { get; set; }
[JsonPropertyName("function_name")] [JsonPropertyName("function")]
public string? FunctionName { get; set; } public string? FunctionName { get; set; }
[JsonPropertyName("args")] [JsonPropertyName("args")]

View file

@ -2,8 +2,11 @@ namespace BotSharp.Abstraction.Functions.Models;
public class FunctionDef public class FunctionDef
{ {
public string Name { get; set; } [JsonPropertyName("name")]
public string Description { get; set; } public string Name { get; set; } = null!;
[JsonPropertyName("description")]
public string Description { get; set; } = null!;
[JsonPropertyName("visibility_expression")] [JsonPropertyName("visibility_expression")]
public string? VisibilityExpression { get; set; } public string? VisibilityExpression { get; set; }
@ -11,6 +14,7 @@ public class FunctionDef
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Impact { get; set; } public string? Impact { get; set; }
[JsonPropertyName("parameters")]
public FunctionParametersDef Parameters { get; set; } = new FunctionParametersDef(); public FunctionParametersDef Parameters { get; set; } = new FunctionParametersDef();
public override string ToString() public override string ToString()

View file

@ -19,6 +19,11 @@ public class FunctionParametersDef
[JsonPropertyName("required")] [JsonPropertyName("required")]
public List<string> Required { get; set; } = new List<string>(); public List<string> Required { get; set; } = new List<string>();
public override string ToString()
{
return $"{{\"type\":\"{Type}\", \"properties\":{JsonSerializer.Serialize(Properties)}, \"required\":[{string.Join(",", Required.Select(x => "\"" + x + "\""))}]}}";
}
public FunctionParametersDef() public FunctionParametersDef()
{ {

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
@ -150,7 +150,7 @@
<PackageReference Include="Aspects.Cache" Version="2.0.4" /> <PackageReference Include="Aspects.Cache" Version="2.0.4" />
<PackageReference Include="Colorful.Console" Version="1.2.15" /> <PackageReference Include="Colorful.Console" Version="1.2.15" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.2.1" /> <PackageReference Include="EntityFrameworkCore.BootKit" Version="8.2.1" />
<PackageReference Include="Fluid.Core" Version="2.8.0" /> <PackageReference Include="Fluid.Core" Version="2.9.0" />
<PackageReference Include="Nanoid" Version="3.0.0" /> <PackageReference Include="Nanoid" Version="3.0.0" />
<PackageReference Include="RedLock.net" Version="2.3.2" /> <PackageReference Include="RedLock.net" Version="2.3.2" />
</ItemGroup> </ItemGroup>

View file

@ -17,8 +17,19 @@ public partial class RoutingService
return false; return false;
} }
var provide = agent.LlmConfig.Provider;
var model = agent.LlmConfig.Model;
if (provide == null || model == null)
{
var agentSettings = _services.GetRequiredService<AgentSettings>();
provide = agentSettings.LlmConfig.Provider;
model = agentSettings.LlmConfig.Model;
}
var chatCompletion = CompletionProvider.GetChatCompletion(_services, var chatCompletion = CompletionProvider.GetChatCompletion(_services,
agentConfig: agent.LlmConfig); provider: provide,
model: model);
var message = dialogs.Last(); var message = dialogs.Last();
var response = await chatCompletion.GetChatCompletions(agent, dialogs); var response = await chatCompletion.GetChatCompletions(agent, dialogs);
@ -31,6 +42,7 @@ public partial class RoutingService
{ {
response.FunctionName = response.FunctionName.Split("/").Last(); response.FunctionName = response.FunctionName.Split("/").Last();
} }
message.ToolCallId = response.ToolCallId;
message.FunctionName = response.FunctionName; message.FunctionName = response.FunctionName;
message.FunctionArgs = response.FunctionArgs; message.FunctionArgs = response.FunctionArgs;
message.CurrentAgentId = agent.Id; message.CurrentAgentId = agent.Id;

View file

@ -27,6 +27,8 @@ public class TemplateRender : ITemplateRender
_options.MemberAccessStrategy.Register<Agent>(); _options.MemberAccessStrategy.Register<Agent>();
_options.MemberAccessStrategy.Register<RoutableAgent>(); _options.MemberAccessStrategy.Register<RoutableAgent>();
_options.MemberAccessStrategy.Register<RoutingHandlerDef>(); _options.MemberAccessStrategy.Register<RoutingHandlerDef>();
_options.MemberAccessStrategy.Register<FunctionDef>();
_options.MemberAccessStrategy.Register<FunctionParametersDef>();
_options.MemberAccessStrategy.Register<UserIdentity>(); _options.MemberAccessStrategy.Register<UserIdentity>();
} }

View file

@ -1,9 +1,14 @@
[Output Requirements] {% if functions and functions != empty %}
1. Read the [Functions] definition, you can utilize the function to retrieve data or execute actions. [FUNCTIONS]
2. Think step by step, check if specific function will provider data to help complete user request based on the conversation. {% for fn in functions -%}
3. If you need to call a function to decide how to response user, {{ fn.name }}: {{ fn.description }}
response in format: {"role": "function", "reason":"why choose this function", "function_name": "", "args": {}}, {{ fn.parameters }}
otherwise response in format: {"role": "assistant", "reason":"why response to user", "content":"next step question"}. {{ "\r\n" }}
4. If the conversation already contains the function execution result, don't need to call it again. {%- endfor %}
5. If user mentioned some specific requirment, don't ask this question in your response. response_to_user: response to user directly without using any function.
6. Don't repeat the same question in your response. {"type": "object", "properties": {"content":{"type": "string", "description": "The content responsed to user"}}, "required":["content"]}
[RESPONSE OUTPUT REQUIREMENTS]
* Pick the appropriate function and populate the arguments defined in properties.
* Output the JSON {"function": "", "args":{}} without other text
{% endif %}

View file

@ -0,0 +1,27 @@
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Plugins;
using BotSharp.Plugin.AnthropicAI.Providers;
using BotSharp.Plugin.AnthropicAI.Settings;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace BotSharp.Plugin.AnthropicAI;
public class AnthropicPlugin : IBotSharpPlugin
{
public string Id => "012119da-8367-4be8-9a75-ab6ae55071e6";
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
var settings = new AnthropicSettings();
config.Bind("Anthropic", settings);
services.AddSingleton(x =>
{
// Console.WriteLine($"Loaded Anthropic settings: {settings.Claude.ApiKey.SubstringMax(4)}");
return settings;
});
services.AddScoped<IChatCompletion, ChatCompletionProvider>();
// services.AddScoped<ITextCompletion, TextCompletionProvider>();
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Anthropic.SDK" Version="3.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,266 @@
using Anthropic.SDK.Common;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace BotSharp.Plugin.AnthropicAI.Providers;
public class ChatCompletionProvider : IChatCompletion
{
public string Provider => "anthropic";
protected readonly AnthropicSettings _settings;
protected readonly IServiceProvider _services;
protected readonly ILogger _logger;
protected string _model;
public ChatCompletionProvider(AnthropicSettings settings,
ILogger<ChatCompletionProvider> logger,
IServiceProvider services)
{
_settings = settings;
_logger = logger;
_services = services;
}
public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
{
var contentHooks = _services.GetServices<IContentGeneratingHook>().ToList();
// Before chat completion hook
foreach (var hook in contentHooks)
{
await hook.BeforeGenerating(agent, conversations);
}
var settingsService = _services.GetRequiredService<ILlmProviderService>();
var settings = settingsService.GetSetting("anthropic", agent.LlmConfig?.Model ?? "claude-3-haiku");
var client = new AnthropicClient(new APIAuthentication(settings.ApiKey));
var (prompt, parameters) = PrepareOptions(agent, conversations);
var response = await client.Messages.GetClaudeMessageAsync(parameters);
RoleDialogModel responseMessage;
if (response.StopReason == "tool_use")
{
var toolResult = response.Content.OfType<ToolUseContent>().First();
responseMessage = new RoleDialogModel(AgentRole.Function, response.FirstMessage?.Text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.Last().MessageId,
ToolCallId = toolResult.Id,
FunctionName = toolResult.Name,
FunctionArgs = JsonSerializer.Serialize(toolResult.Input)
};
}
else
{
var message = response.FirstMessage;
responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.Last().MessageId
};
}
// After chat completion hook
foreach (var hook in contentHooks)
{
await hook.AfterGenerated(responseMessage, new TokenStatsModel
{
Prompt = prompt,
Provider = Provider,
Model = _model,
PromptCount = response.Usage.InputTokens,
CompletionCount = response.Usage.OutputTokens
});
}
return responseMessage;
}
public Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting)
{
throw new NotImplementedException();
}
public Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived)
{
throw new NotImplementedException();
}
private (string, MessageParameters) PrepareOptions(Agent agent, List<RoleDialogModel> conversations)
{
var instruction = "";
var agentService = _services.GetRequiredService<IAgentService>();
if (!string.IsNullOrEmpty(agent.Instruction))
{
instruction += agentService.RenderedInstruction(agent);
}
/*var routing = _services.GetRequiredService<IRoutingService>();
var router = routing.Router;
var render = _services.GetRequiredService<ITemplateRender>();
var template = router.Templates.FirstOrDefault(x => x.Name == "response_with_function").Content;
var response_with_function = render.Render(template, new Dictionary<string, object>
{
{ "functions", agent.Functions }
});
prompt += "\r\n\r\n" + response_with_function;*/
var messages = new List<Message>();
foreach (var conv in conversations)
{
if (conv.Role == AgentRole.User)
{
messages.Add(new Message(RoleType.User, conv.Content));
}
else if (conv.Role == AgentRole.Assistant)
{
messages.Add(new Message(RoleType.Assistant, conv.Content));
}
else if (conv.Role == AgentRole.Function)
{
messages.Add(new Message
{
Role = RoleType.Assistant,
Content = new List<ContentBase>
{
new ToolUseContent()
{
Id = conv.ToolCallId,
Name = conv.FunctionName,
Input = JsonNode.Parse(conv.FunctionArgs ?? "{}")
}
}
});
messages.Add(new Message()
{
Role = RoleType.User,
Content = new List<ContentBase>
{
new ToolResultContent()
{
ToolUseId = conv.ToolCallId,
Content = conv.Content
}
}
});
}
}
var parameters = new MessageParameters()
{
Messages = messages,
MaxTokens = 256,
Model = AnthropicModels.Claude3Haiku,
Stream = false,
Temperature = 0m,
SystemMessage = instruction,
Tools = new List<Function>() { }
};
JsonSerializerOptions jsonSerializationOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
ReferenceHandler = ReferenceHandler.IgnoreCycles,
};
foreach (var fn in agent.Functions)
{
/*var inputschema = new InputSchema()
{
Type = fn.Parameters.Type,
Properties = new Dictionary<string, Property>()
{
{ "location", new Property() { Type = "string", Description = "The location of the weather" } },
{
"tempType", new Property()
{
Type = "string", Enum = Enum.GetNames(typeof(TempType)),
Description = "The unit of temperature, celsius or fahrenheit"
}
}
},
Required = fn.Parameters.Required
};*/
string jsonString = JsonSerializer.Serialize(fn.Parameters, jsonSerializationOptions);
parameters.Tools.Add(new Function(fn.Name, fn.Description,
JsonNode.Parse(jsonString)));
}
var prompt = GetPrompt(parameters);
return (prompt, parameters);
}
private string GetPrompt(MessageParameters parameters)
{
var prompt = $"{parameters.SystemMessage}\r\n";
prompt += "\r\n[CONVERSATION]";
var verbose = string.Join("\r\n", parameters.Messages
.Select(x =>
{
var role = x.Role.ToString().ToLower();
if (x.Role == RoleType.User)
{
var content = string.Join("\r\n", x.Content.Select(c =>
{
if (c is TextContent text)
return text.Text;
else if (c is ToolResultContent tool)
return $"{tool.Content}";
else
return string.Empty;
}));
return $"{role}: {content}";
}
else if (x.Role == RoleType.Assistant)
{
var content = string.Join("\r\n", x.Content.Select(c =>
{
if (c is TextContent text)
return text.Text;
else if (c is ToolUseContent tool)
return $"Call function {tool.Name}({JsonSerializer.Serialize(tool.Input)})";
else
return string.Empty;
}));
return $"{role}: {content}";
}
return string.Empty;
}));
prompt += $"\r\n{verbose}\r\n";
if (parameters.Tools != null && parameters.Tools.Count > 0)
{
var functions = string.Join("\r\n", parameters.Tools.Select(x =>
{
return $"\r\n{x.Name}: {x.Description}\r\n{JsonSerializer.Serialize(x.Parameters)}";
}));
prompt += $"\r\n[FUNCTIONS]\r\n{functions}\r\n";
}
return prompt;
}
public void SetModelName(string model)
{
_model = model;
}
}

View file

@ -0,0 +1,6 @@
namespace BotSharp.Plugin.AnthropicAI.Settings;
public class AnthropicSettings
{
public ClaudeSetting Claude { get; set; }
}

View file

@ -0,0 +1,5 @@
namespace BotSharp.Plugin.AnthropicAI.Settings;
public class ClaudeSetting
{
}

View file

@ -0,0 +1,16 @@
global using Anthropic.SDK;
global using Anthropic.SDK.Constants;
global using Anthropic.SDK.Messaging;
global using BotSharp.Abstraction.Agents;
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Agents.Models;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Functions.Models;
global using BotSharp.Abstraction.Loggers;
global using BotSharp.Abstraction.MLTasks;
global using BotSharp.Abstraction.Routing;
global using BotSharp.Abstraction.Templating;
global using BotSharp.Plugin.AnthropicAI.Settings;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
global using BotSharp.Abstraction.Utilities;

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
@ -11,7 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.15" /> <PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.16" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -52,11 +52,7 @@ public class ChatCompletionProvider : IChatCompletion
var choice = response.Value.Choices[0]; var choice = response.Value.Choices[0];
var message = choice.Message; var message = choice.Message;
var responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Content) RoleDialogModel responseMessage;
{
CurrentAgentId = agent.Id,
MessageId = conversations.Last().MessageId
};
if (choice.FinishReason == CompletionsFinishReason.FunctionCall) if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{ {
@ -74,6 +70,33 @@ public class ChatCompletionProvider : IChatCompletion
responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last(); responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last();
} }
} }
else if (choice.FinishReason == CompletionsFinishReason.ToolCalls)
{
// Add the assistant message with tool calls to the conversation history
// ChatRequestAssistantMessage toolCallHistoryMessage = new(message);
// chatCompletionsOptions.Messages.Add(toolCallHistoryMessage);
// Add a new tool message for each tool call that is resolved
var toolCall = message.ToolCalls.First() as ChatCompletionsFunctionToolCall;
// var toolCallResponseMessage = GetToolCallResponseMessage(toolCall);
// Now make a new request with all the messages thus far, including the original
responseMessage = new RoleDialogModel(AgentRole.Function, message.Content)
{
CurrentAgentId = agent.Id,
MessageId = conversations.Last().MessageId,
FunctionName = toolCall.Name,
FunctionArgs = toolCall.Arguments
};
}
else
{
responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Content)
{
CurrentAgentId = agent.Id,
MessageId = conversations.Last().MessageId
};
}
// After chat completion hook // After chat completion hook
foreach(var hook in contentHooks) foreach(var hook in contentHooks)
@ -222,7 +245,17 @@ public class ChatCompletionProvider : IChatCompletion
if (agentService.RenderFunction(agent, function)) if (agentService.RenderFunction(agent, function))
{ {
var property = agentService.RenderFunctionProperty(agent, function); var property = agentService.RenderFunctionProperty(agent, function);
chatCompletionsOptions.Functions.Add(new FunctionDefinition
// legacy function call
/*chatCompletionsOptions.Functions.Add(new FunctionDefinition
{
Name = function.Name,
Description = function.Description,
Parameters = BinaryData.FromObjectAsJson(property)
});*/
// new chat tool
chatCompletionsOptions.Tools.Add(new ChatCompletionsFunctionToolDefinition
{ {
Name = function.Name, Name = function.Name,
Description = function.Description, Description = function.Description,
@ -241,6 +274,7 @@ public class ChatCompletionProvider : IChatCompletion
}); });
chatCompletionsOptions.Messages.Add(new ChatRequestFunctionMessage(message.FunctionName, message.Content)); chatCompletionsOptions.Messages.Add(new ChatRequestFunctionMessage(message.FunctionName, message.Content));
// chatCompletionsOptions.Messages.Add(new ChatRequestToolMessage(message.Content, message.ToolCallId));
} }
else if (message.Role == ChatRole.User) else if (message.Role == ChatRole.User)
{ {
@ -346,4 +380,15 @@ public class ChatCompletionProvider : IChatCompletion
{ {
_model = model; _model = model;
} }
ChatRequestToolMessage GetToolCallResponseMessage(ChatCompletionsToolCall toolCall)
{
var functionToolCall = toolCall as ChatCompletionsFunctionToolCall;
// Validate and process the JSON arguments for the function call
string unvalidatedArguments = functionToolCall.Arguments;
var functionResultData = (object)null; // GetYourFunctionResultData(unvalidatedArguments);
// Here, replacing with an example as if returned from "GetYourFunctionResultData"
functionResultData = "31 celsius";
return new ChatRequestToolMessage(functionResultData.ToString(), toolCall.Id);
}
} }