Skip to content

Commit

Permalink
Adding option to use private PAT token on GitHub Q&A sample (microsof…
Browse files Browse the repository at this point in the history
…t#713)

### Motivation and Context

1. Why is this change required? What problem does it solve? What
scenario does it contribute to?

Most integrations of OpenAPI and Semantic Kernel would have the
motivation to integrate it with private information or non-public data.
GitHub Q&A is an amazing sample, and be able to try it with private
repositories it is a very straightforward way to solve scenarios where
customers and users want to try it with private data.

Fixes microsoft#444 

### Description

* Added a new Input in the React App, this is a password type of input.
I decided to not include this pat token as part of the verification if
it is a different repo.
* In the GitHub skill I added the PatToken parameter using the existing
context variables pattern. I saw this as straightforward since the
PatToken may be used for other GitHub skill features.
* WebFileDownloadSkill is modified to receive any additional headers to
the request. I see that WebFileDownloadSkill could have had 3 ways to
modify the headers of the request: 1) modify the constructor and class
to receive and/or make HttpClient public, 2) Add the headers as part of
the context variables received, 3) Override the download methods
available to have one that specifically receive custom headers. I
decided to use 3 to have the minimal impact in existing code (and
tests), although I could see 2 could also be a good approach. Then the
GitHub skill detects that an authorization header is necessary, and
calls the specific overrided method.

### Pending

* Add Unit and/or integration tests.

