diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/AzureContentSafetyModeratorTests.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/AzureContentSafetyModeratorTests.cs index ed9b794f9..a3752d604 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/AzureContentSafetyModeratorTests.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/AITests/AzureContentSafetyModeratorTests.cs @@ -97,7 +97,8 @@ public async Task Test_ReviewPrompt_Flagged(ModerationType moderate) }; var clientMock = new Mock(new Uri(endpoint), new AzureKeyCredential(apiKey)); - AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(hateResult: ContentSafetyModelFactory.TextAnalyzeSeverityResult(TextCategory.Hate, 2)); + var analyses = new List() { ContentSafetyModelFactory.TextCategoriesAnalysis(TextCategory.Hate, 2) }; + AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(null, analyses); Response? response = null; clientMock.Setup(client => client.AnalyzeTextAsync(It.IsAny(), It.IsAny())).ReturnsAsync(Response.FromValue(analyzeTextResult, response)); @@ -173,7 +174,8 @@ public async Task Test_ReviewPrompt_NotFlagged(ModerationType moderate) }; var clientMock = new Mock(new Uri(endpoint), new AzureKeyCredential(apiKey)); - AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(hateResult: ContentSafetyModelFactory.TextAnalyzeSeverityResult(TextCategory.Hate, 0)); + var analyses = new List() { ContentSafetyModelFactory.TextCategoriesAnalysis(TextCategory.Hate, 0) }; + AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(null, analyses); Response? response = null; clientMock.Setup(client => client.AnalyzeTextAsync(It.IsAny(), It.IsAny())).ReturnsAsync(Response.FromValue(analyzeTextResult, response)); @@ -237,7 +239,8 @@ public async Task Test_ReviewPlan_Flagged(ModerationType moderate) }); var clientMock = new Mock(new Uri(endpoint), new AzureKeyCredential(apiKey)); - AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(hateResult: ContentSafetyModelFactory.TextAnalyzeSeverityResult(TextCategory.Hate, 2)); + var analyses = new List() { ContentSafetyModelFactory.TextCategoriesAnalysis(TextCategory.Hate, 2) }; + AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(null, analyses); Response? response = null; clientMock.Setup(client => client.AnalyzeTextAsync(It.IsAny(), It.IsAny())).ReturnsAsync(Response.FromValue(analyzeTextResult, response)); @@ -298,7 +301,8 @@ public async Task Test_ReviewPlan_NotFlagged(ModerationType moderate) }); var clientMock = new Mock(new Uri(endpoint), new AzureKeyCredential(apiKey)); - AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(hateResult: ContentSafetyModelFactory.TextAnalyzeSeverityResult(TextCategory.Hate, 0)); + var analyses = new List() { ContentSafetyModelFactory.TextCategoriesAnalysis(TextCategory.Hate, 0) }; + AnalyzeTextResult analyzeTextResult = ContentSafetyModelFactory.AnalyzeTextResult(null, analyses); Response? response = null; clientMock.Setup(client => client.AnalyzeTextAsync(It.IsAny(), It.IsAny())).ReturnsAsync(Response.FromValue(analyzeTextResult, response)); diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModerator.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModerator.cs index c2a6efc44..896a49516 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModerator.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModerator.cs @@ -96,11 +96,8 @@ public async Task ReviewOutputAsync(ITurnContext turnContext, TState turnS { Response response = await _client.AnalyzeTextAsync(analyzeTextOptions); - bool flagged = response.Value.BlocklistsMatchResults.Count > 0 - || _ShouldBeFlagged(response.Value.HateResult) - || _ShouldBeFlagged(response.Value.SelfHarmResult) - || _ShouldBeFlagged(response.Value.SexualResult) - || _ShouldBeFlagged(response.Value.ViolenceResult); + bool flagged = response.Value.BlocklistsMatch.Count > 0 + || response.Value.CategoriesAnalysis.Any((ca) => _ShouldBeFlagged(ca)); if (flagged) { string actionName = isModelInput ? AIConstants.FlaggedInputActionName : AIConstants.FlaggedOutputActionName; @@ -138,17 +135,54 @@ public async Task ReviewOutputAsync(ITurnContext turnContext, TState turnS return null; } - private bool _ShouldBeFlagged(TextAnalyzeSeverityResult result) + private bool _ShouldBeFlagged(TextCategoriesAnalysis result) { return result != null && result.Severity >= _options.SeverityLevel; } private ModerationResult BuildModerationResult(AnalyzeTextResult result) { - bool hate = _ShouldBeFlagged(result.HateResult); - bool selfHarm = _ShouldBeFlagged(result.SelfHarmResult); - bool sexual = _ShouldBeFlagged(result.SexualResult); - bool violence = _ShouldBeFlagged(result.ViolenceResult); + bool hate = false; + int hateSeverity = 0; + bool selfHarm = false; + int selfHarmSeverity = 0; + bool sexual = false; + int sexualSeverity = 0; + bool violence = false; + int violenceSeverity = 0; + + foreach (TextCategoriesAnalysis textAnalysis in result.CategoriesAnalysis) + { + if (textAnalysis.Severity < _options.SeverityLevel) + { + continue; + } + + int severity = textAnalysis.Severity ?? 0; + if (textAnalysis.Category == TextCategory.Hate) + { + hate = true; + hateSeverity = severity; + } + + if (textAnalysis.Category == TextCategory.Violence) + { + violence = true; + violenceSeverity = severity; + } + + if (textAnalysis.Category == TextCategory.SelfHarm) + { + selfHarm = true; + selfHarmSeverity = severity; + } + + if (textAnalysis.Category == TextCategory.Sexual) + { + sexual = true; + sexualSeverity = severity; + } + } return new() { @@ -166,13 +200,13 @@ private ModerationResult BuildModerationResult(AnalyzeTextResult result) CategoryScores = new() { // Normalize the scores to be between 0 and 1 (highest severity is 6) - Hate = (result.HateResult?.Severity ?? 0) / 6.0, - HateThreatening = (result.HateResult?.Severity ?? 0) / 6.0, - SelfHarm = (result.SelfHarmResult?.Severity ?? 0) / 6.0, - Sexual = (result.SexualResult?.Severity ?? 0) / 6.0, - SexualMinors = (result.SexualResult?.Severity ?? 0) / 6.0, - Violence = (result.ViolenceResult?.Severity ?? 0) / 6.0, - ViolenceGraphic = (result.ViolenceResult?.Severity ?? 0) / 6.0 + Hate = hateSeverity / 6.0, + HateThreatening = hateSeverity / 6.0, + SelfHarm = selfHarmSeverity / 6.0, + Sexual = sexualSeverity / 6.0, + SexualMinors = sexualSeverity / 6.0, + Violence = violenceSeverity / 6.0, + ViolenceGraphic = violenceSeverity / 6.0 } }; } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModeratorOptions.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModeratorOptions.cs index 6eae81577..143689dfe 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModeratorOptions.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Moderator/AzureContentSafetyModeratorOptions.cs @@ -36,10 +36,18 @@ public class AzureContentSafetyModeratorOptions public IList? BlocklistNames { get; set; } /// - /// When set to true, further analyses of harmful content will not be performed in cases where blocklists are hit. When set to false, all analyses of harmful content will be performed, whether or not blocklists are hit. + /// When set to true, further analyses of harmful content will not be performed in cases where blocklists are hit. + /// When set to false, all analyses of harmful content will be performed, whether or not blocklists are hit. /// + [Obsolete("use HaltOnBlockListHit")] public bool? BreakByBlocklists { get; set; } + /// + /// When set to true, further analyses of harmful content will not be performed in cases where blocklists are hit. + /// When set to false, all analyses of harmful content will be performed, whether or not blocklists are hit. + /// + public bool? HaltOnBlockListHit { get; set; } + /// /// Create an instance of the AzureContentSafetyModeratorOptions class. /// diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Microsoft.Teams.AI.csproj b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Microsoft.Teams.AI.csproj index bec3a1d80..f2c73fe7b 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Microsoft.Teams.AI.csproj +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Microsoft.Teams.AI.csproj @@ -36,7 +36,7 @@ - + diff --git a/dotnet/samples/05.chatModeration/.editorconfig b/dotnet/samples/05.chatModeration/.editorconfig new file mode 100644 index 000000000..755bfa6c1 --- /dev/null +++ b/dotnet/samples/05.chatModeration/.editorconfig @@ -0,0 +1,240 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/.gitignore b/dotnet/samples/05.chatModeration/.gitignore new file mode 100644 index 000000000..d9db69b0e --- /dev/null +++ b/dotnet/samples/05.chatModeration/.gitignore @@ -0,0 +1,25 @@ +# TeamsFx files +build +appPackage/build +env/.env.*.user +env/.env.local +appsettings.Development.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/ + +# VS files +.vs/ diff --git a/dotnet/samples/05.chatModeration/ActionHandlers.cs b/dotnet/samples/05.chatModeration/ActionHandlers.cs new file mode 100644 index 000000000..dd4a13067 --- /dev/null +++ b/dotnet/samples/05.chatModeration/ActionHandlers.cs @@ -0,0 +1,31 @@ +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.AI.Action; +using Microsoft.Teams.AI.AI; +using System.Text.Json; + +namespace ChatModeration +{ + public class ActionHandlers + { + private static JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + }; + + [Action(AIConstants.FlaggedInputActionName)] + public async Task OnFlaggedInput([ActionTurnContext] ITurnContext turnContext, [ActionParameters] Dictionary entities) + { + string entitiesJsonString = JsonSerializer.Serialize(entities, _jsonSerializerOptions); + await turnContext.SendActivityAsync($"I'm sorry your message was flagged:"); + await turnContext.SendActivityAsync($"```{entitiesJsonString}"); + return ""; + } + + [Action(AIConstants.FlaggedOutputActionName)] + public async Task OnFlaggedOutput([ActionTurnContext] ITurnContext turnContext) + { + await turnContext.SendActivityAsync("I'm not allowed to talk about such things."); + return ""; + } + } +} diff --git a/dotnet/samples/05.chatModeration/AdapterWithErrorHandler.cs b/dotnet/samples/05.chatModeration/AdapterWithErrorHandler.cs new file mode 100644 index 000000000..369476aba --- /dev/null +++ b/dotnet/samples/05.chatModeration/AdapterWithErrorHandler.cs @@ -0,0 +1,26 @@ +using Microsoft.Bot.Builder.TraceExtensions; +using Microsoft.Teams.AI; + +namespace ChatModeration +{ + public class AdapterWithErrorHandler : TeamsAdapter + { + public AdapterWithErrorHandler(IConfiguration configuration, ILogger 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}"); + // 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"); + }; + } + } +} diff --git a/dotnet/samples/05.chatModeration/ChatModeration.csproj b/dotnet/samples/05.chatModeration/ChatModeration.csproj new file mode 100644 index 000000000..e5fc30c1d --- /dev/null +++ b/dotnet/samples/05.chatModeration/ChatModeration.csproj @@ -0,0 +1,47 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + None + + + + + ..\..\packages\Microsoft.TeamsAI\Microsoft.TeamsAI\obj\Debug\netstandard2.0\Microsoft.Teams.AI.dll + + + + + + diff --git a/dotnet/samples/05.chatModeration/ChatModeration.sln b/dotnet/samples/05.chatModeration/ChatModeration.sln new file mode 100644 index 000000000..c807cc153 --- /dev/null +++ b/dotnet/samples/05.chatModeration/ChatModeration.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatModeration", "ChatModeration.csproj", "{C2964D35-6742-4DBF-9685-5DD5A01D8D82}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C2964D35-6742-4DBF-9685-5DD5A01D8D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2964D35-6742-4DBF-9685-5DD5A01D8D82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2964D35-6742-4DBF-9685-5DD5A01D8D82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2964D35-6742-4DBF-9685-5DD5A01D8D82}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {30CCD595-AEBE-4CC2-B016-33E2EA023EAE} + EndGlobalSection +EndGlobal diff --git a/dotnet/samples/05.chatModeration/Config.cs b/dotnet/samples/05.chatModeration/Config.cs new file mode 100644 index 000000000..4080ba712 --- /dev/null +++ b/dotnet/samples/05.chatModeration/Config.cs @@ -0,0 +1,29 @@ +namespace ChatModeration +{ + public class ConfigOptions + { + public string? BOT_ID { get; set; } + public string? BOT_PASSWORD { get; set; } + public OpenAIConfigOptions? OpenAI { get; set; } + public AzureConfigOptions? Azure { get; set; } + } + + /// + /// Options for Open AI + /// + public class OpenAIConfigOptions + { + public string? ApiKey { get; set; } + } + + /// + /// Options for Azure OpenAI and Azure Content Safety + /// + public class AzureConfigOptions + { + public string? OpenAIApiKey { get; set; } + public string? OpenAIEndpoint { get; set; } + public string? ContentSafetyApiKey { get; set; } + public string? ContentSafetyEndpoint { get; set; } + } +} diff --git a/dotnet/samples/05.chatModeration/Controllers/BotController.cs b/dotnet/samples/05.chatModeration/Controllers/BotController.cs new file mode 100644 index 000000000..f0fbca52d --- /dev/null +++ b/dotnet/samples/05.chatModeration/Controllers/BotController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI; + +namespace ChatModeration.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 + ); + } + } +} diff --git a/dotnet/samples/05.chatModeration/Program.cs b/dotnet/samples/05.chatModeration/Program.cs new file mode 100644 index 000000000..cc0816c24 --- /dev/null +++ b/dotnet/samples/05.chatModeration/Program.cs @@ -0,0 +1,128 @@ +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Teams.AI.AI.Models; +using Microsoft.Teams.AI.AI.Planners; +using Microsoft.Teams.AI.AI.Prompts; +using Microsoft.Teams.AI.State; +using Microsoft.Teams.AI; +using ChatModeration; +using Microsoft.Teams.AI.AI.Moderator; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddHttpClient("WebClient", client => client.Timeout = TimeSpan.FromSeconds(600)); +builder.Services.AddHttpContextAccessor(); + +// Prepare Configuration for ConfigurationBotFrameworkAuthentication +var config = builder.Configuration.Get()!; +builder.Configuration["MicrosoftAppType"] = "MultiTenant"; +builder.Configuration["MicrosoftAppId"] = config.BOT_ID; +builder.Configuration["MicrosoftAppPassword"] = config.BOT_PASSWORD; + +// Create the Bot Framework Authentication to be used with the Bot Adapter. +builder.Services.AddSingleton(); + +// Create the Cloud Adapter with error handling enabled. +// Note: some classes expect a BotAdapter and some expect a BotFrameworkHttpAdapter, so +// register the same adapter instance for all types. +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => sp.GetService()!); +builder.Services.AddSingleton(sp => sp.GetService()!); + +builder.Services.AddSingleton(); + +// Create AI Model +if (!string.IsNullOrEmpty(config.OpenAI?.ApiKey)) +{ + // Create OpenAI Model + builder.Services.AddSingleton(sp => new( + new OpenAIModelOptions(config.OpenAI.ApiKey, "gpt-4o") + { + LogRequests = true + }, + sp.GetService() + )); + + builder.Services.AddSingleton>(sp => new OpenAIModerator(new(apiKey: config.OpenAI.ApiKey, ModerationType.Both))); +} +else if (!string.IsNullOrEmpty(config.Azure?.OpenAIApiKey) && !string.IsNullOrEmpty(config.Azure.OpenAIEndpoint)) +{ + // Create Azure OpenAI Model + builder.Services.AddSingleton(sp => new( + new AzureOpenAIModelOptions( + config.Azure.OpenAIApiKey, + "gpt-4o", + config.Azure.OpenAIEndpoint + ) + { + LogRequests = true + }, + sp.GetService() + )); + + builder.Services.AddSingleton>(sp => + new AzureContentSafetyModerator(new(config.Azure.OpenAIApiKey, config.Azure.OpenAIEndpoint, ModerationType.Both)) + ); +} +else +{ + throw new Exception("please configure settings for either OpenAI or Azure"); +} + +// Create the bot as transient. In this case the ASP Controller is expecting an IBot. +builder.Services.AddTransient(sp => +{ + // Create loggers + ILoggerFactory loggerFactory = sp.GetService()!; + + // Create Prompt Manager + PromptManager prompts = new(new() + { + PromptFolder = "./Prompts" + }); + + // Create ActionPlanner + ActionPlanner planner = new( + options: new( + model: sp.GetService()!, + prompts: prompts, + defaultPrompt: async (context, state, planner) => + { + PromptTemplate template = prompts.GetPrompt("Chat"); + return await Task.FromResult(template); + } + ) + { LogRepairs = true }, + loggerFactory: loggerFactory + ); + + Application app = new ApplicationBuilder() + .WithAIOptions(new(planner) { Moderator = sp.GetService>() }) + .WithStorage(sp.GetService()!) + .Build(); + + app.AI.ImportActions(new ActionHandlers()); + + app.OnConversationUpdate("membersAdded", async (context, state, token) => + { + await context.SendActivityAsync("Hello and welcome! With this sample you can see the functionality of the Azure AI Content Safety Moderator " + + "or OpenAI's Moderator based on the setup configurations."); + }); + + return app; +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +app.UseStaticFiles(); +app.UseRouting(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet/samples/05.chatModeration/Prompts/Chat/config.json b/dotnet/samples/05.chatModeration/Prompts/Chat/config.json new file mode 100644 index 000000000..fd6e274a4 --- /dev/null +++ b/dotnet/samples/05.chatModeration/Prompts/Chat/config.json @@ -0,0 +1,18 @@ +{ + "schema": 1.1, + "description": "A bot that is configured to use chat moderation", + "type": "completion", + "completion": { + "model": "gpt-4o", + "completion_type": "chat", + "include_history": true, + "include_input": true, + "max_input_tokens": 2000, + "max_tokens": 1000, + "temperature": 0.2, + "top_p": 0.0, + "presence_penalty": 0.6, + "frequency_penalty": 0.0, + "stop_sequences": [] + } +} \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/Prompts/Chat/skprompt.txt b/dotnet/samples/05.chatModeration/Prompts/Chat/skprompt.txt new file mode 100644 index 000000000..bc17ef457 --- /dev/null +++ b/dotnet/samples/05.chatModeration/Prompts/Chat/skprompt.txt @@ -0,0 +1,3 @@ +You are the AI assistant demonstrating the Azure OpenAI's content safety moderation capabilities. +The following is a conversation with an AI assistant. +You evaluate the moderation severity of human's input in the following categories of moderation: hate, sexual content, self harm, violence. \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/Properties/launchSettings.json b/dotnet/samples/05.chatModeration/Properties/launchSettings.json new file mode 100644 index 000000000..9efe20cd7 --- /dev/null +++ b/dotnet/samples/05.chatModeration/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "profiles": { + // Debug project within Teams + "Microsoft Teams (browser)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "https://teams.microsoft.com/l/app/576b3387-9ef7-4aff-9da7-acc2ad2f6d0f?installAppPackage=true&webjoin=true&appTenantId=d247b24d-59a3-4042-8253-90aa371a6eb4&login_hint=kavinsingh_microsoft.com", + "applicationUrl": "http://localhost:5130", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "hotReloadProfile": "aspnetcore" + } + //// Uncomment following profile to debug project only (without launching Teams) + //, + //"Start Project (not in Teams)": { + // "commandName": "Project", + // "dotnetRunMessages": true, + // "applicationUrl": "https://localhost:7130;http://localhost:5130", + // "environmentVariables": { + // "ASPNETCORE_ENVIRONMENT": "Development" + // }, + // "hotReloadProfile": "aspnetcore" + //} + } +} \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/README.md b/dotnet/samples/05.chatModeration/README.md new file mode 100644 index 000000000..67a9001b5 --- /dev/null +++ b/dotnet/samples/05.chatModeration/README.md @@ -0,0 +1,77 @@ +# Chat Bot with Moderation Control + +## Summary + +This sample shows how to incorporate Content Safety control into a Microsoft Teams application. + +## Set up instructions + +All the samples in the C# .NET SDK can be set up in the same way. You can find the step by step instructions here: [Setup Instructions](../README.md). + +Note that, this sample requires AI service so you need one more pre-step before Local Debug (F5). + +1. Set your Azure OpenAI related settings to *appsettings.Development.json*. + + ```json + "Azure": { + "OpenAIApiKey": "", + "OpenAIEndpoint": "", + "ContentSafetyApiKey": "", + "ContentSafetyEndpoint": "" + } + ``` + +## Interacting with the bot + +You can interact with this bot by sending it a message. If you send it a message that contains inappropriate content, the bot will response with a moderation report that contains the inappropriate content: + +![Moderation Report](./assets/moderation.png) + + +## Deploy to Azure + +You can use Teams Toolkit for Visual Studio or CLI to host the bot in Azure. The sample includes Bicep templates in the `/infra` directory which are used by the tools to create resources in Azure. + +You can find deployment instructions [here](../README.md#deploy-to-azure). + +Note that, this sample requires AI service so you need one more pre-step before deploy to Azure. To configure the Azure resources to have an environment variable for the Azure OpenAI Key and other settings: + +1. In `./env/.env.dev.user` file, paste your Azure OpenAI related variables. + + ```bash + SECRET_AZURE_OPENAI_API_KEY= + SECRET_AZURE_OPENAI_ENDPOINT= + SECRET_AZURE_CONTENT_SAFETY_API_KEY= + SECRET_AZURE_CONTENT_SAFETY_ENDPOINT= + ``` + +The `SECRET_` prefix is a convention used by Teams Toolkit to mask the value in any logging output and is optional. + +## Use OpenAI + +Above steps use Azure OpenAI as AI service, optionally, you can also use OpenAI as AI service. + +**As prerequisites** + +1. Get an OpenAI api key. + +**For debugging (F5)** + +1. Set your [OpenAI API Key](https://platform.openai.com/settings/profile?tab=api-keys) to *appsettings.Development.json*. + + ```json + "OpenAI": { + "ApiKey": "" + }, + ``` + +**For deployment to Azure** + +To configure the Azure resources to have OpenAI environment variables: + +1. In `./env/.env.dev.user` file, paste your [OpenAI API Key](https://platform.openai.com/settings/profile?tab=api-keys) to the environment variable `SECRET_OPENAI_KEY=`. + +## Further reading + +- [Teams Toolkit overview](https://aka.ms/vs-teams-toolkit-getting-started) +- [How Microsoft Teams bots work](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-basics-teams?view=azure-bot-service-4.0&tabs=csharp) diff --git a/dotnet/samples/05.chatModeration/appPackage/manifest.json b/dotnet/samples/05.chatModeration/appPackage/manifest.json new file mode 100644 index 000000000..81f3ef407 --- /dev/null +++ b/dotnet/samples/05.chatModeration/appPackage/manifest.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.15/MicrosoftTeams.schema.json", + "version": "1.1.0", + "manifestVersion": "1.15", + "id": "${{TEAMS_APP_ID}}", + "packageName": "com.package.name", + "name": { + "short": "Moderation${{APP_NAME_SUFFIX}}", + "full": "Moderation Bot" + }, + "developer": { + "name": "Moderation", + "mpnId": "", + "websiteUrl": "https://microsoft.com", + "privacyUrl": "https://privacy.microsoft.com/privacystatement", + "termsOfUseUrl": "https://www.microsoft.com/legal/terms-of-use" + }, + "description": { + "short": "Sample bot that thinks it's a Chef to help you cook Teams apps", + "full": "Sample bot that thinks it's a Chef to help you cook Teams apps" + }, + "icons": { + "outline": "outline.png", + "color": "color.png" + }, + "accentColor": "#FFFFFF", + "staticTabs": [ + { + "entityId": "conversations", + "scopes": ["personal"] + }, + { + "entityId": "about", + "scopes": ["personal"] + } + ], + "bots": [ + { + "botId": "${{BOT_ID}}", + "scopes": ["personal", "team", "groupChat"], + "isNotificationOnly": false, + "supportsCalling": false, + "supportsVideo": false, + "supportsFiles": false + } + ], + "validDomains": [] +} diff --git a/dotnet/samples/05.chatModeration/appsettings.Development.json b/dotnet/samples/05.chatModeration/appsettings.Development.json new file mode 100644 index 000000000..1928e121f --- /dev/null +++ b/dotnet/samples/05.chatModeration/appsettings.Development.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Teams.AI": "Trace" + } + }, + "AllowedHosts": "*", + "BOT_ID": "${botId}", + "BOT_PASSWORD": "${botPassword}", + "Azure": { + "OpenAIApiKey": "", + "OpenAIEndpoint": "", + "ContentSafetyApiKey": "", + "ContentSafetyEndpoint": "" + }, + "OpenAI": { + "ApiKey": "" + } +} \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/appsettings.json b/dotnet/samples/05.chatModeration/appsettings.json new file mode 100644 index 000000000..9ac767903 --- /dev/null +++ b/dotnet/samples/05.chatModeration/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "BOT_ID": "${botId}", + "BOT_PASSWORD": "${botPassword}", + "Azure": { + "OpenAIApiKey": "", + "OpenAIEndpoint": "", + "ContentSafetyApiKey": "", + "ContentSafetyEndpoint": "" + }, + "OpenAI": { + "ApiKey": "" + } +} diff --git a/dotnet/samples/05.chatModeration/assets/moderation.png b/dotnet/samples/05.chatModeration/assets/moderation.png new file mode 100644 index 000000000..92f262e67 --- /dev/null +++ b/dotnet/samples/05.chatModeration/assets/moderation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8c34c851b963ccdc677e158ea8e96eaecec80a0cfdb0800fcc7f6e929fcb2f5 +size 44202 diff --git a/dotnet/samples/05.chatModeration/env/.env.dev b/dotnet/samples/05.chatModeration/env/.env.dev new file mode 100644 index 000000000..efcbe1f06 --- /dev/null +++ b/dotnet/samples/05.chatModeration/env/.env.dev @@ -0,0 +1,18 @@ +# This file includes environment variables that will be committed to git by default. + +# Built-in environment variables +TEAMSFX_ENV=dev + +# Updating AZURE_SUBSCRIPTION_ID or AZURE_RESOURCE_GROUP_NAME after provision may also require an update to RESOURCE_SUFFIX, because some services require a globally unique name across subscriptions/resource groups. +AZURE_SUBSCRIPTION_ID= +AZURE_RESOURCE_GROUP_NAME= +RESOURCE_SUFFIX= + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_AZURE_APP_SERVICE_RESOURCE_ID= +BOT_DOMAIN= + + +APP_NAME_SUFFIX=dev diff --git a/dotnet/samples/05.chatModeration/env/.env.local b/dotnet/samples/05.chatModeration/env/.env.local new file mode 100644 index 000000000..07b69ee56 --- /dev/null +++ b/dotnet/samples/05.chatModeration/env/.env.local @@ -0,0 +1,12 @@ +# This file includes environment variables that can be committed to git. It's gitignored by default because it represents your local development environment. + +# Built-in environment variables +TEAMSFX_ENV=local + +# Generated during provision, you can also add your own variables. +BOT_ID= +TEAMS_APP_ID= +BOT_DOMAIN= + + +APP_NAME_SUFFIX=local \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/infra/azure.bicep b/dotnet/samples/05.chatModeration/infra/azure.bicep new file mode 100644 index 000000000..e3877021b --- /dev/null +++ b/dotnet/samples/05.chatModeration/infra/azure.bicep @@ -0,0 +1,113 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@description('Required when create Azure Bot service') +param botAadAppClientId string + +@secure() +@description('Required by Bot Framework package in your bot project') +param botAadAppClientSecret string + +@secure() +param openAIApiKey string + +@secure() +param azureOpenAIApiKey string + +@secure() +param azureOpenAIEndpoint string + +@secure() +param azureContentSafetyApiKey string + +@secure() +param azureContentSafetyEndpoint string + +param webAppSKU string + +@maxLength(42) +param botDisplayName string + +param serverfarmsName string = resourceBaseName +param webAppName string = resourceBaseName +param location string = resourceGroup().location + +// Compute resources for your Web App +resource serverfarm 'Microsoft.Web/serverfarms@2021-02-01' = { + kind: 'app' + location: location + name: serverfarmsName + sku: { + name: webAppSKU + } +} + +// Web App that hosts your bot +resource webApp 'Microsoft.Web/sites@2021-02-01' = { + kind: 'app' + location: location + name: webAppName + properties: { + serverFarmId: serverfarm.id + httpsOnly: true + siteConfig: { + alwaysOn: true + appSettings: [ + { + name: 'WEBSITE_RUN_FROM_PACKAGE' + value: '1' // Run Azure APP Service from a package file + } + { + name: 'RUNNING_ON_AZURE' + value: '1' + } + { + name: 'BOT_ID' + value: botAadAppClientId + } + { + name: 'BOT_PASSWORD' + value: botAadAppClientSecret + } + { + name: 'OpenAI__ApiKey' + value: openAIApiKey + } + { + name: 'Azure__OpenAIApiKey' + value: azureOpenAIApiKey + } + { + name: 'Azure__OpenAIEndpoint' + value: azureOpenAIEndpoint + } + { + name: 'Azure__ContentSafetyApiKey' + value: azureContentSafetyApiKey + } + { + name: 'Azure__ContentSafetyEndpoint' + value: azureContentSafetyEndpoint + } + ] + ftpsState: 'FtpsOnly' + } + } +} + +// Register your web service as a bot with the Bot Framework +module azureBotRegistration './botRegistration/azurebot.bicep' = { + name: 'Azure-Bot-registration' + params: { + resourceBaseName: resourceBaseName + botAadAppClientId: botAadAppClientId + botAppDomain: webApp.properties.defaultHostName + botDisplayName: botDisplayName + } +} + +// The output will be persisted in .env.{envName}. Visit https://aka.ms/teamsfx-actions/arm-deploy for more details. +output BOT_AZURE_APP_SERVICE_RESOURCE_ID string = webApp.id +output BOT_DOMAIN string = webApp.properties.defaultHostName diff --git a/dotnet/samples/05.chatModeration/infra/azure.parameters.json b/dotnet/samples/05.chatModeration/infra/azure.parameters.json new file mode 100644 index 000000000..00e948732 --- /dev/null +++ b/dotnet/samples/05.chatModeration/infra/azure.parameters.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceBaseName": { + "value": "moderationbot${{RESOURCE_SUFFIX}}" + }, + "botAadAppClientId": { + "value": "${{BOT_ID}}" + }, + "botAadAppClientSecret": { + "value": "${{SECRET_BOT_PASSWORD}}" + }, + "webAppSKU": { + "value": "B1" + }, + "botDisplayName": { + "value": "ModerationBot" + }, + "openAIApiKey": { + "value": "${{SECRET_OPENAI_API_KEY}}" + }, + "azureOpenAIApiKey": { + "value": "${{SECRET_AZURE_OPENAI_API_KEY}}" + }, + "azureOpenAIEndpoint": { + "value": "${{SECRET_AZURE_OPENAI_ENDPOINT}}" + }, + "azureContentSafetyApiKey": { + "value": "${{SECRET_AZURE_CONTENT_SAFETY_API_KEY}}" + }, + "azureContentSafetyEndpoint": { + "value": "${{SECRET_AZURE_CONTENT_SAFETY_ENDPOINT}}" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/infra/botRegistration/azurebot.bicep b/dotnet/samples/05.chatModeration/infra/botRegistration/azurebot.bicep new file mode 100644 index 000000000..ab67c7a56 --- /dev/null +++ b/dotnet/samples/05.chatModeration/infra/botRegistration/azurebot.bicep @@ -0,0 +1,37 @@ +@maxLength(20) +@minLength(4) +@description('Used to generate names for all resources in this file') +param resourceBaseName string + +@maxLength(42) +param botDisplayName string + +param botServiceName string = resourceBaseName +param botServiceSku string = 'F0' +param botAadAppClientId string +param botAppDomain string + +// Register your web service as a bot with the Bot Framework +resource botService 'Microsoft.BotService/botServices@2021-03-01' = { + kind: 'azurebot' + location: 'global' + name: botServiceName + properties: { + displayName: botDisplayName + endpoint: 'https://${botAppDomain}/api/messages' + msaAppId: botAadAppClientId + } + sku: { + name: botServiceSku + } +} + +// Connect the bot service to Microsoft Teams +resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = { + parent: botService + location: 'global' + name: 'MsTeamsChannel' + properties: { + channelName: 'MsTeamsChannel' + } +} diff --git a/dotnet/samples/05.chatModeration/infra/botRegistration/readme.md b/dotnet/samples/05.chatModeration/infra/botRegistration/readme.md new file mode 100644 index 000000000..d5416243c --- /dev/null +++ b/dotnet/samples/05.chatModeration/infra/botRegistration/readme.md @@ -0,0 +1 @@ +The `azurebot.bicep` module is provided to help you create Azure Bot service when you don't use Azure to host your app. If you use Azure as infrastrcture for your app, `azure.bicep` under infra folder already leverages this module to create Azure Bot service for you. You don't need to deploy `azurebot.bicep` again. \ No newline at end of file diff --git a/dotnet/samples/05.chatModeration/teamsapp.local.yml b/dotnet/samples/05.chatModeration/teamsapp.local.yml new file mode 100644 index 000000000..c1950a386 --- /dev/null +++ b/dotnet/samples/05.chatModeration/teamsapp.local.yml @@ -0,0 +1,87 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# +# The teamsapp.local.yml composes automation tasks for Teams Toolkit when running locally. +# This file is used when selecting 'Prepare Teams App Dependencies' menu items in the Teams Toolkit for Visual Studio window +# +# You can customize this file. Visit https://aka.ms/teamsfx-v5.0-guide for more info about Teams Toolkit project files. +version: 1.1.0 + +# Defines what the `provision` lifecycle step does with Teams Toolkit. +provision: + # Automates the creation of a Teams app registration and saves the App ID to an environment file. + - uses: teamsApp/create + with: + # Teams app name + name: Moderation${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Automates the creation an Azure AD app registration which is required for a bot. + # The Bot ID (AAD app client ID) and Bot Password (AAD app client secret) are saved to an environment file. + - uses: botAadApp/create + with: + # The Azure Active Directory application's display name + name: Moderation + writeToEnvironmentFile: + # The Azure Active Directory application's client id created for bot. + botId: BOT_ID + # The Azure Active Directory application's client secret created for bot. + botPassword: SECRET_BOT_PASSWORD + + # Automates the creation and configuration of a Bot Framework registration which is required for a bot. + # This configures the bot to use the Azure AD app registration created in the previous step. + - uses: botFramework/create + with: + botId: ${{BOT_ID}} + name: Moderation + messagingEndpoint: ${{BOT_ENDPOINT}}/api/messages + description: "" + channels: + - name: msteams + + # Generate runtime appsettings to JSON file + - uses: file/createOrUpdateJsonFile + with: + target: ./appsettings.Development.json + content: + BOT_ID: ${{BOT_ID}} + BOT_PASSWORD: ${{SECRET_BOT_PASSWORD}} + + # Optional: Automates schema and error checking of the Teams app manifest and outputs the results in the console. + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Automates the creation of a Teams app package (.zip). + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Automates updating the Teams app manifest in Teams Developer Portal using the App ID from the mainfest file. + # This action ensures that any manifest changes are reflected when launching the app again in Teams. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + + # Create or update debug profile in lauchsettings file + - uses: file/createOrUpdateJsonFile + with: + target: ./Properties/launchSettings.json + content: + profiles: + Microsoft Teams (browser): + commandName: "Project" + dotnetRunMessages: true + launchBrowser: true + 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}}" + applicationUrl: "http://localhost:5130" + environmentVariables: + ASPNETCORE_ENVIRONMENT: "Development" + hotReloadProfile: "aspnetcore" diff --git a/dotnet/samples/05.chatModeration/teamsapp.yml b/dotnet/samples/05.chatModeration/teamsapp.yml new file mode 100644 index 000000000..9af915ea1 --- /dev/null +++ b/dotnet/samples/05.chatModeration/teamsapp.yml @@ -0,0 +1,97 @@ +# yaml-language-server: $schema=https://aka.ms/teams-toolkit/1.1.0/yaml.schema.json +# +# The teamsapp.local.yml composes automation tasks for Teams Toolkit when running locally. +# This file is used when selecting 'Provision' or 'Deploy' menu items in the Teams Toolkit for Visual Studio window +# +# You can customize this file. Visit https://aka.ms/teamsfx-v5.0-guide for more info about Teams Toolkit project files. +version: 1.1.0 + +environmentFolderPath: ./env + +# Defines what the `provision` lifecycle step does with Teams Toolkit. +provision: + # Automates the creation of a Teams app registration and saves the App ID to an environment file. + - uses: teamsApp/create + with: + # Teams app name + name: Moderation${{APP_NAME_SUFFIX}} + # Write the information of created resources into environment file for + # the specified environment variable(s). + writeToEnvironmentFile: + teamsAppId: TEAMS_APP_ID + + # Automates the creation an Azure AD app registration which is required for a bot. + # The Bot ID (AAD app client ID) and Bot Password (AAD app client secret) are saved to an environment file. + - uses: botAadApp/create + with: + # The Azure Active Directory application's display name + name: Moderation + writeToEnvironmentFile: + # The Azure Active Directory application's client id created for bot. + botId: BOT_ID + # The Azure Active Directory application's client secret created for bot. + botPassword: SECRET_BOT_PASSWORD + + # Automates the creation of infrastructure defined in ARM templates to host the bot. + # The created resource IDs are saved to an environment file. + - uses: arm/deploy # Deploy given ARM templates parallelly. + with: + # AZURE_SUBSCRIPTION_ID is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select a subscription. + # Referencing other environment variables with empty values + # will skip the subscription selection prompt. + subscriptionId: ${{AZURE_SUBSCRIPTION_ID}} + # AZURE_RESOURCE_GROUP_NAME is a built-in environment variable, + # if its value is empty, TeamsFx will prompt you to select or create one + # resource group. + # Referencing other environment variables with empty values + # will skip the resource group selection prompt. + resourceGroupName: ${{AZURE_RESOURCE_GROUP_NAME}} + templates: + - path: ./infra/azure.bicep # Relative path to this file + # Relative path to this yaml file. + # Placeholders will be replaced with corresponding environment + # variable before ARM deployment. + parameters: ./infra/azure.parameters.json + # Required when deploying ARM template + deploymentName: Create-resources-for-tab + # Teams Toolkit will download this bicep CLI version from github for you, + # will use bicep CLI in PATH if you remove this config. + bicepCliVersion: v0.9.1 + + # Optional: Automates schema and error checking of the Teams app manifest and outputs the results in the console. + - uses: teamsApp/validateManifest + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + + # Automates creating a final app package (.zip) by replacing any variables in the manifest.json file for the current environment. + - uses: teamsApp/zipAppPackage + with: + # Path to manifest template + manifestPath: ./appPackage/manifest.json + outputZipPath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + outputJsonPath: ./appPackage/build/manifest.${{TEAMSFX_ENV}}.json + + # Automates updating the Teams app manifest in Teams Developer Portal using the App ID from the mainfest file. + # This action ensures that any manifest changes are reflected when launching the app again in Teams. + - uses: teamsApp/update + with: + # Relative path to this file. This is the path for built zip file. + appPackagePath: ./appPackage/build/appPackage.${{TEAMSFX_ENV}}.zip + +# Triggered when 'teamsfx deploy' is executed +deploy: + - uses: cli/runDotnetCommand + with: + args: publish --configuration Release --runtime win-x86 --self-contained + + # Deploy to an Azure App Service using the zip file created in the provision step. + - uses: azureAppService/zipDeploy + with: + # deploy base folder + artifactFolder: bin/Release/net6.0/win-x86/publish + # This example uses the env var thats generated by the arm/deploy action. + # You can replace it with an existing Azure Resource ID or other + # custom environment variable. + resourceId: ${{BOT_AZURE_APP_SERVICE_RESOURCE_ID}}