Skip to content

Commit

Permalink
refactor: add template (#12552)
Browse files Browse the repository at this point in the history
* refactor: add template
  • Loading branch information
xzf0587 authored Oct 22, 2024
1 parent b8a9ab9 commit 1a6ce1d
Show file tree
Hide file tree
Showing 38 changed files with 1,814 additions and 0 deletions.
30 changes: 30 additions & 0 deletions templates/csharp/custom-copilot-rag-microsoft365/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TeamsFx files
build
appPackage/build
env/.env.*.user
env/.env.local
appsettings.Development.json
appsettings.TestTool.json
.deployment

# User-specific files
*.user

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Notification local store
.notification.localstore.json
.notification.testtoolstore.json

# devTools
devTools/
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Overview of the Chat With Your Data (Using Microsoft 365 Data) template

This app template showcases how to build one of the most powerful applications enabled by LLM - sophisticated question-answering (Q&A) chat bots that can answer questions about specific source information right in the Microsoft Teams.
This app template also demonstrates usage of techniques like:
- [Retrieval Augmented Generation](https://python.langchain.com/docs/use_cases/question_answering/#what-is-rag), or RAG.
- [Microsoft Graph Search API](https://learn.microsoft.com/graph/search-concept-overview)
- [Teams AI Library](https://learn.microsoft.com/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/teams-conversation-ai-overview)

## Quick Start

**Prerequisites**
> To run the AI Chat Bot template in your local dev machine, you will need:
>
{{#useOpenAI}}
> - an account with [OpenAI](https://platform.openai.com).
{{/useOpenAI}}
{{#useAzureOpenAI}}
> - [Azure OpenAI](https://aka.ms/oai/access) resource
{{/useAzureOpenAI}}

### Debug bot app in Teams Web Client
{{#useOpenAI}}
1. Ensure your OpenAI API Key is filled in `env/.env.local.user`.
```
SECRET_OPENAI_API_KEY="<your-openai-api-key>"
```
{{/useOpenAI}}
{{#useAzureOpenAI}}
1. Ensure your Azure OpenAI settings are filled in `env/.env.local.user`.
```
SECRET_AZURE_OPENAI_API_KEY="<your-azure-openai-api-key>"
AZURE_OPENAI_ENDPOINT="<your-azure-openai-endpoint>"
AZURE_OPENAI_DEPLOYMENT_NAME="<your-azure-openai-deployment-name>"
```
{{/useAzureOpenAI}}
1. Microsoft Graph Search API is available for searching SharePoint content, thus you just need to ensure your document in *src/data/\*.txt* is [uploaded to SharePoint / OneDrive](https://support.microsoft.com/office/upload-files-and-folders-to-a-library-da549fb1-1fcb-4167-87d0-4693e93cb7a0), no extra data ingestion required.
1. In the debug dropdown menu, select Dev Tunnels > Create A Tunnel (set authentication type to Public) or select an existing public dev tunnel.
1. Right-click your project and select Teams Toolkit > Prepare Teams App Dependencies.
1. If prompted, sign in with a Microsoft 365 account for the Teams organization you want to install the app to.
1. Press F5, or select the Debug > Start Debugging menu in Visual Studio.
1. In the launched browser, select the Add button to load the app in Teams.
1. In the chat bar, type and send anything to your bot to trigger a response.

![M365 RAG Bot](https://github.com/OfficeDev/TeamsFx/assets/13211513/c2fff68c-53ce-445a-a101-97f0c127b825)

> For local debugging using Teams Toolkit CLI, you need to do some extra steps described in [Set up your Teams Toolkit CLI for local debugging](https://aka.ms/teamsfx-cli-debugging).

## Extend the AI Chat Bot template with more AI capabilities

- You can follow [Get started with Teams AI library](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/teams%20conversational%20ai/how-conversation-ai-get-started) to extend the AI Chat Bot template with more AI capabilities.
- Understand more about [how to add additional APIs](https://aka.ms/teamsfx-rag-bot#add-more-api-for-custom-api-as-data-source).

## Additional information and references
- [Teams AI library](https://aka.ms/teams-ai-library)
- [Teams Toolkit Documentations](https://docs.microsoft.com/microsoftteams/platform/toolkit/teams-toolkit-fundamentals)
- [Teams Toolkit CLI](https://aka.ms/teamsfx-toolkit-cli)
- [Teams Toolkit Samples](https://github.com/OfficeDev/TeamsFx-Samples)

## Learn more

New to Teams app development or Teams Toolkit? Learn more about
Teams app manifests, deploying to the cloud, and more in the documentation
at https://aka.ms/teams-toolkit-vs-docs.

## Report an issue

Select Visual Studio > Help > Send Feedback > Report a Problem.
Or, you can create an issue directly in our GitHub repository:
https://github.com/OfficeDev/TeamsFx/issues.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"profiles": {
// Launch project within Teams
"Microsoft Teams (browser)": {
"commandName": "Project",
"launchUrl": "https://teams.microsoft.com/l/app/${{TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&appTenantId=${{TEAMS_APP_TENANT_ID}}&login_hint=${{TEAMSFX_M365_USER_NAME}}",
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.TeamsFx.Sdk">
<ItemGroup>
<ProjectCapability Include="ProjectConfigurationsDeclaredDimensions" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Microsoft Teams (browser)</ActiveDebugProfile>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"Name": "Microsoft Teams (browser)",
"Projects": [
{
"Path": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}",
"Name": "{{NewProjectTypeName}}\\{{NewProjectTypeName}}.{{NewProjectTypeExt}}",
"Action": "StartWithoutDebugging",
"DebugTarget": "Microsoft Teams (browser)"
},
{
{{#PlaceProjectFileInSolutionDir}}
"Path": "{{ProjectName}}.csproj",
"Name": "{{ProjectName}}.csproj",
{{/PlaceProjectFileInSolutionDir}}
{{^PlaceProjectFileInSolutionDir}}
"Path": "{{ProjectName}}\\{{ProjectName}}.csproj",
"Name": "{{ProjectName}}\\{{ProjectName}}.csproj",
{{/PlaceProjectFileInSolutionDir}}
"Action": "Start",
"DebugTarget": "Start Project"
}
]
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.Teams.AI;

namespace {{SafeProjectName}}
{
public class AdapterWithErrorHandler : TeamsAdapter
{
public AdapterWithErrorHandler(IConfiguration configuration, ILogger<TeamsAdapter> logger)
: base(configuration, null, logger)
{
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
// NOTE: In production environment, you should consider logging this to
// Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
// to add telemetry capture to your bot.
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
// Only send error message for user messages, not for other message types so the bot doesn't spam a channel or chat.
if (turnContext.Activity.Type == ActivityTypes.Message)
{
// Send a message to the user
await turnContext.SendActivityAsync($"The bot encountered an unhandled error: {exception.Message}");
await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code.");
// Send a trace activity
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
}
};
}
}
}
43 changes: 43 additions & 0 deletions templates/csharp/custom-copilot-rag-microsoft365/Config.cs.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace {{SafeProjectName}}
{
public class ConfigOptions
{
public string BOT_ID { get; set; }
public string BOT_PASSWORD { get; set; }
public string BOT_TYPE { get; set; }
public string BOT_TENANT_ID { get; set; }
public string BOT_DOMAIN { get; set; }
public string AAD_APP_CLIENT_ID { get; set; }
public string AAD_APP_CLIENT_SECRET { get; set; }
public string AAD_APP_TENANT_ID { get; set; }
public string AAD_APP_OAUTH_AUTHORITY_HOST { get; set; }
{{#useOpenAI}}
public OpenAIConfigOptions OpenAI { get; set; }
{{/useOpenAI}}
{{#useAzureOpenAI}}
public AzureConfigOptions Azure { get; set; }
{{/useAzureOpenAI}}
}

{{#useOpenAI}}
/// <summary>
/// Options for Open AI
/// </summary>
public class OpenAIConfigOptions
{
public string ApiKey { get; set; }
public string DefaultModel = "gpt-3.5-turbo";
}
{{/useOpenAI}}
{{#useAzureOpenAI}}
/// <summary>
/// Options for Azure OpenAI and Azure Content Safety
/// </summary>
public class AzureConfigOptions
{
public string OpenAIApiKey { get; set; }
public string OpenAIEndpoint { get; set; }
public string OpenAIDeploymentName { get; set; }
}
{{/useAzureOpenAI}}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Bot.Builder;
using Microsoft.Teams.AI;

namespace {{SafeProjectName}}.Controllers
{
[Route("api/messages")]
[ApiController]
public class BotController : ControllerBase
{
private readonly TeamsAdapter Adapter;
private readonly IBot Bot;
public BotController(TeamsAdapter adapter, IBot bot)
{
Adapter = adapter;
Bot = bot;
}

[HttpPost]
public async Task PostAsync(CancellationToken cancellationToken = default)
{
await Adapter.ProcessAsync(Request, Response, Bot, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Azure;
using Microsoft.Bot.Builder;
using Microsoft.Graph.SecurityNamespace;
using Microsoft.Teams.AI.AI.DataSources;
using Microsoft.Teams.AI.AI.Prompts.Sections;
using Microsoft.Teams.AI.AI.Tokenizers;
using Microsoft.Teams.AI.State;
using System.Reflection.Metadata;
using System.Text;

namespace {{SafeProjectName}}
{
public class GraphDataSource : IDataSource
{
public string Name { get; }
public GraphDataSource(string name)
{
Name = name;
}
public async Task<RenderedPromptSection<string>> RenderDataAsync(ITurnContext context, IMemory memory, ITokenizer tokenizer, int maxTokens, CancellationToken cancellationToken)
{
string? query = memory.GetValue("temp.input") as string;
if (query == null)
{
return new RenderedPromptSection<string>(string.Empty, 0);
}
var token = (memory as TurnState).Temp.AuthTokens["graph"];
var graphClient = new SimpleGraphClient(token);

var graphQuery = query;
if (query.ToLower().Contains("perksplus"))
{
graphQuery = "perksplus program";
}
else if (query.ToLower().Contains("company"))
{
graphQuery = "company history";
}
else if (query.ToLower().Contains("northwind"))
{
graphQuery = "northwind health";
}

var resourceUrls = await graphClient.GetQuery(graphQuery);
// Concatenate the restaurant documents (i.e json object) string into a single document
// until the maximum token limit is reached. This can be specified in the prompt template.
int usedTokens = 0;
StringBuilder doc = new StringBuilder("");
foreach (var url in resourceUrls)
{
var content = await graphClient.DownloadSharepointFile(SimpleGraphClient.UrlToSharingToken(url));
string document = $"{content}\n\n";
int tokens = tokenizer.Encode(document).Count;
if (usedTokens + tokens > maxTokens)
{
break;
}

doc.Append(document);
usedTokens += tokens;
}

return new RenderedPromptSection<string>(formatDocument(doc.ToString()), usedTokens, usedTokens > maxTokens);
}

private string formatDocument(string result)
{
return $"<context>{result}</context>";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.Teams.AI.State;

namespace {{SafeProjectName}}.Model
{
// Extend the turn state by configuring custom strongly typed state classes.
public class AppState : TurnState
{
public AppState() : base()
{
ScopeDefaults[CONVERSATION_SCOPE] = new ConversationState();
}

/// <summary>
/// Stores all the conversation-related state.
/// </summary>
public new ConversationState Conversation
{
get
{
TurnStateEntry scope = GetScope(CONVERSATION_SCOPE);
if (scope == null)
{
throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first.");
}

return (ConversationState)scope.Value!;
}
set
{
TurnStateEntry scope = GetScope(CONVERSATION_SCOPE);
if (scope == null)
{
throw new ArgumentException("TurnState hasn't been loaded. Call LoadStateAsync() first.");
}

scope.Replace(value!);
}
}
}

// This class adds custom properties to the turn state which will be accessible in the activity handler methods.
public class ConversationState : Record
{
private const string _countKey = "countKey";
public int MessageCount
{
get => Get<int>(_countKey);
set => Set(_countKey, value);
}
}
}
Loading

0 comments on commit 1a6ce1d

Please sign in to comment.