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

View file

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

View file

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

View file

@ -19,6 +19,11 @@ public class FunctionParametersDef
[JsonPropertyName("required")]
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()
{

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
@ -150,7 +150,7 @@
<PackageReference Include="Aspects.Cache" Version="2.0.4" />
<PackageReference Include="Colorful.Console" Version="1.2.15" />
<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="RedLock.net" Version="2.3.2" />
</ItemGroup>

View file

@ -17,8 +17,19 @@ public partial class RoutingService
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,
agentConfig: agent.LlmConfig);
provider: provide,
model: model);
var message = dialogs.Last();
var response = await chatCompletion.GetChatCompletions(agent, dialogs);
@ -31,6 +42,7 @@ public partial class RoutingService
{
response.FunctionName = response.FunctionName.Split("/").Last();
}
message.ToolCallId = response.ToolCallId;
message.FunctionName = response.FunctionName;
message.FunctionArgs = response.FunctionArgs;
message.CurrentAgentId = agent.Id;

View file

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

View file

@ -1,9 +1,14 @@
[Output Requirements]
1. Read the [Functions] definition, you can utilize the function to retrieve data or execute actions.
2. Think step by step, check if specific function will provider data to help complete user request based on the conversation.
3. If you need to call a function to decide how to response user,
response in format: {"role": "function", "reason":"why choose this function", "function_name": "", "args": {}},
otherwise response in format: {"role": "assistant", "reason":"why response to user", "content":"next step question"}.
4. If the conversation already contains the function execution result, don't need to call it again.
5. If user mentioned some specific requirment, don't ask this question in your response.
6. Don't repeat the same question in your response.
{% if functions and functions != empty %}
[FUNCTIONS]
{% for fn in functions -%}
{{ fn.name }}: {{ fn.description }}
{{ fn.parameters }}
{{ "\r\n" }}
{%- endfor %}
response_to_user: response to user directly without using any function.
{"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>
<TargetFramework>netstandard2.1</TargetFramework>
@ -11,7 +11,7 @@
</PropertyGroup>
<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>

View file

@ -52,11 +52,7 @@ public class ChatCompletionProvider : IChatCompletion
var choice = response.Value.Choices[0];
var message = choice.Message;
var responseMessage = new RoleDialogModel(AgentRole.Assistant, message.Content)
{
CurrentAgentId = agent.Id,
MessageId = conversations.Last().MessageId
};
RoleDialogModel responseMessage;
if (choice.FinishReason == CompletionsFinishReason.FunctionCall)
{
@ -74,6 +70,33 @@ public class ChatCompletionProvider : IChatCompletion
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
foreach(var hook in contentHooks)
@ -222,7 +245,17 @@ public class ChatCompletionProvider : IChatCompletion
if (agentService.RenderFunction(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,
Description = function.Description,
@ -241,6 +274,7 @@ public class ChatCompletionProvider : IChatCompletion
});
chatCompletionsOptions.Messages.Add(new ChatRequestFunctionMessage(message.FunctionName, message.Content));
// chatCompletionsOptions.Messages.Add(new ChatRequestToolMessage(message.Content, message.ToolCallId));
}
else if (message.Role == ChatRole.User)
{
@ -346,4 +380,15 @@ public class ChatCompletionProvider : IChatCompletion
{
_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);
}
}