diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dfb25c1a..a8dfadc5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,7 @@ on: env: # GDN_BINSKIM_TARGET: "./src/sWhetstone.ChatGPT/bin/Release/**.dll" OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} - TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} - TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} - TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_CREDS: ${{ secrets.TWITTER_CREDS }} jobs: setup-version: diff --git a/.gitignore b/.gitignore index ff5b00c5..0b3012fb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ local.settings.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +*.pubxml +*[Zip Deploy].json +*[Zip Deploy]/ + # Build results [Dd]ebug/ [Dd]ebugPublic/ diff --git a/src/Whetstone.ChatGPT.Test/ChatClientTest.cs b/src/Whetstone.ChatGPT.Test/ChatClientTest.cs index 8140a813..f1c2783f 100644 --- a/src/Whetstone.ChatGPT.Test/ChatClientTest.cs +++ b/src/Whetstone.ChatGPT.Test/ChatClientTest.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using NuGet.Frameworks; using Whetstone.ChatGPT; using Whetstone.ChatGPT.Models; @@ -114,11 +115,15 @@ public async Task GPTModelsList() [Fact] public async Task GPTModelsListWithHttpClient() { - string apiKey = ChatGPTTestUtilties.GetChatGPTKey(); + + IOptions credsOptions = new OptionsWrapper(new ChatGPTCredentials + { + ApiKey = ChatGPTTestUtilties.GetChatGPTKey() + }); using (HttpClient httpClient = new()) { - IChatGPTClient client = new ChatGPTClient(apiKey, httpClient); + IChatGPTClient client = new ChatGPTClient(credsOptions, httpClient); ChatGPTListResponse? modelResponse = await client.ListModelsAsync(); @@ -180,6 +185,7 @@ public void NullSessionConstruction() Assert.Throws(() => new ChatGPTClient((string?) null)); } + /* [Fact] public void NullHttpClientConstruction() { @@ -189,6 +195,7 @@ public void NullHttpClientConstruction() Assert.Throws(() => new ChatGPTClient(apiKey, client)); } + */ #pragma warning restore CS8604 // Possible null reference argument. #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. diff --git a/src/Whetstone.ChatGPT/ChatGPTClient.cs b/src/Whetstone.ChatGPT/ChatGPTClient.cs index 116a10ca..91b708d2 100644 --- a/src/Whetstone.ChatGPT/ChatGPTClient.cs +++ b/src/Whetstone.ChatGPT/ChatGPTClient.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; @@ -25,9 +26,7 @@ namespace Whetstone.ChatGPT; /// public class ChatGPTClient : IChatGPTClient { - private const string ResponseLinePrefix = "data: "; - - private readonly string _apiKey; + private const string ResponseLinePrefix = "data: "; private readonly HttpClient _client; @@ -36,76 +35,69 @@ public class ChatGPTClient : IChatGPTClient private bool _isDisposed; #region Constructors + /// /// Creates a new instance of the class. /// /// The OpenAI API uses API keys for authentication. Visit your API Keys page to retrieve the API key you'll use in your requests./param> /// - public ChatGPTClient(string apiKey) : this(new ChatGPTCredentials(apiKey), null) + public ChatGPTClient(string apiKey) : this(credentials: new ChatGPTCredentials(apiKey), httpClient: new HttpClient()) { } - + /// /// Creates a new instance of the class. /// /// The OpenAI API uses API keys for authentication. Visit your API Keys page to retrieve the API key you'll use in your requests./param> /// For users who belong to multiple organizations, you can pass a header to specify which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota. /// - public ChatGPTClient(string apiKey, string organization) : this(new ChatGPTCredentials(apiKey, organization), null) + public ChatGPTClient(string apiKey, string organization) : this(new ChatGPTCredentials(apiKey, organization), httpClient: new HttpClient()) { } /// /// Creates a new instance of the class. /// - /// The OpenAI API uses API keys for authentication. Visit your API Keys page to retrieve the API key you'll use in your requests./param> - /// This HttpClient will be used to make requests to the GPT-3 API. The caller is responsible for disposing the HttpClient instance. - /// + /// Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See Requesting Organization. /// - public ChatGPTClient(string apiKey, HttpClient httpClient) : this(new ChatGPTCredentials(apiKey), httpClient) + public ChatGPTClient(ChatGPTCredentials credentials) : this(credentials: credentials, httpClient: new HttpClient()) { - if (httpClient is null) - { - throw new ArgumentNullException(nameof(httpClient)); - } } + /// /// Creates a new instance of the class. /// - /// The OpenAI API uses API keys for authentication. Visit your API Keys page to retrieve the API key you'll use in your requests./param> - /// For users who belong to multiple organizations, you can pass a header to specify which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota. + /// Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See Requesting Organization. /// This HttpClient will be used to make requests to the GPT-3 API. The caller is responsible for disposing the HttpClient instance. - /// - public ChatGPTClient(string apiKey, string organization, HttpClient httpClient) : this(new ChatGPTCredentials(apiKey, organization), httpClient) + /// API Key is required. + public ChatGPTClient(IOptions credentialsOptions) : this(credentials: credentialsOptions.Value, httpClient: new HttpClient()) { } - /// - /// Creates a new instance of the class. - /// - /// Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See Requesting Organization. - /// - public ChatGPTClient(ChatGPTCredentials credentials) : this(credentials, null) + public ChatGPTClient(IOptions credentialsOptions, HttpClient client) : this(credentials: credentialsOptions.Value, httpClient: client) { } - /// /// Creates a new instance of the class. /// /// Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See Requesting Organization. /// This HttpClient will be used to make requests to the GPT-3 API. The caller is responsible for disposing the HttpClient instance. - /// - public ChatGPTClient(ChatGPTCredentials credentials, HttpClient? httpClient) + /// API Key is required. + private ChatGPTClient(ChatGPTCredentials credentials, HttpClient httpClient) { + if(credentials is null) + { + throw new ArgumentNullException(nameof(credentials)); + } + if (string.IsNullOrWhiteSpace(credentials.ApiKey)) { throw new ArgumentException("ApiKey preoperty cannot be null or whitespace.", nameof(credentials)); } - _apiKey = credentials.ApiKey; if (httpClient is null) { @@ -338,8 +330,6 @@ await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false) : { httpReq.Content = formContent; - httpReq.Headers.Add("Authorization", $"Bearer {_apiKey}"); - using (HttpResponseMessage? httpResponse = cancellationToken is null ? await _client.SendAsync(httpReq).ConfigureAwait(false) : await _client.SendAsync(httpReq, cancellationToken.Value).ConfigureAwait(false)) @@ -389,9 +379,6 @@ await _client.SendAsync(httpReq, cancellationToken.Value).ConfigureAwait(false)) using (var httpReq = new HttpRequestMessage(HttpMethod.Get, $"files/{fileId}/content")) { - - httpReq.Headers.Add("Authorization", $"Bearer {_apiKey}"); - using (HttpResponseMessage? httpResponse = cancellationToken is null ? await _client.SendAsync(httpReq) : await _client.SendAsync(httpReq, cancellationToken.Value)) @@ -835,7 +822,8 @@ await _client.SendAsync(httpReq, cancellationToken.Value).ConfigureAwait(false)) throw new ArgumentException($"Url is not a valid Uri", nameof(generatedImage)); } - HttpRequestMessage requestMessage = new() + + using HttpRequestMessage requestMessage = new() { Method = HttpMethod.Get, RequestUri = uri diff --git a/src/Whetstone.ChatGPT/Whetstone.ChatGPT.csproj b/src/Whetstone.ChatGPT/Whetstone.ChatGPT.csproj index 87017c0c..b00e6c10 100644 --- a/src/Whetstone.ChatGPT/Whetstone.ChatGPT.csproj +++ b/src/Whetstone.ChatGPT/Whetstone.ChatGPT.csproj @@ -6,12 +6,20 @@ enable Whetstone ChatGPT README.md - RELEASENOTES.md + + # 1.1.0 + + - Added IChatGPTClient interface method for: + - StreamFineTuneEventsAsync + - DownloadImageAsync + - Small updates from code analysis suggestions + + git chatgpt; api; openapi; gpt; gpt-3 https://github.com/johniwasz/whetstone.chatgpt 2023 - 1.1.0 + 1.1.1 https://github.com/johniwasz/whetstone.chatgpt A simple light-weight library that wraps the ChatGPT API. packlogo.png @@ -53,4 +61,8 @@ + + + + diff --git a/src/examples/AzureARMTemplates/akvdeployment.bicep b/src/examples/AzureARMTemplates/akvdeployment.bicep index 91388772..44f8d133 100644 --- a/src/examples/AzureARMTemplates/akvdeployment.bicep +++ b/src/examples/AzureARMTemplates/akvdeployment.bicep @@ -43,7 +43,7 @@ resource twitter_chatgpt_funcid 'Microsoft.ManagedIdentity/userAssignedIdentitie location: twitgptgrouppname } -resource twitterchatgpt_dev 'Microsoft.KeyVault/vaults@2019-09-01' = { +resource twitterchatgpt_dev_kv01 'Microsoft.KeyVault/vaults@2019-09-01' = { name: 'twitterchatgpt-dev' location: twitgptgrouppname tags: { @@ -77,6 +77,7 @@ resource twitterchatgpt_dev 'Microsoft.KeyVault/vaults@2019-09-01' = { } } ] + enableSoftDelete: true sku: { name: 'standard' family: 'A' @@ -84,8 +85,17 @@ resource twitterchatgpt_dev 'Microsoft.KeyVault/vaults@2019-09-01' = { } } -resource twitterchatgpt_dev_twittercreds 'Microsoft.KeyVault/vaults/secrets@2016-10-01' = { - parent: twitterchatgpt_dev +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2018-09-01-preview' = { + scope: twitterchatgpt_dev_kv01 + name: guid(twitterchatgpt_dev_kv01.id, twitter_chatgpt_funcid.id, 'Key Vault Secrets User') + properties: { + roleDefinitionId: '4633458b-17de-408a-b874-0445c86b69e6' + principalId: twitter_chatgpt_funcid.properties.principalId + } +} + +resource twitterchatgpt_dev_kv01_twittercreds 'Microsoft.KeyVault/vaults/secrets@2016-10-01' = { + parent: twitterchatgpt_dev_kv01 name: 'twittercreds' properties: { value: '{ "AccessToken": "${twitterAccessToken}", "AccessTokenSecret": "${twitterAccessTokenSecret}", "ConsumerKey": "${twitterConsumerKey}", "ConsumerSecret": "${twitterConsumerSecret}"}' @@ -96,9 +106,12 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = { name: storageAccountName location: location sku: { - name: storageAccountType + name: storageAccountType } kind: 'Storage' + properties: { + + } } resource hostingPlan 'Microsoft.Web/serverfarms@2021-03-01' = { @@ -108,20 +121,25 @@ resource hostingPlan 'Microsoft.Web/serverfarms@2021-03-01' = { name: 'Y1' tier: 'Dynamic' } - properties: {} } - resource function 'Microsoft.Web/sites@2020-12-01' = { name: appName location: twitgptgrouppname kind: 'functionapp' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${twitter_chatgpt_funcid.id}' : {} + } + } properties: { serverFarmId: hostingPlan.id siteConfig: { ftpsState: 'Disabled' minTlsVersion: '1.2' - acrUserManagedIdentityID: twitter_chatgpt_funcid.id + http20Enabled: true + keyVaultReferenceIdentity: twitter_chatgpt_funcid.id appSettings: [ { name: 'AzureWebJobsDashboard' @@ -141,7 +159,7 @@ resource function 'Microsoft.Web/sites@2020-12-01' = { } { name: 'FUNCTIONS_EXTENSION_VERSION' - value: '~2' + value: '~4' } { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' @@ -151,6 +169,10 @@ resource function 'Microsoft.Web/sites@2020-12-01' = { name: 'FUNCTIONS_WORKER_RUNTIME' value: 'dotnet' } + { + name: 'TWITTER_CREDS' + value: '@MicrosoftValueSecret(${twitterchatgpt_dev_kv01_twittercreds.id})' + } ] } httpsOnly: true diff --git a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Program.cs b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Program.cs index ea75b057..014665c2 100644 --- a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Program.cs +++ b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Program.cs @@ -6,7 +6,11 @@ using Tweetinvi; using Tweetinvi.AspNet; using Tweetinvi.Models; -using Grpc.Core; +using System.Text.Json; +using Whetstone.ChatGPT; +using Whetstone.ChatGPT.Models; +using Polly.Extensions.Http; +using Polly; public class Program { @@ -24,25 +28,88 @@ public static void Main(string[] args) { // Do NOT clear prior configurations. // configuration.Sources.Clear(); +#if DEBUG configuration.AddUserSecrets("117cec77-f121-4f75-9054-584941f6df04"); - +#endif + configuration.AddEnvironmentVariables(); + + }) .ConfigureServices((context, services) => { + + services.Configure(options => + { + IConfiguration config = context.Configuration; + + string? gptCredentialsText = config["OPENAI_API_CREDS"]; + + if (string.IsNullOrWhiteSpace(gptCredentialsText)) + { + throw new InvalidOperationException("OPENAI_API_CREDS is not set."); + } + + ChatGPTCredentials? gptCredentials = JsonSerializer.Deserialize(gptCredentialsText); + + if (gptCredentials == null) + { + throw new InvalidOperationException("OPENAI_API_CREDS is not set."); + } + + options.ApiKey = gptCredentials.ApiKey; + options.Organization = gptCredentials.Organization; + + }); + + services.Configure(options => { IConfiguration config = context.Configuration; + string? twitterCredString = config["TWITTER_CREDS"]; + + WebhookCredentials? twitterCreds = string.IsNullOrWhiteSpace(twitterCredString) ? + null : JsonSerializer.Deserialize(twitterCredString); + + if (twitterCreds is not null) + { + options.AccessToken = twitterCreds.AccessToken; + options.AccessTokenSecret = twitterCreds.AccessTokenSecret; + options.ConsumerSecret = twitterCreds.ConsumerSecret; + options.ConsumerKey = twitterCreds.ConsumerKey; + } + +#if DEBUG + if (twitterCreds is null) + { + options.AccessToken = config["WebhookCredentials:AccessToken"]; + options.AccessTokenSecret = config["WebhookCredentials:AccessTokenSecret"]; + options.ConsumerSecret = config["WebhookCredentials:ConsumerSecret"]; + options.ConsumerKey = config["WebhookCredentials:ConsumerKey"]; + } +#endif + }); + + + services.AddHttpClient() + .SetHandlerLifetime(TimeSpan.FromSeconds(150)) + .AddPolicyHandler(GetRetryPolicy()); + + services.AddLogging(); - options.AccessToken = config["WebhookCredentials:AccessToken"]; - options.AccessTokenSecret = config["WebhookCredentials:AccessTokenSecret"]; - options.ConsumerSecret = config["WebhookCredentials:ConsumerSecret"]; - options.ConsumerKey = config["WebhookCredentials:ConsumerKey"]; - }); - }); + // services.AddScoped(); + }); var host = hostBuilder.Build(); host.Run(); } + + public static IAsyncPolicy GetRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests) + .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + } } diff --git a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Properties/serviceDependencies.json b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Properties/serviceDependencies.json new file mode 100644 index 00000000..fcc92d11 --- /dev/null +++ b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Properties/serviceDependencies.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "storage1": { + "type": "storage", + "connectionId": "AzureWebJobsStorage" + } + } +} \ No newline at end of file diff --git a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WebhooksRequestHandlerForAzureFunction.cs b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WebhooksRequestHandlerForAzureFunction.cs index aa07823a..0ff5532c 100644 --- a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WebhooksRequestHandlerForAzureFunction.cs +++ b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WebhooksRequestHandlerForAzureFunction.cs @@ -15,6 +15,12 @@ namespace Whetstone.TweetGPT.DirectMessageFunction { + + + /// + /// This class is a wrapper around the Azure Function HttpRequestData class which is used by Tweetinvi internals + /// to process the WebHook request. + /// public class WebhooksRequestHandlerForAzureFunction : IWebhooksRequest { private readonly HttpRequestData _request; @@ -87,9 +93,7 @@ public void SetResponseStatusCode(int statusCode) _response.StatusCode = (HttpStatusCode)statusCode; } -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task WriteInResponseAsync(string content, string contentType) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { await _response.WriteStringAsync(content); _response.Headers.Add("Content-Type", "application/json"); diff --git a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Whetstone.TweetGPT.DirectMessageFunction.csproj b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Whetstone.TweetGPT.DirectMessageFunction.csproj index 9e56b1ff..95aaff16 100644 --- a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Whetstone.TweetGPT.DirectMessageFunction.csproj +++ b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/Whetstone.TweetGPT.DirectMessageFunction.csproj @@ -13,9 +13,11 @@ + + diff --git a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WhetstoneTrigger.cs b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WhetstoneTrigger.cs index ec8c2343..8a235717 100644 --- a/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WhetstoneTrigger.cs +++ b/src/examples/Whetstone.TweetGPT.DirectMessageFunction/WhetstoneTrigger.cs @@ -19,6 +19,12 @@ using Tweetinvi.Logic.Wrapper; using Azure.Core; using Tweetinvi.Parameters; +using Whetstone.ChatGPT; +using Whetstone.ChatGPT.Models; +using Tweetinvi.Exceptions; +using System.Text; +using Tweetinvi.Models.V2; +using System.Threading; namespace Whetstone.TweetGPT.DirectMessageFunction { @@ -31,21 +37,39 @@ public class ChatCPTDirectMessageFunction private readonly ILogger _logger; private readonly WebhookCredentials _creds; private readonly TwitterClient _client; + private readonly IChatGPTClient _chatClient; + private readonly string _basePrompt; - public ChatCPTDirectMessageFunction(IOptions creds, ILogger logger) + public ChatCPTDirectMessageFunction(IChatGPTClient chatGPT, IOptions creds, ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _creds = creds?.Value ?? throw new ArgumentNullException(nameof(creds)); + _chatClient = chatGPT ?? throw new ArgumentNullException(nameof(chatGPT)); + _client = GetClient(); + + StringBuilder promptBuilder = new(); + promptBuilder.AppendLine("Monty is an arrogant, rich, CEO that reluctantly answers questions with condescending responses:"); + promptBuilder.AppendLine(); + promptBuilder.AppendLine("You: How many pounds are in a kilogram?"); + promptBuilder.AppendLine("Monty: This again? There are 2.2 pounds in a kilogram, you ninny."); + promptBuilder.AppendLine("You: What does HTML stand for?"); + promptBuilder.AppendLine("Monty: I'm too busy for this? Hypertext Markup Language. The T is for try to get a job."); + promptBuilder.AppendLine("You: When did the first airplane fly?"); + promptBuilder.AppendLine("Monty: On December 17, 1903, Wilbur and Orville Wright made the first flights. I funded their construction."); + promptBuilder.AppendLine("You: What is the meaning of life?"); + promptBuilder.AppendLine("Monty: To get rich. Family, religion, friendship. These are the three demons you must slay if you wish to succeed in busniess."); + _basePrompt = promptBuilder.ToString(); + } [Function("chatgptdm")] - public async Task RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, - FunctionContext executionContext) + public async Task RunAsync( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + FunctionContext executionContext) { - WebhooksRequestHandlerForAzureFunction request = new WebhooksRequestHandlerForAzureFunction(req); IAccountActivityRequestHandler activityHandler = _client.AccountActivity.CreateRequestHandler(); @@ -54,6 +78,8 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano if (userId.HasValue) { + _logger.LogInformation($"Processing message for user {userId.Value}"); + IAccountActivityStream activityStream = activityHandler.GetAccountActivityStream(userId.Value, "devchatgpt"); if (!_trackedStreams.Contains(activityStream.AccountUserId)) @@ -68,6 +94,7 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano if (isRequestManagedByTweetinvi) { + _logger.LogInformation("Request is managed by Tweetinvi."); var routeHandled = await activityHandler.TryRouteRequestAsync(request).ConfigureAwait(false); if (routeHandled) { @@ -75,52 +102,93 @@ public async Task RunAsync([HttpTrigger(AuthorizationLevel.Ano } } - /* - string? crcToken; - string? nonce; + var httpResp = req.CreateResponse(HttpStatusCode.OK); + return httpResp; + } - if (req.FunctionContext.BindingContext.BindingData.ContainsKey("crc_token")) + private void MessageReceived(object? sender, MessageReceivedEvent? e) + { + if (e is not null) { - crcToken = (string?) req.FunctionContext.BindingContext.BindingData["crc_token"]; - nonce = (string?) req.FunctionContext.BindingContext.BindingData["nonce"]; - var crcResponse = _webhookVerifier.GenerateCrcResponse(crcToken, _creds.ConsumerSecret); - var httpCrcResp = req.CreateResponse(HttpStatusCode.OK); + string messageFromSender = e.Message.Text; - await httpCrcResp.WriteAsJsonAsync(crcResponse); + long senderId = e.Message.SenderId; - return httpCrcResp; - } + _logger.LogInformation($"Received message from {senderId}."); + _logger.LogInformation($"Received message text: {messageFromSender}"); - - var accountActivityHandler = _client.AccountActivity.CreateRequestHandler(); + string userInput = $"You: {messageFromSender}\nMonty: "; - IWebhooksRequest webhookRequest = new WebhooksRequestHandlerForAzureFunction(req); + string userPrompt = string.Concat(_basePrompt, userInput); - bool isRouted = await accountActivityHandler.TryRouteRequestAsync(webhookRequest); + _logger.LogInformation($"User prompt: {userPrompt}"); - //accountActivityHandler.Web + ChatGPTCompletionRequest completionRequest = new() + { + Temperature = 1.0f, + Model = ChatGPTCompletionModels.Davinci, + Prompt = userPrompt, + MaxTokens = 200, + User = senderId.ToString() + }; + + try + { + ChatGPTCompletionResponse? gptResponse = _chatClient.CreateCompletionAsync(completionRequest).GetAwaiter().GetResult(); + + if (gptResponse?.Choices is not null) + { + string? responseMessage = gptResponse.GetCompletionText(); - + if (!string.IsNullOrWhiteSpace(responseMessage)) + { + SendResponseAsync(e, responseMessage).Wait(); + } + } + } + catch(ChatGPTException chatEx) + { + _logger.LogError(chatEx, $"ChatGPT had an error processing prompt: {messageFromSender}"); - Stopwatch funcTime = Stopwatch.StartNew(); + _logger.LogError(chatEx, $" ChatGPT Status: {chatEx.StatusCode}"); + if (chatEx.ChatGPTError is not null) + { + _logger.LogError(chatEx, $" ChatGPT error details: {chatEx.ChatGPTError.Message}, Code: {chatEx.ChatGPTError.Code}, Type: {chatEx.ChatGPTError.Type}"); + } - this._logger.LogInformation($"Alexa request processing time: {funcTime.ElapsedMilliseconds} milliseconds"); - */ - var httpResp = req.CreateResponse(HttpStatusCode.OK); - return httpResp; + SendResponseAsync(e, "I'm attending to important business. Await your turn, plebian.").Wait(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"An error occurred processing prompt: {messageFromSender}"); + } + } } - private void MessageReceived(object? sender, MessageReceivedEvent? e) + private async Task SendResponseAsync(MessageReceivedEvent? e, string responseMessage) { if (e is not null) { - IPublishMessageParameters publishParameters = new PublishMessageParameters("Hello back from the webs", e.Sender.Id); + try + { + _logger.LogInformation($"Sending Response: {responseMessage}"); + + IPublishMessageParameters publishParameters = new PublishMessageParameters(responseMessage, e.Sender.Id); - TwitterClient client = EnsureBearerTokenAsync(_client).Result; + TwitterClient client = await EnsureBearerTokenAsync(_client).ConfigureAwait(false); - client.Messages.PublishMessageAsync(publishParameters).Wait(); + await client.Messages.PublishMessageAsync(publishParameters).ConfigureAwait(false); + } + catch (TwitterAuthException authEx) + { + _logger.LogError(authEx, $"Error authenticating with Twitter credentials while sending response: {responseMessage}"); + } + catch (TwitterResponseException respEx) + { + _logger.LogError(respEx, $"Error sending Twitter message while sending response: {responseMessage}"); + } } } diff --git a/src/examples/Whetstone.TweetGPT.WebhookManager.Test/TwitterClientTest.cs b/src/examples/Whetstone.TweetGPT.WebhookManager.Test/TwitterClientTest.cs index 55d2ff7a..e45c4537 100644 --- a/src/examples/Whetstone.TweetGPT.WebhookManager.Test/TwitterClientTest.cs +++ b/src/examples/Whetstone.TweetGPT.WebhookManager.Test/TwitterClientTest.cs @@ -9,6 +9,7 @@ using Tweetinvi.Parameters.V2; using Whetstone.TweetGPT.WebHookManager; using Whetstone.TweetGPT.WebHookManager.Models; +using System.Text.Json; namespace Whetstone.TweetGPT.WebhookManager.Test { @@ -26,25 +27,20 @@ public async Task TwitterClientConnect() .AddEnvironmentVariables() .Build(); - string? consumerKey = config["WebhookCredentials:ConsumerKey"] is not null - ? config["WebhookCredentials:ConsumerKey"] - : config[EnvironmentSettings.ENV_TWITTER_CONSUMER_KEY]; + string? twitterCredString = config["TWITTER_CREDS"]; - string? consumerSecret = config["WebhookCredentials:ConsumerSecret"] is not null - ? config["WebhookCredentials:ConsumerSecret"] - : config[EnvironmentSettings.ENV_TWITTER_CONSUMER_SECRET]; + WebhookCredentials? webhookCreds = string.IsNullOrWhiteSpace(twitterCredString) ? + null : JsonSerializer.Deserialize(twitterCredString); - string? accessToken = config["WebhookCredentials:AccessToken"] is not null - ? config["WebhookCredentials:AccessToken"] - : config[EnvironmentSettings.ENV_TWITTER_ACCESS_TOKEN]; - string? accessTokenSecret = config["WebhookCredentials:AccessTokenSecret"] is not null - ? config["WebhookCredentials:AccessTokenSecret"] - : config[EnvironmentSettings.ENV_TWITTER_ACCESS_TOKEN_SECRET]; + ConsumerOnlyCredentials consumerCreds = webhookCreds is not null ? + new ConsumerOnlyCredentials(webhookCreds.ConsumerKey, webhookCreds.ConsumerSecret) : + new ConsumerOnlyCredentials(config["WebhookCredentials:ConsumerKey"], config["WebhookCredentials:ConsumerSecret"]); - var consumerOnlyCredentials = new ConsumerOnlyCredentials(consumerKey, consumerSecret); - ReadOnlyTwitterCredentials twitCreds = new ReadOnlyTwitterCredentials(consumerOnlyCredentials, accessToken, accessTokenSecret); + ReadOnlyTwitterCredentials twitCreds = webhookCreds is not null ? + new ReadOnlyTwitterCredentials(consumerCreds, webhookCreds.AccessToken, webhookCreds.AccessTokenSecret) : + new ReadOnlyTwitterCredentials(consumerCreds, config["WebhookCredentials:AccessToken"], config["WebhookCredentials:AccessTokenSecret"]); var appClientWithoutBearer = new TwitterClient(twitCreds); diff --git a/src/examples/Whetstone.WebhookCommander/registerwebhook.bat b/src/examples/Whetstone.WebhookCommander/registerwebhook.bat index ec525ca7..2d972c38 100644 --- a/src/examples/Whetstone.WebhookCommander/registerwebhook.bat +++ b/src/examples/Whetstone.WebhookCommander/registerwebhook.bat @@ -1,4 +1,5 @@ - -bin\Debug\net6.0\twithookconfig addwebhook --env devchatgpt --webhookurl https://ecaa-108-2-209-91.ngrok.io/api/chatgptdm - + REM ngrok http http://localhost:7070 +REM bin\Debug\net6.0\twithookconfig removewebhook --e devchatgpt --wi 1611802887591546883 +bin\Debug\net6.0\twithookconfig addwebhook --e devchatgpt --webhookurl https://fn-twitgpt-uqvq5cmh43iws.azurewebsites.net/api/chatgptdm +bin\Debug\net6.0\twithookconfig subscribe --e devchatgpt \ No newline at end of file