Compare commits

...
1 Commits
Author SHA1 Message Date
saherelm aac3eb92c1 Start Add File Support in Messages Feature ... 2026-09-30 23:13:32 +03:30
8 changed files with 314 additions and 163 deletions
+50
View File
@@ -652,6 +652,56 @@ namespace xAiApi.Controllers
await Response.Body.FlushAsync();
}
}
/// <summary>
/// Ask Question with Files and Get Response ...
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("AskWithFiles")]
[Consumes("multipart/form-data")]
public async Task<ActionResult<string>> AskWithFiles(
[FromForm] XAiResponseRequest request,
[FromForm] IFormFileCollection files,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (!request.IsValid())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
var result = await aiService.AskAsync(
files: files,
userInfo: userInfo,
prompt: request.Prompt,
ownerId: userInfo.UserId,
connectionId: connectionId,
projectId: request.ProjectId,
cancellationToken: cancellationToken,
conversationId: request.ConversationId
);
//
return Ok(result);
}
catch (Exception ex)
{
var result = GetExceptionActionResult(ex);
return result;
}
}
#endregion
}
}
+16 -4
View File
@@ -5,6 +5,7 @@ using System.Threading;
using xAiModels.Models;
using xAiModels.Models.Dtos;
using System.Threading.Tasks;
using xIdentityModels.Models;
using Microsoft.Extensions.AI;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
@@ -87,7 +88,7 @@ namespace xAiApi.Interfaces
#endregion
//
#region Data Provider Based Actions ...
#region Loaders ...
/// <summary>
/// Load AI Projects ...
/// </summary>
@@ -126,7 +127,10 @@ namespace xAiApi.Interfaces
Guid conversationId,
CancellationToken cancellationToken = default
);
#endregion
//
#region Queries ...
/// <summary>
/// Query Projects ...
/// </summary>
@@ -139,7 +143,7 @@ namespace xAiApi.Interfaces
XQuery query,
CancellationToken cancellationToken = default
);
/// <summary>
/// Query Conversations ...
/// </summary>
@@ -154,7 +158,7 @@ namespace xAiApi.Interfaces
XQuery query,
CancellationToken cancellationToken = default
);
/// <summary>
/// Query Messages ...
/// </summary>
@@ -169,7 +173,10 @@ namespace xAiApi.Interfaces
XQuery query,
CancellationToken cancellationToken = default
);
#endregion
//
#region Creators ...
/// <summary>
/// Create an AI Project ...
/// </summary>
@@ -201,7 +208,10 @@ namespace xAiApi.Interfaces
string connectionId = null,
CancellationToken cancellationToken = default
);
#endregion
//
#region Data Actions ...
/// <summary>
/// Ask a Question as Prompt and Retrieve Response ...
/// </summary>
@@ -211,6 +221,7 @@ namespace xAiApi.Interfaces
/// <param name="conversationId"></param>
/// <param name="connectionId"></param>
/// <param name="files"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XAiMessageDto> AskAsync(
@@ -220,6 +231,7 @@ namespace xAiApi.Interfaces
Guid conversationId,
string connectionId = null,
IFormFileCollection files = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
+15
View File
@@ -6,6 +6,7 @@ using xAiModels.Models.Dtos;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace xAiApi.Interfaces
{
@@ -176,6 +177,20 @@ namespace xAiApi.Interfaces
[FromBody] XAiResponseRequest request,
CancellationToken cancellationToken = default
);
/// <summary>
/// Ask Question with Files and Get Response ...
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("AskWithFiles")]
[Consumes("multipart/form-data")]
Task<ActionResult<string>> AskWithFiles(
[FromForm] XAiResponseRequest request,
[FromForm] IFormFileCollection files,
CancellationToken cancellationToken = default
);
#endregion
}
}
+218 -158
View File
@@ -15,11 +15,14 @@ using xCommons.Extensions;
using xAiModels.Constants;
using xAiModels.Interfaces;
using xAiModels.Extensions;
using xAiService.Constants;
using xAiApi.Configurations;
using xExceptions.Constants;
using xAiModels.Models.Dtos;
using xAiService.Extensions;
using System.Threading.Tasks;
using xIdentityModels.Models;
using xFileService.Interfaces;
using Microsoft.Extensions.AI;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
@@ -44,12 +47,14 @@ namespace xAiApi.Providers
/// </summary>
public XAiModelDescriptor Descriptor { get; }
private readonly IXFileProvider fileProvider;
private readonly IXAiDataProvider dataProvider;
private readonly ILogger<XAIServiceBase> logger;
private readonly XAiApiConfiguration configuration;
private readonly XValidationProvider validationProvider;
protected XAIServiceBase(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
ILogger<XAIServiceBase> logger,
XAiApiConfiguration configuration,
@@ -59,6 +64,7 @@ namespace xAiApi.Providers
{
//
this.logger = logger;
this.fileProvider = fileProvider;
this.dataProvider = dataProvider;
this.configuration = configuration;
this.validationProvider = validationProvider;
@@ -347,7 +353,7 @@ namespace xAiApi.Providers
#endregion
//
#region Data Provider Based Actions ...
#region Loaders ...
/// <summary>
/// Load AI Projects ...
/// </summary>
@@ -422,8 +428,6 @@ namespace xAiApi.Providers
return result;
}
/// <summary>
/// Load Conversations of Specified AI Project ...
/// </summary>
@@ -483,7 +487,153 @@ namespace xAiApi.Providers
//
return result;
}
#endregion
//
#region Queries ...
/// <summary>
/// Query Projects ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiProjectDto>> QueryProjects(
string ownerId,
XQuery query,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (ownerId.IsNullOrEmpty())
{
XException.InvalidArgs.Throw();
}
//
var result = await dataProvider.QueryProjects(
query: query,
ownerId: ownerId,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Query Conversations ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="projectId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiConversationDto>> QueryConversations(
string ownerId,
Guid projectId,
XQuery query,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (ownerId.IsNullOrEmpty() ||
projectId.IsDefaultGuid())
{
XException.InvalidArgs.Throw();
}
//
// Retrieve Project ...
var project = await dataProvider.GetProject(
id: projectId,
fillConversations: false,
fillConversationMessages: false,
cancellationToken: cancellationToken
);
if (project.IsNullOrDefault())
{
XException.NotFound.Throw();
}
//
// Validate Owner ...
if (ownerId != project.OwnerId)
{
XException.NotAllowed.Throw();
}
//
var result = await dataProvider.QueryConversations(
query: query,
ownerId: ownerId,
projectId: projectId,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Query Messages ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="conversationId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiMessageDto>> QueryMessages(
string ownerId,
Guid conversationId,
XQuery query,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (ownerId.IsNullOrEmpty() ||
conversationId.IsDefaultGuid())
{
XException.InvalidArgs.Throw();
}
//
// Retrieve Conversation ...
var conversation = await dataProvider.GetConversation(
id: conversationId,
fillMessages: false,
cancellationToken: cancellationToken
);
if (conversation.IsNullOrDefault())
{
XException.NotFound.Throw();
}
//
// Validate Owner ...
if (ownerId != conversation.OwnerId)
{
XException.NotAllowed.Throw();
}
//
var result = await dataProvider.QueryMessages(
query: query,
ownerId: ownerId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
//
return result;
}
#endregion
//
#region Creators ...
/// <summary>
/// Create an AI Project ...
/// </summary>
@@ -540,7 +690,10 @@ namespace xAiApi.Providers
//
return result;
}
#endregion
//
#region Data Actions ...
/// <summary>
/// Ask a Question as Prompt and Retrieve Response ...
/// </summary>
@@ -550,6 +703,7 @@ namespace xAiApi.Providers
/// <param name="conversationId"></param>
/// <param name="connectionId"></param>
/// <param name="files"></param>
/// <param name="userInfo"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XAiMessageDto> AskAsync(
@@ -559,6 +713,7 @@ namespace xAiApi.Providers
Guid conversationId,
string connectionId = null,
IFormFileCollection files = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
)
{
@@ -638,8 +793,26 @@ namespace xAiApi.Providers
);
//
// TODO: Parse Files ...
// for Attached them into Prompt Message ...
// Upload Files ...
IEnumerable<xFileService.Models.Dtos.XFileDto> uploadedFiles = null;
if (files.HasChild())
{
//
// Validate User Info ...
if (userInfo.IsNullOrDefault())
{
XException.InvalidArgs.Throw();
}
//
// Upload Files ...
uploadedFiles = await fileProvider.Upload(
files: files,
userInfo: userInfo,
connectionId: connectionId,
cancellationToken: cancellationToken
);
}
//
// Handle Memory ...
@@ -660,21 +833,55 @@ namespace xAiApi.Providers
ConversationId = conversationId,
ConversationTitle = conversation.Title
};
//
// Attached Uploaded File Id's as Meta Data ...
if (uploadedFiles.HasChild())
{
//
var fileIds = uploadedFiles.Select(f => f.Id).ToList();
promptMessage.MetaDatas = new Dictionary<string, object>
{
{ XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds }
}.ToJSON();
}
//
// Save Prompt Message ...
promptMessage = await dataProvider.AddMessage(
item: promptMessage,
connectionId: connectionId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
promptMessage = await dataProvider.Enrich(
source: promptMessage,
cancellationToken: cancellationToken
);
//
// Attach Uploaded Files ...
// Since Prompt Message Has Id now, we can Attach Uploaded Files to it ...
IList<AIContent> fileContents = null;
if (uploadedFiles.HasChild())
{
//
foreach (var uf in uploadedFiles)
{
//
await fileProvider.AddReference(
id: uf.Id,
userInfo: userInfo,
connectionId: connectionId,
forProvidedId: promptMessage.Id,
cancellationToken: cancellationToken,
providedFor: XAiServiceConstants.XAiMessageFilesIdentifier
);
}
//
// TODO: Extract File Contents ...
}
//
// Converts to ChatMessage ...
var promptChatMessage = promptMessage
.ToChatMessages();
.ToChatMessages(fileContents);
//
// Ask Questions From LLM ...
@@ -701,10 +908,6 @@ namespace xAiApi.Providers
conversationId: conversationId,
cancellationToken: cancellationToken
);
result = await dataProvider.Enrich(
source: result,
cancellationToken: cancellationToken
);
//
return result;
@@ -904,149 +1107,6 @@ namespace xAiApi.Providers
}
#endregion
//
#region Query ...
/// <summary>
/// Query Projects ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiProjectDto>> QueryProjects(
string ownerId,
XQuery query,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (ownerId.IsNullOrEmpty())
{
XException.InvalidArgs.Throw();
}
//
var result = await dataProvider.QueryProjects(
query: query,
ownerId: ownerId,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Query Conversations ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="projectId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiConversationDto>> QueryConversations(
string ownerId,
Guid projectId,
XQuery query,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (ownerId.IsNullOrEmpty() ||
projectId.IsDefaultGuid())
{
XException.InvalidArgs.Throw();
}
//
// Retrieve Project ...
var project = await dataProvider.GetProject(
id: projectId,
fillConversations: false,
fillConversationMessages: false,
cancellationToken: cancellationToken
);
if (project.IsNullOrDefault())
{
XException.NotFound.Throw();
}
//
// Validate Owner ...
if (ownerId != project.OwnerId)
{
XException.NotAllowed.Throw();
}
//
var result = await dataProvider.QueryConversations(
query: query,
ownerId: ownerId,
projectId: projectId,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Query Messages ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="conversationId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XQueryResult<XAiMessageDto>> QueryMessages(
string ownerId,
Guid conversationId,
XQuery query,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (ownerId.IsNullOrEmpty() ||
conversationId.IsDefaultGuid())
{
XException.InvalidArgs.Throw();
}
//
// Retrieve Conversation ...
var conversation = await dataProvider.GetConversation(
id: conversationId,
fillMessages: false,
cancellationToken: cancellationToken
);
if (conversation.IsNullOrDefault())
{
XException.NotFound.Throw();
}
//
// Validate Owner ...
if (ownerId != conversation.OwnerId)
{
XException.NotAllowed.Throw();
}
//
var result = await dataProvider.QueryMessages(
query: query,
ownerId: ownerId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
//
return result;
}
#endregion
/// <summary>
/// Base Dispose Action ...
/// </summary>
+6 -1
View File
@@ -16,6 +16,7 @@ using xAiModels.Interfaces;
using xAiApi.Configurations;
using xExceptions.Constants;
using System.Threading.Tasks;
using xFileService.Interfaces;
using Microsoft.Extensions.AI;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
@@ -28,15 +29,17 @@ namespace xAiApi.Providers
/// </summary>
public abstract class XAiEmbeddingServiceBase : XAIServiceBase, IXAiEmbeddingServiceBase
{
private readonly IXFileProvider fileProvider;
private readonly XAiModelDescriptor descriptor;
private readonly ILogger<XAiEmbeddingServiceBase> logger;
protected XAiEmbeddingServiceBase(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
XAiApiConfiguration configuration,
ILogger<XAiEmbeddingServiceBase> logger,
string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName
) : base(dataProvider, logger, configuration, null)
) : base(fileProvider, dataProvider, logger, configuration, null)
{
//
// Prepare Embedding Model Descriptor ...
@@ -46,6 +49,8 @@ namespace xAiApi.Providers
XException.InvalidConfiguration.Throw();
}
this.fileProvider = fileProvider;
//
this.logger = logger;
}
+3
View File
@@ -3,6 +3,7 @@ using xAiApi.Interfaces;
using xCommons.Providers;
using xAiModels.Interfaces;
using xAiApi.Configurations;
using xFileService.Interfaces;
using Microsoft.Extensions.Logging;
namespace xAiApi.Providers
@@ -10,11 +11,13 @@ namespace xAiApi.Providers
public class XDefaultAiService : XAIServiceBase, IXDefaultAiService
{
public XDefaultAiService(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
ILogger<XDefaultAiService> logger,
XAiApiConfiguration configuration,
XValidationProvider validationProvider
) : base(
fileProvider,
dataProvider,
logger,
configuration,
+3
View File
@@ -2,6 +2,7 @@ using xAiApi.Constants;
using xAiApi.Interfaces;
using xAiModels.Interfaces;
using xAiApi.Configurations;
using xFileService.Interfaces;
using Microsoft.Extensions.Logging;
namespace xAiApi.Providers
@@ -9,10 +10,12 @@ namespace xAiApi.Providers
public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService
{
public XDefaultEmbeddingService(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
XAiApiConfiguration configuration,
ILogger<XDefaultEmbeddingService> logger
) : base(
fileProvider,
dataProvider,
configuration,
logger,
+3
View File
@@ -4,6 +4,7 @@ using xCommons.Providers;
using xAiModels.Interfaces;
using xAiApi.Configurations;
using Microsoft.Extensions.AI;
using xFileService.Interfaces;
using Microsoft.Extensions.Logging;
namespace xAiApi.Providers
@@ -11,6 +12,7 @@ namespace xAiApi.Providers
public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService
{
public XDefaultThinkingAiService(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
ILogger<XAIServiceBase> logger,
XAiApiConfiguration configuration,
@@ -18,6 +20,7 @@ namespace xAiApi.Providers
ReasoningEffort effort = ReasoningEffort.Medium,
ReasoningOutput output = ReasoningOutput.Full
) : base(
fileProvider,
dataProvider,
logger,
configuration,