Uma biblioteca de integração ChatGPT para .NET, apoiando o serviço OpenAI e Azure Openai.
A biblioteca está disponível no NUGET. Basta procurar Chatgptnet na GUI do gerenciador de pacotes ou executar o seguinte comando na .Net CLI :
dotnet add package ChatGptNetRegistre o serviço ChatGPT na inicialização do aplicativo:
builder . Services . AddChatGpt ( options =>
{
// OpenAI.
//options.UseOpenAI(apiKey: "", organization: "");
// Azure OpenAI Service.
//options.UseAzure(resourceName: "", apiKey: "", authenticationType: AzureAuthenticationType.ApiKey);
options . DefaultModel = " my-model " ;
options . DefaultEmbeddingModel = " text-embedding-ada-002 " ;
options . MessageLimit = 16 ; // Default: 10
options . MessageExpiration = TimeSpan . FromMinutes ( 5 ) ; // Default: 1 hour
options . DefaultParameters = new ChatGptParameters
{
MaxTokens = 800 ,
//MaxCompletionTokens = 800, // o1 series models support this property instead of MaxTokens
Temperature = 0.7
} ;
} ) ;O ChatGPTNET suporta o serviço OpenAI e Azure Openai, por isso é necessário definir as configurações corretas com base no provedor escolhido:
O ChatGPT pode ser usado com modelos diferentes para conclusão de bate -papo, tanto no serviço OpenAI quanto o Azure Openai. Com a propriedade DefaultModel , você pode especificar o modelo padrão que será usado, a menos que você passe um valor explícito nos métodos Askasync ou ASyStreamasync .
Mesmo que não seja uma conversa estritamente necessária, a biblioteca suporta também a API de incorporação, tanto no Openai quanto no Azure Openai. Quanto à conclusão do bate -papo, as incorporações podem ser feitas com diferentes modelos. Com a propriedade DefaultEMbeddingModel , você pode especificar o modelo padrão que será usado, a menos que você passe um valor explícito no método GetembeddingAsync .
Os modelos atualmente disponíveis são:
Eles têm nomes corrigidos, disponíveis no arquivo OpenAichatGPTModels.cs.
No serviço do Azure Openai, você deve implantar primeiro um modelo antes de poder fazer chamadas. Quando você implanta um modelo, você precisa atribuir um nome a ele, que deve corresponder ao nome que você usa com o ChatGPTNET .
Nota Alguns modelos não estão disponíveis em todas as regiões. Você pode consultar a página Modelo de Tabela de Resumo e Região para verificar as disponibilidades atuais.
O ChatGPT tem como objetivo suportar cenários de conversação: o usuário pode conversar com o ChatGPT sem especificar o contexto completo para todas as interações. No entanto, o histórico de conversas não é gerenciado pelo serviço OpenAI ou Azure Openai, por isso cabe a nós manter o estado atual. Por padrão, o ChatGPTNet lida com esse requisito usando um MemoryCache que armazena mensagens para cada conversa. O comportamento pode ser definido usando as seguintes propriedades:
Se necessário, é possível fornecer um cache personalizado implementando a interface Ichatgptcache e chamando o método de extensão Withcache :
public class LocalMessageCache : IChatGptCache
{
private readonly Dictionary < Guid , IEnumerable < ChatGptMessage > > localCache = new ( ) ;
public Task SetAsync ( Guid conversationId , IEnumerable < ChatGptMessage > messages , TimeSpan expiration , CancellationToken cancellationToken = default )
{
localCache [ conversationId ] = messages . ToList ( ) ;
return Task . CompletedTask ;
}
public Task < IEnumerable < ChatGptMessage > ? > GetAsync ( Guid conversationId , CancellationToken cancellationToken = default )
{
localCache . TryGetValue ( conversationId , out var messages ) ;
return Task . FromResult ( messages ) ;
}
public Task RemoveAsync ( Guid conversationId , CancellationToken cancellationToken = default )
{
localCache . Remove ( conversationId ) ;
return Task . CompletedTask ;
}
public Task < bool > ExistsAsync ( Guid conversationId , CancellationToken cancellationToken = default )
{
var exists = localCache . ContainsKey ( conversationId ) ;
return Task . FromResult ( exists ) ;
}
}
// Registers the custom cache at application startup.
builder . Services . AddChatGpt ( /* ... */ ) . WithCache < LocalMessageCache > ( ) ;Também podemos definir parâmetros de chatgpt para conclusão de bate -papo na inicialização. Verifique a documentação oficial da lista de parâmetros disponíveis e seu significado.
A configuração pode ser lida automaticamente na Iconfiguration, usando, por exemplo, uma seção ChatGPT no arquivo appSsets.json :
"ChatGPT": {
"Provider": "OpenAI", // Optional. Allowed values: OpenAI (default) or Azure
"ApiKey": "", // Required
//"Organization": "", // Optional, used only by OpenAI
"ResourceName": "", // Required when using Azure OpenAI Service
"ApiVersion": "2024-10-21", // Optional, used only by Azure OpenAI Service (default: 2024-10-21)
"AuthenticationType": "ApiKey", // Optional, used only by Azure OpenAI Service. Allowed values: ApiKey (default) or ActiveDirectory
"DefaultModel": "my-model",
"DefaultEmbeddingModel": "text-embedding-ada-002", // Optional, set it if you want to use embedding
"MessageLimit": 20,
"MessageExpiration": "00:30:00",
"ThrowExceptionOnError": true // Optional, default: true
//"User": "UserName",
//"DefaultParameters": {
// "Temperature": 0.8,
// "TopP": 1,
// "MaxTokens": 500,
// "MaxCompletionTokens": null, // o1 series models support this property instead of MaxTokens
// "PresencePenalty": 0,
// "FrequencyPenalty": 0,
// "ResponseFormat": { "Type": "text" }, // Allowed values for Type: text (default) or json_object
// "Seed": 42 // Optional (any integer value)
//},
//"DefaultEmbeddingParameters": {
// "Dimensions": 1536
//}
}
E use a sobrecarga correspondente do método Che AddChatgpt :
// Adds ChatGPT service using settings from IConfiguration.
builder . Services . AddChatGpt ( builder . Configuration ) ;O método AddChatgpt também possui uma sobrecarga que aceita um ISERVICEPROVER como argumento. Ele pode ser usado, por exemplo, se estivermos em uma API da Web e precisamos oferecer suporte a cenários nos quais todo usuário possui uma chave de API diferente que pode ser recuperada acessando um banco de dados por meio de injeção de dependência:
builder . Services . AddChatGpt ( ( services , options ) =>
{
var accountService = services . GetRequiredService < IAccountService > ( ) ;
// Dynamically gets the API Key from the service.
var apiKey = " ... "
options . UseOpenAI ( apiKyey ) ;
} ) ;Em cenários mais complexos, é possível configurar o ChatGPTNET usando o código e o iconfiguração. Isso pode ser útil se queremos definir várias propriedades comuns, mas, ao mesmo tempo, precisamos de alguma lógica de configuração. Por exemplo:
builder . Services . AddChatGpt ( ( services , options ) =>
{
// Configure common properties (message limit and expiration, default parameters, ecc.) using IConfiguration.
options . UseConfiguration ( builder . Configuration ) ;
var accountService = services . GetRequiredService < IAccountService > ( ) ;
// Dynamically gets the API Key from the service.
var apiKey = " ... "
options . UseOpenAI ( apiKyey ) ;
} ) ;O ChatGPTNET usa um httpclient para chamar as APIs de conclusão e incorporação de bate -papo. Se você precisar personalizá -lo, poderá usar a sobrecarga do método AddChatGPT que aceita uma ação <ihttpClientBuiler> como argumento. Por exemplo, se você deseja adicionar resiliência ao cliente HTTP (digamos uma política de repetição), você pode usar Polly:
// using Microsoft.Extensions.DependencyInjection;
// Requires: Microsoft.Extensions.Http.Resilience
builder . Services . AddChatGpt ( context . Configuration ,
httpClient =>
{
// Configures retry policy on the inner HttpClient using Polly.
httpClient . AddStandardResilienceHandler ( options =>
{
options . AttemptTimeout . Timeout = TimeSpan . FromMinutes ( 1 ) ;
options . CircuitBreaker . SamplingDuration = TimeSpan . FromMinutes ( 3 ) ;
options . TotalRequestTimeout . Timeout = TimeSpan . FromMinutes ( 3 ) ;
} ) ;
} )Mais informações sobre este tópico estão disponíveis sobre a documentação oficial.
A biblioteca pode ser usada em qualquer aplicativo .NET construído com .NET 6.0 ou posterior. Por exemplo, podemos criar uma API mínima dessa maneira:
app . MapPost ( " /api/chat/ask " , async ( Request request , IChatGptClient chatGptClient ) =>
{
var response = await chatGptClient . AskAsync ( request . ConversationId , request . Message ) ;
return TypedResults . Ok ( response ) ;
} )
. WithOpenApi ( ) ;
// ...
public record class Request ( Guid ConversationId , string Message ) ;Se queremos apenas recuperar a mensagem de resposta, podemos chamar o método GetContent :
var content = response . GetContent ( ) ;Nota Se a resposta tiver sido filtrada pelo sistema de filtragem de conteúdo, o GetContent retornará nulo . Portanto, você deve sempre verificar a propriedade
response.IsContentFilteredantes de tentar acessar o conteúdo real.
Usando a configuração, é possível definir parâmetros padrão para a conclusão do bate -papo. No entanto, também podemos especificar parâmetros para cada solicitação, usando as sobrecargas Askasync ou AskStreamasync que aceitam um objeto ChatGPTParameter:
var response = await chatGptClient . AskAsync ( conversationId , message , new ChatGptParameters
{
MaxTokens = 150 ,
Temperature = 0.7
} ) ;Não precisamos especificar todos os parâmetros, apenas os que queremos substituir. Os outros serão retirados da configuração padrão.
O chatgpt é conhecido por ser não determinístico. Isso significa que a mesma entrada pode produzir saídas diferentes. Para tentar controlar esse comportamento, podemos usar os parâmetros de temperatura e TOPP . Por exemplo, definir a temperatura como valores próximos a 0 torna o modelo mais determinístico, enquanto a definir como valores próximos a 1 torna o modelo mais criativo. No entanto, isso nem sempre é suficiente para obter a mesma saída para a mesma entrada. Para resolver esse problema, o OpenAI introduziu o parâmetro de semente . Se especificado, o modelo deve amostrar de forma determinista, de modo que solicitações repetidas com a mesma semente e parâmetros devam retornar o mesmo resultado. No entanto, o determinismo não é garantido nem neste caso, e você deve consultar o parâmetro SystemFingerPrint Response para monitorar as alterações no back -end. As mudanças nesses valores significam que a configuração de back -end mudou, e isso pode afetar o determinismo.
Como sempre, a propriedade SEED pode ser especificada na configuração padrão ou nas sobrecargas Askasync ou AskStreamasync que aceitam um ChatGPTParameter.
Nota As sementes e o SystemFingerPrint são suportadas apenas pelos modelos mais recentes, como o GPT-4-1106-Preview .
Se você deseja prejudicar a resposta no formato JSON, pode usar o parâmetro Responsformat :
var response = await chatGptClient . AskAsync ( conversationId , message , new ChatGptParameters
{
ResponseFormat = ChatGptResponseFormat . Json ,
} ) ;Dessa forma, a resposta sempre será um JSON válido. Observe que também deve instruir o modelo para produzir JSON por meio de um sistema ou mensagem do usuário. Se você não fizer isso, o modelo retornará um erro.
Como sempre, a propriedade Responseformat pode ser especificada na configuração padrão ou nas sobrecargas Askasync ou AskStreamasync que aceitam um ChatGPTParameter.
Nota O ResponseFormat é suportado apenas pelos modelos mais recentes, como o GPT-4-1106-PREVIED .
Os métodos Askasync e AskStreamasync (veja abaixo) fornecem sobrecargas que requerem um parâmetro conversionid . Se passarmos um valor vazio, um aleatório será gerado e devolvido. Podemos passar esse valor nas invocações subsequentes de Askasync ou AskStreamasync , para que a biblioteca recupere mensagens anteriores automaticamente da conversa atual (de acordo com as configurações do Messagelimit e da MessageExpiration ) e enviá -las para a API de conclusão de bate -papo.
Este é o comportamento padrão para todas as interações de bate -papo. Se você deseja expor uma interação específica do histórico de conversas, você pode definir o argumento AddToconversationHistory como false :
var response = await chatGptClient . AskAsync ( conversationId , message , addToConversationHistory : false ) ;Dessa forma, a mensagem será enviada para a API de conclusão de bate -papo, mas a resposta correspondente do ChatGPT não será adicionada ao histórico de conversas.
Por outro lado, em alguns cenários, pode ser útil adicionar manualmente uma interação de bate -papo (ou seja, uma pergunta seguida de uma resposta) ao histórico de conversas. Por exemplo, podemos querer adicionar uma mensagem gerada por um bot. Nesse caso, podemos usar o método AddInteractionAsync :
await chatGptClient . AddInteractionAsync ( conversationId , question : " What is the weather like in Taggia? " ,
answer : " It's Always Sunny in Taggia " ) ;A pergunta será adicionada como mensagem do usuário e a resposta será adicionada como mensagem de assistente no histórico de conversas. Como sempre, essas novas mensagens (respeitando a opção Messagelimit ) serão usadas nas invocações subsequentes de Askasync ou AskStreamasync .
A API de conclusão de bate -papo suporta streaming de resposta. Ao usar esse recurso, os deltas de mensagem parcial serão enviados, como no ChatGPT. Os tokens serão enviados como eventos enviados apenas para servidores apenas dados à medida que estiverem disponíveis. O ChatGPTNET fornece streaming de resposta usando o método AskStreamasync :
// Requests a streaming response.
var responseStream = chatGptClient . AskStreamAsync ( conversationId , message ) ;
await foreach ( var response in responseStream )
{
Console . Write ( response . GetContent ( ) ) ;
await Task . Delay ( 80 ) ;
}NOTA Se a resposta tiver sido filtrada pelo sistema de filtragem de conteúdo, o método GetContent no foreach retornará strings nulos . Portanto, você deve sempre verificar a propriedade
response.IsContentFilteredantes de tentar acessar o conteúdo real.
O streaming de respostas funciona retornando um iAsyncenumerable, para que possa ser usado mesmo em um projeto da API da Web:
app . MapGet ( " /api/chat/stream " , ( Guid ? conversationId , string message , IChatGptClient chatGptClient ) =>
{
async IAsyncEnumerable < string ? > Stream ( )
{
// Requests a streaming response.
var responseStream = chatGptClient . AskStreamAsync ( conversationId . GetValueOrDefault ( ) , message ) ;
// Uses the "AsDeltas" extension method to retrieve partial message deltas only.
await foreach ( var delta in responseStream . AsDeltas ( ) )
{
yield return delta ;
await Task . Delay ( 50 ) ;
}
}
return Stream ( ) ;
} )
. WithOpenApi ( ) ;Nota Se a resposta tiver sido filtrada pelo sistema de filtragem de conteúdo, o método asdeltas na foreach retornará a string nulls .
A biblioteca é 100% compatível também com os aplicativos Blazor WebAssembly:
Confira a pasta de amostras para obter mais informações sobre as diferentes implementações.
O ChatGPT suporta mensagens com a função do sistema para influenciar como o assistente deve se comportar. Por exemplo, podemos dizer para conversar sobre algo assim:
O ChatGPTNet fornece esse recurso usando o método SetupSync :
var conversationId await = chatGptClient . SetupAsync ( " Answer in rhyme " ) ;Se usarmos o mesmo conversão ao ligar para o Askasync , a mensagem do sistema será enviada automaticamente junto com todas as solicitações, para que o assistente saiba como se comportar.
Observe que a mensagem do sistema não conta para o número do limite de mensagens.
O histórico de conversas é excluído automaticamente quando o tempo de expiração (especificado pela propriedade MessageExpiration ) é alcançado. No entanto, se necessário, é possível limpar imediatamente a história:
await chatGptClient . DeleteConversationAsync ( conversationId , preserveSetup : false ) ;O argumento preservesetup permite decidir se o MANTAIN também a mensagem do sistema que foi definida com o método SetupSync (padrão: false ).
Com a chamada de funções, podemos descrever funções e fazer com que o modelo opte de forma inteligente para produzir um objeto JSON contendo argumentos para chamar essas funções. Esta é uma nova maneira de conectar de maneira mais confiável os recursos do GPT com ferramentas e APIs externas.
O ChatGPTNET suporta totalmente a chamada de função, fornecendo uma sobrecarga do método Askasync que permite especificar definições de função. Se esse parâmetro for fornecido, o modelo decidirá quando é apropado usar um das funções. Por exemplo:
var functions = new List < ChatGptFunction >
{
new ( )
{
Name = " GetCurrentWeather " ,
Description = " Get the current weather " ,
Parameters = JsonDocument . Parse ( """
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and/or the zip code"
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the user's location."
}
},
"required": ["location", "format"]
}
""" )
} ,
new ( )
{
Name = " GetWeatherForecast " ,
Description = " Get an N-day weather forecast " ,
Parameters = JsonDocument . Parse ( """
{
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and/or the zip code"
},
"format": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit to use. Infer this from the user's location."
},
"daysNumber": {
"type": "integer",
"description": "The number of days to forecast"
}
},
"required": ["location", "format", "daysNumber"]
}
""" )
}
} ;
var toolParameters = new ChatGptToolParameters
{
FunctionCall = ChatGptToolChoices . Auto , // This is the default if functions are present.
Functions = functions
} ;
var response = await chatGptClient . AskAsync ( " What is the weather like in Taggia? " , toolParameters ) ;Podemos passar um número arbitrário de funções, cada uma com um nome, uma descrição e um esquema JSON que descreve os parâmetros de função, seguindo as referências de esquema JSON. Sob o capô, as funções são injetadas na mensagem do sistema em uma sintaxe em que o modelo foi treinado. Isso significa que as funções contam contra o limite de contexto do modelo e são cobradas como tokens de entrada.
O objeto de resposta retornado pelo método Askasync fornece uma propriedade para verificar se o modelo selecionou uma chamada de função:
if ( response . ContainsFunctionCalls ( ) )
{
Console . WriteLine ( " I have identified a function to call: " ) ;
var functionCall = response . GetFunctionCall ( ) ! ;
Console . WriteLine ( functionCall . Name ) ;
Console . WriteLine ( functionCall . Arguments ) ;
}Este código imprimirá algo assim:
I have identified a function to call:
GetCurrentWeather
{
"location": "Taggia",
"format": "celsius"
}
Observe que a API não executará nenhuma chamada de função. Cabe aos desenvolvedores executar chamadas de função usando saídas do modelo.
Após a execução real, precisamos ligar para o método AddToolResponsEasync no ChatGPTClient para adicionar a resposta ao histórico de conversas, assim como uma mensagem padrão, para que ele seja usado automaticamente para conclusão de bate -papo:
// Calls the remote function API.
var functionResponse = await GetWeatherAsync ( functionCall . Arguments ) ;
await chatGptClient . AddToolResponseAsync ( conversationId , functionCall , functionResponse ) ;Modelos mais recentes como o GPT-4-Turbo suportam uma abordagem mais geral das funções, a chamada de ferramentas . Ao enviar uma solicitação, você pode especificar uma lista de ferramentas que o modelo pode ligar. Atualmente, apenas as funções são suportadas, mas no lançamento futuro outros tipos de ferramentas estarão disponíveis.
Para usar a chamada de ferramentas em vez de chamadas de funções diretas, você precisa definir as propriedades de ferramentas e ferramentas no objeto ChatgptToolParameters (em vez de funçãoCall e função , como no exemplo anterior):
var toolParameters = new ChatGptToolParameters
{
ToolChoice = ChatGptToolChoices . Auto , // This is the default if functions are present.
Tools = functions . ToTools ( )
} ;O método de extensão do TOTOOLS é usado para converter uma lista de Função ChatGPT em uma lista de ferramentas.
Se você usar essa nova abordagem, é claro que ainda precisará verificar se o modelo selecionou uma chamada de ferramenta, usando a mesma abordagem mostrada antes. Então, após a execução real da função, você deve chamar o método AddToolResponsEasync , mas neste caso você precisa especificar a ferramenta (não a função) à qual a resposta se refere:
var tool = response . GetToolCalls ( ) ! . First ( ) ;
var functionCall = response . GetFunctionCall ( ) ! ;
// Calls the remote function API.
var functionResponse = await GetWeatherAsync ( functionCall . Arguments ) ;
await chatGptClient . AddToolResponseAsync ( conversationId , tool , functionResponse ) ;Por fim, você precisa reenviar a mensagem original à API de conclusão de bate -papo, para que o modelo possa continuar a conversa levando em consideração a resposta da chamada de função. Confira a amostra de chamada de função para obter uma implementação completa deste fluxo de trabalho.
Ao usar o serviço Azure OpenAi, obtemos automaticamente a filtragem de conteúdo gratuitamente. Para detalhes sobre como funciona, consulte a documentação. Essas informações são retornadas para todos os cenários ao usar a API versão 2023-06-01-preview ou posterior. O ChatGPTNET suporta totalmente esse modelo de objeto, fornecendo as propriedades correspondentes nas classes ChatGPTRESPO e CHATGPTCHOICE.
A incorporação permite transformar o texto em um espaço vetorial. Isso pode ser útil para comparar a semelhança de duas frases, por exemplo. O ChatGPTNET suporta totalmente esse recurso, fornecendo o método getembeddingasync :
var response = await chatGptClient . GenerateEmbeddingAsync ( message ) ;
var embeddings = response . GetEmbedding ( ) ;Este código fornecerá uma matriz de flutuação que contém todas as incorporações para a mensagem especificada. O comprimento da matriz depende do modelo usado:
| Modelo | Dimensão de saída |
|---|---|
| Encadeamento de texto-Ada-002 | 1536 |
| Text-embebedding-3-small | 1536 |
| Text-3-Large | 3072 |
Modelos mais recentes como o texto-3 e o texto e o text-3-Large permitem que os desenvolvedores trocem o desempenho e o custo do uso de incorporação. Especificamente, os desenvolvedores podem encurtar as incorporações sem a incorporação de perder suas propriedades de representação de conceitos.
Quanto ao chatgpt, essas configurações podem ser feitas de várias maneiras:
builder . Services . AddChatGpt ( options =>
{
// ...
options . DefaultEmbeddingParameters = new EmbeddingParameters
{
Dimensions = 256
} ;
} ) ; "ChatGPT": {
"DefaultEmbeddingParameters": {
"Dimensions": 256
}
}
Então, se você deseja alterar a dimensão de uma solicitação específica, poderá especificar o argumento do incorporador de parâmetros na Invocação Getembeddingasync :
var response = await chatGptClient . GenerateEmbeddingAsync ( request . Message , new EmbeddingParameters
{
Dimensions = 512
} ) ;
var embeddings = response . GetEmbedding ( ) ; // The length of the array is 512Se você precisar calcular a similaridade de cosseno entre duas incorporações, poderá usar o método de incorporação de incorporação .
A documentação técnica completa está disponível aqui.
O projeto está em constante evolução. Contribuições são bem -vindas. Sinta -se à vontade para arquivar problemas e puxar solicitações no repositório e abordá -las como pudermos.
AVISO Lembre -se de trabalhar na filial Desenvolvimento , não use a filial mestre diretamente. Crie solicitações de tração segmentando o desenvolvimento .