From c03ce837df64b1f7a23b95045d65e0c2bba35b54 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Sun, 5 Apr 2020 10:58:59 -0400 Subject: [PATCH 1/7] Updated the project to .NET Core 3.1 --- ConsoleChatbot/ConsoleChatbot.csproj | 10 +++++----- ConsoleChatbot/Program.cs | 3 +-- Fritz.Chatbot/Fritz.Chatbot.csproj | 17 +++++++++-------- Fritz.StreamTools/Fritz.StreamTools.csproj | 16 ++++++++-------- Fritz.StreamTools/Program.cs | 1 - Fritz.StreamTools/Startup.cs | 5 +++-- Test/Test.csproj | 16 ++++++++-------- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/ConsoleChatbot/ConsoleChatbot.csproj b/ConsoleChatbot/ConsoleChatbot.csproj index c2fb7d19..fc14c9bb 100644 --- a/ConsoleChatbot/ConsoleChatbot.csproj +++ b/ConsoleChatbot/ConsoleChatbot.csproj @@ -16,11 +16,11 @@ - - - - - + + + + + diff --git a/ConsoleChatbot/Program.cs b/ConsoleChatbot/Program.cs index ecb5e9cd..2ee3c787 100644 --- a/ConsoleChatbot/Program.cs +++ b/ConsoleChatbot/Program.cs @@ -54,8 +54,7 @@ private static FritzBot CreateFritzBot(IChatService chatService) FritzBot.RegisterCommands(serviceCollection); var svcProvider = serviceCollection.BuildServiceProvider(); - var loggerFactory = svcProvider.GetService() - .AddConsole(LogLevel.Information); + var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); return new FritzBot(config, svcProvider, loggerFactory); diff --git a/Fritz.Chatbot/Fritz.Chatbot.csproj b/Fritz.Chatbot/Fritz.Chatbot.csproj index 7111a966..071edc58 100644 --- a/Fritz.Chatbot/Fritz.Chatbot.csproj +++ b/Fritz.Chatbot/Fritz.Chatbot.csproj @@ -10,15 +10,16 @@ - - - - - - - + + + + + + + + - + diff --git a/Fritz.StreamTools/Fritz.StreamTools.csproj b/Fritz.StreamTools/Fritz.StreamTools.csproj index 243da3b4..e3e0e080 100644 --- a/Fritz.StreamTools/Fritz.StreamTools.csproj +++ b/Fritz.StreamTools/Fritz.StreamTools.csproj @@ -18,16 +18,16 @@ - + - - - + + + - - - - + + + + diff --git a/Fritz.StreamTools/Program.cs b/Fritz.StreamTools/Program.cs index 15082f75..01db4cf4 100644 --- a/Fritz.StreamTools/Program.cs +++ b/Fritz.StreamTools/Program.cs @@ -21,7 +21,6 @@ public static void Main(string[] args) public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) - .UseApplicationInsights() .UseStartup(); } } diff --git a/Fritz.StreamTools/Startup.cs b/Fritz.StreamTools/Startup.cs index ea2a8bcd..8b42c872 100644 --- a/Fritz.StreamTools/Startup.cs +++ b/Fritz.StreamTools/Startup.cs @@ -13,7 +13,7 @@ namespace Fritz.StreamTools { public class Startup { - private static Dictionary _servicesRequiredConfiguration = new Dictionary() + private static Dictionary _ServicesRequiredConfiguration = new Dictionary() { { typeof(SentimentService), new [] { "FritzBot:SentimentAnalysisKey" } } }; @@ -29,8 +29,9 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddTwitchClient(); + services.AddApplicationInsightsTelemetry(); - StartupServices.ConfigureServices.Execute(services, Configuration, _servicesRequiredConfiguration); + StartupServices.ConfigureServices.Execute(services, Configuration, _ServicesRequiredConfiguration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Test/Test.csproj b/Test/Test.csproj index b138fa6d..4257d98e 100644 --- a/Test/Test.csproj +++ b/Test/Test.csproj @@ -7,15 +7,15 @@ - - - - - - + + + + + + - - + + all From 85001db65dc58e083fc7bf98f7a9ad6c54a9576c Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Sun, 5 Apr 2020 12:24:08 -0400 Subject: [PATCH 2/7] Added seed data and EF connections for our QnA --- Fritz.Chatbot/Fritz.Chatbot.csproj | 2 + Fritz.Chatbot/QnA/Data/AlternateQuestions.cs | 21 ++ Fritz.Chatbot/QnA/Data/QnADbContext.cs | 107 +++++++++ Fritz.Chatbot/QnA/Data/QnAPair.cs | 29 +++ Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs | 19 ++ .../20200405154808_Initial_QnA.Designer.cs | 99 ++++++++ .../Migrations/20200405154808_Initial_QnA.cs | 77 ++++++ .../20200405161220_SeedQuestions.Designer.cs | 227 ++++++++++++++++++ .../20200405161220_SeedQuestions.cs | 146 +++++++++++ .../Migrations/QnADbContextModelSnapshot.cs | 225 +++++++++++++++++ Fritz.StreamTools/Fritz.StreamTools.csproj | 5 + .../StartupServices/ConfigureServices.cs | 13 +- Fritz.StreamTools/appsettings.json | 5 +- 13 files changed, 972 insertions(+), 3 deletions(-) create mode 100644 Fritz.Chatbot/QnA/Data/AlternateQuestions.cs create mode 100644 Fritz.Chatbot/QnA/Data/QnADbContext.cs create mode 100644 Fritz.Chatbot/QnA/Data/QnAPair.cs create mode 100644 Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.Designer.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.Designer.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs diff --git a/Fritz.Chatbot/Fritz.Chatbot.csproj b/Fritz.Chatbot/Fritz.Chatbot.csproj index 071edc58..a9548672 100644 --- a/Fritz.Chatbot/Fritz.Chatbot.csproj +++ b/Fritz.Chatbot/Fritz.Chatbot.csproj @@ -13,6 +13,7 @@ + @@ -20,6 +21,7 @@ + diff --git a/Fritz.Chatbot/QnA/Data/AlternateQuestions.cs b/Fritz.Chatbot/QnA/Data/AlternateQuestions.cs new file mode 100644 index 00000000..71da5f0a --- /dev/null +++ b/Fritz.Chatbot/QnA/Data/AlternateQuestions.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Fritz.Chatbot.QnA.Data +{ + public class AlternateQuestion { + + public int Id { get; set; } + + [Required] + public int QuestionId { get; set; } + + [Required] + [MaxLength(280)] // Same length as a Tweet + public string QuestionText { get; set; } + + public QnAPair MainQuestion { get; set; } + + } + + +} diff --git a/Fritz.Chatbot/QnA/Data/QnADbContext.cs b/Fritz.Chatbot/QnA/Data/QnADbContext.cs new file mode 100644 index 00000000..7def8b54 --- /dev/null +++ b/Fritz.Chatbot/QnA/Data/QnADbContext.cs @@ -0,0 +1,107 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Net.Sockets; + +namespace Fritz.Chatbot.QnA.Data +{ + public class QnADbContext : DbContext + { + + public QnADbContext(DbContextOptions options) : base(options) { } + + public DbSet QnAPairs { get; set; } + + public DbSet UnansweredQuestions { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + modelBuilder.Entity() + .HasMany(q => q.AlternateQuestions) + .WithOne(a => a.MainQuestion) + .HasForeignKey(a => a.QuestionId); + + LoadSeedData(modelBuilder); + + base.OnModelCreating(modelBuilder); + + } + + private void LoadSeedData(ModelBuilder modelBuilder) + { + + modelBuilder.Entity().HasData(new QnAPair[] { + + new QnAPair() {Id=1, + QuestionText="What language is Jeff speaking?", + AnswerText="Jeff speaks English, with a Mid-Atlantic / Philadelphia accent." + }, + new QnAPair() {Id=2, + QuestionText="What editor does Jeff use?", + AnswerText="Jeff typically uses Visual Studio 2019 Enterprise edition available at visualstudio.com, and sometimes uses Visual Studio Code from code.visualstudio.com" + },new QnAPair() {Id=3, + QuestionText="Which VS version do you use?", + AnswerText="Jeff uses the Visual Studio Enterprise Edition, in preview mode. The preview is ALWAYS free to try: www.visualstudio.com / vs / preview /" + },new QnAPair() {Id=4, + QuestionText="What music is playing?", + AnswerText="The music comes from Carl Franklin's Music to Code By at http://mtcb.pwop.com and can also be found on the mobile app Music To Flow By that you can get at http://musictoflowby.com" + },new QnAPair() {Id=5, + QuestionText="What language is Jeff coding in?", + AnswerText="Jeff typically writes code in C# with ASP.NET Core. You will also find him regularly writing JavaScript, TypeScript, CSS, and HTML." + },new QnAPair() {Id=6, + QuestionText="Why does Jeff use Powershell to work with Git?", + AnswerText="Powershell with the posh-git plugin from dahlbyk / posh - git gives extra insight into Git repositories. Information on the prompt and tab-completion of git commands are just some of the cool features of posh-git." + },new QnAPair() {Id=7, + QuestionText="How many hats does Jeff own?", + AnswerText="No one knows the real answer to this question, because his wife keeps discarding a different one each month and doesn't tell him. Just ask about his Philly.NET hat..." + },new QnAPair() {Id=8, + QuestionText="Where can I find Jeff's blog?", + AnswerText="Jeff blogs at: www.jeffreyfritz.com" + },new QnAPair() {Id=9, + QuestionText="Where is Jeff's GitHub?", + AnswerText="You can find the source code shared on stream at @csharpfritz" + },new QnAPair() {Id=10, + QuestionText="Where can I find training videos from Jeff?", + AnswerText="Jeff has videos on WintellectNow -http://wintellectnow.com" + },new QnAPair() {Id=11, + QuestionText="Where can I catch Fritz videos?", + AnswerText="All of Jeff's live stream videos are archived on YouTube at: youtube.com/csharpfritz" + },new QnAPair() {Id=12, + QuestionText="Where can I watch the 8 - hour ASP.NET Core workshop?", + AnswerText="The workshop is at youtube.com/watch?v=--lYHxrsLsc" + },new QnAPair() {Id=13, + QuestionText="What is the machine you are using?", + AnswerText="Jeff broadcasts with a Dell Precision Tower 3620 that has a Geforce GTX 1060 video card" + },new QnAPair() {Id=14, + QuestionText="When does Jeff stream?", + AnswerText="Jeff streams regularly on Tuesday, Wednesday, Thursday, Friday, and Sunday at 10am ET." + },new QnAPair() {Id=15, + QuestionText="Where can I watch the C# workshop?", + AnswerText="The C# workshop is available as a playlist on YouTube at: youtube.com/watch?v=9ZmZuUSqQUM&list=PLVMqA0_8O85zIiU-T5h6rn8ortqEUNCeK" + },new QnAPair() {Id=16, + QuestionText="Where can I watch the Architecture workshop?", + AnswerText="The architecture workshop is available as a playlist on YouTube at: youtube.com/watch?v=k8cZUW4MS3I&list=PLVMqA0_8O85x-aurj1KphxUeWTeTlYkGM" + },new QnAPair() {Id=17, + QuestionText="What tool displays your keystrokes?", + AnswerText="That is Carnac from the Code52 project: Code52 / carnac" + },new QnAPair() {Id=18, + QuestionText="What is Madrinas Coffee?", + AnswerText="Madrinas is a sponsor of the Fritz and Friends channel.They make organic, free trade coffee that you can get from madrinascoffee.com.Use the coupon code 'FRITZ' for 20 % off your order." + },new QnAPair() {Id=19, + QuestionText="Who are the Live Coders?", + AnswerText="The Live Coders is a Twitch stream team that Jeff founded and comprised of folks that write code and answer questions about technology.You can learn more about them at livecoders.dev" + }, new QnAPair() {Id=20, + QuestionText="When is the Live Coders Conference ?", + AnswerText="The first Live Coders Conference is April 9, 2020 starting at 9a ET / 6a PT / 1300 UTC.You can learn more at conf.livecoders.dev" + },new QnAPair() {Id=21, + QuestionText="What keyboard are you using?", + AnswerText="Jeff uses a Vortex Race 3 with Cherry MX Blue switches, details on his blog at: jeffreyfritz.com/2018/07/mechanical-keyboards-i-just-got-one-and-why-you-need-one-too" + } + }); + + + } + } + + +} diff --git a/Fritz.Chatbot/QnA/Data/QnAPair.cs b/Fritz.Chatbot/QnA/Data/QnAPair.cs new file mode 100644 index 00000000..5adade28 --- /dev/null +++ b/Fritz.Chatbot/QnA/Data/QnAPair.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace Fritz.Chatbot.QnA.Data +{ + + public class QnAPair + { + + [Key] + public int Id { get; set; } + + [Required] + [MaxLength(280)] // Same length as a Tweet + public string QuestionText { get; set; } + + [Required] + [MaxLength(1000)] + public string AnswerText { get; set; } + + public ICollection AlternateQuestions { get; set; } + + } + + +} diff --git a/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs b/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs new file mode 100644 index 00000000..a06896d2 --- /dev/null +++ b/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Fritz.Chatbot.QnA.Data +{ + public class UnansweredQuestion + { + + public int Id { get; set; } + + [Required] + public DateTime AskedDateStamp { get; set; } = DateTime.UtcNow; + + [Required] + [MaxLength(1000)] + public string QuestionText { get; set; } + + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.Designer.cs b/Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.Designer.cs new file mode 100644 index 00000000..fe4e9239 --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.Designer.cs @@ -0,0 +1,99 @@ +// +using System; +using Fritz.Chatbot.QnA.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Fritz.Chatbot.QnA.Migrations +{ + [DbContext(typeof(QnADbContext))] + [Migration("20200405154808_Initial_QnA")] + partial class Initial_QnA + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("QuestionId") + .HasColumnType("integer"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.ToTable("AlternateQuestion"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.QnAPair", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AnswerText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.ToTable("QnAPairs"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.UnansweredQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AskedDateStamp") + .HasColumnType("timestamp without time zone"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.ToTable("UnansweredQuestions"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.HasOne("Fritz.Chatbot.QnA.Data.QnAPair", "MainQuestion") + .WithMany("AlternateQuestions") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.cs b/Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.cs new file mode 100644 index 00000000..d17ed32a --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/20200405154808_Initial_QnA.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Fritz.Chatbot.QnA.Migrations +{ + public partial class Initial_QnA : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "QnAPairs", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + QuestionText = table.Column(maxLength: 280, nullable: false), + AnswerText = table.Column(maxLength: 1000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_QnAPairs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UnansweredQuestions", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AskedDateStamp = table.Column(nullable: false), + QuestionText = table.Column(maxLength: 1000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UnansweredQuestions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AlternateQuestion", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + QuestionId = table.Column(nullable: false), + QuestionText = table.Column(maxLength: 280, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AlternateQuestion", x => x.Id); + table.ForeignKey( + name: "FK_AlternateQuestion_QnAPairs_QuestionId", + column: x => x.QuestionId, + principalTable: "QnAPairs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AlternateQuestion_QuestionId", + table: "AlternateQuestion", + column: "QuestionId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AlternateQuestion"); + + migrationBuilder.DropTable( + name: "UnansweredQuestions"); + + migrationBuilder.DropTable( + name: "QnAPairs"); + } + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.Designer.cs b/Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.Designer.cs new file mode 100644 index 00000000..f860f5f5 --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.Designer.cs @@ -0,0 +1,227 @@ +// +using System; +using Fritz.Chatbot.QnA.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Fritz.Chatbot.QnA.Migrations +{ + [DbContext(typeof(QnADbContext))] + [Migration("20200405161220_SeedQuestions")] + partial class SeedQuestions + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("QuestionId") + .HasColumnType("integer"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.ToTable("AlternateQuestion"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.QnAPair", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AnswerText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.ToTable("QnAPairs"); + + b.HasData( + new + { + Id = 1, + AnswerText = "Jeff speaks English, with a Mid-Atlantic / Philadelphia accent.", + QuestionText = "What language is Jeff speaking?" + }, + new + { + Id = 2, + AnswerText = "Jeff typically uses Visual Studio 2019 Enterprise edition available at visualstudio.com, and sometimes uses Visual Studio Code from code.visualstudio.com", + QuestionText = "What editor does Jeff use?" + }, + new + { + Id = 3, + AnswerText = "Jeff uses the Visual Studio Enterprise Edition, in preview mode. The preview is ALWAYS free to try: www.visualstudio.com / vs / preview /", + QuestionText = "Which VS version do you use?" + }, + new + { + Id = 4, + AnswerText = "The music comes from Carl Franklin's Music to Code By at http://mtcb.pwop.com and can also be found on the mobile app Music To Flow By that you can get at http://musictoflowby.com", + QuestionText = "What music is playing?" + }, + new + { + Id = 5, + AnswerText = "Jeff typically writes code in C# with ASP.NET Core. You will also find him regularly writing JavaScript, TypeScript, CSS, and HTML.", + QuestionText = "What language is Jeff coding in?" + }, + new + { + Id = 6, + AnswerText = "Powershell with the posh-git plugin from dahlbyk / posh - git gives extra insight into Git repositories. Information on the prompt and tab-completion of git commands are just some of the cool features of posh-git.", + QuestionText = "Why does Jeff use Powershell to work with Git?" + }, + new + { + Id = 7, + AnswerText = "No one knows the real answer to this question, because his wife keeps discarding a different one each month and doesn't tell him. Just ask about his Philly.NET hat...", + QuestionText = "How many hats does Jeff own?" + }, + new + { + Id = 8, + AnswerText = "Jeff blogs at: www.jeffreyfritz.com", + QuestionText = "Where can I find Jeff's blog?" + }, + new + { + Id = 9, + AnswerText = "You can find the source code shared on stream at @csharpfritz", + QuestionText = "Where is Jeff's GitHub?" + }, + new + { + Id = 10, + AnswerText = "Jeff has videos on WintellectNow -http://wintellectnow.com", + QuestionText = "Where can I find training videos from Jeff?" + }, + new + { + Id = 11, + AnswerText = "All of Jeff's live stream videos are archived on YouTube at: youtube.com/csharpfritz", + QuestionText = "Where can I catch Fritz videos?" + }, + new + { + Id = 12, + AnswerText = "The workshop is at youtube.com/watch?v=--lYHxrsLsc", + QuestionText = "Where can I watch the 8 - hour ASP.NET Core workshop?" + }, + new + { + Id = 13, + AnswerText = "Jeff broadcasts with a Dell Precision Tower 3620 that has a Geforce GTX 1060 video card", + QuestionText = "What is the machine you are using?" + }, + new + { + Id = 14, + AnswerText = "Jeff streams regularly on Tuesday, Wednesday, Thursday, Friday, and Sunday at 10am ET.", + QuestionText = "When does Jeff stream?" + }, + new + { + Id = 15, + AnswerText = "The C# workshop is available as a playlist on YouTube at: youtube.com/watch?v=9ZmZuUSqQUM&list=PLVMqA0_8O85zIiU-T5h6rn8ortqEUNCeK", + QuestionText = "Where can I watch the C# workshop?" + }, + new + { + Id = 16, + AnswerText = "The architecture workshop is available as a playlist on YouTube at: youtube.com/watch?v=k8cZUW4MS3I&list=PLVMqA0_8O85x-aurj1KphxUeWTeTlYkGM", + QuestionText = "Where can I watch the Architecture workshop?" + }, + new + { + Id = 17, + AnswerText = "That is Carnac from the Code52 project: Code52 / carnac", + QuestionText = "What tool displays your keystrokes?" + }, + new + { + Id = 18, + AnswerText = "Madrinas is a sponsor of the Fritz and Friends channel.They make organic, free trade coffee that you can get from madrinascoffee.com.Use the coupon code 'FRITZ' for 20 % off your order.", + QuestionText = "What is Madrinas Coffee?" + }, + new + { + Id = 19, + AnswerText = "The Live Coders is a Twitch stream team that Jeff founded and comprised of folks that write code and answer questions about technology.You can learn more about them at livecoders.dev", + QuestionText = "Who are the Live Coders?" + }, + new + { + Id = 20, + AnswerText = "The first Live Coders Conference is April 9, 2020 starting at 9a ET / 6a PT / 1300 UTC.You can learn more at conf.livecoders.dev", + QuestionText = "When is the Live Coders Conference ?" + }, + new + { + Id = 21, + AnswerText = "Jeff uses a Vortex Race 3 with Cherry MX Blue switches, details on his blog at: jeffreyfritz.com/2018/07/mechanical-keyboards-i-just-got-one-and-why-you-need-one-too", + QuestionText = "What keyboard are you using?" + }); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.UnansweredQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AskedDateStamp") + .HasColumnType("timestamp without time zone"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.ToTable("UnansweredQuestions"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.HasOne("Fritz.Chatbot.QnA.Data.QnAPair", "MainQuestion") + .WithMany("AlternateQuestions") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.cs b/Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.cs new file mode 100644 index 00000000..0b039592 --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/20200405161220_SeedQuestions.cs @@ -0,0 +1,146 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Fritz.Chatbot.QnA.Migrations +{ + public partial class SeedQuestions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "QnAPairs", + columns: new[] { "Id", "AnswerText", "QuestionText" }, + values: new object[,] + { + { 1, "Jeff speaks English, with a Mid-Atlantic / Philadelphia accent.", "What language is Jeff speaking?" }, + { 19, "The Live Coders is a Twitch stream team that Jeff founded and comprised of folks that write code and answer questions about technology.You can learn more about them at livecoders.dev", "Who are the Live Coders?" }, + { 18, "Madrinas is a sponsor of the Fritz and Friends channel.They make organic, free trade coffee that you can get from madrinascoffee.com.Use the coupon code 'FRITZ' for 20 % off your order.", "What is Madrinas Coffee?" }, + { 17, "That is Carnac from the Code52 project: Code52 / carnac", "What tool displays your keystrokes?" }, + { 16, "The architecture workshop is available as a playlist on YouTube at: youtube.com/watch?v=k8cZUW4MS3I&list=PLVMqA0_8O85x-aurj1KphxUeWTeTlYkGM", "Where can I watch the Architecture workshop?" }, + { 15, "The C# workshop is available as a playlist on YouTube at: youtube.com/watch?v=9ZmZuUSqQUM&list=PLVMqA0_8O85zIiU-T5h6rn8ortqEUNCeK", "Where can I watch the C# workshop?" }, + { 14, "Jeff streams regularly on Tuesday, Wednesday, Thursday, Friday, and Sunday at 10am ET.", "When does Jeff stream?" }, + { 13, "Jeff broadcasts with a Dell Precision Tower 3620 that has a Geforce GTX 1060 video card", "What is the machine you are using?" }, + { 12, "The workshop is at youtube.com/watch?v=--lYHxrsLsc", "Where can I watch the 8 - hour ASP.NET Core workshop?" }, + { 20, "The first Live Coders Conference is April 9, 2020 starting at 9a ET / 6a PT / 1300 UTC.You can learn more at conf.livecoders.dev", "When is the Live Coders Conference ?" }, + { 11, "All of Jeff's live stream videos are archived on YouTube at: youtube.com/csharpfritz", "Where can I catch Fritz videos?" }, + { 9, "You can find the source code shared on stream at @csharpfritz", "Where is Jeff's GitHub?" }, + { 8, "Jeff blogs at: www.jeffreyfritz.com", "Where can I find Jeff's blog?" }, + { 7, "No one knows the real answer to this question, because his wife keeps discarding a different one each month and doesn't tell him. Just ask about his Philly.NET hat...", "How many hats does Jeff own?" }, + { 6, "Powershell with the posh-git plugin from dahlbyk / posh - git gives extra insight into Git repositories. Information on the prompt and tab-completion of git commands are just some of the cool features of posh-git.", "Why does Jeff use Powershell to work with Git?" }, + { 5, "Jeff typically writes code in C# with ASP.NET Core. You will also find him regularly writing JavaScript, TypeScript, CSS, and HTML.", "What language is Jeff coding in?" }, + { 4, "The music comes from Carl Franklin's Music to Code By at http://mtcb.pwop.com and can also be found on the mobile app Music To Flow By that you can get at http://musictoflowby.com", "What music is playing?" }, + { 3, "Jeff uses the Visual Studio Enterprise Edition, in preview mode. The preview is ALWAYS free to try: www.visualstudio.com / vs / preview /", "Which VS version do you use?" }, + { 2, "Jeff typically uses Visual Studio 2019 Enterprise edition available at visualstudio.com, and sometimes uses Visual Studio Code from code.visualstudio.com", "What editor does Jeff use?" }, + { 10, "Jeff has videos on WintellectNow -http://wintellectnow.com", "Where can I find training videos from Jeff?" }, + { 21, "Jeff uses a Vortex Race 3 with Cherry MX Blue switches, details on his blog at: jeffreyfritz.com/2018/07/mechanical-keyboards-i-just-got-one-and-why-you-need-one-too", "What keyboard are you using?" } + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 1); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 2); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 3); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 4); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 5); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 6); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 7); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 8); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 9); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 10); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 11); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 12); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 13); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 14); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 15); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 16); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 17); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 18); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 19); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 20); + + migrationBuilder.DeleteData( + table: "QnAPairs", + keyColumn: "Id", + keyValue: 21); + } + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs b/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs new file mode 100644 index 00000000..3141589e --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs @@ -0,0 +1,225 @@ +// +using System; +using Fritz.Chatbot.QnA.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Fritz.Chatbot.QnA.Migrations +{ + [DbContext(typeof(QnADbContext))] + partial class QnADbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("QuestionId") + .HasColumnType("integer"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.ToTable("AlternateQuestion"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.QnAPair", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AnswerText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.ToTable("QnAPairs"); + + b.HasData( + new + { + Id = 1, + AnswerText = "Jeff speaks English, with a Mid-Atlantic / Philadelphia accent.", + QuestionText = "What language is Jeff speaking?" + }, + new + { + Id = 2, + AnswerText = "Jeff typically uses Visual Studio 2019 Enterprise edition available at visualstudio.com, and sometimes uses Visual Studio Code from code.visualstudio.com", + QuestionText = "What editor does Jeff use?" + }, + new + { + Id = 3, + AnswerText = "Jeff uses the Visual Studio Enterprise Edition, in preview mode. The preview is ALWAYS free to try: www.visualstudio.com / vs / preview /", + QuestionText = "Which VS version do you use?" + }, + new + { + Id = 4, + AnswerText = "The music comes from Carl Franklin's Music to Code By at http://mtcb.pwop.com and can also be found on the mobile app Music To Flow By that you can get at http://musictoflowby.com", + QuestionText = "What music is playing?" + }, + new + { + Id = 5, + AnswerText = "Jeff typically writes code in C# with ASP.NET Core. You will also find him regularly writing JavaScript, TypeScript, CSS, and HTML.", + QuestionText = "What language is Jeff coding in?" + }, + new + { + Id = 6, + AnswerText = "Powershell with the posh-git plugin from dahlbyk / posh - git gives extra insight into Git repositories. Information on the prompt and tab-completion of git commands are just some of the cool features of posh-git.", + QuestionText = "Why does Jeff use Powershell to work with Git?" + }, + new + { + Id = 7, + AnswerText = "No one knows the real answer to this question, because his wife keeps discarding a different one each month and doesn't tell him. Just ask about his Philly.NET hat...", + QuestionText = "How many hats does Jeff own?" + }, + new + { + Id = 8, + AnswerText = "Jeff blogs at: www.jeffreyfritz.com", + QuestionText = "Where can I find Jeff's blog?" + }, + new + { + Id = 9, + AnswerText = "You can find the source code shared on stream at @csharpfritz", + QuestionText = "Where is Jeff's GitHub?" + }, + new + { + Id = 10, + AnswerText = "Jeff has videos on WintellectNow -http://wintellectnow.com", + QuestionText = "Where can I find training videos from Jeff?" + }, + new + { + Id = 11, + AnswerText = "All of Jeff's live stream videos are archived on YouTube at: youtube.com/csharpfritz", + QuestionText = "Where can I catch Fritz videos?" + }, + new + { + Id = 12, + AnswerText = "The workshop is at youtube.com/watch?v=--lYHxrsLsc", + QuestionText = "Where can I watch the 8 - hour ASP.NET Core workshop?" + }, + new + { + Id = 13, + AnswerText = "Jeff broadcasts with a Dell Precision Tower 3620 that has a Geforce GTX 1060 video card", + QuestionText = "What is the machine you are using?" + }, + new + { + Id = 14, + AnswerText = "Jeff streams regularly on Tuesday, Wednesday, Thursday, Friday, and Sunday at 10am ET.", + QuestionText = "When does Jeff stream?" + }, + new + { + Id = 15, + AnswerText = "The C# workshop is available as a playlist on YouTube at: youtube.com/watch?v=9ZmZuUSqQUM&list=PLVMqA0_8O85zIiU-T5h6rn8ortqEUNCeK", + QuestionText = "Where can I watch the C# workshop?" + }, + new + { + Id = 16, + AnswerText = "The architecture workshop is available as a playlist on YouTube at: youtube.com/watch?v=k8cZUW4MS3I&list=PLVMqA0_8O85x-aurj1KphxUeWTeTlYkGM", + QuestionText = "Where can I watch the Architecture workshop?" + }, + new + { + Id = 17, + AnswerText = "That is Carnac from the Code52 project: Code52 / carnac", + QuestionText = "What tool displays your keystrokes?" + }, + new + { + Id = 18, + AnswerText = "Madrinas is a sponsor of the Fritz and Friends channel.They make organic, free trade coffee that you can get from madrinascoffee.com.Use the coupon code 'FRITZ' for 20 % off your order.", + QuestionText = "What is Madrinas Coffee?" + }, + new + { + Id = 19, + AnswerText = "The Live Coders is a Twitch stream team that Jeff founded and comprised of folks that write code and answer questions about technology.You can learn more about them at livecoders.dev", + QuestionText = "Who are the Live Coders?" + }, + new + { + Id = 20, + AnswerText = "The first Live Coders Conference is April 9, 2020 starting at 9a ET / 6a PT / 1300 UTC.You can learn more at conf.livecoders.dev", + QuestionText = "When is the Live Coders Conference ?" + }, + new + { + Id = 21, + AnswerText = "Jeff uses a Vortex Race 3 with Cherry MX Blue switches, details on his blog at: jeffreyfritz.com/2018/07/mechanical-keyboards-i-just-got-one-and-why-you-need-one-too", + QuestionText = "What keyboard are you using?" + }); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.UnansweredQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AskedDateStamp") + .HasColumnType("timestamp without time zone"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.HasKey("Id"); + + b.ToTable("UnansweredQuestions"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.HasOne("Fritz.Chatbot.QnA.Data.QnAPair", "MainQuestion") + .WithMany("AlternateQuestions") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fritz.StreamTools/Fritz.StreamTools.csproj b/Fritz.StreamTools/Fritz.StreamTools.csproj index e3e0e080..b2f36c7e 100644 --- a/Fritz.StreamTools/Fritz.StreamTools.csproj +++ b/Fritz.StreamTools/Fritz.StreamTools.csproj @@ -24,7 +24,12 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/Fritz.StreamTools/StartupServices/ConfigureServices.cs b/Fritz.StreamTools/StartupServices/ConfigureServices.cs index cf63f5e5..0ac8ea16 100644 --- a/Fritz.StreamTools/StartupServices/ConfigureServices.cs +++ b/Fritz.StreamTools/StartupServices/ConfigureServices.cs @@ -11,6 +11,7 @@ using Fritz.StreamTools.TagHelpers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -46,8 +47,7 @@ public static void Execute(IServiceCollection services, IConfiguration configura services.AddSingleton(); - // Add the SentimentSink - //services.AddSingleton(); + services.AddQnAFeatures(); services.AddHostedService(); @@ -199,6 +199,15 @@ private static void RegisterTwitchPubSub(this IServiceCollection services) { } + private static void AddQnAFeatures(this IServiceCollection services) { + + services.AddDbContext(options => + { + options.UseNpgsql(_Configuration["FritzBot:QnA:ConnectionString"]); + }); + + } + private static bool IsTwitchEnabled { get { return string.IsNullOrEmpty(_Configuration["StreamServices:Twitch:ClientId"]); } } diff --git a/Fritz.StreamTools/appsettings.json b/Fritz.StreamTools/appsettings.json index 4304c9cf..28ad7ddf 100644 --- a/Fritz.StreamTools/appsettings.json +++ b/Fritz.StreamTools/appsettings.json @@ -212,7 +212,10 @@ "command": "youtube", "response": "Find the archive of videos from our channel at: https://youtube.com/csharpfritz" } - ] + ], + "QnA": { + "ConnectionString": "User ID=postgres;Password=password;Host=localhost;Port=5432;Database=postgres;Pooling=true;" + } }, "FollowerGoal": { "Caption": "Follower Goal", From 43554710fdba66de9c493d6d7372f7e734e7fc41 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Tue, 7 Apr 2020 12:32:11 -0400 Subject: [PATCH 3/7] Added a Proxy for the QnAMaker --- Fritz.Chatbot/QnA/QnAMaker/Add.cs | 11 +++++ .../QnA/QnAMaker/Alternatequestionclusters.cs | 9 ++++ Fritz.Chatbot/QnA/QnAMaker/Delete.cs | 9 ++++ Fritz.Chatbot/QnA/QnAMaker/DownloadPayload.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/File.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/Metadata.cs | 11 +++++ Fritz.Chatbot/QnA/QnAMaker/MetadataAdd.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/MetadataDelete.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/Promptstoadd.cs | 12 +++++ Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs | 49 +++++++++++++++++++ Fritz.Chatbot/QnA/QnAMaker/QnAToAdd.cs | 15 ++++++ Fritz.Chatbot/QnA/QnAMaker/QnAToAddContext.cs | 11 +++++ Fritz.Chatbot/QnA/QnAMaker/Qnadocument.cs | 14 ++++++ Fritz.Chatbot/QnA/QnAMaker/Qnalist.cs | 13 +++++ .../QnA/QnAMaker/QuestionChangeOperations.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/Response.cs | 15 ++++++ Fritz.Chatbot/QnA/QnAMaker/Update.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/UpdateContext.cs | 11 +++++ Fritz.Chatbot/QnA/QnAMaker/UpdateMetadata.cs | 10 ++++ Fritz.Chatbot/QnA/QnAMaker/UpdatePayload.cs | 11 +++++ Fritz.Chatbot/QnA/QnAMaker/UpdateQnAList.cs | 15 ++++++ .../StartupServices/ConfigureServices.cs | 11 +++++ Fritz.StreamTools/appsettings.json | 4 +- 23 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Add.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Alternatequestionclusters.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Delete.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/DownloadPayload.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/File.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Metadata.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/MetadataAdd.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/MetadataDelete.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Promptstoadd.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/QnAToAdd.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/QnAToAddContext.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Qnadocument.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Qnalist.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/QuestionChangeOperations.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Response.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/Update.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/UpdateContext.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/UpdateMetadata.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/UpdatePayload.cs create mode 100644 Fritz.Chatbot/QnA/QnAMaker/UpdateQnAList.cs diff --git a/Fritz.Chatbot/QnA/QnAMaker/Add.cs b/Fritz.Chatbot/QnA/QnAMaker/Add.cs new file mode 100644 index 00000000..7b5fb53c --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Add.cs @@ -0,0 +1,11 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Add + { + public Qnalist[] qnaList { get; set; } + public string[] urls { get; set; } + public File[] files { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Alternatequestionclusters.cs b/Fritz.Chatbot/QnA/QnAMaker/Alternatequestionclusters.cs new file mode 100644 index 00000000..0e7a519b --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Alternatequestionclusters.cs @@ -0,0 +1,9 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Alternatequestionclusters + { + public object[] delete { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Delete.cs b/Fritz.Chatbot/QnA/QnAMaker/Delete.cs new file mode 100644 index 00000000..45cb65bb --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Delete.cs @@ -0,0 +1,9 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Delete + { + public int[] ids { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/DownloadPayload.cs b/Fritz.Chatbot/QnA/QnAMaker/DownloadPayload.cs new file mode 100644 index 00000000..8067f3a1 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/DownloadPayload.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class DownloadPayload + { + public Qnadocument[] qnaDocuments { get; set; } + } + + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/File.cs b/Fritz.Chatbot/QnA/QnAMaker/File.cs new file mode 100644 index 00000000..5b3c85ba --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/File.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class File + { + public string fileName { get; set; } + public string fileUri { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Metadata.cs b/Fritz.Chatbot/QnA/QnAMaker/Metadata.cs new file mode 100644 index 00000000..a32a5580 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Metadata.cs @@ -0,0 +1,11 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Metadata + { + public string name { get; set; } + public string value { get; set; } + } + + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/MetadataAdd.cs b/Fritz.Chatbot/QnA/QnAMaker/MetadataAdd.cs new file mode 100644 index 00000000..00326b06 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/MetadataAdd.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class MetadataAdd + { + public string name { get; set; } + public string value { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/MetadataDelete.cs b/Fritz.Chatbot/QnA/QnAMaker/MetadataDelete.cs new file mode 100644 index 00000000..468d2186 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/MetadataDelete.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class MetadataDelete + { + public string name { get; set; } + public string value { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Promptstoadd.cs b/Fritz.Chatbot/QnA/QnAMaker/Promptstoadd.cs new file mode 100644 index 00000000..bb363dc0 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Promptstoadd.cs @@ -0,0 +1,12 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Promptstoadd + { + public string displayText { get; set; } + public int displayOrder { get; set; } + public QnAToAdd qna { get; set; } + public int qnaId { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs new file mode 100644 index 00000000..d3362705 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Fritz.Chatbot.QnA.QnAMaker +{ + + public class Proxy + { + private readonly HttpClient _Client; + + private readonly string _KnowledgeBaseId; + + + public Proxy(HttpClient client, IConfiguration configuration) + { + _Client = client; + this._KnowledgeBaseId = configuration["FritzBot:QnA:KnowledgeBaseId"]; + } + + public async Task Download() + { + + var response = await _Client.GetAsync($"/qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}/Prod/qna"); + response.EnsureSuccessStatusCode(); + + var jsonBody = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(jsonBody); + + } + + public async Task Update(UpdatePayload payload) { + + var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(payload)); + var response = await _Client.PatchAsync($"/qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", content); + response.EnsureSuccessStatusCode(); + + var jsonBody = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(jsonBody); + + } + + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/QnAToAdd.cs b/Fritz.Chatbot/QnA/QnAMaker/QnAToAdd.cs new file mode 100644 index 00000000..25b6f9cf --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/QnAToAdd.cs @@ -0,0 +1,15 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class QnAToAdd + { + public int id { get; set; } + public string answer { get; set; } + public string source { get; set; } + public string[] questions { get; set; } + public object[] metadata { get; set; } + public object[] alternateQuestionClusters { get; set; } + public QnAToAddContext context { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/QnAToAddContext.cs b/Fritz.Chatbot/QnA/QnAMaker/QnAToAddContext.cs new file mode 100644 index 00000000..146dc417 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/QnAToAddContext.cs @@ -0,0 +1,11 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class QnAToAddContext + { + + public bool isContextOnly { get; set; } + public object[] prompts { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Qnadocument.cs b/Fritz.Chatbot/QnA/QnAMaker/Qnadocument.cs new file mode 100644 index 00000000..9fc99a8d --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Qnadocument.cs @@ -0,0 +1,14 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Qnadocument + { + public int id { get; set; } + public string answer { get; set; } + public string source { get; set; } + public string[] questions { get; set; } + public Metadata[] metadata { get; set; } + } + + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Qnalist.cs b/Fritz.Chatbot/QnA/QnAMaker/Qnalist.cs new file mode 100644 index 00000000..4a1a0b28 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Qnalist.cs @@ -0,0 +1,13 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Qnalist + { + public int id { get; set; } + public string answer { get; set; } + public string source { get; set; } + public string[] questions { get; set; } + public object[] metadata { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/QuestionChangeOperations.cs b/Fritz.Chatbot/QnA/QnAMaker/QuestionChangeOperations.cs new file mode 100644 index 00000000..9efb2626 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/QuestionChangeOperations.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class QuestionChangeOperations + { + public object[] add { get; set; } + public object[] delete { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Response.cs b/Fritz.Chatbot/QnA/QnAMaker/Response.cs new file mode 100644 index 00000000..5e17f644 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Response.cs @@ -0,0 +1,15 @@ +using System; + +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Response + { + public string operationState { get; set; } + public DateTime createdTimestamp { get; set; } + public DateTime lastActionTimestamp { get; set; } + public string userId { get; set; } + public string operationId { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/Update.cs b/Fritz.Chatbot/QnA/QnAMaker/Update.cs new file mode 100644 index 00000000..8c95e8d1 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/Update.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class Update + { + public string name { get; set; } + public UpdateQnAList[] qnaList { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/UpdateContext.cs b/Fritz.Chatbot/QnA/QnAMaker/UpdateContext.cs new file mode 100644 index 00000000..64d3aa0e --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/UpdateContext.cs @@ -0,0 +1,11 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class UpdateContext + { + public bool isContextOnly { get; set; } + public Promptstoadd[] promptsToAdd { get; set; } + public int[] promptsToDelete { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/UpdateMetadata.cs b/Fritz.Chatbot/QnA/QnAMaker/UpdateMetadata.cs new file mode 100644 index 00000000..afed9d48 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/UpdateMetadata.cs @@ -0,0 +1,10 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class UpdateMetadata + { + public MetadataAdd[] add { get; set; } + public MetadataDelete[] delete { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/UpdatePayload.cs b/Fritz.Chatbot/QnA/QnAMaker/UpdatePayload.cs new file mode 100644 index 00000000..b03170a9 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/UpdatePayload.cs @@ -0,0 +1,11 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class UpdatePayload + { + public Add add { get; set; } + public Delete delete { get; set; } + public Update update { get; set; } + } + + +} diff --git a/Fritz.Chatbot/QnA/QnAMaker/UpdateQnAList.cs b/Fritz.Chatbot/QnA/QnAMaker/UpdateQnAList.cs new file mode 100644 index 00000000..917daf16 --- /dev/null +++ b/Fritz.Chatbot/QnA/QnAMaker/UpdateQnAList.cs @@ -0,0 +1,15 @@ +namespace Fritz.Chatbot.QnA.QnAMaker +{ + public class UpdateQnAList + { + public int id { get; set; } + public string answer { get; set; } + public string source { get; set; } + public QuestionChangeOperations questions { get; set; } + public UpdateMetadata metadata { get; set; } + public Alternatequestionclusters alternateQuestionClusters { get; set; } + public UpdateContext context { get; set; } + } + + +} diff --git a/Fritz.StreamTools/StartupServices/ConfigureServices.cs b/Fritz.StreamTools/StartupServices/ConfigureServices.cs index 0ac8ea16..643029ed 100644 --- a/Fritz.StreamTools/StartupServices/ConfigureServices.cs +++ b/Fritz.StreamTools/StartupServices/ConfigureServices.cs @@ -206,6 +206,17 @@ private static void AddQnAFeatures(this IServiceCollection services) { options.UseNpgsql(_Configuration["FritzBot:QnA:ConnectionString"]); }); + services.AddTransient(); + services.AddHttpClient(config => + { + + config.BaseAddress = new Uri(_Configuration["FritzBot:QnA:EndPoint"]); + config.DefaultRequestHeaders.Add("Authorization", $"Ocp-Apim-Subscription-Key {_Configuration["AzureServices:QnASubscriptionKey"]}"); + config.DefaultRequestHeaders.Add("Accept", "application/json"); + config.DefaultRequestHeaders.Add("Content-Type", "application/json"); + + }); + } private static bool IsTwitchEnabled { diff --git a/Fritz.StreamTools/appsettings.json b/Fritz.StreamTools/appsettings.json index 28ad7ddf..820cb87e 100644 --- a/Fritz.StreamTools/appsettings.json +++ b/Fritz.StreamTools/appsettings.json @@ -214,7 +214,9 @@ } ], "QnA": { - "ConnectionString": "User ID=postgres;Password=password;Host=localhost;Port=5432;Database=postgres;Pooling=true;" + "ConnectionString": "User ID=postgres;Password=password;Host=localhost;Port=5432;Database=postgres;Pooling=true;", + "KnowledgeBaseId": "34d70910-0c78-43e4-bcdb-21ed7f606b5e", + "Endpoint": "https://fritzbotqna.azurewebsites.net/qnamaker" } }, "FollowerGoal": { From d3c83f44ec8b1f1f2c865e5f45ad70cd0d7d3326 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Wed, 8 Apr 2020 12:15:03 -0400 Subject: [PATCH 4/7] Moved Azure QnA logic completely into the Proxy --- Fritz.Chatbot/Commands/AzureQnACommand.cs | 89 +++++-------------- Fritz.Chatbot/Commands/QnAMakerResult.cs | 17 +--- Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs | 62 +++++++++++-- .../StartupServices/ConfigureServices.cs | 13 ++- Fritz.StreamTools/appsettings.json | 3 +- 5 files changed, 89 insertions(+), 95 deletions(-) diff --git a/Fritz.Chatbot/Commands/AzureQnACommand.cs b/Fritz.Chatbot/Commands/AzureQnACommand.cs index 9962c019..6293fcea 100644 --- a/Fritz.Chatbot/Commands/AzureQnACommand.cs +++ b/Fritz.Chatbot/Commands/AzureQnACommand.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Fritz.Chatbot.Helpers; +using Fritz.Chatbot.QnA.QnAMaker; using Fritz.ChatBot.Helpers; using Fritz.StreamLib.Core; using Microsoft.Extensions.Configuration; @@ -16,21 +17,19 @@ namespace Fritz.Chatbot.Commands public class AzureQnACommand : IExtendedCommand { - public string AzureKey => _configuration["AzureServices:QnASubscriptionKey"]; - public string KnowledgebaseId => _configuration["FritzBot:QnAKnowledgeBaseId"]; - public string Name => "AzureQnA"; public string Description => "Answer questions using Azure Cognitive Services and Jeff's FAQ on the LiveStream wiki"; public int Order => 1; public bool Final => true; public TimeSpan? Cooldown => null; - private readonly IConfiguration _configuration; + public Proxy Proxy { get; } + private readonly ILogger _logger; - public AzureQnACommand(IConfiguration configuration, ILogger logger) + public AzureQnACommand(QnA.QnAMaker.Proxy proxy, ILogger logger) { - _configuration = configuration; + Proxy = proxy; _logger = logger; } @@ -43,8 +42,6 @@ public bool CanExecute(string userName, string fullCommandText) public async Task Execute(IChatService chatService, string userName, string fullCommandText) { - // Exit now if we don't know how to connect to Azure - if (string.IsNullOrEmpty(AzureKey)) return; _logger.LogInformation($"Handling question: \"{fullCommandText}\" from {userName}"); @@ -53,52 +50,27 @@ public async Task Execute(IChatService chatService, string userName, string full public async Task Query(IChatService chatService, string userName, string query) { - var responseString = string.Empty; - // query = WebUtility.UrlEncode(query); - - //Build the URI - var qnamakerUriBase = new Uri("https://fritzbotqna.azurewebsites.net/qnamaker"); - var builder = new UriBuilder($"{qnamakerUriBase}/knowledgebases/{KnowledgebaseId}/generateAnswer"); - - //Add the question as part of the body - var postBody = $"{{\"question\": \"{query}\"}}"; - - //Send the POST request - using(var client = new WebClient()) + QnAMakerResult response = null; + try { - //Set the encoding to UTF8 - client.Encoding = System.Text.Encoding.UTF8; - - //Add the subscription key header - client.Headers.Add("Authorization", $"EndpointKey {AzureKey}"); - client.Headers.Add("Content-Type", "application/json"); - // client.Headers.Add("Host", "https://fritzbotqna.azurewebsites.net/qnamaker"); - - try - { - responseString = await client.UploadStringTaskAsync(builder.Uri, postBody).OrTimeout(); - } - catch (TimeoutException) - { - _logger.LogWarning($"Azure Services did not respond in time to question '{query}'"); - chatService.SendMessageAsync($"Unable to answer the question '{query}' at this time").Forget(); - return; - } - catch(Exception ex) - { - _logger.LogError($">>> Error while communicating with QnA service: {ex.ToString()}"); - return; - } + response = await Proxy.Query(query).OrTimeout(); + } + catch (TimeoutException) + { + _logger.LogWarning($"Azure Services did not respond in time to question '{query}'"); + chatService.SendMessageAsync($"Unable to answer the question '{query}' at this time").Forget(); + return; + } + catch (Exception ex) + { + _logger.LogError($">>> Error while communicating with QnA service: {ex.ToString()}"); + return; } - QnAMakerResult response; try { - response = JsonConvert.DeserializeObject(responseString); - var thisAnswer = response.Answers.OrderByDescending(a => a.Score).FirstOrDefault(); - thisAnswer.Answer = WebUtility.HtmlDecode(thisAnswer.Answer).HandleMarkdownLinks(); @@ -117,32 +89,11 @@ public async Task Query(IChatService chatService, string userName, string query) } } - catch (Exception ex) when(_logger.LogAndSwallow("asking knowledgebase", ex)) + catch (Exception ex) when (_logger.LogAndSwallow("asking knowledgebase", ex)) { } } - public async Task Retrain() - { - var qnamakerUriBase = new Uri("https://westus.api.cognitive.microsoft.com/qnamaker/v2.0"); - var builder = new UriBuilder($"{qnamakerUriBase}/knowledgebases/{KnowledgebaseId}"); - - //Send the POST request - using(var client = new WebClient()) - { - //Set the encoding to UTF8 - client.Encoding = System.Text.Encoding.UTF8; - - //Add the subscription key header - client.Headers.Add("Ocp-Apim-Subscription-Key", AzureKey); - client.Headers.Add("Content-Type", "application/json"); - - //Add the question as part of the body - var postBody = $"{{\"add\": {{\"urls\": [\"https://github.com/csharpfritz/Fritz.LiveStream/wiki/Frequently-Asked-Questions\"]}} }}"; - - var responseString = await client.UploadStringTaskAsync(builder.Uri, "PATCH", postBody); - } - } } } diff --git a/Fritz.Chatbot/Commands/QnAMakerResult.cs b/Fritz.Chatbot/Commands/QnAMakerResult.cs index bbddf54d..90c7e887 100644 --- a/Fritz.Chatbot/Commands/QnAMakerResult.cs +++ b/Fritz.Chatbot/Commands/QnAMakerResult.cs @@ -3,22 +3,11 @@ namespace Fritz.Chatbot.Commands { - internal class QnAMakerResult + public class QnAMakerResult { - - // public string Answer { get; set; } - - // /// - // /// The score in range [0, 100] corresponding to the top answer found in the QnA Service. - // /// - // [JsonProperty(PropertyName = "score")] - // public double Score { get; set; } - - [JsonProperty("answers")] - public QnAMakerAnswer[] Answers { get; set; } - - + [JsonProperty("answers")] + public QnAMakerAnswer[] Answers { get; set; } } diff --git a/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs index d3362705..9febd503 100644 --- a/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs +++ b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.Configuration; +using Fritz.Chatbot.Commands; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Net.Http; using System.Text; @@ -13,33 +16,76 @@ public class Proxy private readonly HttpClient _Client; private readonly string _KnowledgeBaseId; + private readonly bool _MissingAzureKey; + private readonly IHttpClientFactory _HttpClientFactory; + public const string MaintenanceClientName = "QnAMaintainanace"; + public const string QuestionClientName = "QnAQuestion"; - public Proxy(HttpClient client, IConfiguration configuration) + public Proxy(IHttpClientFactory httpClientFactory, IConfiguration configuration) { - _Client = client; this._KnowledgeBaseId = configuration["FritzBot:QnA:KnowledgeBaseId"]; + _MissingAzureKey = String.IsNullOrEmpty(configuration["AzureServices:QnASubscriptionKey"]); + _HttpClientFactory = httpClientFactory; } public async Task Download() { - var response = await _Client.GetAsync($"/qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}/Prod/qna"); + // Exit now if we don't know how to connect to Azure + if (_MissingAzureKey) return null; + + var client = _HttpClientFactory.CreateClient(MaintenanceClientName); + var response = await client.GetAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}/Prod/qna"); response.EnsureSuccessStatusCode(); var jsonBody = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(jsonBody); + return System.Text.Json.JsonSerializer.Deserialize(jsonBody); + + } + + public async Task Query(string question) { + + // Exit now if we don't know how to connect to Azure + if (_MissingAzureKey) return null; + + var payload = new StringContent($"{{\"question\": \"{question}\"}}", Encoding.UTF8, "application/json"); + + var client = _HttpClientFactory.CreateClient(QuestionClientName); + var response = await client.PostAsync($"knowledgebases/{_KnowledgeBaseId}/generateAnswer", payload); + + response.EnsureSuccessStatusCode(); + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); } public async Task Update(UpdatePayload payload) { - var content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(payload)); - var response = await _Client.PatchAsync($"/qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", content); + // Exit now if we don't know how to connect to Azure + if (_MissingAzureKey) return null; + + var client = _HttpClientFactory.CreateClient(MaintenanceClientName); + var content = new ByteArrayContent(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(payload)); + var response = await _Client.PatchAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", content); response.EnsureSuccessStatusCode(); var jsonBody = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(jsonBody); + return System.Text.Json.JsonSerializer.Deserialize(jsonBody); + + } + + /// + /// Publish the Knowledgebase on QnAMaker so that we can start interacting with the updated data + /// + /// + public async Task Publish() { + + // Exit now if we don't know how to connect to Azure + if (_MissingAzureKey) return; + + var client = _HttpClientFactory.CreateClient(MaintenanceClientName); + var response = await _Client.PostAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", new StringContent("")); + response.EnsureSuccessStatusCode(); } diff --git a/Fritz.StreamTools/StartupServices/ConfigureServices.cs b/Fritz.StreamTools/StartupServices/ConfigureServices.cs index 643029ed..fb1943fa 100644 --- a/Fritz.StreamTools/StartupServices/ConfigureServices.cs +++ b/Fritz.StreamTools/StartupServices/ConfigureServices.cs @@ -207,13 +207,20 @@ private static void AddQnAFeatures(this IServiceCollection services) { }); services.AddTransient(); - services.AddHttpClient(config => + services.AddHttpClient(Fritz.Chatbot.QnA.QnAMaker.Proxy.MaintenanceClientName, config => { - config.BaseAddress = new Uri(_Configuration["FritzBot:QnA:EndPoint"]); + config.BaseAddress = new Uri(_Configuration["FritzBot:QnA:MaintenanceEndpoint"]); config.DefaultRequestHeaders.Add("Authorization", $"Ocp-Apim-Subscription-Key {_Configuration["AzureServices:QnASubscriptionKey"]}"); config.DefaultRequestHeaders.Add("Accept", "application/json"); - config.DefaultRequestHeaders.Add("Content-Type", "application/json"); + + }); + services.AddHttpClient(Fritz.Chatbot.QnA.QnAMaker.Proxy.QuestionClientName, config => + { + + config.BaseAddress = new Uri(_Configuration["FritzBot:QnA:RuntimeEndpoint"]); + config.DefaultRequestHeaders.Add("Authorization", $"Endpointkey {_Configuration["AzureServices:QnASubscriptionKey"]}"); + config.DefaultRequestHeaders.Add("Accept", "application/json"); }); diff --git a/Fritz.StreamTools/appsettings.json b/Fritz.StreamTools/appsettings.json index 820cb87e..c970975b 100644 --- a/Fritz.StreamTools/appsettings.json +++ b/Fritz.StreamTools/appsettings.json @@ -216,7 +216,8 @@ "QnA": { "ConnectionString": "User ID=postgres;Password=password;Host=localhost;Port=5432;Database=postgres;Pooling=true;", "KnowledgeBaseId": "34d70910-0c78-43e4-bcdb-21ed7f606b5e", - "Endpoint": "https://fritzbotqna.azurewebsites.net/qnamaker" + "RuntimeEndpoint": "https://fritzbotqna.azurewebsites.net/qnamaker/", + "MaintenanceEndpoint": "https://westus.api.cognitive.microsoft.com/qnamaker/v4.0/" } }, "FollowerGoal": { From 9cee241157b86809b7626b716cd18ad9d3db9393 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Fri, 10 Apr 2020 12:21:54 -0400 Subject: [PATCH 5/7] Started work on the question cache service for followup question interactions --- Fritz.Chatbot/Commands/AzureQnACommand.cs | 32 +++++++++++-- Fritz.Chatbot/QnA/QuestionCacheService.cs | 33 +++++++++++++ .../StartupServices/ConfigureServices.cs | 3 ++ Test/Chatbot/QuestionsCanBeAnswered.cs | 46 +++++++++++++++++++ 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 Fritz.Chatbot/QnA/QuestionCacheService.cs create mode 100644 Test/Chatbot/QuestionsCanBeAnswered.cs diff --git a/Fritz.Chatbot/Commands/AzureQnACommand.cs b/Fritz.Chatbot/Commands/AzureQnACommand.cs index 6293fcea..a2b89b07 100644 --- a/Fritz.Chatbot/Commands/AzureQnACommand.cs +++ b/Fritz.Chatbot/Commands/AzureQnACommand.cs @@ -3,8 +3,10 @@ using System.Linq; using System.Net; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Fritz.Chatbot.Helpers; +using Fritz.Chatbot.QnA; using Fritz.Chatbot.QnA.QnAMaker; using Fritz.ChatBot.Helpers; using Fritz.StreamLib.Core; @@ -17,6 +19,7 @@ namespace Fritz.Chatbot.Commands public class AzureQnACommand : IExtendedCommand { + public string Name => "AzureQnA"; public string Description => "Answer questions using Azure Cognitive Services and Jeff's FAQ on the LiveStream wiki"; public int Order => 1; @@ -26,17 +29,36 @@ public class AzureQnACommand : IExtendedCommand public Proxy Proxy { get; } private readonly ILogger _logger; + private readonly QuestionCacheService _QuestionCacheService; + private static readonly Regex _UserNameRegEx = new Regex(@"(@\w+)"); - public AzureQnACommand(QnA.QnAMaker.Proxy proxy, ILogger logger) + public AzureQnACommand(QnA.QnAMaker.Proxy proxy, ILogger logger, QuestionCacheService questionCacheService) { Proxy = proxy; _logger = logger; + _QuestionCacheService = questionCacheService; } public bool CanExecute(string userName, string fullCommandText) { - return fullCommandText.Length >= 10 && fullCommandText.EndsWith("?"); + var allowedQuestionTargets = new[] { "@csharpfritz", "@thefritzbot" }; + var firstTests = fullCommandText.Length >= 10 && fullCommandText.EndsWith("?"); + if (!firstTests) return false; + + var matches = _UserNameRegEx.Matches(fullCommandText); + if (matches.Count == 0) return true; + + for (var i=0; i matches[i].Value.Equals(s, StringComparison.InvariantCultureIgnoreCase))) { + return true; + } + + } + + return false; } @@ -76,11 +98,13 @@ public async Task Query(IChatService chatService, string userName, string query) if (thisAnswer.Score > 50) { - await chatService.SendMessageAsync(thisAnswer.Answer); + _QuestionCacheService.Add(userName, query, thisAnswer.Id); + await chatService.SendMessageAsync($"@{userName}, I know the answer to your question ({thisAnswer.Id}): {thisAnswer.Answer}"); } else if (thisAnswer.Score > 30) { - await chatService.SendMessageAsync("I'm not certain, but perhaps this will help: " + thisAnswer.Answer + $@"({thisAnswer.Score.ToString("0.0")}% certainty)"); + _QuestionCacheService.Add(userName, query, thisAnswer.Id); + await chatService.SendMessageAsync($"I'm not certain, @{userName}, but perhaps this will help ({thisAnswer.Id}): " + thisAnswer.Answer + $@"({thisAnswer.Score.ToString("0.0")}% certainty)"); } else diff --git a/Fritz.Chatbot/QnA/QuestionCacheService.cs b/Fritz.Chatbot/QnA/QuestionCacheService.cs new file mode 100644 index 00000000..3d65d625 --- /dev/null +++ b/Fritz.Chatbot/QnA/QuestionCacheService.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Fritz.Chatbot.QnA +{ + public class QuestionCacheService + { + + private MemoryCache _Cache = new MemoryCache(new MemoryCacheOptions() + { + ExpirationScanFrequency = TimeSpan.FromSeconds(5), + + }); + + public void Add(string userName, string questionText, long answerId) + { + + _Cache.Set(userName, (questionText, answerId), TimeSpan.FromSeconds(30)); + + } + + public (string questionText,long answerId) GetQuestionForUser(string userName) + { + + return ((string,long))_Cache.Get(userName); + + } + + + } +} diff --git a/Fritz.StreamTools/StartupServices/ConfigureServices.cs b/Fritz.StreamTools/StartupServices/ConfigureServices.cs index fb1943fa..3e43a330 100644 --- a/Fritz.StreamTools/StartupServices/ConfigureServices.cs +++ b/Fritz.StreamTools/StartupServices/ConfigureServices.cs @@ -3,6 +3,7 @@ using System.Linq; using Fritz.Chatbot; using Fritz.Chatbot.Commands; +using Fritz.Chatbot.QnA; using Fritz.StreamLib.Core; using Fritz.StreamTools.Hubs; using Fritz.StreamTools.Interfaces; @@ -224,6 +225,8 @@ private static void AddQnAFeatures(this IServiceCollection services) { }); + services.AddSingleton(); + } private static bool IsTwitchEnabled { diff --git a/Test/Chatbot/QuestionsCanBeAnswered.cs b/Test/Chatbot/QuestionsCanBeAnswered.cs new file mode 100644 index 00000000..5424b6b5 --- /dev/null +++ b/Test/Chatbot/QuestionsCanBeAnswered.cs @@ -0,0 +1,46 @@ +using Fritz.Chatbot.Commands; +using Octokit; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +using Xunit.Sdk; + +namespace Test.Chatbot +{ + public class QuestionsCanBeAnswered + { + + public const string UserName = "TestUser"; + + [Theory] + [InlineData("What is this music?")] + [InlineData("Hey @csharpfritz, What is this music?")] + [InlineData("Hey @thefritzbot, What is this music?")] + public void ShouldBeAnswered(string test) + { + + var sut = new AzureQnACommand(null, null, null); + + var result = sut.CanExecute(UserName, test); + Assert.True(result); + + } + + [Theory] + [InlineData("I like turtles")] + [InlineData("Wassup?")] + [InlineData("Hey @somebodyelse, did you see the game this weekend?")] + [InlineData("Hey @csharpfritzfoobar, did you see the game this weekend?")] + public void ShouldNotBeAnswered(string test) + { + + var sut = new AzureQnACommand(null, null, null); + + var result = sut.CanExecute(UserName, test); + Assert.False(result); + + } + + } +} From b6a7a8404212dbda8a954bab5a3f9f6baee2ef73 Mon Sep 17 00:00:00 2001 From: Kahbazi Date: Sun, 19 Apr 2020 18:56:11 +0430 Subject: [PATCH 6/7] Use client from HttpClientFactory (#389) --- Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs index 9febd503..bd84ea9d 100644 --- a/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs +++ b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs @@ -13,7 +13,6 @@ namespace Fritz.Chatbot.QnA.QnAMaker public class Proxy { - private readonly HttpClient _Client; private readonly string _KnowledgeBaseId; private readonly bool _MissingAzureKey; @@ -66,7 +65,7 @@ public async Task Update(UpdatePayload payload) { var client = _HttpClientFactory.CreateClient(MaintenanceClientName); var content = new ByteArrayContent(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(payload)); - var response = await _Client.PatchAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", content); + var response = await client.PatchAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", content); response.EnsureSuccessStatusCode(); var jsonBody = await response.Content.ReadAsStringAsync(); @@ -84,7 +83,7 @@ public async Task Publish() { if (_MissingAzureKey) return; var client = _HttpClientFactory.CreateClient(MaintenanceClientName); - var response = await _Client.PostAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", new StringContent("")); + var response = await client.PostAsync($"qnamaker/v4.0/knowledgebases/{_KnowledgeBaseId}", new StringContent("")); response.EnsureSuccessStatusCode(); } From 059f9921930ef03860d760fcab6861446ead5376 Mon Sep 17 00:00:00 2001 From: "Jeffrey T. Fritz" Date: Sun, 19 Apr 2020 12:16:16 -0400 Subject: [PATCH 7/7] Started work on moderators saving questions --- .gitignore | 2 + Fritz.Chatbot/Commands/AzureQnACommand.cs | 23 +- .../Commands/AzureQnACreateCommand.cs | 40 +++ Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs | 8 + ...200419150731_Add wrong answers.Designer.cs | 237 ++++++++++++++++++ .../20200419150731_Add wrong answers.cs | 43 ++++ .../Migrations/QnADbContextModelSnapshot.cs | 10 + Fritz.Chatbot/QnA/QuestionCacheService.cs | 7 + Test/Chatbot/QuestionsCanBeAnswered.cs | 6 +- Test/Startup/ConfigureServicesTests.cs | 17 ++ global.json | 2 +- 11 files changed, 391 insertions(+), 4 deletions(-) create mode 100644 Fritz.Chatbot/Commands/AzureQnACreateCommand.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.Designer.cs create mode 100644 Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.cs diff --git a/.gitignore b/.gitignore index c9bfc73a..7a7e82b6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ package-lock.json *.user *.userosscache *.sln.docstates +serviceDependencies.*.json +serviceDependencies.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Fritz.Chatbot/Commands/AzureQnACommand.cs b/Fritz.Chatbot/Commands/AzureQnACommand.cs index a2b89b07..c013cfeb 100644 --- a/Fritz.Chatbot/Commands/AzureQnACommand.cs +++ b/Fritz.Chatbot/Commands/AzureQnACommand.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http.Headers; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Fritz.Chatbot.Helpers; using Fritz.Chatbot.QnA; +using Fritz.Chatbot.QnA.Data; using Fritz.Chatbot.QnA.QnAMaker; using Fritz.ChatBot.Helpers; using Fritz.StreamLib.Core; @@ -30,13 +32,15 @@ public class AzureQnACommand : IExtendedCommand private readonly ILogger _logger; private readonly QuestionCacheService _QuestionCacheService; + private readonly QnADbContext _Context; private static readonly Regex _UserNameRegEx = new Regex(@"(@\w+)"); - public AzureQnACommand(QnA.QnAMaker.Proxy proxy, ILogger logger, QuestionCacheService questionCacheService) + public AzureQnACommand(QnA.QnAMaker.Proxy proxy, ILogger logger, QuestionCacheService questionCacheService, QnADbContext context) { Proxy = proxy; _logger = logger; _QuestionCacheService = questionCacheService; + _Context = context; } public bool CanExecute(string userName, string fullCommandText) @@ -98,17 +102,20 @@ public async Task Query(IChatService chatService, string userName, string query) if (thisAnswer.Score > 50) { + await LogInaccurateAnswer(query, thisAnswer.Answer, (decimal)thisAnswer.Score); _QuestionCacheService.Add(userName, query, thisAnswer.Id); await chatService.SendMessageAsync($"@{userName}, I know the answer to your question ({thisAnswer.Id}): {thisAnswer.Answer}"); } else if (thisAnswer.Score > 30) { + await LogInaccurateAnswer(query, thisAnswer.Answer, (decimal)thisAnswer.Score); _QuestionCacheService.Add(userName, query, thisAnswer.Id); await chatService.SendMessageAsync($"I'm not certain, @{userName}, but perhaps this will help ({thisAnswer.Id}): " + thisAnswer.Answer + $@"({thisAnswer.Score.ToString("0.0")}% certainty)"); } else { + await LogInaccurateAnswer(query); _logger.LogInformation($"Unable to find suitable answer to {userName}'s question: {query}"); } @@ -119,5 +126,19 @@ public async Task Query(IChatService chatService, string userName, string query) } } + private async Task LogInaccurateAnswer(string questionText, string? answer = null, decimal? answerPct = null) { + + _Context.UnansweredQuestions.Add(new UnansweredQuestion + { + QuestionText = questionText, + AnswerTextProvided = answer, + AnswerPct = answerPct, + AskedDateStamp = DateTime.UtcNow + }); + + await _Context.SaveChangesAsync(); + + } + } } diff --git a/Fritz.Chatbot/Commands/AzureQnACreateCommand.cs b/Fritz.Chatbot/Commands/AzureQnACreateCommand.cs new file mode 100644 index 00000000..50af2e22 --- /dev/null +++ b/Fritz.Chatbot/Commands/AzureQnACreateCommand.cs @@ -0,0 +1,40 @@ +using Fritz.StreamLib.Core; +using Fritz.Chatbot.QnA.Data; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Fritz.Chatbot.QnA; + +namespace Fritz.Chatbot.Commands +{ + public class AzureQnACreateCommand : IBasicCommand2 + { + public string Trigger { get; } = "q"; + public string Description { get; } = "Moderators can add new questions and answers to the stream knowledgebase"; + public TimeSpan? Cooldown { get; } = TimeSpan.FromSeconds(30); + private readonly QnADbContext _Context; + private readonly QuestionCacheService _CacheService; + + public AzureQnACreateCommand(QnADbContext context, QuestionCacheService cacheService) { + + _Context = context; + _CacheService = cacheService; + } + + public Task Execute(IChatService chatService, string userName, bool isModerator, bool isVip, bool isBroadcaster, ReadOnlyMemory rhs) + { + if (!(isModerator || isBroadcaster)) return Task.CompletedTask; + + _CacheService.AddQuestionForModerator(userName, $"!q {rhs}"); + + return Task.CompletedTask; + + } + + public Task Execute(IChatService chatService, string userName, ReadOnlyMemory rhs) + { + throw new NotImplementedException(); + } + } +} diff --git a/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs b/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs index a06896d2..d2b35ee6 100644 --- a/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs +++ b/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs @@ -15,5 +15,13 @@ public class UnansweredQuestion [MaxLength(1000)] public string QuestionText { get; set; } + public decimal? AnswerPct { get; set; } + + [MaxLength(1000)] + public string? AnswerTextProvided { get; set; } + + [Required] + public DateTime ReviewDate { get; set; } = new DateTime(2079, 6, 1); + } } diff --git a/Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.Designer.cs b/Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.Designer.cs new file mode 100644 index 00000000..4c7ac89a --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.Designer.cs @@ -0,0 +1,237 @@ +// +using System; +using Fritz.Chatbot.QnA.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Fritz.Chatbot.QnA.Migrations +{ + [DbContext(typeof(QnADbContext))] + [Migration("20200419150731_Add wrong answers")] + partial class Addwronganswers + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("QuestionId") + .HasColumnType("integer"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.HasIndex("QuestionId"); + + b.ToTable("AlternateQuestion"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.QnAPair", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AnswerText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(280)") + .HasMaxLength(280); + + b.HasKey("Id"); + + b.ToTable("QnAPairs"); + + b.HasData( + new + { + Id = 1, + AnswerText = "Jeff speaks English, with a Mid-Atlantic / Philadelphia accent.", + QuestionText = "What language is Jeff speaking?" + }, + new + { + Id = 2, + AnswerText = "Jeff typically uses Visual Studio 2019 Enterprise edition available at visualstudio.com, and sometimes uses Visual Studio Code from code.visualstudio.com", + QuestionText = "What editor does Jeff use?" + }, + new + { + Id = 3, + AnswerText = "Jeff uses the Visual Studio Enterprise Edition, in preview mode. The preview is ALWAYS free to try: www.visualstudio.com / vs / preview /", + QuestionText = "Which VS version do you use?" + }, + new + { + Id = 4, + AnswerText = "The music comes from Carl Franklin's Music to Code By at http://mtcb.pwop.com and can also be found on the mobile app Music To Flow By that you can get at http://musictoflowby.com", + QuestionText = "What music is playing?" + }, + new + { + Id = 5, + AnswerText = "Jeff typically writes code in C# with ASP.NET Core. You will also find him regularly writing JavaScript, TypeScript, CSS, and HTML.", + QuestionText = "What language is Jeff coding in?" + }, + new + { + Id = 6, + AnswerText = "Powershell with the posh-git plugin from dahlbyk / posh - git gives extra insight into Git repositories. Information on the prompt and tab-completion of git commands are just some of the cool features of posh-git.", + QuestionText = "Why does Jeff use Powershell to work with Git?" + }, + new + { + Id = 7, + AnswerText = "No one knows the real answer to this question, because his wife keeps discarding a different one each month and doesn't tell him. Just ask about his Philly.NET hat...", + QuestionText = "How many hats does Jeff own?" + }, + new + { + Id = 8, + AnswerText = "Jeff blogs at: www.jeffreyfritz.com", + QuestionText = "Where can I find Jeff's blog?" + }, + new + { + Id = 9, + AnswerText = "You can find the source code shared on stream at @csharpfritz", + QuestionText = "Where is Jeff's GitHub?" + }, + new + { + Id = 10, + AnswerText = "Jeff has videos on WintellectNow -http://wintellectnow.com", + QuestionText = "Where can I find training videos from Jeff?" + }, + new + { + Id = 11, + AnswerText = "All of Jeff's live stream videos are archived on YouTube at: youtube.com/csharpfritz", + QuestionText = "Where can I catch Fritz videos?" + }, + new + { + Id = 12, + AnswerText = "The workshop is at youtube.com/watch?v=--lYHxrsLsc", + QuestionText = "Where can I watch the 8 - hour ASP.NET Core workshop?" + }, + new + { + Id = 13, + AnswerText = "Jeff broadcasts with a Dell Precision Tower 3620 that has a Geforce GTX 1060 video card", + QuestionText = "What is the machine you are using?" + }, + new + { + Id = 14, + AnswerText = "Jeff streams regularly on Tuesday, Wednesday, Thursday, Friday, and Sunday at 10am ET.", + QuestionText = "When does Jeff stream?" + }, + new + { + Id = 15, + AnswerText = "The C# workshop is available as a playlist on YouTube at: youtube.com/watch?v=9ZmZuUSqQUM&list=PLVMqA0_8O85zIiU-T5h6rn8ortqEUNCeK", + QuestionText = "Where can I watch the C# workshop?" + }, + new + { + Id = 16, + AnswerText = "The architecture workshop is available as a playlist on YouTube at: youtube.com/watch?v=k8cZUW4MS3I&list=PLVMqA0_8O85x-aurj1KphxUeWTeTlYkGM", + QuestionText = "Where can I watch the Architecture workshop?" + }, + new + { + Id = 17, + AnswerText = "That is Carnac from the Code52 project: Code52 / carnac", + QuestionText = "What tool displays your keystrokes?" + }, + new + { + Id = 18, + AnswerText = "Madrinas is a sponsor of the Fritz and Friends channel.They make organic, free trade coffee that you can get from madrinascoffee.com.Use the coupon code 'FRITZ' for 20 % off your order.", + QuestionText = "What is Madrinas Coffee?" + }, + new + { + Id = 19, + AnswerText = "The Live Coders is a Twitch stream team that Jeff founded and comprised of folks that write code and answer questions about technology.You can learn more about them at livecoders.dev", + QuestionText = "Who are the Live Coders?" + }, + new + { + Id = 20, + AnswerText = "The first Live Coders Conference is April 9, 2020 starting at 9a ET / 6a PT / 1300 UTC.You can learn more at conf.livecoders.dev", + QuestionText = "When is the Live Coders Conference ?" + }, + new + { + Id = 21, + AnswerText = "Jeff uses a Vortex Race 3 with Cherry MX Blue switches, details on his blog at: jeffreyfritz.com/2018/07/mechanical-keyboards-i-just-got-one-and-why-you-need-one-too", + QuestionText = "What keyboard are you using?" + }); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.UnansweredQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AnswerPct") + .HasColumnType("numeric"); + + b.Property("AnswerTextProvided") + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.Property("AskedDateStamp") + .HasColumnType("timestamp without time zone"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + + b.Property("ReviewDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.ToTable("UnansweredQuestions"); + }); + + modelBuilder.Entity("Fritz.Chatbot.QnA.Data.AlternateQuestion", b => + { + b.HasOne("Fritz.Chatbot.QnA.Data.QnAPair", "MainQuestion") + .WithMany("AlternateQuestions") + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.cs b/Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.cs new file mode 100644 index 00000000..028d1330 --- /dev/null +++ b/Fritz.Chatbot/QnA/Migrations/20200419150731_Add wrong answers.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Fritz.Chatbot.QnA.Migrations +{ + public partial class Addwronganswers : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AnswerPct", + table: "UnansweredQuestions", + nullable: true); + + migrationBuilder.AddColumn( + name: "AnswerTextProvided", + table: "UnansweredQuestions", + maxLength: 1000, + nullable: true); + + migrationBuilder.AddColumn( + name: "ReviewDate", + table: "UnansweredQuestions", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AnswerPct", + table: "UnansweredQuestions"); + + migrationBuilder.DropColumn( + name: "AnswerTextProvided", + table: "UnansweredQuestions"); + + migrationBuilder.DropColumn( + name: "ReviewDate", + table: "UnansweredQuestions"); + } + } +} diff --git a/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs b/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs index 3141589e..d1360dd6 100644 --- a/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs +++ b/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs @@ -198,6 +198,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + b.Property("AnswerPct") + .HasColumnType("numeric"); + + b.Property("AnswerTextProvided") + .HasColumnType("character varying(1000)") + .HasMaxLength(1000); + b.Property("AskedDateStamp") .HasColumnType("timestamp without time zone"); @@ -206,6 +213,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(1000)") .HasMaxLength(1000); + b.Property("ReviewDate") + .HasColumnType("timestamp without time zone"); + b.HasKey("Id"); b.ToTable("UnansweredQuestions"); diff --git a/Fritz.Chatbot/QnA/QuestionCacheService.cs b/Fritz.Chatbot/QnA/QuestionCacheService.cs index 3d65d625..94f9e1af 100644 --- a/Fritz.Chatbot/QnA/QuestionCacheService.cs +++ b/Fritz.Chatbot/QnA/QuestionCacheService.cs @@ -10,6 +10,7 @@ public class QuestionCacheService private MemoryCache _Cache = new MemoryCache(new MemoryCacheOptions() { + ExpirationScanFrequency = TimeSpan.FromSeconds(5), }); @@ -28,6 +29,12 @@ public void Add(string userName, string questionText, long answerId) } + public void AddQuestionForModerator(string userName, string questionText) { + + _Cache.Set(userName, (questionText, 0), TimeSpan.FromMinutes(5)); + + } + } } diff --git a/Test/Chatbot/QuestionsCanBeAnswered.cs b/Test/Chatbot/QuestionsCanBeAnswered.cs index 5424b6b5..c9dc1097 100644 --- a/Test/Chatbot/QuestionsCanBeAnswered.cs +++ b/Test/Chatbot/QuestionsCanBeAnswered.cs @@ -20,7 +20,7 @@ public class QuestionsCanBeAnswered public void ShouldBeAnswered(string test) { - var sut = new AzureQnACommand(null, null, null); + var sut = GetEmptyCommand(); var result = sut.CanExecute(UserName, test); Assert.True(result); @@ -35,12 +35,14 @@ public void ShouldBeAnswered(string test) public void ShouldNotBeAnswered(string test) { - var sut = new AzureQnACommand(null, null, null); + var sut = GetEmptyCommand(); var result = sut.CanExecute(UserName, test); Assert.False(result); } + private AzureQnACommand GetEmptyCommand() => new AzureQnACommand(null, null, null, null); + } } diff --git a/Test/Startup/ConfigureServicesTests.cs b/Test/Startup/ConfigureServicesTests.cs index 4663d749..e000df27 100644 --- a/Test/Startup/ConfigureServicesTests.cs +++ b/Test/Startup/ConfigureServicesTests.cs @@ -9,6 +9,8 @@ using Fritz.StreamTools.StartupServices; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -31,6 +33,7 @@ public void Execute_ShouldRegitserService_WhenAllRequiredConfigurationDone() var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(configuration); serviceCollection.AddSingleton(NullLogger.Instance); + serviceCollection.AddSingleton(new StubHostEnvironment()); var serviceRequriedConfiguration = new Dictionary() @@ -56,6 +59,7 @@ public void Execute_ShouldSkipRegisterServices_IfAnyOfRequiredConfigurationNotPa var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(configuration); serviceCollection.AddSingleton(NullLogger.Instance); + serviceCollection.AddSingleton(new StubHostEnvironment()); var serviceRequriedConfiguration = new Dictionary() @@ -80,6 +84,7 @@ public void Execute_RegisterStreamServicesWithVariousConfigurations_ReturnExpect serviceCollection.AddSingleton(new LoggerFactory()); serviceCollection.AddSingleton(configuration); serviceCollection.AddSingleton(NullLogger.Instance); + serviceCollection.AddSingleton(new StubHostEnvironment()); // act ConfigureServices.Execute(serviceCollection, configuration, new Dictionary()); @@ -126,4 +131,16 @@ public Task StopAsync(CancellationToken cancellationToken) } } } + + internal class StubHostEnvironment : IHostEnvironment + { + public StubHostEnvironment() + { + } + + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + } } diff --git a/global.json b/global.json index 32ee2104..261005f0 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "3.1.100" + "version": "3.1.201" } }