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/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/Commands/AzureQnACommand.cs b/Fritz.Chatbot/Commands/AzureQnACommand.cs
index 9962c019..c013cfeb 100644
--- a/Fritz.Chatbot/Commands/AzureQnACommand.cs
+++ b/Fritz.Chatbot/Commands/AzureQnACommand.cs
@@ -2,9 +2,14 @@
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;
using Microsoft.Extensions.Configuration;
@@ -16,8 +21,6 @@ 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";
@@ -25,26 +28,46 @@ public class AzureQnACommand : IExtendedCommand
public bool Final => true;
public TimeSpan? Cooldown => null;
- private readonly IConfiguration _configuration;
+ public Proxy Proxy { get; }
+
private readonly ILogger _logger;
+ private readonly QuestionCacheService _QuestionCacheService;
+ private readonly QnADbContext _Context;
+ private static readonly Regex _UserNameRegEx = new Regex(@"(@\w+)");
- public AzureQnACommand(IConfiguration configuration, ILogger logger)
+ public AzureQnACommand(QnA.QnAMaker.Proxy proxy, ILogger logger, QuestionCacheService questionCacheService, QnADbContext context)
{
- _configuration = configuration;
+ Proxy = proxy;
_logger = logger;
+ _QuestionCacheService = questionCacheService;
+ _Context = context;
}
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;
}
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,96 +76,69 @@ 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();
if (thisAnswer.Score > 50)
{
- await chatService.SendMessageAsync(thisAnswer.Answer);
+ 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 chatService.SendMessageAsync("I'm not certain, but perhaps this will help: " + thisAnswer.Answer + $@"({thisAnswer.Score.ToString("0.0")}% certainty)");
+ 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}");
}
}
- 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}");
+ private async Task LogInaccurateAnswer(string questionText, string? answer = null, decimal? answerPct = null) {
- //Send the POST request
- using(var client = new WebClient())
+ _Context.UnansweredQuestions.Add(new UnansweredQuestion
{
- //Set the encoding to UTF8
- client.Encoding = System.Text.Encoding.UTF8;
+ QuestionText = questionText,
+ AnswerTextProvided = answer,
+ AnswerPct = answerPct,
+ AskedDateStamp = DateTime.UtcNow
+ });
- //Add the subscription key header
- client.Headers.Add("Ocp-Apim-Subscription-Key", AzureKey);
- client.Headers.Add("Content-Type", "application/json");
+ await _Context.SaveChangesAsync();
- //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/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/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/Fritz.Chatbot.csproj b/Fritz.Chatbot/Fritz.Chatbot.csproj
index 7111a966..a9548672 100644
--- a/Fritz.Chatbot/Fritz.Chatbot.csproj
+++ b/Fritz.Chatbot/Fritz.Chatbot.csproj
@@ -10,15 +10,18 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
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..d2b35ee6
--- /dev/null
+++ b/Fritz.Chatbot/QnA/Data/UnansweredQuestion.cs
@@ -0,0 +1,27 @@
+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; }
+
+ 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/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/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
new file mode 100644
index 00000000..d1360dd6
--- /dev/null
+++ b/Fritz.Chatbot/QnA/Migrations/QnADbContextModelSnapshot.cs
@@ -0,0 +1,235 @@
+//
+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("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/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..bd84ea9d
--- /dev/null
+++ b/Fritz.Chatbot/QnA/QnAMaker/QnAMakerProxy.cs
@@ -0,0 +1,94 @@
+using Fritz.Chatbot.Commands;
+using Microsoft.Extensions.Configuration;
+using Newtonsoft.Json;
+using System;
+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 string _KnowledgeBaseId;
+ private readonly bool _MissingAzureKey;
+ private readonly IHttpClientFactory _HttpClientFactory;
+ public const string MaintenanceClientName = "QnAMaintainanace";
+ public const string QuestionClientName = "QnAQuestion";
+
+
+ public Proxy(IHttpClientFactory httpClientFactory, IConfiguration configuration)
+ {
+ this._KnowledgeBaseId = configuration["FritzBot:QnA:KnowledgeBaseId"];
+ _MissingAzureKey = String.IsNullOrEmpty(configuration["AzureServices:QnASubscriptionKey"]);
+ _HttpClientFactory = httpClientFactory;
+ }
+
+ public async Task Download()
+ {
+
+ // 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 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) {
+
+ // 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 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.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.Chatbot/QnA/QuestionCacheService.cs b/Fritz.Chatbot/QnA/QuestionCacheService.cs
new file mode 100644
index 00000000..94f9e1af
--- /dev/null
+++ b/Fritz.Chatbot/QnA/QuestionCacheService.cs
@@ -0,0 +1,40 @@
+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);
+
+ }
+
+ public void AddQuestionForModerator(string userName, string questionText) {
+
+ _Cache.Set(userName, (questionText, 0), TimeSpan.FromMinutes(5));
+
+ }
+
+
+ }
+}
diff --git a/Fritz.StreamTools/Fritz.StreamTools.csproj b/Fritz.StreamTools/Fritz.StreamTools.csproj
index 243da3b4..b2f36c7e 100644
--- a/Fritz.StreamTools/Fritz.StreamTools.csproj
+++ b/Fritz.StreamTools/Fritz.StreamTools.csproj
@@ -18,16 +18,21 @@
-
+
-
-
-
+
+
+
-
-
-
-
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
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/Fritz.StreamTools/StartupServices/ConfigureServices.cs b/Fritz.StreamTools/StartupServices/ConfigureServices.cs
index cf63f5e5..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;
@@ -11,6 +12,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 +48,7 @@ public static void Execute(IServiceCollection services, IConfiguration configura
services.AddSingleton();
- // Add the SentimentSink
- //services.AddSingleton();
+ services.AddQnAFeatures();
services.AddHostedService();
@@ -199,6 +200,35 @@ private static void RegisterTwitchPubSub(this IServiceCollection services) {
}
+ private static void AddQnAFeatures(this IServiceCollection services) {
+
+ services.AddDbContext(options =>
+ {
+ options.UseNpgsql(_Configuration["FritzBot:QnA:ConnectionString"]);
+ });
+
+ services.AddTransient();
+ services.AddHttpClient(Fritz.Chatbot.QnA.QnAMaker.Proxy.MaintenanceClientName, config =>
+ {
+
+ 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");
+
+ });
+ 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");
+
+ });
+
+ services.AddSingleton();
+
+ }
+
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..c970975b 100644
--- a/Fritz.StreamTools/appsettings.json
+++ b/Fritz.StreamTools/appsettings.json
@@ -212,7 +212,13 @@
"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;",
+ "KnowledgeBaseId": "34d70910-0c78-43e4-bcdb-21ed7f606b5e",
+ "RuntimeEndpoint": "https://fritzbotqna.azurewebsites.net/qnamaker/",
+ "MaintenanceEndpoint": "https://westus.api.cognitive.microsoft.com/qnamaker/v4.0/"
+ }
},
"FollowerGoal": {
"Caption": "Follower Goal",
diff --git a/Test/Chatbot/QuestionsCanBeAnswered.cs b/Test/Chatbot/QuestionsCanBeAnswered.cs
new file mode 100644
index 00000000..c9dc1097
--- /dev/null
+++ b/Test/Chatbot/QuestionsCanBeAnswered.cs
@@ -0,0 +1,48 @@
+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 = GetEmptyCommand();
+
+ 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 = 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/Test/Test.csproj b/Test/Test.csproj
index b138fa6d..4257d98e 100644
--- a/Test/Test.csproj
+++ b/Test/Test.csproj
@@ -7,15 +7,15 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
all
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"
}
}