Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{F4243136
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KernelBuilder", "..\samples\dotnet\KernelBuilder\KernelBuilder.csproj", "{A52818AC-57FB-495F-818F-9E1E7BC5618C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Skills.Code", "src\SemanticKernel.Skills\Skills.Code\Skills.Code.csproj", "{0EE82492-0176-43D5-A8E0-F2E2A766B5D5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -110,6 +112,10 @@ Global
{A52818AC-57FB-495F-818F-9E1E7BC5618C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A52818AC-57FB-495F-818F-9E1E7BC5618C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A52818AC-57FB-495F-818F-9E1E7BC5618C}.Release|Any CPU.Build.0 = Release|Any CPU
{0EE82492-0176-43D5-A8E0-F2E2A766B5D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0EE82492-0176-43D5-A8E0-F2E2A766B5D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EE82492-0176-43D5-A8E0-F2E2A766B5D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EE82492-0176-43D5-A8E0-F2E2A766B5D5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -130,6 +136,7 @@ Global
{107156B4-5A8B-45C7-97A2-4544D7FA19DE} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
{F4243136-252A-4459-A7C4-EE8C056D6B0B} = {158A4E5E-AEE0-4D60-83C7-8E089B2D881D}
{A52818AC-57FB-495F-818F-9E1E7BC5618C} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{0EE82492-0176-43D5-A8E0-F2E2A766B5D5} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Expand Down
187 changes: 187 additions & 0 deletions dotnet/src/SemanticKernel.Skills/Skills.Code/CodeSkill.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.KernelExtensions;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Skills.Document;
using Microsoft.SemanticKernel.Skills.Web;

namespace Microsoft.SemanticKernel.Skills.Code;

/// <summary>
/// Skill for interacting with code files (e.g. C#)
/// </summary>
public class CodeSkill
{
/// <summary>
/// Parameter names.
/// <see cref="ContextVariables"/>
/// </summary>
public static class Parameters
{
/// <summary>
/// Document file path.
/// </summary>
public const string FilePath = "filePath";

/// <summary>
/// Directory to which to extract compressed file's data.
/// </summary>
public const string DestinationDirectoryPath = "destinationDirectoryPath";

/// <summary>
/// Name of the memory collection used to store the code summaries.
/// </summary>
public const string MemoryCollectionName = "memoryCollectionName";
}

/// <summary>
/// The max tokens to process in a single semantic function call.
/// </summary>
private const int MaxTokens = 1024;

private readonly ISKFunction _summarizeCodeFunction;
private readonly IKernel _kernel;
private readonly WebFileDownloadSkill _downloadSkill;
private readonly DocumentSkill _documentSkill;
private readonly ILogger<CodeSkill> _logger;

internal const string SummarizeCodeSnippetDefinition =
@"BEGIN CONTENT TO SUMMARIZE:
{{$INPUT}}
END CONTENT TO SUMMARIZE.

Summarize the content in 'CONTENT TO SUMMARIZE', identifying main points.
Do not incorporate other general knowledge.
Summary is in plain text, in complete sentences, with no markup or tags.

BEGIN SUMMARY:
";

internal const string MemoryCollectionName = "CodeSkillMemory"; // TODO Should this be configurable


/// <summary>
/// Initializes a new instance of the <see cref="CodeSkill"/> class.
/// </summary>
/// <param name="kernel">Kernel instance</param>
/// <param name="downloadSkill">Instance of WebFileDownloadSkill used to download web files</param>
/// <param name="documentSkill">Instance of DocumentSkill used to read files</param>
/// <param name="logger">Optional logger</param>
public CodeSkill(IKernel kernel, WebFileDownloadSkill downloadSkill, DocumentSkill documentSkill, ILogger<CodeSkill>? logger = null)
{
this._kernel = kernel;
this._downloadSkill = downloadSkill;
this._documentSkill = documentSkill;
this._logger = logger ?? NullLogger<CodeSkill>.Instance;

this._summarizeCodeFunction = kernel.CreateSemanticFunction(
SummarizeCodeSnippetDefinition,
skillName: nameof(CodeSkill),
description: "Given a snippet of code, summarize the part of the file.",
maxTokens: MaxTokens,
temperature: 0.1,
topP: 0.5);
}

/// <summary>
/// Summarize a code file into an embedding
/// </summary>
/// <param name="filePath">Path of file to summarize</param>
/// <param name="context">Semantic kernal context</param>
/// <returns>Task</returns>
public async Task SummarizeCodeFileAsync(string filePath, SKContext context)
{
// TODO do we need to extend the DocumentSkill to read raw content?
// string code = await this._documentSkill.ReadTextAsync(filePath, context);
string code = File.ReadAllText(filePath);

if (code != null && code.Length > 0)
{
// TODO should we create a new SKContext here?
context.Variables.Update(code);
await this._summarizeCodeFunction.InvokeAsync(context);

var result = context.Variables.ToString();
// TODO Include the file URI in the text
await this._kernel.Memory.SaveInformationAsync(MemoryCollectionName, text: result, id: filePath);
}
}

/// <summary>
/// Summarize the code found under a directory into embeddings (one per file)
/// </summary>
/// <param name="directoryPath">Path of directory to summarize</param>
/// <param name="context">Semantic kernal context</param>
/// <returns>Task</returns>
public async Task SummarizeCodeDirectoryAsync(string directoryPath, SKContext context)
{
// TODO Use the document skill for recursion
// TODO Allow the wildcard match to be configurable
string[] filePaths = await Task.FromResult(Directory.GetFiles(directoryPath, "*.md", SearchOption.AllDirectories));

if (filePaths != null && filePaths.Length > 0)
{
this._logger.LogDebug("Found {0} files to summarize", filePaths.Length);

foreach (string filePath in filePaths)
{
await this.SummarizeCodeFileAsync(filePath, context);
}

_ = context.Variables.Update($"Found {filePaths.Length} files to summarize");
context.Variables.Set(Parameters.MemoryCollectionName, MemoryCollectionName);
}
}

/// <summary>
/// Summarize the code downloaded from the specified URI.
/// </summary>
/// <param name="source">URI to download the respository content to be summarized</param>
/// <param name="context">Semantic kernal context</param>
/// <returns>Task</returns>
[SKFunction("Downloads a repository and summarizes the content")]
[SKFunctionName("SummarizeRepository")]
[SKFunctionInput(Description = "URL of repository to summarize")]
public async Task SummarizeRepositoryAsync(string source, SKContext context)
{
// TODO Accept the repo uri and branch as separate parameters
// 1. Down URI would be calculated in the Skill rather than the client
// 2. We cna compute file URI's so the responses can include a link to the relevant file
string filePath = Environment.ExpandEnvironmentVariables($"%temp%\\SK-{Guid.NewGuid()}.zip");
string directoryPath = Environment.ExpandEnvironmentVariables($"%temp%\\SK-{Guid.NewGuid()}");

try
{
// TODO should we create a new SKContext here?
context.Variables.Set(Parameters.FilePath, filePath);
await this._downloadSkill.DownloadToFileAsync(source, context);
context.Variables.Set(Parameters.FilePath, null);

filePath = Environment.ExpandEnvironmentVariables(filePath);
// TODO Use the file compression skill
ZipFile.ExtractToDirectory(filePath, directoryPath);

await this.SummarizeCodeDirectoryAsync(directoryPath, context);
}
finally
{
// Cleanup downloaded file and also unzipped content
if (File.Exists(filePath))
{
File.Delete(filePath);
}
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, true);
}
}
}
}
25 changes: 25 additions & 0 deletions dotnet/src/SemanticKernel.Skills/Skills.Code/Skills.Code.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RepoRoot>$([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))</RepoRoot>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />

<PropertyGroup>
<AssemblyName>Microsoft.SemanticKernel.Skills.Code</AssemblyName>
<RootNamespace>Microsoft.SemanticKernel.Skills.Code</RootNamespace>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<PropertyGroup>
<!-- NuGet Package Settings -->
<PackageId>Microsoft.SemanticKernel.Skills.Code</PackageId>
<Title>Semantic Kernel - Code Skill</Title>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
<ProjectReference Include="..\Skills.Document\Skills.Document.csproj" />
<ProjectReference Include="..\Skills.Web\Skills.Web.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { Body1, Button, Input, Label, Spinner, Subtitle2, Title3 } from '@fluentui/react-components';
import { ArrowDownload16Regular, CheckmarkCircle20Filled } from '@fluentui/react-icons';
import { FC, useState } from "react";
import { FC, useState } from 'react';

import { useSemanticKernel } from '../hooks/useSemanticKernel';
import { IKeyConfig } from '../model/KeyConfig';

Expand All @@ -25,67 +26,102 @@ const GitHubProjectSelection: FC<IData> = ({ uri, keyConfig, onLoadProject, onBa
let cleanProjectUri = project?.trim();

if (!cleanProjectUri?.endsWith('/')) {
cleanProjectUri = `${cleanProjectUri}/`
cleanProjectUri = `${cleanProjectUri}/`;
}

const url = `${cleanProjectUri}archive/refs/heads/${branch}.zip`;
const path = `%temp%\\${branch}_${new Date().getTime()}.zip`;

try {
var result = await sk.invokeAsync(keyConfig, { value: url, inputs: [{ key: 'filePath', value: path }] }, 'WebFileDownloadSkill', 'DownloadToFile');
var result = await sk.invokeAsync(
keyConfig,
{ value: url, inputs: [] },
'CodeSkill',
'SummarizeRepository',
);
setIsLoaded(true);
console.log(result);
} catch {
setIsLoadError(true);
alert('Something went wrong. Please check that the function is running and accessible from this location.');
}
}
};

return (
<div style={{ paddingTop: 20, gap: 20, display: "flex", flexDirection: "column", alignItems: "left" }}>
<div style={{ paddingTop: 20, gap: 20, display: 'flex', flexDirection: 'column', alignItems: 'left' }}>
<Title3 style={{ alignItems: 'left' }}>Enter in the GitHub Project URL</Title3>
<Subtitle2>Start by entering a GitHub Repository URL. We will pull the public repository into local memory so you can ask any questions about the repository and get help. </Subtitle2>
<Subtitle2>
Start by entering a GitHub Repository URL. We will pull the public repository into local memory so you
can ask any questions about the repository and get help.{' '}
</Subtitle2>
<br></br>
<Label><strong>GitHub Repository URL</strong></Label>
<Label>
<strong>GitHub Repository URL</strong>
</Label>
<div style={{ display: 'flex', flexDirection: 'row', gap: 10 }}>
<Input style={{ width: '100%' }} type="text" value={project} onChange={(e) => setProject(e.target.value)} placeholder="https://github.com/microsoft/semantic-kernel" />
<Input
style={{ width: '100%' }}
type="text"
value={project}
onChange={(e) => setProject(e.target.value)}
placeholder="https://github.com/microsoft/semantic-kernel"
/>
</div>
<Label><strong>Branch Name</strong></Label>
<Label>
<strong>Branch Name</strong>
</Label>
<div style={{ display: 'flex', flexDirection: 'row', gap: 10 }}>
<Input style={{ width: '100%' }} type="text" value={branch} onChange={(e) => setBranch(e.target.value)} placeholder="main" />
<Button disabled={project === undefined || branch === undefined} appearance='transparent' icon={<ArrowDownload16Regular />} onClick={() => download()} />
<Input
style={{ width: '100%' }}
type="text"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="main"
/>
<Button
disabled={project === undefined || branch === undefined}
appearance="transparent"
icon={<ArrowDownload16Regular />}
onClick={() => download()}
/>
</div>
{isLoading ?
{isLoading ? (
<div>
<Spinner />
<Body1>
Downloading respository...
</Body1>
<Body1>Downloading respository...</Body1>
</div>
:
) : (
<></>
}
{isLoaded ?
)}
{isLoaded ? (
<div>
<CheckmarkCircle20Filled />
<Body1>
Repository downloaded. You can ask questions about it on the next page.
</Body1>
</div> : <></>}
{isLoadError ?
<Body1>Repository downloaded. You can ask questions about it on the next page.</Body1>
</div>
) : (
<></>
)}
{isLoadError ? (
<div>
<Body1>
There was an error downloading the repository. Please try again.
</Body1>
<Body1>There was an error downloading the repository. Please try again.</Body1>
</div>
:
) : (
<></>
}
)}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'left', gap: 20 }}>
<Button style={{ width: 54 }} appearance="secondary" onClick={() => onBack()}>Back</Button>
<Button disabled={!isLoaded} appearance="primary" onClick={() => { if (project !== undefined && branch !== undefined) onLoadProject(project, branch) }}>Next</Button>
<Button style={{ width: 54 }} appearance="secondary" onClick={() => onBack()}>
Back
</Button>
<Button
disabled={!isLoaded}
appearance="primary"
onClick={() => {
if (project !== undefined && branch !== undefined) onLoadProject(project, branch);
}}
>
Next
</Button>
</div>
</div>
);
};

export default GitHubProjectSelection;
export default GitHubProjectSelection;
Loading