SeemGen/Services/AIService.cs

1381 lines
73 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 users 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 websites 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 users unmodified query.\r\n\r\n" +
"3. **chatGPTExaminationResult** (for analyzing the currently displayed page only):\r\n" +
" - `type`: \"examinationresult\"\r\n" +
" - `text`: The users 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);
}
}
}
}