This commit is contained in:
2026-04-27 21:43:06 +03:30
parent bb5dc64a7d
commit 2b5e09912f
14 changed files with 877 additions and 387 deletions
+12
View File
@@ -1,6 +1,8 @@
using AutoMapper;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using xAiApi.AI.Interfaces;
using xAiApi.AI.Mppings;
using xAiApi.AI.Services;
namespace xAiApi.AI.DI
@@ -16,7 +18,17 @@ namespace xAiApi.AI.DI
)
{
//
// Register AutoMapper Using Mapping Profiles ...
services.AddAutoMapper(typeof(MappingProfiles).Assembly);
//
// TODO: Remove this ...
services.AddSingleton<IXAIService, XAIService>();
//
// Since we Used Repositories in this Service,
// we have to Register it Scoped Lifetime ...
services.AddScoped<IXAiProvider, XAiProvider>();
}
/// <summary>
+21 -2
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -21,7 +20,7 @@ namespace xAiApi.AI.Interfaces
/// </summary>
/// <value></value>
public XAiApiConfiguration Configuration { get; }
//
#region Helpers ...
/// <summary>
@@ -161,6 +160,8 @@ namespace xAiApi.AI.Interfaces
);
#endregion
//
#region Actions ...
/// <summary>
/// Ask a Question ...
/// </summary>
@@ -177,5 +178,23 @@ namespace xAiApi.AI.Interfaces
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
/// <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>
IAsyncEnumerable<XAiMessageUpdateDto> AskStream(
string prompt,
string projectId = null,
string conversationId = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
#endregion
}
}
+64
View File
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using xAiModels.Models;
using xCommons.Extensions;
using xModels.Base;
namespace xAiApi.AI.Models.Dtos
{
public class XAiRequirementDto : XBaseDto
{
/// <summary>
/// Prepared Ai Project ...
/// if Exists one Using Project ID,
/// if not Created Default Project ...
/// </summary>
public XAiProjectDto AiProject { get; set; }
/// <summary>
/// Prepared Ai Conversation ...
/// if Exists one Using Conversation ID,
/// if not Created new Conversation ...
/// </summary>
public XAiConversationDto AiConversation { get; set; }
/// <summary>
/// Prepared Chat History for Asking ...
/// including Asked Prompt ...
/// </summary>
public IList<ChatMessage> ChatHistory { get; set; } = new List<ChatMessage>();
/// <summary>
/// Check User has Permission on
/// Project, Conversation and LLM Usages also ...
/// </summary>
public bool HasPermission { get; set; } = false;
/// <summary>
/// Check Ai Project is Default Project of Specified User or not ...
/// Default Project is Main Conversations Contains ...
/// </summary>
public bool IsDefaultProject { get; set; } = false;
/// <summary>
/// Check Ai Conversations is New Conversation or not ...
/// </summary>
public bool IsFirstConversation { get; set; } = false;
/// <summary>
/// Check Model is Validate or not ...
/// </summary>
/// <returns></returns>
public bool IsValid()
{
//
var result =
HasPermission &&
!AiProject.IsNullOrDefault() &&
!AiConversation.IsNullOrDefault();
//
return result;
}
}
}
+43
View File
@@ -0,0 +1,43 @@
using AutoMapper;
using xAiApi.AI.Models.Entities;
using xAiModels.Models;
namespace xAiApi.AI.Mppings
{
public class MappingProfiles : Profile
{
public MappingProfiles()
{
//
#region Entity to Dto ...
//
CreateMap<XAiProject, XAiProjectDto>()
.ForMember(
x => x.Owner,
opt => opt.MapFrom<XAiProjectOwnerMappingResolver>()
);
//
CreateMap<XAiMessage, XAiMessageDto>()
.ForMember(
x => x.Owner,
opt => opt.MapFrom<XAiMessageOwnerMappingResolver>()
);
//
CreateMap<XAiConversation, XAiConversationDto>()
.ForMember(
x => x.Owner,
opt => opt.MapFrom<XAiConversationOwnerMappingResolver>()
);
#endregion
//
#region Dto to Entity ...
CreateMap<XAiProjectDto, XAiProject>();
CreateMap<XAiMessageDto, XAiMessage>();
CreateMap<XAiConversationDto, XAiConversation>();
#endregion
}
}
}
@@ -0,0 +1,14 @@
using xAiApi.AI.Models.Entities;
using xAiModels.Models;
using xIdentityService.Interfaces;
namespace xAiApi.AI.Mppings
{
public class XAiConversationOwnerMappingResolver : XBaseOwnerMappingResolver<XAiConversation, XAiConversationDto>
{
public XAiConversationOwnerMappingResolver(
IXIdentityProvider identityProvider
) : base(identityProvider)
{ }
}
}
@@ -0,0 +1,14 @@
using xAiApi.AI.Models.Entities;
using xAiModels.Models;
using xIdentityService.Interfaces;
namespace xAiApi.AI.Mppings
{
public class XAiMessageOwnerMappingResolver : XBaseOwnerMappingResolver<XAiMessage, XAiMessageDto>
{
public XAiMessageOwnerMappingResolver(
IXIdentityProvider identityProvider
) : base(identityProvider)
{ }
}
}
@@ -0,0 +1,14 @@
using xAiApi.AI.Models.Entities;
using xAiModels.Models;
using xIdentityService.Interfaces;
namespace xAiApi.AI.Mppings
{
public class XAiProjectOwnerMappingResolver : XBaseOwnerMappingResolver<XAiProject, XAiProjectDto>
{
public XAiProjectOwnerMappingResolver(
IXIdentityProvider identityProvider
) : base(identityProvider)
{ }
}
}
+97
View File
@@ -0,0 +1,97 @@
using AutoMapper;
using xModels.Dtos;
using xCommons.Extensions;
using xIdentityService.Interfaces;
using xIdentityService.Extensions;
namespace xAiApi.AI.Mppings
{
/// <summary>
/// an AutoMapper Value Resolver for Mapping TEntity to TDto
/// When TEntity Has OwnerId Property and Owner ...
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TDto"></typeparam>
public abstract class XBaseOwnerMappingResolver<TEntity, TDto> : IValueResolver<TEntity, TDto, XPersonDto>
where TDto : class
where TEntity : class
{
//
private string destPropertyName = "Owner";
private string srcPropertyName = "OwnerId";
private readonly IXIdentityProvider identityProvider;
public XBaseOwnerMappingResolver(
IXIdentityProvider identityProvider
)
{
this.identityProvider = identityProvider;
}
public XPersonDto Resolve(
TEntity source,
TDto destination,
XPersonDto destMember,
ResolutionContext context
)
{
//
XPersonDto result = null;
//
// Check Source is not Null ...
if (source.IsNull())
{
return result;
}
//
// Check OwnerId Exists ...
var isOwnerExists = source.HasProperty(srcPropertyName);
if (!isOwnerExists)
{
return result;
}
//
// Retrieve OwnerId from Source ...
var ownerId = source.GetProperty<TEntity, string>(srcPropertyName);
isOwnerExists = !ownerId.IsNullOrEmpty();
if (!isOwnerExists)
{
return result;
}
//
// Retrieve Private Trusted Device ...
var device = identityProvider.GetDevice();
//
// Retrieve Specified OwnerId's UserInfo ...
var userInfo = identityProvider.GetUserInfo(
device: device,
userSelectByParam: ownerId
).RunTask();
//
// Check UserInfo Exists ...
if (!userInfo.IsNullOrDefault())
{
//
// Converts UserInfo to XPersonDto ...
result = userInfo.ToXPersonDto();
}
//
// Setting Dest Member ...
destMember = result;
//
// Setting Destination Property Value ...
destination.SetProperty(destPropertyName, result);
//
return result;
}
}
}
+444 -301
View File
@@ -1,16 +1,17 @@
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 Microsoft.SemanticKernel.ChatCompletion;
using OllamaSharp;
using xAiApi.AI.Configuration;
using xAiApi.AI.Constants;
using xAiApi.AI.Extensions;
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;
@@ -30,25 +31,64 @@ namespace xAiApi.AI.Services
/// </summary>
public class XAiProvider : IXAiProvider
{
//
#region Properties ...
/// <summary>
/// Configuration ...
/// </summary>
/// <value></value>
public XAiApiConfiguration Configuration { get; }
//
private readonly IChatClient chatClient;
private readonly ChatOptions chatOptions;
private readonly IXIdentityProvider identityProvider;
private readonly IXAiProjectRepository aiProjectRepository;
private readonly IXAiMessageRepository aiMessagetRepository;
private readonly IXAiConversationRepository aiConversationRepository;
/// <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 aiMessagetRepository,
IXAiMessageRepository aiMessageRepository,
IXAiConversationRepository aiConversationRepository
)
{
@@ -56,9 +96,10 @@ namespace xAiApi.AI.Services
Configuration = configuration;
//
this.mapper = mapper;
this.identityProvider = identityProvider;
this.aiProjectRepository = aiProjectRepository;
this.aiMessagetRepository = aiMessagetRepository;
this.aiMessageRepository = aiMessageRepository;
this.aiConversationRepository = aiConversationRepository;
//
@@ -96,6 +137,7 @@ namespace xAiApi.AI.Services
// ],
};
}
#endregion
//
#region Helpers ...
@@ -112,28 +154,7 @@ namespace xAiApi.AI.Services
)
{
//
XAiProjectDto result = null;
//
if (!entity.IsNullOrDefault())
{
//
result = entity.ToDto();
if (!entity.OwnerId.IsNullOrEmpty())
{
//
result.Owner = await GetPerson(
entity.OwnerId,
cancellationToken
);
}
//
result.Conversations = await ToDtos(
entity.Conversations,
cancellationToken
);
}
var result = mapper.Map<XAiProjectDto>(entity);
//
return result;
@@ -152,28 +173,7 @@ namespace xAiApi.AI.Services
)
{
//
XAiMessageDto result = null;
//
if (!entity.IsNullOrDefault())
{
//
result = entity.ToDto();
if (!entity.OwnerId.IsNullOrEmpty())
{
//
result.Owner = await GetPerson(
entity.OwnerId,
cancellationToken
);
}
//
result.Conversation = await ToDto(
entity.Conversation,
cancellationToken
);
}
var result = mapper.Map<XAiMessageDto>(entity);
//
return result;
@@ -192,34 +192,7 @@ namespace xAiApi.AI.Services
)
{
//
XAiConversationDto result = null;
//
if (!entity.IsNullOrDefault())
{
//
result = entity.ToDto();
if (!entity.OwnerId.IsNullOrEmpty())
{
//
result.Owner = await GetPerson(
entity.OwnerId,
cancellationToken
);
}
//
result.Project = await ToDto(
entity.Project,
cancellationToken
);
//
result.Messages = await ToDtos(
entity.Messages,
cancellationToken
);
}
var result = mapper.Map<XAiConversationDto>(entity);
//
return result;
@@ -238,25 +211,10 @@ namespace xAiApi.AI.Services
)
{
//
IList<XAiProjectDto> list = new List<XAiProjectDto>();
var result = mapper.Map<IEnumerable<XAiProjectDto>>(entities);
//
var enumerable = entities.ToAsyncEnumerable(cancellationToken);
await foreach (var entity in enumerable)
{
//
var dto = await ToDto(
entity,
cancellationToken
);
if (!dto.IsNullOrDefault())
{
list.Add(dto);
}
}
//
return list.AsEnumerable();
return result;
}
/// <summary>
@@ -272,25 +230,10 @@ namespace xAiApi.AI.Services
)
{
//
IList<XAiMessageDto> list = new List<XAiMessageDto>();
var result = mapper.Map<IEnumerable<XAiMessageDto>>(entities);
//
var enumerable = entities.ToAsyncEnumerable(cancellationToken);
await foreach (var entity in enumerable)
{
//
var dto = await ToDto(
entity,
cancellationToken
);
if (!dto.IsNullOrDefault())
{
list.Add(dto);
}
}
//
return list.AsEnumerable();
return result;
}
/// <summary>
@@ -306,25 +249,10 @@ namespace xAiApi.AI.Services
)
{
//
IList<XAiConversationDto> list = new List<XAiConversationDto>();
var result = mapper.Map<IEnumerable<XAiConversationDto>>(entities);
//
var enumerable = entities.ToAsyncEnumerable(cancellationToken);
await foreach (var entity in enumerable)
{
//
var dto = await ToDto(
entity,
cancellationToken
);
if (!dto.IsNullOrDefault())
{
list.Add(dto);
}
}
//
return list.AsEnumerable();
return result;
}
/// <summary>
@@ -340,30 +268,7 @@ namespace xAiApi.AI.Services
)
{
//
var result = new XQueryResult<XAiProjectDto>();
//
if (!queryResult.IsNullOrDefault())
{
//
result = result.UpdateData(
updateWith: queryResult,
propertyBlackList: new List<string>
{
nameof(XQueryResult<XAiProject>.Items)
}
);
//
if (queryResult.Items.HasChild())
{
//
result.Items = await ToDtos(
queryResult.Items,
cancellationToken
);
}
}
var result = mapper.Map<XQueryResult<XAiProjectDto>>(queryResult);
//
return result;
@@ -382,30 +287,7 @@ namespace xAiApi.AI.Services
)
{
//
var result = new XQueryResult<XAiMessageDto>();
//
if (!queryResult.IsNullOrDefault())
{
//
result = result.UpdateData(
updateWith: queryResult,
propertyBlackList: new List<string>
{
nameof(XQueryResult<XAiMessage>.Items)
}
);
//
if (queryResult.Items.HasChild())
{
//
result.Items = await ToDtos(
queryResult.Items,
cancellationToken
);
}
}
var result = mapper.Map<XQueryResult<XAiMessageDto>>(queryResult);
//
return result;
@@ -424,30 +306,7 @@ namespace xAiApi.AI.Services
)
{
//
var result = new XQueryResult<XAiConversationDto>();
//
if (!queryResult.IsNullOrDefault())
{
//
result = result.UpdateData(
updateWith: queryResult,
propertyBlackList: new List<string>
{
nameof(XQueryResult<XAiConversation>.Items)
}
);
//
if (queryResult.Items.HasChild())
{
//
result.Items = await ToDtos(
queryResult.Items,
cancellationToken
);
}
}
var result = mapper.Map<XQueryResult<XAiConversationDto>>(queryResult);
//
return result;
@@ -522,6 +381,8 @@ namespace xAiApi.AI.Services
}
#endregion
//
#region Actions ...
/// <summary>
/// Ask a Question ...
/// </summary>
@@ -540,124 +401,195 @@ namespace xAiApi.AI.Services
)
{
//
// Access Project ...
var project = await GetProject(
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
var isValid = !project.IsNullOrDefault();
if (!isValid)
{
XException.NotFound.Throw();
}
//
// Check Project is Default Project or not ...
var isDefaultProject = project.Title == XAiProjectEnum.Default.GetStringValue();
//
// Access Conversation ...
var conversation = await GetConversation(
// 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
);
isValid = !conversation.IsNullOrDefault();
if (!isValid)
{
XException.NotFound.Throw();
}
//
// Check First Question or not ...
var isFirstConversation = !conversation.Messages.HasChild();
//
// Check Permissions ...
isValid = await HasPermission(
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
var isValid =
!requirements.IsNullOrDefault() &&
requirements.HasPermission;
if (!isValid)
{
XException.NotAllowed.Throw();
}
//
// Preparing Chat History ...
IList<ChatMessage> chatHistory = null;
//
// Check if First Question ...
if (isFirstConversation)
{
//
// Add Introduction Prompt ...
chatHistory.Add(
new ChatMessage(
content: Configuration.Introduction,
role: XAiChatRole.System.ToChatRole()
)
);
//
// Add Project Prompt, if not Default Project ...
if (!isDefaultProject)
{
//
chatHistory.Add(
new ChatMessage(
content: project.Prompt,
role: XAiChatRole.System.ToChatRole()
)
);
}
}
else
{
//
// Complete Chat History ...
// TODO: Complete this ...
}
//
// Prepare Prompt ...
prompt = await PreparePrompt(
prompt: prompt,
userInfo: userInfo,
projectId: projectId,
cancellationToken: cancellationToken
);
if (chatHistory.IsNull())
{
chatHistory = new List<ChatMessage>();
}
chatHistory.Add(
new ChatMessage(
content: prompt,
role: XAiChatRole.User.ToChatRole()
)
);
//
// Getting Response Message from LLM ...
var chatResult = await chatClient
.GetResponseAsync(
options: chatOptions,
messages: chatHistory,
messages: requirements.ChatHistory,
cancellationToken: cancellationToken
);
//
// TODO: Handle Here ...
// Handle Issued Chat Response ...
isValid =
!chatResult.IsNullOrDefault() &&
chatResult.FinishReason == ChatFinishReason.Stop;
if (!isValid)
{
XException.ActionFailed.Throw();
}
//
throw new NotImplementedException();
// 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 chatResponseEnumerable = chatClient
.GetStreamingResponseAsync(
options: chatOptions,
messages: requirements.ChatHistory,
cancellationToken: cancellationToken
);
//
// 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
);
//
// Collecting Data through Enumeration ...
string content = string.Empty;
var sequence = DateTime.UtcNow.ToTimestamp();
await foreach (var data in chatResponseEnumerable)
{
//
// Crucially, check the cancellation token before continuing
// Throwing OperationCanceledException is the standard way to signal cancellation.
if (cancellationToken.CanBeCanceled)
{
yield break;
}
//
// Append recieved data to Content ...
content += data;
//
// Crate Message Update Model ...
var item = new XAiMessageUpdateDto
{
Sequense = sequence,
Content = data.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();
}
//
yield break;
}
#endregion
//
#region Private ...
/// <summary>
@@ -893,6 +825,217 @@ namespace xAiApi.AI.Services
//
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 ...
foreach (var msg in conversation.Messages)
{
//
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();
}
//
result.ChatHistory.Add(
new ChatMessage(
content: prompt,
role: XAiChatRole.User.ToChatRole()
)
);
//
return result;
}
#endregion
}
}
+53 -83
View File
@@ -6,22 +6,34 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using OpenAI.Realtime;
using xAiApi.AI.Interfaces;
using xAiApi.Base;
using xAiModels.Models;
using xCommons.Configurations;
using xCommons.Extensions;
using xCommons.Providers;
using xIdentityHelper;
using xIdentityService.Constants;
using xIdentityService.Interfaces;
namespace xAiApi.Controllers.V1
{
/// <summary>
/// a Controller Which Provides Actions for
/// using Ai Service ...
/// </summary>
public class AIController : XIBaseV1Controller
{
private readonly IXAIService aiService;
/// <summary>
/// Ai Provider ...
/// </summary>
private readonly IXAiProvider aiService;
//
#region Constructor ...
public AIController(
IXAIService aiService,
IXAiProvider aiService,
ILogger<XIBaseV1Controller> logger,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
@@ -35,13 +47,26 @@ namespace xAiApi.Controllers.V1
{
this.aiService = aiService;
}
#endregion
//
#region Actions ...
/// <summary>
/// As a Message from Ai ...
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="prompt">specified prompt</param>
/// <param name="projectId">active Project Id</param>
/// <param name="conversationId">active Conversation Id</param>
/// <param name="cancellationToken"></param>
/// <returns>Ai Model's answer <see cref="XAiMessageDto"/></returns>
[HttpGet("AskAI")]
[Authorize(Policy = XPolicies.EnabledUser)]
public async Task<ActionResult<string>> AskAI(
public async Task<ActionResult<XAiMessageDto>> AskAI(
[FromQuery] string prompt,
[FromQuery] string projectId = null,
[FromQuery] string conversationId = null,
CancellationToken cancellationToken = default
)
{
@@ -50,43 +75,18 @@ namespace xAiApi.Controllers.V1
try
{
//
var userInfo = await GetUserInfo();
var result = await aiService
.GetTextResponseAsync(
.Ask(
prompt: prompt,
userInfo: userInfo,
projectId: projectId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
[HttpGet("AskAIAsChatMessage")]
[Authorize(Policy = XPolicies.EnabledUser)]
public async Task<ActionResult<ChatMessage>> AskAIAsChatMessage(
[FromQuery] string prompt,
CancellationToken cancellationToken = default
)
{
//
// Do ...
try
{
//
var result = await aiService
.GetResponseAsync(
prompt: prompt,
cancellationToken: cancellationToken
);
//
return Ok(result);
return Ok(result.ToDynamicObject());
}
catch (Exception ex)
{
@@ -100,6 +100,8 @@ namespace xAiApi.Controllers.V1
[Authorize(Policy = XPolicies.EnabledUser)]
public async Task AskAIStream(
[FromQuery] string prompt,
[FromQuery] string projectId = null,
[FromQuery] string conversationId = null,
CancellationToken cancellationToken = default
)
{
@@ -113,69 +115,37 @@ namespace xAiApi.Controllers.V1
.Append("Content-Type", "text/event-stream");
//
// Retrieving Stream ...
var userInfo = await GetUserInfo();
var stream = aiService
.GetTextReponseStreamAsync(
.AskStream(
prompt: prompt,
userInfo: userInfo,
projectId: projectId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
//
await foreach (var message in stream)
{
//
// Do What we Have to Do with Message ...
// Converts Model to Json String ...
var json = message.ToJSON();
//
var msgBytes = message.ToBytes();
// Generates Json byte[] ...
var bytes = json.ToBytes();
//
// Write Bytes to Stream ...
await Response.Body.WriteAsync(
msgBytes,
bytes,
0,
msgBytes.Length
bytes.Length,
cancellationToken
);
await Response.Body.FlushAsync();
}
//
// Closing Connection ...
await Response.Body.FlushAsync();
}
catch (Exception)
{ }
}
[HttpGet("AskAIStreamAsChatMessage")]
[Authorize(Policy = XPolicies.EnabledUser)]
public async Task AskAIStreamAsChatMessage(
[FromQuery] string prompt,
CancellationToken cancellationToken = default
)
{
//
// Do ...
try
{
//
// Adding Content Type ...
Response.Headers
.Append("Content-Type", "text/event-stream");
//
// Retrieving Stream ...
var stream = aiService
.GetReponseStreamAsync(
prompt: prompt,
cancellationToken: cancellationToken
);
//
await foreach (var message in stream)
{
//
await Response.Body.WriteModelToStream(
model: message,
cancellationToken: cancellationToken
);
// Flushing Stream ...
await Response.Body.FlushAsync(cancellationToken);
}
//
@@ -0,0 +1,33 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using xAiApi.AI.Interfaces.Entities;
using xAiApi.AI.Models.Entities;
using xAiApi.Base;
using xCommons.Configurations;
using xCommons.Providers;
using xIdentityService.Interfaces;
using xPushService.Base;
namespace xAiApi.Controllers.V1.Entities
{
public class AiConversationsController : XIBaseV1EntityHubController<XAiConversation, Guid>
{
public AiConversationsController(
ILogger<AiConversationsController> logger,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider,
IXAiConversationRepository repository,
IHubContext<XBaseEntityHub<XAiConversation, Guid>> hub = null
) : base(
logger,
appConfiguration,
identityProvider,
validationProvider,
repository,
hub
)
{ }
}
}
@@ -0,0 +1,33 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using xAiApi.AI.Interfaces.Entities;
using xAiApi.AI.Models.Entities;
using xAiApi.Base;
using xCommons.Configurations;
using xCommons.Providers;
using xIdentityService.Interfaces;
using xPushService.Base;
namespace xAiApi.Controllers.V1.Entities
{
public class AiMessagesController : XIBaseV1EntityHubController<XAiMessage, Guid>
{
public AiMessagesController(
ILogger<AiMessagesController> logger,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider,
IXAiMessageRepository repository,
IHubContext<XBaseEntityHub<XAiMessage, Guid>> hub = null
) : base(
logger,
appConfiguration,
identityProvider,
validationProvider,
repository,
hub
)
{ }
}
}
@@ -0,0 +1,33 @@
using System;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using xAiApi.AI.Interfaces.Entities;
using xAiApi.AI.Models.Entities;
using xAiApi.Base;
using xCommons.Configurations;
using xCommons.Providers;
using xIdentityService.Interfaces;
using xPushService.Base;
namespace xAiApi.Controllers.V1.Entities
{
public class AiProjectsController : XIBaseV1EntityHubController<XAiProject, Guid>
{
public AiProjectsController(
ILogger<AiProjectsController> logger,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider,
IXAiProjectRepository repository,
IHubContext<XBaseEntityHub<XAiProject, Guid>> hub = null
) : base(
logger,
appConfiguration,
identityProvider,
validationProvider,
repository,
hub
)
{ }
}
}
@@ -36,7 +36,8 @@ namespace xAiApi.Data.Repositories.Ef
{
//
return dbSet
.Include(x => x.Conversations);
.Include(x => x.Conversations)
.ThenInclude(x => x.Messages);
}
//