### Contribution Checklist
<!-- Before submitting this PR, please make sure: -->
- [X] The code builds clean without any errors or warnings
- [X] The PR follows SK Contribution Guidelines
(https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
- [X] The code follows the .NET coding conventions
(https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions)
verified with `dotnet format`
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄

---------

Co-authored-by: Shawn Callegari <36091529+shawncal@users.noreply.github.com>
Co-authored-by: Lee Miller <lemiller@microsoft.com>
Co-authored-by: Adrian Bonar <56417140+adrianwyatt@users.noreply.github.com>
Co-authored-by: Harleen Thind <39630244+hathind-ms@users.noreply.github.com>
Co-authored-by: Craig Presti <146438+craigomatic@users.noreply.github.com>
Co-authored-by: Adrian Bonar <adribona@microsoft.com>
  • Loading branch information
7 people authored and name committed Jul 5, 2023
1 parent a2c5b74 commit d54bd59
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

import { Body1, Button, Input, Label, Spinner, Title3 } from '@fluentui/react-components';
import { Body1, Button, Input, Label, Spinner, Title3, Tooltip } from '@fluentui/react-components';
import { InfoLabel } from '@fluentui/react-components/unstable';
import { ArrowDownload16Regular, CheckmarkCircle20Filled, ErrorCircle20Regular } from '@fluentui/react-icons';
import { ArrowDownload16Regular, CheckmarkCircle20Filled, ErrorCircle20Regular, Info24Regular } from '@fluentui/react-icons';
import { FC, useEffect, useState } from 'react';
import { useSemanticKernel } from '../hooks/useSemanticKernel';
import { IKeyConfig } from '../model/KeyConfig';
Expand All @@ -23,9 +23,22 @@ const enum DownloadState {
Error = 3,
}

const GitHubTokenInformationButton: React.FC = () => {
const openLink = () => {
window.open("https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens", '_blank');
};

return (
<Tooltip content="More information about GitHub Personal Tokens" relationship="label">
<Button size="small" icon={<Info24Regular />} onClick={openLink} />
</Tooltip>
);
};

const GitHubProjectSelection: FC<IData> = ({ uri, keyConfig, prevProject, prevBranch, onLoadProject, onBack }) => {
const [project, setProject] = useState<string>(prevProject);
const [branch, setBranch] = useState<string>(prevBranch);
const [patToken, setToken] = useState<string>('');
const [downloadState, setDownloadState] = useState<DownloadState>(DownloadState.Setup);
const sk = useSemanticKernel(uri);

Expand All @@ -42,6 +55,7 @@ const GitHubProjectSelection: FC<IData> = ({ uri, keyConfig, prevProject, prevBr
value: project || '',
inputs: [
{ key: 'repositoryBranch', value: branch || '' },
{ key: 'patToken', value: patToken || '' },
{ key: 'searchPattern', value: '*.md' },
],
},
Expand Down Expand Up @@ -98,6 +112,21 @@ const GitHubProjectSelection: FC<IData> = ({ uri, keyConfig, prevProject, prevBr
placeholder="https://github.com/microsoft/semantic-kernel"
/>
</div>
<Label>
<strong>GitHub Personal Access Token (optional)</strong>
<GitHubTokenInformationButton />
</Label>
<div style={{ display: 'flex', flexDirection: 'row', gap: 10 }}>
<Input
style={{ width: '100%' }}
type="password"
value={patToken}
onChange={(e) => {
setToken(e.target.value);
}}
placeholder=""
/>
</div>
<Label>
<strong>Branch Name</strong>
</Label>
Expand Down
3 changes: 1 addition & 2 deletions samples/dotnet/KernelHttpServer/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ internal static void RegisterNativeSkills(this IKernel kernel, IEnumerable<strin

if (ShouldLoad(nameof(GitHubSkill), skillsToLoad))
{
var downloadSkill = new WebFileDownloadSkill();
GitHubSkill githubSkill = new(kernel, downloadSkill);
GitHubSkill githubSkill = new(kernel);
_ = kernel.ImportSkill(githubSkill, nameof(GitHubSkill));
}
}
Expand Down
53 changes: 45 additions & 8 deletions samples/dotnet/github-skills/GitHubSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Skills.Web;
using Microsoft.SemanticKernel.Text;

namespace GitHubSkills;
Expand All @@ -35,6 +37,11 @@ public class GitHubSkill
/// </summary>
public const string FilePathParamName = "filePath";

/// <summary>
/// Personal access token for private repositories.
/// </summary>
public const string PatTokenParamName = "patToken";

/// <summary>
/// Directory to which to extract compressed file's data.
/// </summary>
Expand All @@ -57,7 +64,6 @@ public class GitHubSkill

private readonly ISKFunction _summarizeCodeFunction;
private readonly IKernel _kernel;
private readonly WebFileDownloadSkill _downloadSkill;
private readonly ILogger<GitHubSkill> _logger;
private static readonly char[] s_trimChars = new char[] { ' ', '/' };

Expand All @@ -79,10 +85,9 @@ public class GitHubSkill
/// <param name="kernel">Kernel instance</param>
/// <param name="downloadSkill">Instance of WebFileDownloadSkill used to download web files</param>
/// <param name="logger">Optional logger</param>
public GitHubSkill(IKernel kernel, WebFileDownloadSkill downloadSkill, ILogger<GitHubSkill>? logger = null)
public GitHubSkill(IKernel kernel, ILogger<GitHubSkill>? logger = null)
{
this._kernel = kernel;
this._downloadSkill = downloadSkill;
this._logger = logger ?? NullLogger<GitHubSkill>.Instance;

this._summarizeCodeFunction = kernel.CreateSemanticFunction(
Expand Down Expand Up @@ -124,10 +129,22 @@ public async Task SummarizeRepositoryAsync(string source, SKContext context)

try
{
var repositoryUri = source.Trim(s_trimChars);
var context1 = new SKContext(logger: context.Log);
context1.Variables.Set(FilePathParamName, filePath);
await this._downloadSkill.DownloadToFileAsync($"{repositoryUri}/archive/refs/heads/{repositoryBranch}.zip", context1);
var repositoryUri = Regex.Replace(source.Trim(s_trimChars), "github.com", "api.github.com/repos", RegexOptions.IgnoreCase);
var repoBundle = $"{repositoryUri}/zipball/{repositoryBranch}";

this._logger.LogDebug("Downloading {RepoBundle}", repoBundle);

var headers = new Dictionary<string, string>();
if (context.Variables.TryGetValue(PatTokenParamName, out string? pat))
{
this._logger.LogDebug("Access token detected, adding authorization headers");
headers.Add("Authorization", $"Bearer {pat}");
headers.Add("X-GitHub-Api-Version", "2022-11-28");
headers.Add("Accept", "application/vnd.github+json");
headers.Add("User-Agent", "msft-semantic-kernel-sample");
}

await this.DownloadToFileAsync(repoBundle, headers, filePath, context.CancellationToken);

ZipFile.ExtractToDirectory(filePath, directoryPath);

Expand All @@ -150,6 +167,26 @@ public async Task SummarizeRepositoryAsync(string source, SKContext context)
}
}

private async Task DownloadToFileAsync(string uri, IDictionary<string, string> headers, string filePath, CancellationToken cancellationToken)
{
// Download URI to file.
using HttpClient client = new();

using HttpRequestMessage request = new(HttpMethod.Get, uri);
foreach (var header in headers)
{
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}

using HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();

using Stream contentStream = await response.Content.ReadAsStreamAsync();
using FileStream fileStream = File.Create(filePath);
await contentStream.CopyToAsync(fileStream, 81920, cancellationToken);
await fileStream.FlushAsync(cancellationToken);
}

/// <summary>
/// Summarize a code file into an embedding
/// </summary>
Expand Down

0 comments on commit d54bd59

Please sign in to comment.