Files
xSaherElmAiApi/AI/Services/XAiProvider.cs
T
2026-04-30 12:01:56 +03:30

1057 lines
33 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.AI;
using OllamaSharp;
using xAiApi.AI.Configuration;
using xAiApi.AI.Constants;
using xAiApi.AI.Interfaces;
using xAiApi.AI.Interfaces.Entities;
using xAiApi.AI.Models.Dtos;
using xAiApi.AI.Models.Entities;
using xAiModels.Constants;
using xAiModels.Extensions;
using xAiModels.Models;
using xCommons.Extensions;
using xExceptions.Constants;
using xIdentityModels.Models;
using xIdentityService.Extensions;
using xIdentityService.Interfaces;
using xModels.Dtos;
namespace xAiApi.AI.Services
{
/// <summary>
/// Provide Ai Capabilities Using AI Service ...
/// using User Data Info ...
/// </summary>
public class XAiProvider : IXAiProvider
{
//
#region Properties ...
/// <summary>
/// Configuration ...
/// </summary>
/// <value></value>
public XAiApiConfiguration Configuration { get; }
/// <summary>
/// Mapper Service ...
/// </summary>
private readonly IMapper mapper;
/// <summary>
/// LLm Chat Client ...
/// used for Interacting by LLM ...
/// </summary>
private readonly IChatClient chatClient;
/// <summary>
/// LLM Configuration Options ...
/// usually used for Enable Function Tools and etc ...
/// </summary>
private readonly ChatOptions chatOptions;
/// <summary>
/// Identity Provider Service ...
/// use for Validate User Info ...
/// </summary>
private readonly IXIdentityProvider identityProvider;
/// <summary>
/// Ai Project Repository Service ...
/// <see cref="IXAiProjectRepository"/>
/// </summary>
private readonly IXAiProjectRepository aiProjectRepository;
/// <summary>
/// Ai Message Repository Service ...
/// <see cref="IXAiMessageRepository"/>
/// </summary>
private readonly IXAiMessageRepository aiMessageRepository;
/// <summary>
/// Ai Conversation Repository Service ...
/// <see cref="IXAiConversationRepository"/>
/// </summary>
private readonly IXAiConversationRepository aiConversationRepository;
#endregion
//
#region Constructor ...
public XAiProvider(
IMapper mapper,
XAiApiConfiguration configuration,
IXIdentityProvider identityProvider,
IXAiProjectRepository aiProjectRepository,
IXAiMessageRepository aiMessageRepository,
IXAiConversationRepository aiConversationRepository
)
{
//
Configuration = configuration;
//
this.mapper = mapper;
this.identityProvider = identityProvider;
this.aiProjectRepository = aiProjectRepository;
this.aiMessageRepository = aiMessageRepository;
this.aiConversationRepository = aiConversationRepository;
//
// Prepare Chat Model ...
chatClient = new ChatClientBuilder(new OllamaApiClient(
new Uri(configuration.Url),
configuration.Model
))
// .UseFunctionInvocation()
.Build();
//
// Configuring Chant Options ...
chatOptions = new ChatOptions()
{
//
// Preparing Tools ...
// Tools = [
// //
// // Tempreature Tool ...
// AIFunctionFactory.Create((string location, string unit) => {
// //
// var temp = Random.Shared.Next(5, 20);
// var cond = Random.Shared.Next(0, 1) == 0 ? "sunny" : "rainy";
// //
// var result = $"The weather is {temp} degrees C and {cond}.";
// //
// return result;
// },
// "get_current_weather",
// "Get the current weather in given location"
// )
// ],
};
}
#endregion
//
#region Helpers ...
/// <summary>
/// Converts Entity to Dto ...
/// <see cref="XAiProjectDto"/>
/// </summary>
/// <param name="entity"><see cref="XAiProject"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XAiProjectDto> ToDto(
XAiProject entity,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<XAiProjectDto>(entity);
//
return result;
}
/// <summary>
/// Converts Entity to Dto ...
/// <see cref="XAiMessageDto"/>
/// </summary>
/// <param name="entity"><see cref="XAiMessage"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XAiMessageDto> ToDto(
XAiMessage entity,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<XAiMessageDto>(entity);
//
return result;
}
/// <summary>
/// Converts Entity to Dto ...
/// <see cref="XAiConversationDto"/>
/// </summary>
/// <param name="entity"><see cref="XAiConversation"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XAiConversationDto> ToDto(
XAiConversation entity,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<XAiConversationDto>(entity);
//
return result;
}
/// <summary>
/// Converts a List of Entities to Dtos ...
/// <see cref="XAiProjectDto"/>
/// </summary>
/// <param name="entities"><see cref="XAiProject"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<XAiProjectDto>> ToDtos(
IEnumerable<XAiProject> entities,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<IEnumerable<XAiProjectDto>>(entities);
//
return result;
}
/// <summary>
/// Converts a List of Entities to Dtos ...
/// <see cref="XAiMessageDto"/>
/// </summary>
/// <param name="entities"><see cref="XAiMessage"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<XAiMessageDto>> ToDtos(
IEnumerable<XAiMessage> entities,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<IEnumerable<XAiMessageDto>>(entities);
//
return result;
}
/// <summary>
/// Converts a List of Entities to Dtos ...
/// <see cref="XAiConversationDto"/>
/// </summary>
/// <param name="entities"><see cref="XAiConversation"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IEnumerable<XAiConversationDto>> ToDtos(
IEnumerable<XAiConversation> entities,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<IEnumerable<XAiConversationDto>>(entities);
//
return result;
}
/// <summary>
/// Converts a QueryResult of Entities to Dtos ...
/// <see cref="XAiProjectDto"/>
/// </summary>
/// <param name="queryResult"><see cref="XAiProject"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiProjectDto>> ToDtoQueryResult(
XQueryResult<XAiProject> queryResult,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<XQueryResult<XAiProjectDto>>(queryResult);
//
return result;
}
/// <summary>
/// Converts a QueryResult of Entities to Dtos ...
/// <see cref="XAiMessageDto"/>
/// </summary>
/// <param name="queryResult"><see cref="XAiMessage"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiMessageDto>> ToDtoQueryResult(
XQueryResult<XAiMessage> queryResult,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<XQueryResult<XAiMessageDto>>(queryResult);
//
return result;
}
/// <summary>
/// Converts a QueryResult of Entities to Dtos ...
/// <see cref="XAiConversationDto"/>
/// </summary>
/// <param name="queryResult"><see cref="XAiConversation"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiConversationDto>> ToDtoQueryResult(
XQueryResult<XAiConversation> queryResult,
CancellationToken cancellationToken = default
)
{
//
var result = mapper.Map<XQueryResult<XAiConversationDto>>(queryResult);
//
return result;
}
/// <summary>
/// Check Permission for Ai Actions ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<bool> HasPermission(
string projectId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
//
var result =
!userInfo.IsNullOrDefault() &&
!userInfo.UserId.IsNullOrEmpty();
if (!result)
{
return result;
}
//
try
{
//
var person = await GetPerson(
ownerId: userInfo.UserId,
cancellationToken: cancellationToken
);
//
result = !person.IsNullOrDefault();
}
catch
{
result = false;
}
//
return result;
}
/// <summary>
/// Prepare Prompt ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="projectId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> PreparePrompt(
string prompt,
string projectId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
//
var result = prompt;
//
// TODO: Complete this ...
//
return await Task.FromResult(result);
}
#endregion
//
#region Actions ...
/// <summary>
/// Ask a Question ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="projectId"></param>
/// <param name="conversationId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XAiMessageDto> Ask(
string prompt,
string projectId = null,
string conversationId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
//
// Get All Requirements and also
// Prepare Ai Project / Conversations and also ChatHistory
// which Including Prompt ...
var requirements = await GetRequirements(
prompt: prompt,
userInfo: userInfo,
projectId: projectId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
var isValid =
!requirements.IsNullOrDefault() &&
requirements.HasPermission;
if (!isValid)
{
XException.NotAllowed.Throw();
}
//
// Getting Response Message from LLM ...
var chatResult = await chatClient
.GetResponseAsync(
options: chatOptions,
messages: requirements.ChatHistory,
cancellationToken: cancellationToken
);
//
// Handle Issued Chat Response ...
isValid =
!chatResult.IsNullOrDefault() &&
chatResult.FinishReason == ChatFinishReason.Stop;
if (!isValid)
{
XException.ActionFailed.Throw();
}
//
// Prepare Response Message ...
var responseMessage = new XAiMessage
{
OwnerId = userInfo.UserId,
Content = chatResult.Text,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Role = XAiChatRole.Assistant,
ConversationId = requirements.AiConversation.Id
};
responseMessage = await aiMessageRepository.AddAsync(
saveChanges: true,
item: responseMessage
);
isValid = !responseMessage.IsNullOrDefault();
if (!isValid)
{
XException.InvalidData.Throw();
}
//
// Converts Entity to Dto ...
var result = await ToDto(
entity: responseMessage,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Ask a Question in Streaming ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="projectId"></param>
/// <param name="conversationId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async IAsyncEnumerable<XAiMessageUpdateDto> AskStream(
string prompt,
string projectId = null,
string conversationId = null,
XUserClaimsInfoDto userInfo = null,
[EnumeratorCancellation]
CancellationToken cancellationToken = default
)
{
//
// Get All Requirements and also
// Prepare Ai Project / Conversations and also ChatHistory
// which Including Prompt ...
var requirements = await GetRequirements(
prompt: prompt,
userInfo: userInfo,
projectId: projectId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
var isValid =
!requirements.IsNullOrDefault() &&
requirements.HasPermission;
if (!isValid)
{
XException.NotAllowed.Throw();
}
//
// Getting Response Message from LLM ...
var chatStream = chatClient
.GetStreamingResponseAsync(
options: chatOptions,
messages: requirements.ChatHistory,
cancellationToken: cancellationToken
);
isValid = !chatStream.IsNull();
if (!isValid)
{
yield break;
}
//
// Create an Empty Message for ID ...
var responseMessage = new XAiMessage
{
OwnerId = userInfo.UserId,
Role = XAiChatRole.Assistant,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Content = "Streaming Content",
ConversationId = requirements.AiConversation.Id
};
responseMessage = await aiMessageRepository.AddAsync(
saveChanges: true,
item: responseMessage
);
//
// Loop through Async Enumerable ...
string content = string.Empty;
var sequence = DateTime.UtcNow.ToTimestamp();
await foreach(var msg in chatStream)
{
//
// Crucially, check the cancellation token before continuing
// Throwing OperationCanceledException is the standard way to signal cancellation.
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
//
// Append recieved data to Content ...
content += msg.Text;
//
// Crate Message Update Model ...
var item = new XAiMessageUpdateDto
{
Sequense = sequence,
Content = msg.Text,
Id = responseMessage.Id,
OwnerId = userInfo.UserId,
UpdatedAt = DateTime.UtcNow,
Role = XAiChatRole.Assistant,
ConversationId = requirements.AiConversation.Id
};
//
sequence++;
//
yield return item;
}
//
// Update Response Message Entity ...
responseMessage.Content = content;
responseMessage.UpdatedAt = DateTime.UtcNow;
responseMessage = await aiMessageRepository.UpdateAsync(
saveChanges: true,
item: responseMessage,
id: responseMessage.Id
);
isValid = !responseMessage.IsNullOrDefault();
if (!isValid)
{
XException.ActionFailed.Throw();
}
//
// End Async Enumerable ...
yield break;
}
#endregion
//
#region Private ...
/// <summary>
/// Access Person class based on OwnerId ...
/// <see cref="XPersonDto"/>
/// </summary>
/// <param name="ownerId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<XPersonDto> GetPerson(
string ownerId,
CancellationToken cancellationToken = default
)
{
//
XPersonDto result = null;
//
if (!ownerId.IsNullOrEmpty())
{
//
var device = identityProvider.GetDevice();
var userInfo = await identityProvider.GetUserInfo(
device: device,
userSelectByParam: ownerId
);
if (userInfo.IsNullOrDefault())
{
XException.NotFound.Throw();
}
//
result = userInfo.ToXPersonDto();
}
//
return result;
}
/// <summary>
/// Retrieve Specified Project or Default ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<XAiProjectDto> GetProject(
string projectId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
//
// Validate UserInfo ...
var isValid =
!userInfo.IsNullOrDefault() &&
!userInfo.UserId.IsNullOrEmpty();
if (!isValid)
{
XException.NotAllowed.Throw();
}
//
// Check UserInfo ...
var personDto = await GetPerson(
ownerId: userInfo.UserId,
cancellationToken: cancellationToken
);
isValid = !personDto.IsNullOrDefault();
if (!isValid)
{
XException.NotFound.Throw();
}
//
XAiProject project = null;
//
// Check Default Project is Exists or not ...
var defaultProject = await aiProjectRepository.FindOneAsync(
containsDetail: true,
ignoreSoftDeleteds: true,
whereClause: x =>
x.OwnerId == userInfo.UserId &&
x.Title == XAiProjectEnum.Default.GetStringValue()
);
var isExists = !defaultProject.IsNullOrDefault();
if (!isExists)
{
//
// Create Default Project ...
defaultProject = new XAiProject
{
Prompt = string.Empty,
OwnerId = userInfo.UserId,
Description = string.Empty,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Title = XAiProjectEnum.Default.GetStringValue(),
};
//
defaultProject = await aiProjectRepository.AddAsync(
saveChanges: true,
item: defaultProject
);
//
isExists = !defaultProject.IsNullOrDefault();
}
//
// Validate Project ID ...
if (projectId.IsNullOrEmpty())
{
//
// Default Project ...
project = defaultProject;
projectId = defaultProject.Id.ToString();
}
else
{
//
// Special Project ...
project = await aiProjectRepository.FindOneAsync(
containsDetail: false,
ignoreSoftDeleteds: true,
whereClause: x =>
x.Id.ToString() == projectId &&
x.OwnerId == userInfo.UserId
);
isExists = !project.IsNullOrDefault();
if (!isExists)
{
//
// If Not Exists Use Default ...
project = defaultProject;
}
}
//
// Converts to Dto ...
var result = await ToDto(
entity: project,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Retrieve Conversation for Asking ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="conversationId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<XAiConversationDto> GetConversation(
string projectId = null,
string conversationId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
//
// Validate User Info ...
var isValid = await HasPermission(
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
if (!isValid)
{
XException.NotAllowed.Throw();
}
//
var project = await GetProject(
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
isValid = !project.IsNullOrDefault();
if (!isValid)
{
XException.NotFound.Throw();
}
//
// Check Conversation Exists ...
var result =
project.Conversations
.FirstOrDefault(x =>
x.OwnerId == userInfo.UserId &&
x.Id.ToString() == conversationId
);
var isExists =
!result.IsNullOrDefault() &&
!conversationId.IsNullOrEmpty();
if (!isExists)
{
//
var title = $"Conversation {Guid.NewGuid()}";
var conversationEntity = new XAiConversation
{
Title = title,
ProjectId = project.Id,
OwnerId = userInfo.UserId,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
//
conversationEntity = await aiConversationRepository.AddAsync(
saveChanges: true,
item: conversationEntity
);
isExists = !conversationEntity.IsNullOrDefault();
if (!isExists)
{
XException.NotFound.Throw();
}
//
result = await ToDto(
entity: conversationEntity,
cancellationToken: cancellationToken
);
}
//
return result;
}
/// <summary>
/// Prepare Requirements for Asking a Prompt from Model ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="projectId"></param>
/// <param name="conversationId"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<XAiRequirementDto> GetRequirements(
string prompt,
string projectId = null,
string conversationId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
//
var result = new XAiRequirementDto();
//
// Access Project ...
var project = await GetProject(
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
var isValid = !project.IsNullOrDefault();
if (!isValid)
{
XException.NotFound.Throw();
}
//
result.AiProject = project;
//
// Check Project is Default Project or not ...
var isDefaultProject = project.Title == XAiProjectEnum.Default.GetStringValue();
//
result.IsDefaultProject = isDefaultProject;
//
// Access Conversation ...
var conversation = await GetConversation(
userInfo: userInfo,
projectId: projectId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
isValid = !conversation.IsNullOrDefault();
if (!isValid)
{
XException.NotFound.Throw();
}
//
// Check First Question or not ...
var isFirstConversation = !conversation.Messages.HasChild();
//
result.AiConversation = conversation;
result.IsFirstConversation = isFirstConversation;
//
// Check Permissions ...
isValid = await HasPermission(
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
if (!isValid)
{
XException.NotAllowed.Throw();
}
//
result.HasPermission = isValid;
//
// Check if First Question ...
if (isFirstConversation)
{
//
// Add Introduction Prompt ...
var aiMessage = new XAiMessage
{
OwnerId = userInfo.UserId,
Role = XAiChatRole.System,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
ConversationId = conversation.Id,
Content = Configuration.Introduction,
};
aiMessage = await aiMessageRepository.AddAsync(
item: aiMessage,
saveChanges: true
);
isValid = !aiMessage.IsNullOrDefault();
if (!isValid)
{
XException.ActionFailed.Throw();
}
//
result.ChatHistory.Add(
new ChatMessage(
content: Configuration.Introduction,
role: XAiChatRole.System.ToChatRole()
)
);
//
// Add Project Prompt, if not Default Project ...
if (!isDefaultProject)
{
//
aiMessage = new XAiMessage
{
Content = project.Prompt,
OwnerId = userInfo.UserId,
Role = XAiChatRole.System,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
ConversationId = conversation.Id,
};
aiMessage = await aiMessageRepository.AddAsync(
item: aiMessage,
saveChanges: true
);
isValid = !aiMessage.IsNullOrDefault();
if (!isValid)
{
XException.ActionFailed.Throw();
}
//
result.ChatHistory.Add(
new ChatMessage(
content: project.Prompt,
role: XAiChatRole.System.ToChatRole()
)
);
}
}
else
{
//
// Complete Chat History ...
var orderedByTimeMessage = conversation.Messages
.OrderBy(x => x.CreatedOn)
.AsEnumerable();
foreach (var msg in orderedByTimeMessage)
{
//
result.ChatHistory.Add(
new ChatMessage(
content: msg.Content,
role: msg.Role.ToChatRole()
)
);
}
}
//
// Prepare Prompt ...
prompt = await PreparePrompt(
prompt: prompt,
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
if (result.ChatHistory.IsNull())
{
result.ChatHistory = new List<ChatMessage>();
}
//
// Prepare Prompt XAiMessage ...
var promptAiMessage = new XAiMessage
{
Content = prompt,
OwnerId = userInfo.UserId,
Role = XAiChatRole.User,
CreatedOn = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
ConversationId = conversation.Id,
};
//
// Add Prompt Message to Database ...
promptAiMessage = await aiMessageRepository.AddAsync(
saveChanges: true,
item: promptAiMessage
);
isValid = !promptAiMessage.IsNullOrDefault();
if (!isValid)
{
XException.InvalidData.Throw();
}
//
// Update Conversation ...
var conversationEntity = await aiConversationRepository
.GetAsync(id: result.AiConversation.Id);
conversation = mapper.Map<XAiConversationDto>(conversationEntity);
result.AiConversation = conversation;
//
result.ChatHistory.Add(
new ChatMessage(
content: promptAiMessage.Content,
role: promptAiMessage.Role.ToChatRole()
)
);
//
return result;
}
#endregion
}
}