1381 lines
73 KiB
C#
1381 lines
73 KiB
C#
using System.Net.Http;
|
||
using System.Net.Http.Json;
|
||
using System.Text.Json;
|
||
using System.Text;
|
||
using DocumentFormat.OpenXml.Bibliography;
|
||
using DocumentFormat.OpenXml.Wordprocessing;
|
||
using BLAIzor.Models;
|
||
using Newtonsoft.Json;
|
||
using Microsoft.AspNetCore.Components.Routing;
|
||
using Microsoft.AspNetCore.Components;
|
||
using static Google.Apis.Requests.BatchRequest;
|
||
using DocumentFormat.OpenXml.Spreadsheet;
|
||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
|
||
using NuGet.Packaging;
|
||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||
using Google.Api;
|
||
using System.CodeDom;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using System.Text.Json.Serialization;
|
||
using BLAIzor.Services;
|
||
using Microsoft.DotNet.Scaffolding.Shared;
|
||
using System.Xml.XPath;
|
||
using Azure.Identity;
|
||
using BLAIzor.Helpers;
|
||
|
||
namespace BLAIzor.Services
|
||
{
|
||
public class AIService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ContentService _contentService;
|
||
private readonly ScopedContentService _scopedContentService;
|
||
private readonly OpenAIEmbeddingService _openAIEmbeddingService;
|
||
private readonly OpenAIApiService _openAIApiService;
|
||
private readonly OpenAiRealtimeService _openAIRealtimeService;
|
||
private readonly DeepSeekApiService _deepSeekApiService;
|
||
private readonly CerebrasAPIService _cerebrasAPIService;
|
||
private readonly QDrantService _qDrantService;
|
||
private readonly NavigationManager _navigationManager;
|
||
|
||
|
||
public static IConfiguration? _configuration;
|
||
|
||
|
||
public AIService(HttpClient httpClient, ContentService contentService, ScopedContentService scopedContentService, QDrantService qDrantService, OpenAIEmbeddingService openAIEmbeddingService, OpenAIApiService openAIApiService, DeepSeekApiService deepSeekApiService, OpenAiRealtimeService openAIRealtimeService, CerebrasAPIService cerebrasAPIService, NavigationManager navigationManager, IConfiguration? configuration)
|
||
{
|
||
_httpClient = httpClient;
|
||
_contentService = contentService;
|
||
_scopedContentService = scopedContentService;
|
||
_qDrantService = qDrantService;
|
||
_openAIEmbeddingService = openAIEmbeddingService;
|
||
_openAIApiService = openAIApiService;
|
||
_deepSeekApiService = deepSeekApiService;
|
||
_openAIRealtimeService = openAIRealtimeService;
|
||
_cerebrasAPIService = cerebrasAPIService;
|
||
_navigationManager = navigationManager;
|
||
_configuration = configuration;
|
||
|
||
_openAIApiService.RegisterCallback(HandleActionInvoked, HandleFinishedInvoked, HandleErrorInvoked);
|
||
_cerebrasAPIService.RegisterCallback(HandleActionInvoked, HandleFinishedInvoked, HandleErrorInvoked);
|
||
_openAIRealtimeService.RegisterCallback(HandleActionInvoked);
|
||
}
|
||
|
||
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
|
||
public string _apiKey;
|
||
public static event Action<string, string>? OnContentReceived;
|
||
public static event Action<string>? OnContentReceiveFinished;
|
||
public static event Action<string, string>? OnContentReceivedError;
|
||
public static event Action<string, string>? OnStatusChangeReceived;
|
||
public static event Action<string, string>? OnTextContentAvailable;
|
||
public string Mood = "cool, and professional";
|
||
private string _workingContent = null;
|
||
public bool UseWebsocket = false;
|
||
private string AiProvider = "";
|
||
|
||
private string GetAiSettings() =>
|
||
_configuration?.GetSection("AiSettings")?.GetValue<string>("Provider") ?? string.Empty;
|
||
|
||
private void HandleActionInvoked(string sessionId, string streamedHtmlContent)
|
||
{
|
||
OnContentReceived?.Invoke(sessionId, streamedHtmlContent);
|
||
}
|
||
|
||
private void HandleErrorInvoked(string sessionId, string streamedHtmlContent)
|
||
{
|
||
OnContentReceivedError?.Invoke(sessionId, streamedHtmlContent);
|
||
}
|
||
|
||
private void HandleFinishedInvoked(string sessionId)
|
||
{
|
||
OnContentReceiveFinished?.Invoke(sessionId);
|
||
}
|
||
|
||
public string GetApiKey()
|
||
{
|
||
if (_configuration == null)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
if (_configuration.GetSection("OpenAI") == null)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
return _configuration.GetSection("OpenAI").GetValue<string>("ApiKey")!;
|
||
|
||
}
|
||
|
||
public async Task GetChatGptWelcomeMessage(string sessionId, int SiteId, string menuList = "")
|
||
{
|
||
string currentUri = _navigationManager.Uri;
|
||
//Console.Write($"\n\n SessionId: {sessionId}\n\n");
|
||
//string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _contentService.SelectedDocument);
|
||
|
||
_apiKey = GetApiKey();
|
||
string qdrantPoint = await _qDrantService.GetContentAsync(SiteId, 0);
|
||
string extractedText = "";
|
||
//TODO: this is the full object, should get the text from it, it sends the vectors too at the moment
|
||
var selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(qdrantPoint)!;
|
||
if (selectedPoint != null)
|
||
{
|
||
extractedText = selectedPoint.result.payload.content;
|
||
////Console.Write($"\n -------------------------------- Found point: {selectedPoint.result.payload.content} \n");
|
||
//Console.Write($"\n -------------------------------- Found point: {selectedPoint.result.id} \n");
|
||
}
|
||
|
||
SiteInfo site = await _scopedContentService.GetSiteInfoByIdAsync(SiteId);
|
||
string siteEntity;
|
||
if (!string.IsNullOrWhiteSpace(site.Entity))
|
||
{
|
||
siteEntity = site.Entity;
|
||
}
|
||
else {
|
||
siteEntity = "brand or company";
|
||
}
|
||
|
||
string systemMessage = "You are a helpful, " + Mood + " assistant that welcomes the user speaking in the name of the " + siteEntity + " described by the content, on a website of " + _scopedContentService.SelectedBrandName + " in " + _scopedContentService.SelectedLanguage + ". Use the following content: `" +
|
||
extractedText + "` " +
|
||
//"and generate a short" +Mood+ "but kind marketing-oriented welcome message and introduction of the brand for the user, constructed as simple Bootstrap HTML codeblock with a <h1> tagged title and a paragraph." +
|
||
"and generate a" + Mood + " marketing-oriented welcome message and a summary of the content and introduction of the brand for the user, aiming to explain clearly, what does the company/person offer, constructed as simple Bootstrap HTML <div clss=\"container\"> codeblock with a <h1> tagged title and a paragraph." +
|
||
"If there is any logo, or not logo but main brand image in the document use that url, and add that as a bootstrap responsive ('img-fluid py-3') image, with the maximum height of 30vh." +
|
||
//"In the end of your answer, always add in a new row: 'If you have any questions, you can simply ask either by typing in the message box, or by clicking the microphone icon on the top of the page. '"+
|
||
"Here is a list of topics " + menuList +
|
||
", make a new bootstrap clearfix and after that make a clickable bootstrap styled (btn btn-primary) button from each of the determined topics, " +
|
||
"that calls the javascript function 'callAI({the name of the topic})' on click. " +
|
||
"Do not include anything else than the html title and text elements, no css, no scripts, no head or other tags." +
|
||
"Do not mark your answer with ```html or any other mark.";
|
||
string userMessage = "Hello";
|
||
string streamedHtmlContent = string.Empty;
|
||
|
||
if (!UseWebsocket)
|
||
{
|
||
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
await _cerebrasAPIService.GetCerebrasStreamedResponse(sessionId, systemMessage, userMessage);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
//await _deepSeekApiService.GetChatGPTStreamedResponse(systemMessage, userMessage);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
await _openAIRealtimeService.GetChatGPTResponseAsync(sessionId, systemMessage, userMessage);
|
||
|
||
}
|
||
|
||
//_scopedContentService.CurrentDOM = streamedHtmlContent;
|
||
////Console.Write("Answer: " + streamedHtmlContent);
|
||
//return streamedHtmlContent;
|
||
|
||
}
|
||
|
||
public async Task ProcessUserIntent(string sessionId, string userPrompt, int siteId, int templateId, string collectionName, string menuList = "")
|
||
{
|
||
//Console.WriteLine($"SITE ID: {siteId}");
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Understanding your request...");
|
||
|
||
// Get JSON result based on siteId presence
|
||
string resultJson = siteId >= 0
|
||
? await GetJsonResultFromQuery(sessionId, siteId, userPrompt)
|
||
: await GetJsonResultFromQuery(sessionId, userPrompt);
|
||
|
||
//Console.WriteLine(resultJson);
|
||
|
||
|
||
var baseResult = await ValidateAndFixJson<ChatGPTResultBase>(resultJson, FixJsonWithAI);
|
||
//var baseResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTResultBase>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||
var fixedResult = System.Text.Json.JsonSerializer.Serialize(baseResult);
|
||
|
||
if (baseResult == null)
|
||
{
|
||
//Console.WriteLine("Invalid JSON response.");
|
||
return;
|
||
}
|
||
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Making a decision");
|
||
|
||
// Process result based on type
|
||
switch (baseResult.Type.ToLower())
|
||
{
|
||
case "methodresult":
|
||
await ProcessMethodResult(sessionId, resultJson);
|
||
break;
|
||
|
||
case "textresult":
|
||
await ProcessTextResult(sessionId, fixedResult, templateId, collectionName);
|
||
break;
|
||
|
||
case "examinationresult":
|
||
await ProcessExaminationResult(sessionId, fixedResult, templateId, collectionName);
|
||
break;
|
||
|
||
case "errorresult":
|
||
await ProcessErrorResult(sessionId, fixedResult);
|
||
break;
|
||
|
||
default:
|
||
//Console.WriteLine("Unknown result type.");
|
||
break;
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// No reasoning needed just content retrieved and displayed as html
|
||
/// </summary>
|
||
/// <param name="sessionId"></param>
|
||
/// <param name="userPrompt"></param>
|
||
/// <param name="siteId"></param>
|
||
/// <param name="templateId"></param>
|
||
/// <param name="collectionName"></param>
|
||
/// <param name="menuList"></param>
|
||
/// <returns></returns>
|
||
public async Task ProcessContentRequest(string sessionId, MenuItem requestedMenu, int siteId, int templateId, string collectionName, string menuList = "", bool forceUnmodified = false)
|
||
{
|
||
|
||
//Console.Write($"\n\n SessionId: {sessionId}\n\n");
|
||
//string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _contentService.SelectedDocument);
|
||
|
||
string extractedText = "";
|
||
|
||
|
||
if (requestedMenu != null)
|
||
{
|
||
|
||
string qDrantData = await _qDrantService.GetContentAsync(siteId, requestedMenu.PointId);
|
||
QDrantGetContentPointResult _selectedPoint = new QDrantGetContentPointResult();
|
||
|
||
if (qDrantData != null)
|
||
{
|
||
_selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(qDrantData)!;
|
||
}
|
||
extractedText = _selectedPoint.result.payload.name + ": " + _selectedPoint.result.payload.content + ", ";
|
||
}
|
||
|
||
|
||
string contentJson = await GetContentFromQuery(sessionId,
|
||
"Enhance this text if needed, making its style and grammar suitable to be displayed as the content of a webpage",
|
||
extractedText,
|
||
forceUnmodified);
|
||
|
||
await ProcessContent(sessionId, contentJson, templateId, collectionName);
|
||
}
|
||
|
||
public async Task ProcessContentRequest(string sessionId, string requestedMenu, int siteId, int templateId, string collectionName, string menuList = "", bool forceUnmodified = false)
|
||
{
|
||
|
||
//Console.Write($"\n\n SessionId: {sessionId}\n\n");
|
||
//string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _contentService.SelectedDocument);
|
||
|
||
string extractedText = "";
|
||
|
||
float[] vector = [];
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Determining search vectors");
|
||
vector = await _openAIEmbeddingService.GenerateEmbeddingAsync(requestedMenu);
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Looking up content in the knowledge database");
|
||
var pointId = await _qDrantService.QueryContentAsync(siteId, vector, 3);
|
||
|
||
|
||
if (pointId.Length > 0)
|
||
{
|
||
foreach (var item in pointId)
|
||
{
|
||
string qDrantData = await _qDrantService.GetContentAsync(siteId, item);
|
||
QDrantGetContentPointResult _selectedPoint = new QDrantGetContentPointResult();
|
||
|
||
if (qDrantData != null)
|
||
{
|
||
_selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(qDrantData)!;
|
||
}
|
||
extractedText += _selectedPoint.result.payload.name + ": " + _selectedPoint.result.payload.content + ", ";
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
extractedText = "VECTOR ERROR: ZERO INFORMATION FOUND";
|
||
}
|
||
|
||
|
||
string contentJson = await GetContentFromQuery(sessionId,
|
||
"Enhance this text if needed, making its style and grammar suitable to be displayed as the content of a webpage",
|
||
extractedText,
|
||
forceUnmodified);
|
||
|
||
await ProcessContent(sessionId, contentJson, templateId, collectionName);
|
||
}
|
||
|
||
// Refactored helper methods
|
||
|
||
private async Task ProcessMethodResult(string sessionId, string resultJson)
|
||
{
|
||
var fixedResult = await ValidateAndFixJson<ChatGPTMethodResult>(resultJson, FixJsonWithAI);
|
||
//var methodResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTMethodResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||
if (fixedResult != null)
|
||
{
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Initiating the task you requested");
|
||
await DisplayHtml(sessionId, fixedResult.Text, fixedResult.MethodToCall, fixedResult.Parameter);
|
||
}
|
||
}
|
||
|
||
private async Task ProcessTextResult(string sessionId, string resultJson, int templateId, string collectionName)
|
||
{
|
||
var fixedResult = await ValidateAndFixJson<ChatGPTTextResult>(resultJson, FixJsonWithAI);
|
||
//var textResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTTextResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||
if (fixedResult != null)
|
||
{
|
||
string contentJson = await GetContentFromQuery(sessionId, fixedResult.Text, _workingContent);
|
||
//Console.Write("\r \n ProcessTextResult: Content: " + contentJson + "\r \n");
|
||
await ProcessContent(sessionId, contentJson, templateId, collectionName);
|
||
}
|
||
}
|
||
|
||
public async Task<T?> ValidateAndFixJson<T>(string json, Func<string, Task<string>> aiFixer)
|
||
{
|
||
try
|
||
{
|
||
return System.Text.Json.JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
//Console.WriteLine($"❌ JSON parse failed: {ex.Message}");
|
||
|
||
var prompt = BuildJsonFixPrompt(json, ex.Message, typeof(T).Name);
|
||
var fixedJson = await aiFixer(prompt);
|
||
|
||
try
|
||
{
|
||
return System.Text.Json.JsonSerializer.Deserialize<T>(fixedJson, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
}
|
||
catch (Exception ex2)
|
||
{
|
||
//Console.WriteLine($"❌ AI-fix parse failed: {ex2.Message}");
|
||
return default;
|
||
}
|
||
}
|
||
}
|
||
|
||
public async Task<string> FixJsonWithAI(string prompt)
|
||
{
|
||
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
return await _cerebrasAPIService.GetSimpleCerebrasResponseNoSession("You are a JSON-fixing assistant.", prompt);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
return await _openAIApiService.GetSimpleChatGPTResponseNoSession("You are a JSON-fixing assistant.", prompt);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
return await _deepSeekApiService.GetSimpleChatGPTResponse("You are a JSON-fixing assistant.", prompt);
|
||
}
|
||
else { return ""; }
|
||
|
||
//return await _openAIApiService.GetSimpleChatGPTResponseNoSession("You are a JSON-fixing assistant.", prompt);
|
||
}
|
||
|
||
private string BuildJsonFixPrompt(string json, string errorMessage, string targetTypeName)
|
||
{
|
||
return $"""
|
||
The following JSON was supposed to be parsed into an object of type {targetTypeName}, but it failed with this error:
|
||
|
||
{errorMessage}
|
||
|
||
Please fix the formatting of the JSON so that it becomes valid and deserializable:
|
||
|
||
--- JSON START ---
|
||
{json}
|
||
--- JSON END ---
|
||
|
||
Only return the fixed JSON, with no explanation or formatting like ```json.
|
||
""";
|
||
}
|
||
|
||
|
||
private async Task ProcessExaminationResult(string sessionId, string resultJson, int templateId, string collectionName)
|
||
{
|
||
var fixedResult = await ValidateAndFixJson<ChatGPTExaminationResult>(resultJson, FixJsonWithAI);
|
||
//var explanationResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTExaminationResult>(resultJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||
if (fixedResult != null)
|
||
{
|
||
string contentJson = await GetExplanationFromQuery(sessionId, fixedResult.Text, _scopedContentService.CurrentDOM);
|
||
await ProcessContent(sessionId, contentJson, templateId, collectionName);
|
||
}
|
||
}
|
||
|
||
private async Task ProcessContent(string sessionId, string contentJson, int templateId, string collectionName)
|
||
{
|
||
try
|
||
{
|
||
var fixedResult = await ValidateAndFixJson<ChatGPTContentResult>(contentJson, FixJsonWithAI);
|
||
//var contentResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTContentResult>(contentJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||
|
||
if (fixedResult != null)
|
||
{
|
||
Console.WriteLine($"\n\n Actual content: {fixedResult.Text} \n\n");
|
||
|
||
// Add reaction GIFs
|
||
//contentResult.Photos.Add("Clarification request", "https://www.reactiongifs.com/r/martin.gif");
|
||
//contentResult.Photos.Add("Compliment response", "https://www.reactiongifs.com/r/review.gif");
|
||
|
||
//We have the text all available now, let's pass it to the voice generator
|
||
//TODO modify photos handling, move audio generation to the layout area
|
||
string removedNumbers = TextHelper.ReplaceNumbersAndSpecialCharacters(fixedResult.Text);
|
||
Console.WriteLine(removedNumbers);
|
||
OnTextContentAvailable?.Invoke(sessionId, removedNumbers);
|
||
List<HtmlSnippet> snippets = await GetSnippetsForDisplay(sessionId, collectionName);
|
||
//await DisplayLayoutPlanFromContent(sessionId, fixedResult.Text, snippets, fixedResult.Topics, fixedResult.Photos);
|
||
var result = await DisplayLayoutPlanFromContent(sessionId, fixedResult.Text, snippets, fixedResult.Topics, fixedResult.Photos);
|
||
if (result == null) result = new LayoutPlan();
|
||
await DisplayHtml(sessionId, result, snippets, fixedResult.Topics);
|
||
//OnContentReceived?.Invoke(sessionId, result.Blocks.Count.ToString());
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
//Console.WriteLine($"Error processing content: {ex.Message}");
|
||
OnContentReceived?.Invoke(sessionId, ex.Message);
|
||
}
|
||
}
|
||
|
||
private async Task ProcessErrorResult(string sessionId, string resultJson)
|
||
{
|
||
var errorResult = System.Text.Json.JsonSerializer.Deserialize<ChatGPTErrorResult>(resultJson);
|
||
if (errorResult != null)
|
||
{
|
||
//Console.WriteLine($"Error Result: {errorResult.Text}");
|
||
await DisplayLayoutPlanFromContent(sessionId, errorResult.Text, null, null, null);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Let's get the actual content
|
||
/// </summary>
|
||
/// <param name="userPrompt"></param>
|
||
/// <returns></returns>
|
||
public async Task<string> GetContentFromQuery(string sessionId, string userPrompt, string content = null, bool forceUnmodified = false)
|
||
{
|
||
string extractedText;
|
||
if (content == null)
|
||
{
|
||
string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _scopedContentService.SelectedDocument);
|
||
extractedText = WordFileReader.ExtractText(rootpath);
|
||
}
|
||
else extractedText = content;
|
||
|
||
_apiKey = GetApiKey();
|
||
|
||
////Console.Write("GetJSONResult called: " +extractedText);
|
||
string systemMessage = "";
|
||
if (forceUnmodified)
|
||
{
|
||
systemMessage = "You are a helpful assistant of a website. Display the Content strictly in " + _scopedContentService.SelectedLanguage + " with a plain JSON object in the following format:\r\n\r\n1. " +
|
||
//"**chatGPTContentResult**:\r\n " +
|
||
"- `type`: A string with value `contentresult`.\r\n " +
|
||
"- `text`: A string with the actual response.\r\n " +
|
||
"- `topics`: A list of sections of the initial document." +
|
||
"- `photos`: A dictionary of string key and string values, where the keys are the name of the subject that the photo is related to (like a person's name, or a section)," +
|
||
" and the value is the actual, unmodified photo url.\r\n" +
|
||
"**Document-Specific Instructions**:\r\n" +
|
||
"Step 1: Start with defining above mentioned key topics of the initial document, and making the list of them. " +
|
||
"Step 2: After that add the above mentioned relevant image urls list." +
|
||
"Step 3: " +
|
||
"- Turn the following content into a nice informative webpage content (DO NOT REMOVE URLS, PHOTO URLS though).\r\n " +
|
||
"- Start with the page title.\r\n" +
|
||
"- Structure it nicely without leaving out any information.\r\n " +
|
||
//"*** CONTENT START *** {" + extractedText + "} *** CONTENT END ***.\r\n" +
|
||
"**Style and Image Handling**:\r\n" +
|
||
"- Make sure the json is valid json." +
|
||
"- Do NOT include extraneous text outside the JSON structure.\r\n\r\n" +
|
||
"When you understand the input, follow these rules strictly. Otherwise, seek clarification.\r\n" +
|
||
"Do not include linebreaks or any formatting, just the plain json string. Make sure it is valid json, and every objects is closed properly" +
|
||
"Do NOT mark your answer with anything like `````json, and do not add any explanation.";
|
||
userPrompt = "Give me a formatted json from this content, without modifying the text: " + extractedText;
|
||
}
|
||
else
|
||
{
|
||
systemMessage = "You are a helpful assistant of a website. Respond in the name of the brand or person in the content, strictly in " + _scopedContentService.SelectedLanguage + " with a plain JSON object in the following format:\r\n\r\n1. " +
|
||
//"**chatGPTContentResult**:\r\n " +
|
||
"- `type`: A string with value `contentresult`.\r\n " +
|
||
"- `text`: A string with the actual response.\r\n " +
|
||
"- `topics`: A list of sections of the initial document." +
|
||
"- `photos`: A dictionary of string key and string values, where the keys are the name of the subject that the photo is related to (like a person's name, or a section)," +
|
||
" and the value is the actual, unmodified photo url.\r\n" +
|
||
"**Document-Specific Instructions**:\r\n" +
|
||
"Step 1: Start with defining above mentioned key topics of the initial document, and making the list of them. " +
|
||
"Step 2: After that add the above mentioned relevant image urls list." +
|
||
"Step 3: " +
|
||
"- Base a detailed, but not lengthy response solely on the initial document provided below. " +
|
||
"- In your response, summarize ALL relevant information in the document, that is connected to the question." +
|
||
"*** CONTENT START *** {" + extractedText + "} *** CONTENT END ***.\r\n" +
|
||
"- For missing information: Inform the user and ask if you can help with something else. " +
|
||
//"- Do not generate lengthy answers." +
|
||
"- If the user prompt is clear and they ask specific, well defined question, do not add other infromation or welcome message." +
|
||
"- If the user prompt is unclear, or makes no sense, ask for clarification." +
|
||
//"You can decorate your clarification" +
|
||
//"request with this image URL: `https://www.reactiongifs.com/r/martin.gif` added to the photo dictionary.\r\n\r\n" +
|
||
//"- For compliments from the user: Express our happiness about it " +
|
||
//"and apply this image URL: `https://www.reactiongifs.com/r/review.gif` in the photo dictionary.\r\n\r\n" +
|
||
"**Style and Image Handling**:\r\n" +
|
||
"- Make sure the json is valid json." +
|
||
"- Do NOT include extraneous text outside the JSON structure.\r\n\r\n" +
|
||
"When you understand the input, follow these rules strictly. Otherwise, seek clarification.\r\n" +
|
||
"Do not include linebreaks or any formatting, just the plain json string. Make sure it is valid json, and every objects is closed properly" +
|
||
"Do NOT mark your answer with anything like `````json, and do not add any explanation.";
|
||
}
|
||
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Constructing the answer");
|
||
string interMediateResult = string.Empty;
|
||
if (!UseWebsocket)
|
||
{
|
||
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
interMediateResult = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
interMediateResult = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
interMediateResult = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
|
||
interMediateResult = await _openAIRealtimeService.GetFullChatGPTResponseAsync(sessionId, systemMessage, userPrompt);
|
||
}
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Mkay, I know now");
|
||
//Console.Write("GetContentFromQuery: Result decision: " + interMediateResult);
|
||
return interMediateResult;
|
||
}
|
||
|
||
public async Task<string> GetExplanationFromQuery(string sessionId, string userPrompt, string content = null)
|
||
{
|
||
string extractedText;
|
||
if (content == null)
|
||
{
|
||
string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _scopedContentService.SelectedDocument);
|
||
extractedText = WordFileReader.ExtractText(rootpath);
|
||
}
|
||
else extractedText = content;
|
||
|
||
_apiKey = GetApiKey();
|
||
|
||
////Console.Write("GetJSONResult called: " +extractedText);
|
||
|
||
var systemMessage = "You are a helpful assistant. Respond strictly in " + _scopedContentService.SelectedLanguage + " as a JSON object in the following format:\r\n\r\n1. " +
|
||
//"**chatGPTContentResult**:\r\n " +
|
||
"- `type`: A string with value `contentresult`.\r\n " +
|
||
"- `text`: A string with the actual response.\r\n " +
|
||
"- `topics`: A list of sections of the initial document." +
|
||
"- `photos`: A dictionary of string key and string values, where the keys are the name of the subject that the photo is related to (like a person's name, or a section)," +
|
||
" and the value is the actual, unmodified photo url.\r\n" +
|
||
"**Document-Specific Instructions**:\r\n- Base responses solely on the initial document: {" + extractedText + "}.\r\n" +
|
||
"- For missing information: Inform the user and provide a clarification. " +
|
||
"- If the user prompt is clear and they ask specific, well defined question, do not add other infromation or welcome message." +
|
||
"- If the user prompt is unclear, or makes no sense, ask for clarification. " +
|
||
"You may decorate your clarification" +
|
||
"request with this image URL: `https://www.reactiongifs.com/r/martin.gif` added to the photo dictionary.\r\n\r\n" +
|
||
"- For compliments from the user: Express our happiness about it " +
|
||
"and apply this image URL: `https://www.reactiongifs.com/r/review.gif` in the photo dictionary.\r\n\r\n" +
|
||
"**Style and Image Handling**:\r\n" +
|
||
//"- Copy styles explicitly from the document into the response.\r\n" +
|
||
//"- Only use image URLs found in the document for relevant content.\r\n" +
|
||
"- Do NOT include extraneous text outside the JSON structure.\r\n\r\n" +
|
||
"When you understand the input, follow these rules strictly. Otherwise, seek clarification.\r\n" +
|
||
"Do not include linebreaks or any formatting, just the plain json string. Make sure it is valid json, and every objects is closed properly" +
|
||
"Do NOT mark your answer with anything like `````json or such.";
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Constructing the answer");
|
||
string interMediateResult = string.Empty;
|
||
if (!UseWebsocket)
|
||
{
|
||
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
interMediateResult = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
interMediateResult = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
interMediateResult = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
|
||
interMediateResult = await _openAIRealtimeService.GetFullChatGPTResponseAsync(sessionId, systemMessage, userPrompt);
|
||
}
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Mkay, I know now");
|
||
//Console.Write("GetExaminationResult: Result decision: " + interMediateResult);
|
||
return interMediateResult;
|
||
}
|
||
|
||
/// <summary>
|
||
/// What does the user want? Answer or action?
|
||
/// </summary>
|
||
/// <param name="userPrompt"></param>
|
||
/// <returns></returns>
|
||
public async Task<string> GetJsonResultFromQuery(string sessionId, string userPrompt)
|
||
{
|
||
string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _scopedContentService.SelectedDocument);
|
||
_apiKey = GetApiKey();
|
||
string extractedText = WordFileReader.ExtractText(rootpath);
|
||
|
||
//Console.Write("GetJSONResult called!");
|
||
|
||
var systemMessage = "You are a helpful assistant. Respond strictly in " + _scopedContentService.SelectedLanguage + " as a JSON object in the following formats:\r\n\r\n1. " +
|
||
"1. MethodResult:\r\n " +
|
||
"- `type`: A string with value `methodresult`.\r\n " +
|
||
"- `text`: A string explaining the result.\r\n " +
|
||
"- `methodToCall`: One of these values: " +
|
||
"[openContactForm, openCalendar, openApplicationForm].\r\n" +
|
||
"- `parameter`: One of these: \r\n" +
|
||
"[email address for openContactForm, calendlyUserName for openCalendar, empty string for openApplicationForm]" +
|
||
"2. TextResult:\r\n " +
|
||
"- `type`: A string with value `textresult`.\r\n " +
|
||
"- `text`: Contains the user query without any modification.\r\n " +
|
||
"3. ExaminationResult:\r\n " +
|
||
"- `type`: A string with value `examinationresult`.\r\n " +
|
||
"- `text`: Contains the user query without any modification.\r\n " +
|
||
"4. ErrorResult:\r\n " +
|
||
"- `type`: A string with value `errorresult`. \r\n " +
|
||
"- `text`: The description of the problem you found. " +
|
||
"**Document-Specific Instructions**:\r\n- Base responses solely on the following initial document: {" + extractedText + "}.\r\n" +
|
||
"**Rules for Decision Making**:\r\n" +
|
||
"- If the user’s input indicates a method invocation, and you find the relevant parameter in the initial document, generate a `methodresult`.\r\n" +
|
||
" In the explanation, put a short sentence about what the user has requested by your understanding. \r\n" +
|
||
"- If the user asks about the current content displayed for them, generate an examinationResult. \r\n" +
|
||
"- If you don't find the relevant parameter in the initial document, generate an errorResult. \r\n" +
|
||
//"- If the user asks for contact form but the initial document doesn't contain contact email, generate an errorResult. \r\n"+
|
||
|
||
"- Otherwise, create a `textresult` with the unmoddified user query.\r\n\r\n" +
|
||
|
||
"Do NOT mark your answer with anything like `````json or such.";
|
||
string interMediateResult = string.Empty;
|
||
if (!UseWebsocket)
|
||
{
|
||
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
interMediateResult = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
interMediateResult = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
interMediateResult = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
interMediateResult = await _openAIRealtimeService.GetFullChatGPTResponseAsync(sessionId, systemMessage, userPrompt);
|
||
}
|
||
//Console.Write("Result decision: " + interMediateResult);
|
||
return interMediateResult;
|
||
}
|
||
|
||
public async Task<string> GetJsonResultFromQuery(string sessionId, int siteId, string userPrompt)
|
||
{
|
||
//string rootpath = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "wwwroot/Documents/" + _contentService.SelectedDocument);
|
||
//_apiKey = GetApiKey();
|
||
//start with embeddings
|
||
float[] vector = [];
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Determining search vectors");
|
||
vector = await _openAIEmbeddingService.GenerateEmbeddingAsync(userPrompt);
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Looking up content in the knowledge database");
|
||
var pointId = await _qDrantService.QueryContentAsync(siteId, vector, 3);
|
||
|
||
string extractedText = "Sections: ";
|
||
if (pointId.Length > 0)
|
||
{
|
||
foreach (var item in pointId)
|
||
{
|
||
string qDrantData = await _qDrantService.GetContentAsync(siteId, item);
|
||
QDrantGetContentPointResult selectedPoint = new QDrantGetContentPointResult();
|
||
|
||
if (qDrantData != null)
|
||
{
|
||
selectedPoint = JsonConvert.DeserializeObject<QDrantGetContentPointResult>(qDrantData)!;
|
||
}
|
||
extractedText += selectedPoint.result.payload.name + ": " + selectedPoint.result.payload.content + ", ";
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
extractedText = "VECTOR ERROR: ZERO INFORMATION FOUND";
|
||
}
|
||
|
||
_workingContent = extractedText.Replace("\"", "'");
|
||
//Console.Write("\r \n GetJsonResultFromQuery: Working content: " + _workingContent + "\r \n");
|
||
//string extractedText = WordFileReader.ExtractText(rootpath);
|
||
|
||
//Console.Write("GetJSONResult called!");
|
||
|
||
string systemMessage = $"You are a helpful assistant built in a website, trying to figure out what the User wants to do or know about.\r\n" +
|
||
"Your job is to classify the user's request into one of the following categories:\r\n" +
|
||
"1. **Ask about or search infromation in the website’s content** (Return a 'Text result')\r\n" +
|
||
"2. **Analyze the currently displayed HTML content** (Return an 'Examination result')\r\n" +
|
||
"3. **Initiate an action** (Return a 'Method result')\r\n" +
|
||
"If none of the above applies, return an 'Error result'.\r\n\r\n" +
|
||
|
||
"**Response format:**\r\n" +
|
||
"Strictly respond in " + _scopedContentService.SelectedLanguage + " as a JSON object, using one of the following formats:\r\n" +
|
||
|
||
"1. **chatGPTMethodResult** (for initiating actions):\r\n" +
|
||
" - `type`: \"methodresult\"\r\n" +
|
||
" - `text`: A short explanation of what the user wants to do.\r\n" +
|
||
" - `methodToCall`: One of: [openContactForm, openCalendar, openApplicationForm]\r\n" +
|
||
" - `parameter`: [email address for openContactForm, calendlyUserName for openCalendar, empty string for openApplicationForm]\r\n\r\n" +
|
||
|
||
"2. **chatGPTTextResult** (for general website content searches):\r\n" +
|
||
" - `type`: \"textresult\"\r\n" +
|
||
" - `text`: The user’s unmodified query.\r\n\r\n" +
|
||
|
||
"3. **chatGPTExaminationResult** (for analyzing the currently displayed page only):\r\n" +
|
||
" - `type`: \"examinationresult\"\r\n" +
|
||
" - `text`: The user’s unmodified query.\r\n\r\n" +
|
||
|
||
"4. **chatGPTErrorResult** (for errors):\r\n" +
|
||
" - `type`: \"errorresult\"\r\n" +
|
||
" - `text`: A description of the issue encountered.\r\n\r\n" +
|
||
|
||
"**Decision Rules:**\r\n" +
|
||
"- If the user is **searching for website content** beyond what is currently displayed (e.g., 'Find information about our services'), return a `textresult`.\r\n" +
|
||
"- If the user is **asking about the currently visible content** (e.g., 'What is shown on the page?'), return an `examinationresult`.\r\n" +
|
||
"- If the user wants to **perform an action**, return a `methodresult`.\r\n" +
|
||
"- If the required parameter is missing, return an `errorresult`.\r\n\r\n" +
|
||
|
||
"**Examples:**\r\n" +
|
||
"- User asks: 'Show me information about pricing' → `textresult`\r\n" +
|
||
"- User asks: 'What is displayed right now?' → `examinationresult`\r\n" +
|
||
"- User asks: 'Open the contact form' → `methodresult`\r\n" +
|
||
"- User asks: 'Contact support' but no email is found → `errorresult`\r\n\r\n" +
|
||
|
||
"**Context:**\r\n" +
|
||
"- Base responses on this initial document: {" + extractedText + "}\r\n" +
|
||
"- Current displayed HTML: {" + _scopedContentService.CurrentDOM + "}\r\n" +
|
||
|
||
"**IMPORTANT:**\r\n" +
|
||
"- If the request is about general content, **DO NOT use 'examinationresult'**.\r\n" +
|
||
"- If the request is about the currently displayed page, **DO NOT use 'textresult'**.\r\n" +
|
||
"- Do NOT format the response with markdown, code blocks, or `json` tags, do not add any title, or explanation besides the plain json object";
|
||
|
||
|
||
string interMediateResult = string.Empty;
|
||
if (!UseWebsocket)
|
||
{
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
interMediateResult = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
interMediateResult = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
interMediateResult = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userPrompt);
|
||
}
|
||
|
||
}
|
||
else
|
||
{
|
||
interMediateResult = await _openAIRealtimeService.GetFullChatGPTResponseAsync(sessionId, systemMessage, userPrompt);
|
||
}
|
||
|
||
//Console.Write("\r \n GetJsonResultFromQuery: Result decision: " + interMediateResult + "\r \n");
|
||
return interMediateResult;
|
||
}
|
||
|
||
public async Task<List<HtmlSnippet>> GetSnippetsForDisplay(string sessionId, string collectionName)
|
||
{
|
||
_apiKey = GetApiKey();
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Looking up the UI template elements for you");
|
||
//string availableSnippetList = "";
|
||
|
||
List<HtmlSnippet> snippets = new List<HtmlSnippet>();
|
||
|
||
var snippetscount = await _qDrantService.GetCollectionCount(collectionName);
|
||
for (int j = 1; j <= snippetscount; j++)
|
||
{
|
||
var snippet = await _qDrantService.GetSnippetAsync(j, collectionName);
|
||
QDrantGetPointResult x = JsonConvert.DeserializeObject<QDrantGetPointResult>(snippet);
|
||
|
||
snippets.Add(new HtmlSnippet { Id = x.result.payload.Id,
|
||
Name = x.result.payload.Name,
|
||
Description = x.result.payload.Description,
|
||
Type = x.result.payload.Type,
|
||
Variant = x.result.payload.Variant,
|
||
Tags = x.result.payload.Tags,
|
||
Slots = x.result.payload.Slots,
|
||
Html = x.result.payload.Html,
|
||
SampleHtml = x.result.payload.SampleHtml
|
||
});
|
||
//availableSnippetList += ("- " + x.result.payload.Name + ": " + x.result.payload.Description + ".\r\n");
|
||
}
|
||
////Console.Write(availableSnippetList);
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Loading UI elements from the design database");
|
||
|
||
//WTF????????
|
||
|
||
|
||
//var systemMessage = "You are a helpful assistant for generating responses using HTML templates. Analyze the user query and choose the most suitable snippet type(s) for rendering the response." +
|
||
// "Respond with only the snippet name(s), comma-separated. No explanation. Identify the most suitable " +
|
||
// "HTML code snippet type to use for rendering your response based on user queries. The available snippet types are: " +
|
||
// availableSnippetList +
|
||
// //"The content to answer from from is here: " + extractedText + ". " +
|
||
// "If multiple snippets apply, list them in order of priority. ";
|
||
|
||
//var userMessage = "How would you render the following text in html: " + interMediateResult + " ? ";
|
||
|
||
//string result = string.Empty;
|
||
//if (!UseWebsocket)
|
||
//{
|
||
|
||
// AiProvider = GetAiSettings();
|
||
// if (AiProvider == "cerebras")
|
||
// {
|
||
// result = await _cerebrasAPIService.GetSimpleCerebrasResponse(sessionId, systemMessage, userMessage);
|
||
// }
|
||
// else if (AiProvider == "chatgpt")
|
||
// {
|
||
// result = await _openAIApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userMessage);
|
||
|
||
// }
|
||
// else if (AiProvider == "deepseek")
|
||
// {
|
||
// result = await _deepSeekApiService.GetSimpleChatGPTResponse(sessionId, systemMessage, userMessage);
|
||
// }
|
||
|
||
//}
|
||
//else
|
||
//{
|
||
// result = await _openAIRealtimeService.GetFullChatGPTResponseAsync(sessionId, systemMessage, userMessage);
|
||
//}
|
||
|
||
//////Console.Write(result);
|
||
//List<string> snippetList = result.Split(new[] { ',' }).ToList();
|
||
|
||
//int i = 0;
|
||
|
||
//OnStatusChangeReceived?.Invoke(sessionId, "Oooh got it! Loading UI elements from the design database");
|
||
////Console.Write("ChatGPT decided!!!! ");
|
||
//let's send result to embeddingservice
|
||
|
||
//List<float[]> vectorsList = new List<float[]>();
|
||
|
||
//foreach (var snippet in snippetList)
|
||
//{
|
||
// var vectors = await _openAIEmbeddingService.GenerateEmbeddingAsync(result);
|
||
// vectorsList.Add(vectors);
|
||
//}
|
||
|
||
//List<int> pointIds = new List<int>();
|
||
//var collectionCount = await _qDrantService.GetCollectionCount(collectionName);
|
||
|
||
//if (collectionCount > 0)
|
||
//{
|
||
// foreach (var vector in vectorsList)
|
||
// {
|
||
// var qDrantresult = await _qDrantService.QuerySnippetAsync(vector, 3, collectionName);
|
||
// pointIds.Add(qDrantresult);
|
||
// }
|
||
|
||
// List<string> qDrantDataList = new List<string>();
|
||
|
||
// foreach (var pointId in pointIds)
|
||
// {
|
||
// var qDrantData = await _qDrantService.GetSnippetAsync(pointId, collectionName);
|
||
// qDrantDataList.Add(qDrantData);
|
||
// }
|
||
|
||
// List<QDrantGetPointResult> selectedPointList = new List<QDrantGetPointResult>();
|
||
// if (qDrantDataList != null)
|
||
// {
|
||
// foreach (var x in qDrantDataList)
|
||
// {
|
||
// var selectedPoint = JsonConvert.DeserializeObject<QDrantGetPointResult>(x)!;
|
||
// selectedPointList.Add(selectedPoint);
|
||
// }
|
||
// }
|
||
|
||
// string htmlToUse = "Your snippets to use: ";
|
||
// foreach (var selectedPoint in selectedPointList)
|
||
// {
|
||
// htmlToUse += selectedPoint.result.payload.Name + ": " + selectedPoint.result.payload.Html + ", ";
|
||
|
||
// }
|
||
// ////Console.Write(htmlToUse);
|
||
// return htmlToUse;
|
||
//}
|
||
|
||
return snippets;
|
||
|
||
}
|
||
|
||
public async Task DisplayHtml(string sessionId, LayoutPlan layoutPlan, List<HtmlSnippet> htmlToUse, string[]? topics = null)
|
||
{
|
||
//for textresult and errorresult
|
||
//Console.Write($"\n SessionId: {sessionId} \n");
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Casting spells to draw customized UI");
|
||
|
||
//Console.WriteLine($"DISPLAYHTML Snippets: {htmlToUse.Count}\n\n");
|
||
//Console.WriteLine($"DISPLAYHTML Topics: {topics} \n\n");
|
||
|
||
StringBuilder lst = new StringBuilder("You are a helpful assistant generating HTML content in " + _scopedContentService.SelectedLanguage + " using Bootstrap. \n\n" +
|
||
"### Rules to Follow: \n" +
|
||
"- Please generate clean and structured HTML that goes between the menu and the footer of a Bootstrap5 webpage.\n" +
|
||
"- DO NOT include `<head>`, `<body>`, or `<script>` tags—only content inside the Bootstrap container.\n" +
|
||
"- Use `<h1 class='p-3'>` for the title, based on the user's query.\n" +
|
||
"- Structure content using **separate `row` elements** for different sections.\n" +
|
||
"- Use Bootstrap classes to ensure proper spacing and alignment.\n" +
|
||
" - Use 'row justify-content-center' for layout, 'col-xx-x' classes for columns (ONLY IF multiple columns are needed), and always use 'img-fluid' class for images. \r\n" +
|
||
" - Do NOT use and 'col' class if there is only one column to display in the row. \n" +
|
||
"- Do NOT use additional class for paragraphs! \n" +
|
||
"- Do NOT nest images inside paragraphs! \n" +
|
||
"- Ensure clear **separation of content into multiple sections if multiple snippets are provided**.\n");
|
||
|
||
|
||
if (htmlToUse != null)
|
||
{
|
||
lst.AppendLine(
|
||
"### Using Provided Snippets:\n" +
|
||
"- You have been given **multiple HTML snippets**:\n \n");
|
||
foreach (var snippet in htmlToUse)
|
||
{
|
||
lst.AppendLine($"{snippet.Id}: {snippet.Name}: {snippet.Html}. \n \n");
|
||
lst.AppendLine($"Type: {snippet.Type}, Tags: {snippet.Tags}, Variant: {snippet.Variant}. \n \n");
|
||
}
|
||
lst.AppendLine("**DO NOT merge them into one**.\n" +
|
||
"- Use each snippet **as a separate section** inside its own div with class:`container-fluid`.\n" +
|
||
"- Using all the snippets is NOT mandatory, use them only if you identified content that fits in a specific snippet.\n" +
|
||
"- Validate if there are multiple variants of a snippet, and choose the best option, for example if the block has text and photo url, choose the variant that has image in it.\n \n" +
|
||
//"- Maintain their order and **DO NOT omit any snippet**.\n" +
|
||
//"- Example structure:\n\n" +
|
||
//" ```html\n" +
|
||
//" <div class='container-fluid'>\n" +
|
||
//" <div class='row'>\n" +
|
||
//" <!-- First snippet -->\n" +
|
||
|
||
//" </div>\n" +
|
||
//" <div class='row'>\n" +
|
||
//" <!-- Second snippet -->\n" +
|
||
//" </div>\n" +
|
||
//" </div>\n" +
|
||
//" ```\n\n" +
|
||
|
||
"- If a snippet contains a button, ensure it is placed inside a `div.text-center` for proper alignment.\n");
|
||
}
|
||
|
||
//if (photos != null && photos.Any())
|
||
//{
|
||
// lst.AppendLine(
|
||
// "### Handling Photos:\n" +
|
||
// "- ONLY use the provided photo URLs **as they are**, without modification.\n" +
|
||
// "- Do **NOT** generate or assume image URLs.\n" +
|
||
// "- Use these photos where appropriate:\n" +
|
||
// " " + string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}")) + "\n\n" +
|
||
// "- Example usage:\n" +
|
||
// //" ```html\n" +
|
||
// " <img src='" + photos.First().Value + "' class='img-fluid' alt='" + photos.First().Key + "' />\n" +
|
||
// //" ```\n\n" +
|
||
// "DO NOT modifiy the photo urls in any way."
|
||
// );
|
||
//}
|
||
|
||
if (topics != null && topics.Any())
|
||
{
|
||
lst.AppendLine(
|
||
"### Generating Topic Buttons:\n" +
|
||
"Start this section with a title `Related` " +
|
||
"- Create a **separate button** for each topic.\n" +
|
||
$"- Make sure the topics are in {_scopedContentService.SelectedLanguage}, if not, translate the name of them." +
|
||
"- Each button should use `btn btn-primary` and call `callAI('{original_non_translated_topicName}')` on click.\n" +
|
||
"- List of topics:\n" +
|
||
" " + string.Join(", ", topics) + "\n\n" +
|
||
"- Example:\n" +
|
||
" <button class='btn btn-primary' onclick='callAI(\"" + topics.FirstOrDefault() + "\")'>" + topics.FirstOrDefault() + "</button>\n" +
|
||
"Put this section always as last, on the bottom of the page.\n"
|
||
);
|
||
|
||
}
|
||
else
|
||
{
|
||
lst.AppendLine("- **No topics provided** → **Do NOT generate topic buttons.**");
|
||
}
|
||
|
||
|
||
lst.AppendLine(
|
||
"- DO **NOT** merge different content sections.\n" +
|
||
"- DO **NOT** wrap the entire content in a single `div`—use separate `container-fluid` divs.\n" +
|
||
"- DO **NOT** modify the photo urls in any way." +
|
||
"- Do **NOT** generate or assume new photo URLs.\n" +
|
||
"- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html\n" +
|
||
"- Do **NOT** assume ANY content, like missing prices, missing links. \n" +
|
||
"- If the snippet contains an image, but there is no photo url available, SKIP adding the image tag." +
|
||
"- **Never** add explanations or start with ```html syntax markers.\n");
|
||
|
||
string systemMessage = lst.ToString();
|
||
string userMessage = "Create a perfect, production ready, structured, responsive Bootstrap 5 webpage " +
|
||
"content from the provided layout and available html snippets. \r \n" +
|
||
"Read the layout plan, analyze the blocks, and identify if there is any matching html snippets for each block folowing these rules: \r \n" +
|
||
"- If there is no matching html snippet, generate the bootstrap 5 based layout for the given block. Else parse the block content into the available code snippet." +
|
||
"- For each block, add am opening and closing comment with the name of the block like: <!-- Hero start --> \r \n" +
|
||
"If the block's rawcontent contains photo url, display that photo within that section, not elsewhere.";
|
||
|
||
string assistantMessage = "`Provided layout plan, that contains the text to be displayed as HTML`:";
|
||
|
||
foreach (var block in layoutPlan.Blocks)
|
||
{
|
||
assistantMessage += $"Block type: {block.Type}, block content: {block.RawContent}";
|
||
}
|
||
//int mode = -1;
|
||
|
||
if (!UseWebsocket)
|
||
{
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
await _cerebrasAPIService.GetCerebrasStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
//await _deepSeekApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
}
|
||
|
||
|
||
//await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
|
||
}
|
||
else
|
||
{
|
||
await _openAIRealtimeService.GetChatGPTResponseAsync(sessionId, systemMessage, userMessage + assistantMessage);
|
||
}
|
||
}
|
||
|
||
|
||
public async Task<LayoutPlan?> DisplayLayoutPlanFromContent(string sessionId, string interMediateResult, List<HtmlSnippet> htmlToUse, string[]? topics = null, Dictionary<string, string>? photos = null)
|
||
{
|
||
//Console.Write($"\n SessionId: {sessionId} \n");
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Planning layout based on the provided content");
|
||
|
||
//Console.WriteLine($"LAYOUTBUILDER Text: {interMediateResult}\n\n");
|
||
//Console.WriteLine($"LAYOUTBUILDER Snippets: {htmlToUse.Count}\n\n");
|
||
//Console.WriteLine($"LAYOUTBUILDER Photos: {photos} \n\n");
|
||
//Console.WriteLine($"LAYOUTBUILDER Topics: {topics} \n\n");
|
||
|
||
var sb = new StringBuilder();
|
||
|
||
|
||
// 📌 STRICT FORMAT INSTRUCTION
|
||
sb.AppendLine("You are a helpful assistant whose ONLY task is to break down THE PROVIDED CONTENT into a structured JSON object for Bootstrap 5 pages, using .\n" +
|
||
"**layoutplan**:\r\n" +
|
||
"- `title`: \"The title of the page\"\r\n" +
|
||
"- `blocks`: \"an array of layoutblocks\"\r\n" +
|
||
".");
|
||
|
||
|
||
if ((htmlToUse != null))
|
||
{
|
||
sb.AppendLine("\n### Snippets Provided:");
|
||
foreach (var snippet in htmlToUse)
|
||
{
|
||
sb.AppendLine($"Snippet id: {snippet.Id}, Snippet name: {snippet.Name}, Snippet description: {snippet.Description}, Snippet type: {snippet.Type}, Snippet template code: {snippet.Html}");
|
||
}
|
||
sb.AppendLine("- Use them ONLY if relevant.");
|
||
}
|
||
|
||
if (photos != null && photos.Any())
|
||
{
|
||
sb.AppendLine("\n### Photos to be used in the blocks:");
|
||
sb.AppendLine(string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}")));
|
||
}
|
||
|
||
if (topics != null && topics.Any())
|
||
{
|
||
sb.AppendLine("\n### Topics:");
|
||
sb.AppendLine(string.Join(", ", topics));
|
||
}
|
||
|
||
sb.AppendLine("### ⛔️ ABSOLUTELY DO NOT:");
|
||
sb.AppendLine("- ❌ Return HTML structure trees like `<section><div>`...");
|
||
sb.AppendLine("- ❌ Include keys like `src`, `alt`, `url`, `items`, or nested object lists.");
|
||
sb.AppendLine("- ❌ Invent block types not on the list.");
|
||
sb.AppendLine("- ❌ Return any explanation, markdown, prose, comments, or fallback formats.");
|
||
sb.AppendLine("- ❌ Use any of these block types: `image`, `quote`, `list`, `layout`, `header`, `footer`, `sidebar`, etc.");
|
||
|
||
sb.AppendLine("\n### ✅ ALLOWED OUTPUT FORMAT:");
|
||
sb.AppendLine("{");
|
||
sb.AppendLine(" \"title\": \"string\",");
|
||
sb.AppendLine(" \"blocks\": [");
|
||
sb.AppendLine(" { \"type\": \"string\", \"rawcontent\": \"string (text)\", \"preferredsnippetid\" : an integer id of matching html snippet if one found., \"order\": an incremented integer to set the blocks in order }");
|
||
sb.AppendLine(" ]");
|
||
sb.AppendLine("}");
|
||
|
||
sb.AppendLine("\n### ✅ Allowed `type` values (only use these):");
|
||
sb.AppendLine("- hero");
|
||
sb.AppendLine("- text");
|
||
sb.AppendLine("- text-image");
|
||
sb.AppendLine("- features");
|
||
sb.AppendLine("- cta");
|
||
sb.AppendLine("- video");
|
||
sb.AppendLine("- icon-list");
|
||
sb.AppendLine("- event-list");
|
||
sb.AppendLine("- audio-player");
|
||
sb.AppendLine("- topic-buttons");
|
||
|
||
sb.AppendLine("\n### ❗ GOOD EXAMPLE:");
|
||
sb.AppendLine("{");
|
||
sb.AppendLine(" \"title\": \"Welcome to Our Product\",");
|
||
sb.AppendLine(" \"blocks\": [");
|
||
sb.AppendLine(" { \"type\": \"hero\", \"rawcontent\": \"Example text\", \"preferredsnippetid\": 1, \"order\": 0 },");
|
||
sb.AppendLine(" { \"type\": \"features\", \"rawcontent\": \"Example features\" }, \"order\": 1 },");
|
||
sb.AppendLine(" { \"type\": \"cta\", \"rawcontent\": \"call to action content\", \"order\": 2 } }");
|
||
sb.AppendLine(" ]");
|
||
sb.AppendLine("}");
|
||
|
||
sb.AppendLine("\n### ⛔ BAD EXAMPLES (DO NOT DO THIS):");
|
||
sb.AppendLine("- { \"type\": \"image\", \"src\": \"...\", \"alt\": \"...\" }");
|
||
sb.AppendLine("- { \"type\": \"list\", \"items\": [\"...\", \"...\"] }");
|
||
sb.AppendLine("- { \"layout\": { \"header\": ..., \"footer\": ... } }");
|
||
|
||
|
||
sb.AppendLine("\n### FINAL TASK:");
|
||
sb.AppendLine("Based on the following content, generate a JSON object following the exact format above. Output only the JSON. No markdown. No explanation.");
|
||
|
||
//sb.AppendLine("\nCONTENT TO ANALYZE FOR STRUCTURING:");
|
||
//sb.AppendLine(interMediateResult);
|
||
|
||
string systemMessage = sb.ToString();
|
||
string userMessage = "Based on the following content:\n\n*** " + interMediateResult + " ***\n\n" +
|
||
"and the available photos:";
|
||
if (photos != null && photos.Any())
|
||
{
|
||
userMessage += "\n### Photo urls to be used WITHOUT ANY MODIFICATION in the blocks:";
|
||
userMessage += string.Join(", ", photos.Select(kv => $"{kv.Key}: {kv.Value}"));
|
||
}
|
||
userMessage += ", return ONLY a JSON object representing a structured content layout with this exact shape:\n" +
|
||
"{ \"title\": string, \"blocks\": [ { \"type\": \"string\", \"rawcontent\": \"string (text)\", \"preferredsnippetid\" : an integer id of matching html snippet if one found., \"order\": an incremented integer to set the blocks in order } ] }\n\n" +
|
||
"⚠️ DO NOT use layout structures like 'header', 'sidebar', 'mainContent', or 'footer'.\n" +
|
||
"⚠️ DO NOT explain anything. Just return the pure JSON. No markdown, no ``` markers.\n\n" +
|
||
"🎯 OBJECTIVE:\n" +
|
||
"1. Determine 1 to 5 major content blocks from the input, grouping full paragraphs or sections together.\n" +
|
||
"Prefer **1 to 5 blocks max**, unless the content is extremely long. Each block should represent a semantically complete section.\r\n" +
|
||
"Group related ideas into one block, even if they are multiple paragraphs. Do NOT break up content just because it is a new sentence. \r\n" +
|
||
"2. Use meaningful, semantically grouped layout block types like: 'hero', 'features list', 'text with image', 'text', 'product list', 'call to action', 'videoplayer', 'audioplayer', etc.\n" +
|
||
"3. Each block should contain **rich content**, not just one or two sentences. Group related ideas into a single `rawcontent` field.\n\n" +
|
||
"DO NOT REMOVE ANY URLS (photo, wen link, etc) from the section or paragraph that it follows. \n\n" +
|
||
//"4. For ALL available image urls in the text, find a relevant block, and attach the UNMODIFIED photo url, to the specific block's rawcontent, like \"Photo url\": \"the provided photo url\".\n\n" +
|
||
"📌 Examples of good block grouping:\n" +
|
||
"- All introductory marketing text together in one 'hero' or 'text' block\n" +
|
||
"- A group of benefits or features into a single 'features' block\n" +
|
||
"- A pitch paragraph into a 'call to action' block\n\n" +
|
||
"🎨 Naming:\n" +
|
||
"- Try to find related type values in the available snippets list. If you find a relevant snippet for that block, use the name of that snippet for type, and add the id if the snippet as preferredsnippetid." +
|
||
"- If there is no relevant snippet, use such layout `type` values: hero, text, features, text with image, call to action, product list, team members, testimonial, event list, blogpost, article, video player, audio player, etc\n" +
|
||
//"- Use lowercase strings only.\n\n" +
|
||
"X Restrictions:"+
|
||
"- DO **NOT** modify the photo urls in any way." +
|
||
"- Do **NOT** generate or assume new photo URLs.\n" +
|
||
"- Do **NOT** modify photo URLs in any way.\n" +
|
||
"- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html \n" +
|
||
"- Do **NOT** assume ANY content, like missing prices, missing links, etc. \n"+
|
||
"✅ Final output: JSON only, well grouped, no extra explanation or formatting, no markdown, no ``` markers.\n";
|
||
string assistantMessage = "";
|
||
|
||
//Console.WriteLine($"LAYOUTBUILDER PROMPT: {systemMessage}\n\n");
|
||
|
||
// 🛡 Retry logic
|
||
string aiResponse = string.Empty;
|
||
LayoutPlan? layoutPlan = null;
|
||
int retry = 0;
|
||
|
||
AiProvider = GetAiSettings();
|
||
|
||
while (layoutPlan == null && retry < 2)
|
||
{
|
||
if (AiProvider == "chatgpt")
|
||
{
|
||
aiResponse = await _openAIApiService.GetSimpleChatGPTResponse(systemMessage, userMessage, assistantMessage);
|
||
}
|
||
else if (AiProvider == "cerebras")
|
||
{
|
||
aiResponse = await _cerebrasAPIService.GetSimpleCerebrasResponse(systemMessage, userMessage, assistantMessage);
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
aiResponse = await _deepSeekApiService.GetSimpleChatGPTResponse(systemMessage, userMessage, assistantMessage);
|
||
}
|
||
else
|
||
{
|
||
aiResponse = await _openAIRealtimeService.GetFullChatGPTResponseAsync(sessionId, systemMessage, userMessage, null);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(aiResponse))
|
||
{
|
||
//Console.WriteLine("Empty response from AI.");
|
||
return null;
|
||
}
|
||
|
||
try
|
||
{
|
||
//Console.WriteLine("AI Response:");
|
||
//Console.WriteLine(aiResponse);
|
||
|
||
aiResponse = TextHelper.FixJsonWithoutAI(aiResponse);
|
||
aiResponse = TextHelper.RemoveTabs(aiResponse);
|
||
layoutPlan = System.Text.Json.JsonSerializer.Deserialize<LayoutPlan>(aiResponse, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
|
||
if (layoutPlan?.Blocks == null || layoutPlan.Blocks.Any(b => string.IsNullOrEmpty(b.Type) || string.IsNullOrEmpty(b.RawContent)))
|
||
{
|
||
//try to fix with AI)
|
||
//Console.WriteLine("Trying to fix with AI.");
|
||
layoutPlan = await ValidateAndFixJson<LayoutPlan>(aiResponse, FixJsonWithAI);
|
||
if (layoutPlan?.Blocks == null || layoutPlan.Blocks.Any(b => string.IsNullOrEmpty(b.Type) || string.IsNullOrEmpty(b.RawContent)))
|
||
{
|
||
Console.WriteLine("Invalid block structure in response.");
|
||
layoutPlan = null;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine("Deserialization failed: " + ex.Message);
|
||
Console.WriteLine("Deserialization failed on json: " + aiResponse);
|
||
layoutPlan = null;
|
||
}
|
||
|
||
if (layoutPlan == null)
|
||
{
|
||
retry++;
|
||
Console.WriteLine("Retrying due to invalid format...");
|
||
}
|
||
}
|
||
|
||
return layoutPlan;
|
||
}
|
||
|
||
|
||
|
||
public async Task DisplayHtml(string sessionId, string interMediateResult, string methodToCall = "", string methodParameter = "")//, string[]? topics = null)
|
||
{
|
||
//for methodResult
|
||
//Console.Write($"\n SessionId: {sessionId} \n");
|
||
OnStatusChangeReceived?.Invoke(sessionId, "Casting spells to draw customized UI");
|
||
|
||
////Console.WriteLine($"DISPLAYHTML Text: {interMediateResult}\n\n");
|
||
////Console.WriteLine($"DISPLAYHTML Topics: {topics} \n\n");
|
||
|
||
StringBuilder lst = new StringBuilder("You are a helpful assistant generating HTML content in " + _scopedContentService.SelectedLanguage + " using Bootstrap. \n\n" +
|
||
"### Rules to Follow: \n" +
|
||
"- Please generate clean and structured HTML that fits inside a Bootstrap container.\n" +
|
||
"- DO NOT include `<head>`, `<body>`, or `<script>` tags—only content inside the Bootstrap container.\n" +
|
||
"- Use `<h1 class='p-3'>` for the title, based on the user's query.\n" +
|
||
"- Structure content using **separate `row` elements** for different sections.\n" +
|
||
"- Use Bootstrap classes to ensure proper spacing and alignment.\n" +
|
||
" - Use 'row justify-content-center' for layout, 'col-xx-x' classes for columns (ONLY IF multiple columns are needed), and always use 'img-fluid' class for images. \r\n" +
|
||
" - Do NOT use and 'col' class if there is only one column to display in the row. \n" +
|
||
"- Do NOT use additional class for paragraphs! \n" +
|
||
"- Do NOT nest images inside paragraphs! \n" +
|
||
"- Ensure clear **separation of content into multiple sections if multiple snippets are provided**.\n");
|
||
|
||
if (!string.IsNullOrEmpty(methodToCall))
|
||
{
|
||
lst.AppendLine("- At the END of the content, include a **single** Bootstrap button (`btn btn-primary`) that calls `" + methodToCall + "` with `" + methodParameter + "` on click.\n");
|
||
}
|
||
|
||
|
||
lst.AppendLine(
|
||
"- DO **NOT** merge different content sections.\n" +
|
||
"- DO **NOT** wrap the entire content in a single `div`—use separate `row` divs.\n" +
|
||
"- Do **NOT** skip or deny ANY part of the provided text. All of the provided text should be displayed as html\n" +
|
||
"- Do **NOT** assume ANY content, like missing prices, missing links. \n" +
|
||
"- If the snippet contains an image, but there is no photo url available, SKIP adding the image tag." +
|
||
"- **Never** add explanations or start with ```html syntax markers.\n");
|
||
|
||
string systemMessage = lst.ToString();
|
||
string userMessage = "Create a perfect, production ready, structured, responsive Bootstrap 5 webpage content from the provided text, please.";
|
||
string assistantMessage = "`Provided text to display as HTML`: `" + interMediateResult + "`";
|
||
//int mode = -1;
|
||
|
||
if (!UseWebsocket)
|
||
{
|
||
AiProvider = GetAiSettings();
|
||
if (AiProvider == "cerebras")
|
||
{
|
||
await _cerebrasAPIService.GetCerebrasStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
}
|
||
else if (AiProvider == "chatgpt")
|
||
{
|
||
await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
|
||
}
|
||
else if (AiProvider == "deepseek")
|
||
{
|
||
//await _deepSeekApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
}
|
||
|
||
|
||
//await _openAIApiService.GetChatGPTStreamedResponse(sessionId, systemMessage, userMessage, assistantMessage, -1);
|
||
|
||
}
|
||
else
|
||
{
|
||
await _openAIRealtimeService.GetChatGPTResponseAsync(sessionId, systemMessage, userMessage + assistantMessage);
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
|
||
}
|
||
|