Anthropic AI

This commit is contained in:
Haiping Chen 2024-05-02 17:07:12 -05:00
parent 2574523844
commit 7fb386f176
16 changed files with 383 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,212 @@
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, tools) = PrepareOptions(agent, conversations);
var response = await client.Messages.GetClaudeMessageAsync(parameters, tools);
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, List<Anthropic.SDK.Common.Tool>) PrepareOptions(Agent agent, List<RoleDialogModel> conversations)
{
var prompt = "";
var agentService = _services.GetRequiredService<IAgentService>();
if (!string.IsNullOrEmpty(agent.Instruction))
{
prompt += 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 = prompt
};
JsonSerializerOptions jsonSerializationOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
ReferenceHandler = ReferenceHandler.IgnoreCycles,
};
var tools = new List<Anthropic.SDK.Common.Tool>();
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);
tools.Add(new Function(fn.Name, fn.Description,
JsonNode.Parse(jsonString)));
}
return (prompt, parameters, tools);
}
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);
}
}