Skip to content

Commit

Permalink
Azure function testing (#6)
Browse files Browse the repository at this point in the history
* Added IChatGPTClient interface method for:
StreamFineTuneEventsAsync
DownloadImageAsync

* Refined azure function deployment process.

* Securing storage account.

* bicep updates

* Tweaking bicep template and testing deployment.

* Loosening bicep storage security.

* Enabled DI with ChatGPTClient.
  • Loading branch information
johniwasz committed Jan 8, 2023
1 parent 60eb830 commit 9d3a437
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 108 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
11 changes: 9 additions & 2 deletions src/Whetstone.ChatGPT.Test/ChatClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -114,11 +115,15 @@ public async Task GPTModelsList()
[Fact]
public async Task GPTModelsListWithHttpClient()
{
string apiKey = ChatGPTTestUtilties.GetChatGPTKey();

IOptions<ChatGPTCredentials> credsOptions = new OptionsWrapper<ChatGPTCredentials>(new ChatGPTCredentials
{
ApiKey = ChatGPTTestUtilties.GetChatGPTKey()
});

using (HttpClient httpClient = new())
{
IChatGPTClient client = new ChatGPTClient(apiKey, httpClient);
IChatGPTClient client = new ChatGPTClient(credsOptions, httpClient);

ChatGPTListResponse<ChatGPTModel>? modelResponse = await client.ListModelsAsync();

Expand Down Expand Up @@ -180,6 +185,7 @@ public void NullSessionConstruction()
Assert.Throws<ArgumentException>(() => new ChatGPTClient((string?) null));
}

/*
[Fact]
public void NullHttpClientConstruction()
{
Expand All @@ -189,6 +195,7 @@ public void NullHttpClientConstruction()
Assert.Throws<ArgumentNullException>(() => 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.

Expand Down
58 changes: 23 additions & 35 deletions src/Whetstone.ChatGPT/ChatGPTClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -25,9 +26,7 @@ namespace Whetstone.ChatGPT;
/// </summary>
public class ChatGPTClient : IChatGPTClient
{
private const string ResponseLinePrefix = "data: ";

private readonly string _apiKey;
private const string ResponseLinePrefix = "data: ";

private readonly HttpClient _client;

Expand All @@ -36,76 +35,69 @@ public class ChatGPTClient : IChatGPTClient
private bool _isDisposed;

#region Constructors

/// <summary>
/// Creates a new instance of the <see cref="ChatGPTClient"/> class.
/// </summary>
/// <param name="apiKey">The OpenAI API uses API keys for authentication. Visit your <see href="https://beta.openai.com/account/api-keys">API Keys</see> page to retrieve the API key you'll use in your requests./param>
/// <exception cref="ArgumentException"></exception>
public ChatGPTClient(string apiKey) : this(new ChatGPTCredentials(apiKey), null)
public ChatGPTClient(string apiKey) : this(credentials: new ChatGPTCredentials(apiKey), httpClient: new HttpClient())
{
}


/// <summary>
/// Creates a new instance of the <see cref="ChatGPTClient"/> class.
/// </summary>
/// <param name="apiKey">The OpenAI API uses API keys for authentication. Visit your <see href="https://beta.openai.com/account/api-keys">API Keys</see> page to retrieve the API key you'll use in your requests./param>
/// <param name="organization">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.</param>
/// <exception cref="ArgumentException"></exception>
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())
{
}

/// <summary>
/// Creates a new instance of the <see cref="ChatGPTClient"/> class.
/// </summary>
/// <param name="apiKey">The OpenAI API uses API keys for authentication. Visit your <see href="https://beta.openai.com/account/api-keys">API Keys</see> page to retrieve the API key you'll use in your requests./param>
/// <param name="httpClient">This HttpClient will be used to make requests to the GPT-3 API. The caller is responsible for disposing the HttpClient instance.</param>
/// <exception cref="ArgumentNullException"></exception>
/// <param name="credentials">Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See <see cref="https://beta.openai.com/docs/api-reference/requesting-organization">Requesting Organization</see>.</param>
/// <exception cref="ArgumentException"></exception>
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));
}
}


/// <summary>
/// Creates a new instance of the <see cref="ChatGPTClient"/> class.
/// </summary>
/// <param name="apiKey">The OpenAI API uses API keys for authentication. Visit your <see href="https://beta.openai.com/account/api-keys">API Keys</see> page to retrieve the API key you'll use in your requests./param>
/// <param name="organization">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.</param>
/// <param name="credentialsOptions">Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See <see cref="https://beta.openai.com/docs/api-reference/requesting-organization">Requesting Organization</see>.</param>
/// <param name="httpClient">This HttpClient will be used to make requests to the GPT-3 API. The caller is responsible for disposing the HttpClient instance.</param>
/// <exception cref="ArgumentException"></exception>
public ChatGPTClient(string apiKey, string organization, HttpClient httpClient) : this(new ChatGPTCredentials(apiKey, organization), httpClient)
/// <exception cref="ArgumentException">API Key is required.</exception>
public ChatGPTClient(IOptions<ChatGPTCredentials> credentialsOptions) : this(credentials: credentialsOptions.Value, httpClient: new HttpClient())
{
}


/// <summary>
/// Creates a new instance of the <see cref="ChatGPTClient"/> class.
/// </summary>
/// <param name="credentials">Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See <see cref="https://beta.openai.com/docs/api-reference/requesting-organization">Requesting Organization</see>.</param>
/// <exception cref="ArgumentException"></exception>
public ChatGPTClient(ChatGPTCredentials credentials) : this(credentials, null)
public ChatGPTClient(IOptions<ChatGPTCredentials> credentialsOptions, HttpClient client) : this(credentials: credentialsOptions.Value, httpClient: client)
{
}


/// <summary>
/// Creates a new instance of the <see cref="ChatGPTClient"/> class.
/// </summary>
/// <param name="credentials">Supplies the GPT-3 API key and the organization. The organization is only needed if the caller belongs to more than one organziation. See <see cref="https://beta.openai.com/docs/api-reference/requesting-organization">Requesting Organization</see>.</param>
/// <param name="httpClient">This HttpClient will be used to make requests to the GPT-3 API. The caller is responsible for disposing the HttpClient instance.</param>
/// <exception cref="ArgumentException"></exception>
public ChatGPTClient(ChatGPTCredentials credentials, HttpClient? httpClient)
/// <exception cref="ArgumentException">API Key is required.</exception>
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)
{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions src/Whetstone.ChatGPT/Whetstone.ChatGPT.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@
<Nullable>enable</Nullable>
<Title>Whetstone ChatGPT</Title>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>RELEASENOTES.md</PackageReleaseNotes>
<PackageReleaseNotes>
# 1.1.0

- Added IChatGPTClient interface method for:
- StreamFineTuneEventsAsync
- DownloadImageAsync
- Small updates from code analysis suggestions
</PackageReleaseNotes>

<RepositoryType>git</RepositoryType>
<PackageTags>chatgpt; api; openapi; gpt; gpt-3</PackageTags>
<RepositoryUrl>https://github.com/johniwasz/whetstone.chatgpt</RepositoryUrl>
<Copyright>2023</Copyright>
<Version>1.1.0</Version>
<Version>1.1.1</Version>
<PackageProjectUrl>https://github.com/johniwasz/whetstone.chatgpt</PackageProjectUrl>
<Description>A simple light-weight library that wraps the ChatGPT API.</Description>
<PackageIcon>packlogo.png</PackageIcon>
Expand Down Expand Up @@ -53,4 +61,8 @@
<PackageReference Include="System.Text.Json" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
</ItemGroup>

</Project>
38 changes: 30 additions & 8 deletions src/examples/AzureARMTemplates/akvdeployment.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -77,15 +77,25 @@ resource twitterchatgpt_dev 'Microsoft.KeyVault/vaults@2019-09-01' = {
}
}
]
enableSoftDelete: true
sku: {
name: 'standard'
family: 'A'
}
}
}

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}"}'
Expand All @@ -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' = {
Expand All @@ -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'
Expand All @@ -141,7 +159,7 @@ resource function 'Microsoft.Web/sites@2020-12-01' = {
}
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~2'
value: '~4'
}
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
Expand All @@ -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
Expand Down
Loading

0 comments on commit 9d3a437

Please sign in to comment.