Compare commits

...
18 Commits
Author SHA1 Message Date
saherelm 70c2d5a649 add support for OCR ... 2026-10-03 22:26:03 +03:30
saherelm 7cad889cd0 last ... 2026-10-03 17:42:36 +03:30
saherelm c30d93be0b last ... 2026-10-02 22:12:46 +03:30
saherelm 4521a53734 fix file content extractor mechanism ... 2026-10-02 00:07:58 +03:30
saherelm f32972b55f adding File Content Extraction Services ... 2026-10-01 01:16:24 +03:30
saherelm aac3eb92c1 Start Add File Support in Messages Feature ... 2026-09-30 23:13:32 +03:30
saherelm efaf71d962 last ... 2026-09-30 15:27:39 +03:30
saherelm 383e42a8d2 last ... 2026-09-29 17:41:48 +03:30
saherelm fc679fe294 last ... 2026-09-28 22:36:02 +03:30
saherelm 767479e80d last ... 2026-09-28 16:35:04 +03:30
saherelm 7ddf4d90c4 last ... 2026-09-27 22:01:25 +03:30
saherelm d638292216 last ... 2026-09-27 16:08:51 +03:30
saherelm b596afbe57 last ... 2026-09-26 16:46:30 +03:30
saherelm 3fbdebbdd3 last ... 2026-09-26 14:03:25 +03:30
saherelm ed8a66f432 refactor for better support of streaming and etc ... 2026-09-26 10:46:14 +03:30
saherelm 9afa413c6f adding vector model enabling and also test all using OpenAI model ... 2026-09-26 01:24:19 +03:30
saherelm 22de433023 last ... 2026-09-25 19:47:30 +03:30
saherelm ae2713eddd fix typos ... 2026-09-19 12:14:44 +03:30
49 changed files with 4167 additions and 450 deletions
+6
View File
@@ -1,4 +1,5 @@
using xAiApi.Models;
using xAiModels.Models;
namespace xAiApi.Configurations
{
@@ -13,5 +14,10 @@ namespace xAiApi.Configurations
/// Allowed Prompts to Use ...
/// </summary>
public XAiPromptDescriptor[] Prompts { get; set; } = [];
/// <summary>
/// OCR Configuration ...
/// </summary>
public XAiOCRDescriptor OCR { get; set; }
}
}
+1 -1
View File
@@ -2,6 +2,6 @@ namespace xAiApi.Constants
{
public partial struct ConfigurationNodeNames
{
public const string AI_API_NODE = "AiAPiConfiguration";
public const string AI_API_NODE = "AiApiConfiguration";
}
}
+10 -1
View File
@@ -9,10 +9,19 @@ namespace xAiApi.Constants
//
public const string XDILogTag = "XAiApi";
public const string XAiApiIntroductionPromptName = "Introduction";
//
public const string XAiApiIntroductionPromptName = "Introduction";
public const string XAiApiContentExtractionPromptName = "ContentExtraction";
//
public const string XAiDefaultModelName = "Gemma";
public const string XAiEmbeddingModelName = "Embed";
public const string XAiDefaultVisionModelName = "Qwen";
public const string XAiDefaultThinkingModelName = "Qwen";
public const string XAiEmbeddingPromptName = "Embedding";
//
public const string XOpenAINoKey = "no-key-required";
}
}
-10
View File
@@ -1,10 +0,0 @@
namespace xAiApi.Constants
{
public enum XAiProvider
{
None,
Ollama,
DeepSeek,
HuggingFace
}
}
+8 -5
View File
@@ -1,25 +1,28 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using xAiApi.Interfaces;
using xCommons.Configurations;
using xCommons.Providers;
using xCommons.Configurations;
using Microsoft.AspNetCore.Mvc;
using xIdentityService.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
namespace xAiApi.Controllers
{
[Authorize]
[Route ("[controller]")]
[Route("[controller]")]
public class DefaultAiController : XAiServiceControllerBase<IXDefaultAiService>
{
public DefaultAiController(
ILogger<DefaultAiController> logger,
IXDefaultAiService aiService,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider
) : base(
logger,
aiService,
appConfiguration,
identityProvider,
validationProvider
)
{ }
+7 -8
View File
@@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using xAiApi.Interfaces;
using xCommons.Configurations;
using xCommons.Providers;
using xCommons.Configurations;
using Microsoft.AspNetCore.Mvc;
using xIdentityService.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
namespace xAiApi.Controllers
{
@@ -19,11 +16,13 @@ namespace xAiApi.Controllers
ILogger<DefaultEmbeddingController> logger,
IXDefaultEmbeddingService aiService,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider
) : base(
logger,
aiService,
appConfiguration,
identityProvider,
validationProvider
)
{ }
@@ -0,0 +1,26 @@
using xAiApi.Interfaces;
using xCommons.Providers;
using xCommons.Configurations;
using xIdentityService.Interfaces;
using Microsoft.Extensions.Logging;
namespace xAiApi.Controllers
{
public class DefaultThinkingAiController : XAiServiceControllerBase<IXDefaultThinkingAiService>
{
public DefaultThinkingAiController(
ILogger<DefaultThinkingAiController> logger,
IXDefaultThinkingAiService aiService,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider
) : base(
logger,
aiService,
appConfiguration,
identityProvider,
validationProvider
)
{ }
}
}
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using xAiApi.Interfaces;
using xCommons.Configurations;
using xCommons.Providers;
using System.Threading.Tasks;
using xCommons.Configurations;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using xIdentityService.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
namespace xAiApi.Controllers
{
@@ -22,11 +23,13 @@ namespace xAiApi.Controllers
ILogger logger,
TAiService aiService,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider
) : base(
logger,
aiService,
appConfiguration,
identityProvider,
validationProvider
)
{
@@ -40,8 +43,8 @@ namespace xAiApi.Controllers
/// <param name="cancellationToken"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("Embeding")]
public async Task<ActionResult<float[]>> Embeding(
[HttpGet("Embedding")]
public async Task<ActionResult<float[]>> Embedding(
[FromQuery] string content,
CancellationToken cancellationToken = default
)
@@ -54,7 +57,7 @@ namespace xAiApi.Controllers
ValidationProvider.NotEmpty(content);
//
var result = await aiService.Embeding(
var result = await aiService.Embedding(
content: content,
cancellationToken: cancellationToken
);
@@ -77,8 +80,8 @@ namespace xAiApi.Controllers
/// <param name="cancellationToken"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("Embedings")]
public async Task<ActionResult<IReadOnlyList<float[]>>> Embedings(
[HttpGet("Embeddings")]
public async Task<ActionResult<IReadOnlyList<float[]>>> Embeddings(
[FromQuery] string[] batch,
CancellationToken cancellationToken = default
)
@@ -91,7 +94,7 @@ namespace xAiApi.Controllers
ValidationProvider.NotZeroChilds(batch);
//
var result = await aiService.Embedings(
var result = await aiService.Embeddings(
batch: batch,
cancellationToken: cancellationToken
);
+702 -11
View File
@@ -1,19 +1,30 @@
using System;
using xModels.Dtos;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using xAiModels.Models;
using xAiApi.Interfaces;
using xCommons.Configurations;
using xCommons.Controllers;
using xCommons.Providers;
using xCommons.Extensions;
using xAiModels.Extensions;
using xAiModels.Models.Dtos;
using xExceptions.Constants;
using System.Threading.Tasks;
using xCommons.Configurations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using xIdentityService.Interfaces;
using Microsoft.Extensions.Logging;
using xIdentityService.Controllers;
using Microsoft.AspNetCore.Authorization;
using System.Linq;
namespace xAiApi.Controllers
{
[Authorize]
[Route ("[controller]")]
public abstract class XAiServiceControllerBase<TAiService> : XBaseController, IXAiServiceControllerBase
[Route("[controller]")]
public abstract class XAiServiceControllerBase<TAiService> : XBaseIdentityApiV1Controller, IXAiServiceControllerBase
where TAiService : IXAiServiceBase
{
private readonly TAiService aiService;
@@ -22,16 +33,20 @@ namespace xAiApi.Controllers
ILogger logger,
TAiService aiService,
XAppConfiguration appConfiguration,
IXIdentityProvider identityProvider,
XValidationProvider validationProvider
) : base(
logger,
appConfiguration,
identityProvider,
validationProvider
)
{
this.aiService = aiService;
}
//
#region Actions ...
/// <summary>
/// Ask Question and Get Response ...
/// </summary>
@@ -39,8 +54,8 @@ namespace xAiApi.Controllers
/// <param name="cancellationToken"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("Ask")]
public virtual async Task<ActionResult<string>> Ask(
[HttpGet("AskText")]
public virtual async Task<ActionResult<string>> AskText(
[FromQuery] string question,
CancellationToken cancellationToken = default
)
@@ -53,7 +68,7 @@ namespace xAiApi.Controllers
ValidationProvider.NotEmpty(question);
//
var result = await aiService.AskAsync(
var result = await aiService.AskTextAsync(
prompt: question,
cancellationToken: cancellationToken
);
@@ -68,5 +83,681 @@ namespace xAiApi.Controllers
return result;
}
}
/// <summary>
/// Ask Question and Get Response Stream ...
/// </summary>
/// <param name="question"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("AskTextStream")]
public virtual async Task AskTextStream(
[FromQuery] string question,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
ValidationProvider.NotEmpty(question);
//
// Configure SSE Response Headers ...
Response.Headers["X-Accel-Buffering"] = "no";
Response.Headers["Connection"] = "keep-alive";
Response.Headers["Content-Type"] = "text/event-stream";
Response.Headers["Cache-Control"] = "no-cache, no-store";
//
// Disable response buffering ...
var responseBodyFeature = HttpContext.Features
.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
responseBodyFeature?.DisableBuffering();
//
// Stream Response ...
var jsonOptions = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
//
var enumerable = aiService.AskTextAsEnumerable(
prompt: question,
cancellationToken: cancellationToken
);
//
await foreach (var chunk in enumerable)
{
//
// Skip empty chunks ...
if (string.IsNullOrEmpty(chunk))
{
continue;
}
//
// Build SSE message ...
// Format: data: <json>\n\n
// var payload = JsonSerializer.Serialize(
// new { text = chunk },
// jsonOptions
// );
var payload = chunk;
//
// Write SSE Event ...
await Response.WriteAsync($"{payload}\n\n", cancellationToken);
await Response.Body.FlushAsync(cancellationToken);
}
//
await Response.Body.FlushAsync(cancellationToken);
}
catch (Exception)
{
await Response.Body.FlushAsync();
}
}
#endregion
//
#region Loaders ...
/// <summary>
/// Retrieve AI Projects ...
/// </summary>
/// <param name="includeDefaults"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects")]
public async Task<ActionResult<IEnumerable<XAiProjectDto>>> LoadProjects(
[FromQuery] bool includeDefaults = false,
CancellationToken cancellationToken = default
)
{
//
try
{
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
var result = await aiService.LoadProjects(
ownerId: userInfo.UserId,
connectionId: connectionId,
includeDefaults: includeDefaults,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
/// <summary>
/// Retrieve Specified AI Project's Conversations ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects/{projectId}/Conversations")]
public async Task<ActionResult<IEnumerable<XAiConversationDto>>> LoadConversations(
[FromRoute] Guid projectId,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (projectId.IsDefaultGuid())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
var result = await aiService.LoadConversations(
projectId: projectId,
connectionId: connectionId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
/// <summary>
/// Retrieve Specified AI Conversation's Messages ...
/// </summary>
/// <param name="conversationId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Conversations/{conversationId}/Messages")]
public async Task<ActionResult<IEnumerable<XAiMessageDto>>> LoadMessages(
[FromRoute] Guid conversationId,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (conversationId.IsDefaultGuid())
{
XException.InvalidArgs.Throw();
}
//
var result = await aiService.LoadMessages(
conversationId: conversationId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
#endregion
//
#region Queries ...
/// <summary>
/// Query Projects ...
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects/Query")]
public async Task<ActionResult<XQueryResult<XAiProjectDto>>> QueryProjects(
[FromQuery] XQuery query,
CancellationToken cancellationToken = default
)
{
//
try
{
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
var result = await aiService.QueryProjects(
query: query,
ownerId: userInfo.UserId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
/// <summary>
/// Query Conversations ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects/{projectId}/Conversations/Query")]
public async Task<ActionResult<XQueryResult<XAiConversationDto>>> QueryConversations(
[FromRoute] Guid projectId,
[FromQuery] XQuery query,
CancellationToken cancellationToken = default
)
{
//
try
{
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
var result = await aiService.QueryConversations(
query: query,
projectId: projectId,
ownerId: userInfo.UserId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
/// <summary>
/// Query Messages ...
/// </summary>
/// <param name="conversationId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Conversations/{conversationId}/Messages/Query")]
public async Task<ActionResult<XQueryResult<XAiMessageDto>>> QueryMessages(
[FromRoute] Guid conversationId,
[FromQuery] XQuery query,
CancellationToken cancellationToken = default
)
{
//
try
{
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
var result = await aiService.QueryMessages(
query: query,
ownerId: userInfo.UserId,
conversationId: conversationId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
#endregion
//
#region Creators ...
/// <summary>
/// Create an AI Project ...
/// </summary>
/// <param name="item"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("Projects")]
public async Task<ActionResult<XAiProjectDto>> CreateProject(
[FromBody] XAiProjectDto item,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (item.IsNullOrDefault())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
// Update Required Params ...
item.OwnerId = userInfo.UserId;
if (!item.IsValidForAddOrUpdate())
{
XException.InvalidData.Throw();
}
//
var result = await aiService.CreateProject(
item: item,
ownerId: userInfo.UserId,
connectionId: connectionId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
/// <summary>
/// Create an AI Conversation ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="item"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("Projects/{projectId}/Conversations")]
public async Task<ActionResult<XAiConversationDto>> CreateConversation(
[FromRoute] Guid projectId,
[FromBody] XAiConversationDto item,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (projectId.IsDefaultGuid() ||
item.IsNullOrDefault())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
// Update Required Params ...
item.OwnerId = userInfo.UserId;
if (!item.IsValidForAddOrUpdate())
{
XException.InvalidData.Throw();
}
//
var result = await aiService.CreateConversation(
item: item,
projectId: projectId,
ownerId: userInfo.UserId,
connectionId: connectionId,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
//
var result = GetExceptionActionResult(ex);
return result;
}
}
#endregion
//
#region Content Extraction ...
/// <summary>
/// Extract Files Content ...
/// </summary>
/// <param name="files"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("ExtractContent")]
[Consumes("multipart/form-data")]
public async Task<ActionResult<string>> ExtractContent(
[FromForm] IFormFileCollection files,
[FromForm] string extractors = null,
CancellationToken cancellationToken = default
) {
//
try
{
//
// Validate ...
if (!files.HasChild())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
string[] extractorArr = null;
if (!extractors.IsNullOrEmpty())
{
extractorArr = [.. extractors.ParseListString<string>()];
}
//
var result = await aiService.ExtractContent(
files: files,
extractors: extractorArr,
cancellationToken: cancellationToken
);
//
return Ok(result);
}
catch (Exception ex)
{
var result = GetExceptionActionResult(ex);
return result;
}
}
#endregion
//
#region Data Actions ...
/// <summary>
/// Ask Question and Get Response ...
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("Ask")]
public async Task<ActionResult<string>> Ask(
[FromBody] XAiResponseRequest request,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (!request.IsValid())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
// TODO: Reading Fiels Form Collection and Attach it ...
var result = await aiService.AskAsync(
files: null,
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;
}
}
/// <summary>
/// Ask Question and Get Response Stream ...
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("AskStream")]
public async Task AskStream(
[FromBody] XAiResponseRequest request,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
if (!request.IsValid())
{
XException.InvalidArgs.Throw();
}
//
var userInfo = await GetUserInfo();
var connectionId = GetConnectionId();
//
// Configure SSE Response Headers ...
Response.Headers["X-Accel-Buffering"] = "no";
Response.Headers["Connection"] = "keep-alive";
Response.Headers["Content-Type"] = "text/event-stream";
Response.Headers["Cache-Control"] = "no-cache, no-store";
//
// Disable response buffering ...
var responseBodyFeature = HttpContext.Features
.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
responseBodyFeature?.DisableBuffering();
//
// TODO: Reading Fiels Form Collection and Attach it ...
var enumerable = aiService.AskAsEnumerable(
files: null,
prompt: request.Prompt,
ownerId: userInfo.UserId,
connectionId: connectionId,
projectId: request.ProjectId,
cancellationToken: cancellationToken,
conversationId: request.ConversationId
);
//
await foreach (var resp in enumerable)
{
//
// Skip Empty Responses ...
if (resp.IsNullOrDefault())
{
continue;
}
//
var payload = resp.ToJSON(camelCase: true);
//
// Write SSE Event ...
await Response.WriteAsync($"{payload}", cancellationToken);
await Response.Body.FlushAsync(cancellationToken);
}
//
await Response.Body.FlushAsync(cancellationToken);
}
catch (Exception)
{
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
}
}
+3 -3
View File
@@ -1,11 +1,11 @@
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using xAiApi.Configurations;
using xAiApi.Constants;
using xAiApi.Extensions;
using xCommons.Extensions;
using xAiApi.Configurations;
using xExceptions.Constants;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace xAiApi.DI
{
+116 -69
View File
@@ -1,34 +1,25 @@
using System.Collections.Generic;
using System;
using OpenAI;
using System.Linq;
using xAiApi.Configurations;
using OllamaSharp;
using System.Net.Http;
using xAiApi.Constants;
using xAiApi.Models;
using xAiModels.Models;
using System.Threading;
using System.ClientModel;
using xCommons.Extensions;
using xAiModels.Constants;
using xAiModels.Extensions;
using xAiApi.Configurations;
using xExceptions.Constants;
using Microsoft.Extensions.AI;
using System.Collections.Generic;
using System.ClientModel.Primitives;
namespace xAiApi.Extensions
{
public static class XModelsExtensions
{
/// <summary>
/// Validate a Model Descriptor ...
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static bool IsValid(this XAiModelDescriptor source)
{
//
var result =
!source.IsNullOrDefault() &&
!source.Name.IsNullOrEmpty() &&
!source.Url.IsNullOrEmpty() &&
source.Url.IsValidUrl() &&
!source.LLM.IsNullOrEmpty() &&
source.Provider != Constants.XAiProvider.None;
//
return result;
}
/// <summary>
/// Validate Configurations ...
/// </summary>
@@ -51,52 +42,6 @@ namespace xAiApi.Extensions
return result;
}
/// <summary>
/// Validate Specified Param ...
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static bool IsValid(this XAiPromptParam source)
{
//
var result =
!source.IsNullOrDefault() &&
!source.Name.IsNullOrEmpty() &&
!source.Param.IsNullOrEmpty();
//
return result;
}
/// <summary>
/// Validate Specified Prompt ...
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static bool IsValid(this XAiPromptDescriptor source)
{
//
var result =
!source.IsNullOrDefault() &&
!source.Name.IsNullOrEmpty() &&
!source.Template.IsNullOrEmpty();
if (!result)
{
return result;
}
//
if (source.Params.HasChild())
{
result =
result &&
source.Params.All(x => x.IsValid());
}
//
return result;
}
/// <summary>
/// Extract Default Model Descriptor ...
/// </summary>
@@ -241,5 +186,107 @@ namespace xAiApi.Extensions
//
return result;
}
/// <summary>
/// Provide HttpClient for Common Usages ...
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static HttpClient GetHttpClient(this string source)
{
//
var result = new HttpClient
{
//
// Disable timeout completely (not recommended for production)
Timeout = Timeout.InfiniteTimeSpan,
//
BaseAddress = source.IsNullOrEmpty()
? null
: new Uri(source),
//
// OR set a very long timeout (e.g., 30 minutes for large file uploads)
// Timeout = TimeSpan.FromMinutes(30)
};
//
return result;
}
/// <summary>
/// Instance and Retrieve Chat Client ...
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static IChatClient GetClient(this XAiModelDescriptor source)
{
//
// Try to Initialize LLm ...
var model = source.LLM;
var apiKey = source.ApiKey;
var url = new Uri(source.Url);
var httpClient = source.Url.GetHttpClient();
//
IChatClient result = null;
switch (source.Provider)
{
//
case XAiModelProviderType.Ollama:
//
// Instance Ollama Client using Custom HttpCLient ...
var ollamaClient = new OllamaApiClient(httpClient, model);
result =
new ChatClientBuilder(ollamaClient)
.UseFunctionInvocation()
.Build();
break;
//
case XAiModelProviderType.OpenAI:
//
// Instance Ollama Client using Custom HttpCLient ...
var openAiClient = new OpenAIClient(
new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey),
new OpenAIClientOptions
{
Endpoint = url,
Transport = new HttpClientPipelineTransport(httpClient)
}
);
result =
new ChatClientBuilder(
openAiClient
.GetChatClient(model)
.AsIChatClient())
.UseFunctionInvocation()
.Build();
break;
//
case XAiModelProviderType.DeepSeek:
break;
//
case XAiModelProviderType.HuggingFace:
break;
//
default:
break;
}
//
if (result.IsNullOrDefault())
{
XException.InvalidData.Throw();
}
//
return result;
}
}
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXAudioFileContentExtractor : IXFileContentExtractorBase
{ }
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXDocxFileContentExtractor : IXFileContentExtractorBase
{ }
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXExcelFileContentExtractor : IXFileContentExtractorBase
{ }
}
@@ -0,0 +1,94 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using xAiModels.Models;
namespace xAiApi.Interfaces.Extractors
{
/// <summary>
/// a Base Service for Extracting File Content ...
/// </summary>
public interface IXFileContentExtractorBase
{
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
bool CanExtract(string mimeType);
/// <summary>
/// Extract text content from file stream ...
/// </summary>
Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
);
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
);
}
/// <summary>
/// a Service for Extracting File Content ...
/// </summary>
public interface IXFileContentExtractor
{
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
/// <param name="mimeType"></param>
/// <param name="extractors"></param>
/// <returns></returns>
bool CanExtract(
string mimeType,
string[] extractors = null
);
/// <summary>
/// Extract text content from file stream ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="mimeType"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
string[] extractors = null,
CancellationToken cancellationToken = default
);
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
string[] extractors = null,
CancellationToken cancellationToken = default
);
}
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXImageFileContentExtractor : IXFileContentExtractorBase
{ }
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXPdfFileContentExtractor : IXFileContentExtractorBase
{ }
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXPlainTextFileContentExtractor : IXFileContentExtractorBase
{ }
}
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces.Extractors
{
public interface IXVisionFileContentExtractor : IXFileContentExtractorBase
{ }
}
+5 -4
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using OllamaSharp;
namespace xAiApi.Interfaces
@@ -16,7 +17,7 @@ namespace xAiApi.Interfaces
/// <param name="content"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<float[]> Embeding(
Task<float[]> Embedding(
string content,
CancellationToken cancellationToken = default
);
@@ -27,15 +28,15 @@ namespace xAiApi.Interfaces
/// <param name="batch"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IReadOnlyList<float[]>> Embedings(
Task<IReadOnlyList<float[]>> Embeddings(
string[] batch,
CancellationToken cancellationToken = default
);
/// <summary>
/// Access Embedding Client ...
/// Embedding Generator ...
/// </summary>
/// <returns></returns>
OllamaApiClient GetEmbeddingClient();
IEmbeddingGenerator<string, Embedding<float>> GetEmbeddingGenerator();
}
}
@@ -13,8 +13,8 @@ namespace xAiApi.Interfaces
/// <param name="content"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Embeding")]
Task<ActionResult<float[]>> Embeding(
[HttpGet("Embedding")]
Task<ActionResult<float[]>> Embedding(
[FromQuery] string content,
CancellationToken cancellationToken = default
);
@@ -25,8 +25,8 @@ namespace xAiApi.Interfaces
/// <param name="batch"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Embedings")]
Task<ActionResult<IReadOnlyList<float[]>>> Embedings(
[HttpGet("Embeddings")]
Task<ActionResult<IReadOnlyList<float[]>>> Embeddings(
[FromQuery] string[] batch,
CancellationToken cancellationToken = default
);
+207 -17
View File
@@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using xModels.Dtos;
using System.Net.Http;
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;
namespace xAiApi.Interfaces
{
@@ -12,13 +17,25 @@ namespace xAiApi.Interfaces
/// </summary>
public interface IXAiServiceBase : IDisposable
{
/// <summary>
/// Chat Options ...
/// </summary>
ChatOptions Options { get; }
/// <summary>
/// Descriptor of Models which used in Service ...
/// </summary>
XAiModelDescriptor Descriptor { get; }
//
#region Actions ...
/// <summary>
/// Base Method for Communicate with LLM ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<string> AskAsync(
Task<string> AskTextAsync(
string prompt,
CancellationToken cancellationToken = default
);
@@ -29,25 +46,11 @@ namespace xAiApi.Interfaces
/// <param name="prompt"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
IAsyncEnumerable<string> AskAsEnumerable(
IAsyncEnumerable<string> AskTextAsEnumerable(
string prompt,
CancellationToken cancellationToken = default
);
/// <summary>
/// Get LLM Client instance for Communicating with LLM ...
/// </summary>
/// <param name="llm"></param>
/// <returns></returns>
IChatClient GetClient(string llm = null);
/// <summary>
/// Create Custom HttpClient for Communicating with LLM API ...
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
HttpClient GetHttpClient(string url = null);
/// <summary>
/// Prepare a Message History List for LLM Communication based on Prompt ...
/// </summary>
@@ -69,5 +72,192 @@ namespace xAiApi.Interfaces
ChatMessage message = null,
bool forceMessage = true
);
#endregion
//
#region Loaders ...
/// <summary>
/// Load AI Projects ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="connectionId"></param>
/// <param name="includeDefaults"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<XAiProjectDto>> LoadProjects(
string ownerId,
string connectionId = null,
bool includeDefaults = false,
CancellationToken cancellationToken = default
);
/// <summary>
/// Load Conversations of Specified AI Project ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="connectionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<XAiConversationDto>> LoadConversations(
Guid projectId,
string connectionId = null,
CancellationToken cancellationToken = default
);
/// <summary>
/// Load Messages of Specified Conversation ...
/// </summary>
/// <param name="conversationId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<XAiMessageDto>> LoadMessages(
Guid conversationId,
CancellationToken cancellationToken = default
);
#endregion
//
#region Queries ...
/// <summary>
/// Query Projects ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XQueryResult<XAiProjectDto>> QueryProjects(
string ownerId,
XQuery query,
CancellationToken cancellationToken = default
);
/// <summary>
/// Query Conversations ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="projectId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XQueryResult<XAiConversationDto>> QueryConversations(
string ownerId,
Guid projectId,
XQuery query,
CancellationToken cancellationToken = default
);
/// <summary>
/// Query Messages ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="conversationId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XQueryResult<XAiMessageDto>> QueryMessages(
string ownerId,
Guid conversationId,
XQuery query,
CancellationToken cancellationToken = default
);
#endregion
//
#region Creators ...
/// <summary>
/// Create an AI Project ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="item"></param>
/// <param name="connectionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XAiProjectDto> CreateProject(
string ownerId,
XAiProjectDto item,
string connectionId = null,
CancellationToken cancellationToken = default
);
/// <summary>
/// Create an AI Conversation ...
/// </summary>
/// <param name="ownerId"></param>
/// <param name="projectId"></param>
/// <param name="item"></param>
/// <param name="connectionId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<XAiConversationDto> CreateConversation(
string ownerId,
Guid projectId,
XAiConversationDto item,
string connectionId = null,
CancellationToken cancellationToken = default
);
#endregion
//
#region Content Extraction ...
/// <summary>
/// Try to Extract Files Content ...
/// </summary>
/// <param name="files"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<string> ExtractContent(
IFormFileCollection files,
string[] extractors = null,
CancellationToken cancellationToken = default
);
#endregion
//
#region Data Actions ...
/// <summary>
/// Ask a Question as Prompt and Retrieve Response ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="ownerId"></param>
/// <param name="projectId"></param>
/// <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(
string prompt,
string ownerId,
Guid projectId,
Guid conversationId,
string connectionId = null,
IFormFileCollection files = null,
XUserClaimsInfoDto userInfo = null,
CancellationToken cancellationToken = default
);
/// <summary>
/// Ask a Question as Prompt and Retrieve Response as Async Enumerable ...
/// </summary>
/// <param name="prompt"></param>
/// <param name="ownerId"></param>
/// <param name="projectId"></param>
/// <param name="conversationId"></param>
/// <param name="connectionId"></param>
/// <param name="files"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
IAsyncEnumerable<XAiResonseMessage> AskAsEnumerable(
string prompt,
string ownerId,
Guid projectId,
Guid conversationId,
string connectionId = null,
IFormFileCollection files = null,
CancellationToken cancellationToken = default
);
#endregion
}
}
+195 -2
View File
@@ -1,21 +1,214 @@
using System;
using xModels.Dtos;
using System.Threading;
using xAiModels.Models;
using xAiModels.Models.Dtos;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
namespace xAiApi.Interfaces
{
public interface IXAiServiceControllerBase
{
//
#region Actions ...
/// <summary>
/// Ask Question and Get Response ...
/// </summary>
/// <param name="question"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Ask")]
Task<ActionResult<string>> Ask(
[HttpGet("AskText")]
Task<ActionResult<string>> AskText(
[FromQuery] string question,
CancellationToken cancellationToken = default
);
/// <summary>
/// Ask Question and Get Response Stream ...
/// </summary>
/// <param name="question"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("AskTextStream")]
Task AskTextStream(
[FromQuery] string question,
CancellationToken cancellationToken = default
);
#endregion
//
#region Loaders ...
/// <summary>
/// Retrieve AI Projects ...
/// </summary>
/// <param name="includeDefaults"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects")]
Task<ActionResult<IEnumerable<XAiProjectDto>>> LoadProjects(
[FromQuery] bool includeDefaults = false,
CancellationToken cancellationToken = default
);
/// <summary>
/// Retrieve Specified AI Project's Conversations ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects/{projectId}/Conversations")]
Task<ActionResult<IEnumerable<XAiConversationDto>>> LoadConversations(
[FromRoute] Guid projectId,
CancellationToken cancellationToken = default
);
/// <summary>
/// Retrieve Specified AI Conversation's Messages ...
/// </summary>
/// <param name="conversationId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Conversations/{conversationId}/Messages")]
Task<ActionResult<IEnumerable<XAiMessageDto>>> LoadMessages(
[FromRoute] Guid conversationId,
CancellationToken cancellationToken = default
);
#endregion
//
#region Queries ...
/// <summary>
/// Query Projects ...
/// </summary>
/// <remarks>
/// </remarks>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects/Query")]
Task<ActionResult<XQueryResult<XAiProjectDto>>> QueryProjects(
[FromQuery] XQuery query,
CancellationToken cancellationToken = default
);
/// <summary>
/// Query Conversations ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Projects/{projectId}/Conversations/Query")]
Task<ActionResult<XQueryResult<XAiConversationDto>>> QueryConversations(
[FromRoute] Guid projectId,
[FromQuery] XQuery query,
CancellationToken cancellationToken = default
);
/// <summary>
/// Query Messages ...
/// </summary>
/// <param name="conversationId"></param>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("Conversations/{conversationId}/Messages/Query")]
Task<ActionResult<XQueryResult<XAiMessageDto>>> QueryMessages(
[FromRoute] Guid conversationId,
[FromQuery] XQuery query,
CancellationToken cancellationToken = default
);
#endregion
//
#region Creators ...
/// <summary>
/// Create an AI Project ...
/// </summary>
/// <param name="item"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("Projects")]
Task<ActionResult<XAiProjectDto>> CreateProject(
[FromBody] XAiProjectDto item,
CancellationToken cancellationToken = default
);
/// <summary>
/// Create an AI Conversation ...
/// </summary>
/// <param name="projectId"></param>
/// <param name="item"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("Projects/{projectId}/Conversations")]
Task<ActionResult<XAiConversationDto>> CreateConversation(
[FromRoute] Guid projectId,
[FromBody] XAiConversationDto item,
CancellationToken cancellationToken = default
);
#endregion
//
#region Content Extraction ...
/// <summary>
/// Extract Files Content ...
/// </summary>
/// <param name="files"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("ExtractContent")]
[Consumes("multipart/form-data")]
Task<ActionResult<string>> ExtractContent(
[FromForm] IFormFileCollection files,
[FromForm] string extractors = null,
CancellationToken cancellationToken = default
);
#endregion
//
#region Data Actions ...
/// <summary>
/// Ask Question and Get Response ...
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("Ask")]
Task<ActionResult<string>> Ask(
[FromBody] XAiResponseRequest request,
CancellationToken cancellationToken = default
);
/// <summary>
/// Ask Question and Get Response Stream ...
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpPost("AskStream")]
Task AskStream(
[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
}
}
+5
View File
@@ -0,0 +1,5 @@
namespace xAiApi.Interfaces
{
public interface IXDefaultThinkingAiService: IXAiServiceBase
{ }
}
View File
-35
View File
@@ -1,35 +0,0 @@
using xAiApi.Constants;
namespace xAiApi.Models
{
/// <summary>
/// Describe an Ai Model ...
/// </summary>
public class XAiModelDescriptor
{
/// <summary>
/// Provider Name ...
/// </summary>
public string Name { get; set; }
/// <summary>
/// LLM Provider Url ...
/// </summary>
public string Url { get; set; }
/// <summary>
/// Model Name ...
/// </summary>
public string LLM { get; set; }
/// <summary>
/// Api Key for Connection ...
/// </summary>
public string ApiKey { get; set; } = null;
/// <summary>
/// Model Provider ...
/// </summary>
public XAiProvider Provider { get; set; } = XAiProvider.Ollama;
}
}
+27
View File
@@ -0,0 +1,27 @@
using Tesseract;
namespace xAiApi.Models
{
public class XAiOCRDescriptor
{
/// <summary>
/// Provides Trained Data Path ...
/// </summary>
public string DataPath { get; set; }
/// <summary>
/// The Engine for Doing OCR ...
/// </summary>
public EngineMode EngineMode { get; set; }
/// <summary>
/// Enable OCR Fallback using Model ...
/// </summary>
public bool EnableOcrFallback { get; set; }
/// <summary>
/// Default OCR Language ...
/// </summary>
public string DefaultLanguage { get; set; }
}
}
-49
View File
@@ -1,49 +0,0 @@
namespace xAiApi.Models
{
/// <summary>
/// Describe Specified Prompt ...
/// </summary>
public class XAiPromptDescriptor
{
/// <summary>
/// Prompt Name ...
/// </summary>
public string Name { get; set; }
/// <summary>
/// Prompt Template ...
/// </summary>
public string Template { get; set; }
/// <summary>
/// Prompt Description ...
/// </summary>
public string Description { get; set; }
/// <summary>
/// Prompt Template Params ...
/// </summary>
public XAiPromptParam[] Params { get; set; } = [];
}
/// <summary>
/// Specified a Prompt Param ...
/// </summary>
public class XAiPromptParam
{
/// <summary>
/// Param Name ...
/// </summary>
public string Name { get; set; }
/// <summary>
/// Param Identifier ...
/// </summary>
public string Param { get; set; }
/// <summary>
/// Parma Description ...
/// </summary>
public string Description { get; set; }
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,159 @@
using System;
using System.IO;
using System.Linq;
using Whisper.net;
using System.Threading;
using xAiModels.Models;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
using DocumentFormat.OpenXml.EMMA;
namespace xAiApi.Providers.Extractors
{
/// <summary>
/// Extracts text content from Audio files ...
/// </summary>
public class XAudioFileContentExtractor : IXAudioFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"audio/mpeg",
"audio/mp3",
"audio/wav",
"audio/wave",
"audio/ogg",
"audio/m4a",
"audio/mp4",
"audio/webm"
];
private readonly string language;
private readonly string whisperModelPath;
public XAudioFileContentExtractor() : this(
language: "fa",
whisperModelPath: "Models/ggml-base.bin"
)
{ }
public XAudioFileContentExtractor(
string whisperModelPath = "Models/ggml-base.bin",
string language = "fa"
)
{
//
this.language = language;
this.whisperModelPath = whisperModelPath;
}
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = await ExtractRichAsync(
fileStream,
"audio",
mimeType,
cancellationToken
);
//
return result.AudioTranscript;
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType
};
//
try
{
//
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
//
using var wavStream = await ConvertToWavAsync(
memoryStream,
cancellationToken
);
//
using var factory = WhisperFactory.FromPath(whisperModelPath);
using var processor = factory.CreateBuilder()
.WithLanguage(language)
.Build();
//
var segments = new System.Text.StringBuilder();
await foreach (var segment in processor.ProcessAsync(wavStream, cancellationToken))
{
segments.Append(segment.Text);
}
//
result.AudioTranscript = segments.ToString().Trim();
result.Text = result.AudioTranscript;
}
catch (Exception ex)
{
result.ErrorMessage = $"Audio extraction failed: {ex.Message}";
}
//
return result;
}
/// <summary>
/// Audio Format Converting ...
/// </summary>
private async Task<Stream> ConvertToWavAsync(
Stream inputStream,
CancellationToken cancellationToken
)
{
//
// TODO: Complete this ...
return await Task.FromResult(inputStream);
}
}
}
@@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using xAiModels.Models;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
using DocumentFormat.OpenXml.Drawing;
using DocumentFormat.OpenXml.Packaging;
namespace xAiApi.Providers.Extractors
{
/// <summary>
/// Extracts text content from DOCX files using DocumentFormat.OpenXml ...
/// </summary>
public class XDocxFileContentExtractor : IXDocxFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
];
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
//
var stringBuilder = new StringBuilder();
//
using (var wordDocument = WordprocessingDocument.Open(memoryStream, false))
{
//
var body = wordDocument.MainDocumentPart?.Document?.Body;
if (body != null)
{
//
var paragraphs = body.Elements<Paragraph>();
foreach (var paragraph in paragraphs)
{
//
var text = paragraph.InnerText?.Trim();
if (!string.IsNullOrEmpty(text))
{
stringBuilder.AppendLine(text);
}
}
}
}
//
return stringBuilder.ToString();
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var content = await ExtractAsync(
mimeType: mimeType,
fileStream: fileStream,
cancellationToken: cancellationToken
);
//
var result = new XFileExtractionResult
{
Text = content,
FileName = fileName,
MimeType = mimeType
};
//
return result;
}
}
}
@@ -0,0 +1,128 @@
using System;
using System.IO;
using System.Data;
using System.Linq;
using System.Text;
using ExcelDataReader;
using System.Threading;
using xAiModels.Models;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
namespace xAiApi.Providers.Extractors
{
public class XExcelFileContentExtractor : IXExcelFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
"application/vnd.ms-excel" // .xls
];
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
//
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
//
var stringBuilder = new StringBuilder();
//
using (var reader = ExcelReaderFactory.CreateReader(memoryStream))
{
//
var result = reader.AsDataSet();
foreach (DataTable table in result.Tables)
{
//
stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---");
//
var headers = new string[table.Columns.Count];
for (int i = 0; i < table.Columns.Count; i++)
{
headers[i] = table.Columns[i].ColumnName;
}
stringBuilder.AppendLine(string.Join(" | ", headers));
stringBuilder.AppendLine(new string('-', 50));
//
foreach (DataRow row in table.Rows)
{
//
var rowValues = new string[row.ItemArray.Length];
for (int i = 0; i < row.ItemArray.Length; i++)
{
rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty;
}
stringBuilder.AppendLine(string.Join(" | ", rowValues));
}
stringBuilder.AppendLine();
}
}
//
return stringBuilder.ToString();
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var content = await ExtractAsync(
mimeType: mimeType,
fileStream: fileStream,
cancellationToken: cancellationToken
);
//
var result = new XFileExtractionResult
{
Text = content,
FileName = fileName,
MimeType = mimeType
};
//
return result;
}
}
}
@@ -0,0 +1,154 @@
using System.IO;
using System.Linq;
using System.Threading;
using xAiModels.Models;
using xCommons.Extensions;
using xExceptions.Constants;
using System.Threading.Tasks;
using System.Collections.Generic;
using xAiApi.Interfaces.Extractors;
namespace xAiApi.Providers.Extractors
{
/// <summary>
/// Composite extractor that delegates to appropriate extractor
/// based on MIME type ...
/// </summary>
public class XFileContentExtractor : IXFileContentExtractor
{
//
private readonly IList<IXFileContentExtractorBase> _extractors = [];
public XFileContentExtractor(
IEnumerable<IXFileContentExtractorBase> availableExtractors
)
{
//
_extractors = [.. availableExtractors
.Where(e => e.GetType() != typeof(XFileContentExtractor))];
}
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
/// <param name="mimeType"></param>
/// <param name="extractors"></param>
/// <returns></returns>
public bool CanExtract(
string mimeType,
string[] extractors = null
)
{
//
return GetExtractors(extractors)
.Any(e => e.CanExtract(mimeType));
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="mimeType"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
string[] extractors = null,
CancellationToken cancellationToken = default
)
{
//
var extractor = GetExtractors(extractors)
.FirstOrDefault(e => e.CanExtract(mimeType));
if (extractor.IsNull())
{
//
XException.NotAllowed.Throw(
$"Unsupported file type: {mimeType}"
);
}
//
var result = await extractor
.ExtractAsync(
mimeType: mimeType,
fileStream: fileStream,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="extractors"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
string[] extractors = null,
CancellationToken cancellationToken = default
)
{
//
var extractor = GetExtractors(extractors)
.FirstOrDefault(e => e.CanExtract(mimeType));
if (extractor.IsNull())
{
//
XException.NotAllowed.Throw(
$"Unsupported file type: {mimeType}"
);
}
//
var result = await extractor
.ExtractRichAsync(
mimeType: mimeType,
fileName: fileName,
fileStream: fileStream,
cancellationToken: cancellationToken
);
//
return result;
}
/// <summary>
/// Extract Provided Extractors ...
/// </summary>
/// <param name="extractors"></param>
/// <returns></returns>
private IList<IXFileContentExtractorBase> GetExtractors(string[] extractors = null)
{
//
var result = new List<IXFileContentExtractorBase>();
//
if (extractors.IsNull() || !extractors.HasChild())
{
//
result = [.. _extractors];
}
else
{
//
result = [.. _extractors
.Where(e => extractors
.Any(ex => e.GetType().FullName.Contains(ex)))];
}
//
return result;
}
}
}
@@ -0,0 +1,192 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using xAiModels.Models;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
using xAiApi.Configurations;
using xCommons.Extensions;
using xExceptions.Constants;
using Tesseract;
namespace xAiApi.Providers.Extractors
{
public class XImageFileContentExtractor : IXImageFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"image/png",
"image/jpeg",
"image/jpg",
"image/gif",
"image/webp",
"image/bmp"
];
/// <summary>
/// AI Api Configuration ...
/// </summary>
private readonly XAiApiConfiguration configuration;
public XImageFileContentExtractor(
XAiApiConfiguration configuration
)
{
//
if (configuration.IsNullOrDefault() ||
configuration.OCR.IsNullOrDefault() ||
configuration.OCR.DataPath.IsNullOrEmpty() ||
configuration.OCR.DefaultLanguage.IsNullOrEmpty())
{
XException.InvalidConfiguration.Throw();
}
//
this.configuration = configuration;
}
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = await ExtractRichAsync(
fileStream,
"image",
mimeType,
cancellationToken
);
//
return result.Text;
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType
};
//
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken);
var imageBytes = memoryStream.ToArray();
//
result.Images.Add(new XExtractedImage
{
Bytes = imageBytes,
MimeType = mimeType,
Description = $"Attached image: {fileName}"
});
//
// OCR Fallback ...
if (configuration.OCR.EnableOcrFallback)
{
//
try
{
//
var ocrText = await PerformOcrAsync(imageBytes, cancellationToken);
if (!string.IsNullOrWhiteSpace(ocrText))
{
result.Text = ocrText;
}
}
catch
{
// OCR failed, but image is still available for Vision
}
}
//
return result;
}
/// <summary>
/// Do OCR on Image for Text Extraction ...
/// </summary>
private async Task<string> PerformOcrAsync(
byte[] imageBytes,
CancellationToken cancellationToken
)
{
//
return await Task.Run(() =>
{
//
// Validate Path ...
if (!Directory.Exists(configuration.OCR.DataPath))
{
//
throw new DirectoryNotFoundException(
$"TessData directory not found at: {configuration.OCR.DataPath}. " +
"Please download traineddata files from https://github.com/tesseract-ocr/tessdata"
);
}
//
using var engine = new TesseractEngine(
datapath: configuration.OCR.DataPath,
language: configuration.OCR.DefaultLanguage,
engineMode: configuration.OCR.EngineMode
);
//
using var pix = Pix.LoadFromMemory(imageBytes);
//
cancellationToken.ThrowIfCancellationRequested();
//
using var page = engine.Process(pix);
//
var text = page.GetText();
//
text = text?.Trim();
//
return text ?? string.Empty;
}, cancellationToken);
}
}
}
@@ -0,0 +1,293 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using PdfiumViewer;
using System.Threading;
using xAiModels.Models;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
namespace xAiApi.Providers.Extractors
{
/// <summary>
/// Extracts content from PDF files using PdfPig ...
/// </summary>
public class XPdfFileContentExtractor : IXPdfFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"application/pdf"
];
/// <summary>
/// Minimum Length for Fallback Using Images ...
/// </summary>
private const int MinTextLengthThreshold = 50;
/// <summary>
/// Max Pages for Falling Back Using Images ...
/// </summary>
private const int MaxPagesForImageFallback = 10;
/// <summary>
/// Default DPI for Rendering Images ...
/// </summary>
private const int RenderDpi = 150;
/// <summary>
/// Max Rendered Image Width ...
/// </summary>
private const int MaxImageWidth = 2000;
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = await ExtractRichAsync(
fileStream,
"document.pdf",
mimeType,
cancellationToken
);
//
return result.Text;
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType
};
//
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
//
var extractedText = await ExtractTextAsync(
memoryStream,
cancellationToken
);
if (IsTextSufficient(extractedText))
{
// PDF متنی است
result.Text = extractedText;
}
else
{
// PDF اسکن‌شده است - Fallback به تصویر
memoryStream.Position = 0;
result = await FallbackToImageExtractionAsync(
memoryStream,
fileName,
mimeType,
cancellationToken
);
result.UsedImageFallback = true;
}
//
return result;
}
/// <summary>
/// Extract Text From PDF Files using Common way ...
/// </summary>
/// <param name="pdfStream"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private async Task<string> ExtractTextAsync(
Stream pdfStream,
CancellationToken cancellationToken
)
{
//
var result = await Task.Run(() =>
{
//
var sb = new StringBuilder();
using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream);
foreach (var page in document.GetPages())
{
//
var text = page.Text?.Trim();
if (!string.IsNullOrEmpty(text))
{
//
sb.AppendLine(text);
sb.AppendLine();
}
}
//
return sb.ToString();
}, cancellationToken);
//
return result;
}
/// <summary>
/// Check Extracted Content is Sufficient or not ...
/// if not try to Falling Back using Images ...
/// </summary>
private bool IsTextSufficient(string text)
{
//
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
//
// حذف فاصله‌ها و بررسی طول
var cleanText = new string(text.Where(c => !char.IsWhiteSpace(c)).ToArray());
//
return cleanText.Length >= MinTextLengthThreshold;
}
/// <summary>
/// Fallback Converts PDF Pages to Images ...
/// </summary>
private async Task<XFileExtractionResult> FallbackToImageExtractionAsync(
Stream pdfStream,
string fileName,
string mimeType,
CancellationToken cancellationToken
)
{
//
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType,
UsedImageFallback = true
};
//
await Task.Run(() =>
{
//
try
{
//
using var document = PdfDocument.Load(pdfStream);
//
var pageCount = Math.Min(
document.PageCount,
MaxPagesForImageFallback
);
//
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
//
if (cancellationToken.IsCancellationRequested)
{
break;
}
//
try
{
//
var pageSize = document.PageSizes[pageIndex];
//
var scaleFactor = RenderDpi / 72.0;
var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor);
var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor);
//
if (renderWidth > MaxImageWidth)
{
//
var ratio = (double)MaxImageWidth / renderWidth;
renderWidth = MaxImageWidth;
renderHeight = (int)(renderHeight * ratio);
}
//
using var bitmap = document.Render(
dpiX: RenderDpi,
dpiY: RenderDpi,
page: pageIndex,
width: renderWidth,
forPrinting: false,
height: renderHeight
);
//
byte[] imageBytes;
using (var memoryStream = new MemoryStream())
{
//
bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
imageBytes = memoryStream.ToArray();
}
//
result.Images.Add(new XExtractedImage
{
Bytes = imageBytes,
MimeType = "image/png",
PageNumber = pageIndex + 1,
Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})"
});
}
catch (Exception)
{ }
}
}
catch (Exception ex)
{
result.ErrorMessage = $"PDF image extraction failed: {ex.Message}";
}
}, cancellationToken);
//
return result;
}
}
}
@@ -0,0 +1,89 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using xAiModels.Models;
using System.Threading.Tasks;
using xAiApi.Interfaces.Extractors;
namespace xAiApi.Providers.Extractors
{
/// <summary>
/// Extracts content from plain text files (txt, md, csv, json, xml) ...
/// </summary>
public class XPlainTextFileContentExtractor : IXPlainTextFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"text/plain",
"text/markdown",
"text/csv",
"text/html",
"text/xml",
"application/json",
"application/xml"
];
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
using var reader = new StreamReader(fileStream);
return await reader.ReadToEndAsync();
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var content = await ExtractAsync(
mimeType: mimeType,
fileStream: fileStream,
cancellationToken: cancellationToken
);
//
var result = new XFileExtractionResult
{
Text = content,
FileName = fileName,
MimeType = mimeType
};
//
return result;
}
}
}
@@ -0,0 +1,272 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using xAiApi.Constants;
using xAiModels.Models;
using xAiApi.Extensions;
using xAiApi.Interfaces;
using xCommons.Extensions;
using xAiModels.Extensions;
using xAiApi.Configurations;
using xExceptions.Constants;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using System.Collections.Generic;
using xAiApi.Interfaces.Extractors;
using System.Runtime.CompilerServices;
namespace xAiApi.Providers.Extractors
{
/// <summary>
/// Extracts content from files using Vision Model ...
/// </summary>
public class XVisionFileContentExtractor : IXVisionFileContentExtractor
{
/// <summary>
/// Supported MIME Types ...
/// </summary>
private static readonly string[] SupportedMimeTypes =
[
"image/png",
"image/jpeg",
"image/jpg",
"image/webp",
"image/bmp",
"application/pdf",
];
//
private readonly string prompt;
private readonly XAiModelDescriptor descriptor;
public XVisionFileContentExtractor(
XAiApiConfiguration configuration,
string promptName = XAiApiConstants.XAiApiContentExtractionPromptName,
string modelName = XAiApiConstants.XAiDefaultVisionModelName
)
{
//
// Retrieve Descriptoir ...
if (modelName.IsNullOrEmpty())
{
XException.InvalidArgs.Throw();
}
descriptor = configuration.GetModel(modelName);
//
// Retrieve Extraction Prompt ...
if (promptName.IsNullOrEmpty())
{
XException.InvalidArgs.Throw();
}
prompt = configuration.GetPrompt(
name: promptName,
@params: null
);
//
// Validate Requirements ...
if (prompt.IsNullOrEmpty() || descriptor.IsNullOrDefault())
{
XException.InvalidData.Throw();
}
}
/// <summary>
/// Check if this extractor supports the specified MIME type ...
/// </summary>
public bool CanExtract(string mimeType)
{
//
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// <summary>
/// Extract text content from file stream ...
/// </summary>
public async Task<string> ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
var result = await ExtractRichAsync(fileStream, "file", mimeType, cancellationToken);
return result.Text;
}
/// <summary>
/// Extract content from stream as Rich Result ...
/// </summary>
/// <param name="fileStream"></param>
/// <param name="fileName"></param>
/// <param name="mimeType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<XFileExtractionResult> ExtractRichAsync(
Stream fileStream,
string fileName,
string mimeType,
CancellationToken cancellationToken = default
)
{
//
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType
};
//
try
{
//
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream, cancellationToken);
var imageBytes = memoryStream.ToArray();
var messages = new[]
{
//
new ChatMessage(ChatRole.User, new[]
{
new DataContent(imageBytes, mimeType)
})
};
//
var extractedContent = await RequestOCRAsync(
messages: messages,
cancellationToken: cancellationToken
);
if (!extractedContent.IsNullOrEmpty())
{
result.Text = extractedContent.Trim();
}
else
{
result.ErrorMessage = "there is not any Extracted Content ...";
}
}
catch (Exception ex)
{
result.ErrorMessage = $"Error in Vision Extraction: {ex.Message} ...";
}
//
return result;
}
//
#region OCR ...
/// <summary>
/// Request for Doing OCR on Given Data Contents ...
/// </summary>
/// <param name="messages"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual async Task<string> RequestOCRAsync(
IList<ChatMessage> messages,
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (!messages.HasChild())
{
XException.InvalidArgs.Throw();
}
//
using var client = descriptor.GetClient();
//
// Preparing Extraction Prompt Message ...
var pMessage = new ChatMessage(
ChatRole.System,
prompt
);
//
messages = [pMessage, .. messages];
//
var response = await client.GetResponseAsync(
messages: messages,
cancellationToken: cancellationToken
);
//
// Validate Response ...
if (!response.IsValid())
{
//
// Dispose Client ...
client.Dispose();
XException.ActionFailed.Throw();
}
//
// Retrieve Response Text ...
var result = response.Text;
//
return result;
}
/// <summary>
/// Request for Doing OCR on Given Data Contents as Stream ...
/// </summary>
/// <param name="messages"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual async IAsyncEnumerable<string> RequestOCRAsEnumerable(
IList<ChatMessage> messages,
[EnumeratorCancellation]
CancellationToken cancellationToken = default
)
{
//
// Validate ...
if (!messages.HasChild())
{
XException.InvalidArgs.Throw();
}
//
using var client = descriptor.GetClient();
//
// Preparing Extraction Prompt Message ...
var pMessage = new ChatMessage(
ChatRole.System,
prompt
);
//
messages = [pMessage, .. messages];
//
var enumerable = client.GetStreamingResponseAsync(
options: null,
messages: messages
);
//
await foreach (var res in enumerable)
{
//
// Cancellation Token ...
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
//
yield return res.Text;
}
}
#endregion
}
}
+1059 -136
View File
File diff suppressed because it is too large Load Diff
+95 -56
View File
@@ -1,20 +1,28 @@
using System;
using System.Collections.Generic;
using OpenAI;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OllamaSharp;
using OllamaSharp.Models;
using xAiApi.Configurations;
using System.Threading;
using xAiApi.Constants;
using xAiModels.Models;
using xAiApi.Extensions;
using xAiApi.Interfaces;
using xAiApi.Models;
using xAiModels.Interfaces;
using xAiService.Helpers;
using System.ClientModel;
using xCommons.Providers;
using xCommons.Extensions;
using xAiModels.Constants;
using xAiModels.Extensions;
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;
using xAiApi.Interfaces.Extractors;
using System.ClientModel.Primitives;
namespace xAiApi.Providers
{
@@ -23,17 +31,26 @@ 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,
XValidationProvider validationProvider,
ILogger<XAiEmbeddingServiceBase> logger,
IXFileContentExtractor fileContentExtractor,
string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName
) : base(
fileProvider,
dataProvider,
logger,
configuration
configuration,
validationProvider,
fileContentExtractor,
null
)
{
//
@@ -43,6 +60,11 @@ namespace xAiApi.Providers
{
XException.InvalidConfiguration.Throw();
}
this.fileProvider = fileProvider;
//
this.logger = logger;
}
/// <summary>
@@ -51,7 +73,7 @@ namespace xAiApi.Providers
/// <param name="content"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual async Task<float[]> Embeding(
public virtual async Task<float[]> Embedding(
string content,
CancellationToken cancellationToken = default
)
@@ -64,23 +86,16 @@ namespace xAiApi.Providers
}
//
// Retrieve Ollama Client ...
using var client = GetEmbeddingClient();
using var generator = GetEmbeddingGenerator();
//
// Create Embeding Request ....
var request = new EmbedRequest
{
Input = [content],
Model = descriptor.LLM,
};
//
// Make Request and Recieve Response ...
var response = await client.EmbedAsync(
request: request,
GeneratedEmbeddings<Embedding<float>> response = null;
response = await generator.GenerateAsync(
values: [content],
cancellationToken: cancellationToken
);
//
if (response.IsNullOrDefault())
{
XException.ActionFailed.Throw();
@@ -88,10 +103,11 @@ namespace xAiApi.Providers
//
// Generae Result ...
var result = response?
.Embeddings?
.FirstOrDefault()
?? [];
var result = response
.First()?
.Vector
.ToArray()
?? [];
if (result.Length == 0)
{
XException.ActionFailed.Throw();
@@ -111,7 +127,7 @@ namespace xAiApi.Providers
/// <param name="batch"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual async Task<IReadOnlyList<float[]>> Embedings(
public virtual async Task<IReadOnlyList<float[]>> Embeddings(
string[] batch,
CancellationToken cancellationToken = default
)
@@ -126,41 +142,25 @@ namespace xAiApi.Providers
}
//
// Retrieve Ollama Client ...
using var client = GetEmbeddingClient();
using var generator = GetEmbeddingGenerator();
//
// Create Embeding Request ....
var request = new EmbedRequest
{
Input = [.. batch],
Model = descriptor.LLM,
};
//
// Make Request and Recieve Response ...
var response = await client.EmbedAsync(
request: request,
GeneratedEmbeddings<Embedding<float>> response = null;
response = await generator.GenerateAsync(
values: [.. batch],
cancellationToken: cancellationToken
);
//
if (response.IsNullOrDefault())
{
XException.ActionFailed.Throw();
}
//
// Generae Result ...
var result = response?
.Embeddings
.ToArray()
?? [];
//
// Validate Result ...
if (result.Length == 0)
{
XException.ActionFailed.Throw();
}
var result = response
.Select(e => e.Vector.ToArray())
.ToList();
//
// Normalize ...
@@ -174,13 +174,52 @@ namespace xAiApi.Providers
}
/// <summary>
/// Access Embedding Client ...
/// Embedding Generator ...
/// </summary>
/// <returns></returns>
public OllamaApiClient GetEmbeddingClient()
public IEmbeddingGenerator<string, Embedding<float>> GetEmbeddingGenerator()
{
//
var result = new OllamaApiClient(new Uri(descriptor.Url));
IEmbeddingGenerator<string, Embedding<float>> result = null;
//
if (!descriptor.IsValid())
{
XException.InvalidData.Throw("Invalid Embedding Model ...");
}
//
switch (descriptor.Provider)
{
//
case XAiModelProviderType.OpenAI:
//
var openAiClient = new OpenAIClient(
new ApiKeyCredential(descriptor.ApiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : descriptor.ApiKey),
new OpenAIClientOptions
{
Endpoint = new Uri(descriptor.Url),
Transport = new HttpClientPipelineTransport(descriptor.Url.GetHttpClient())
}
);
result = openAiClient
.GetEmbeddingClient(descriptor.LLM)
.AsIEmbeddingGenerator();
break;
//
case XAiModelProviderType.Ollama:
//
var ollamaClient = new OllamaApiClient(new Uri(descriptor.Url));
result = ollamaClient;
break;
}
//
if (result.IsNull())
{
XException.InvalidData.Throw("Invalid Embedding Model ...");
}
//
return result;
+15 -4
View File
@@ -1,20 +1,31 @@
using Microsoft.Extensions.Logging;
using xAiApi.Configurations;
using xAiApi.Constants;
using xAiApi.Interfaces;
using xCommons.Providers;
using xAiModels.Interfaces;
using xAiApi.Configurations;
using xFileService.Interfaces;
using Microsoft.Extensions.Logging;
using xAiApi.Interfaces.Extractors;
namespace xAiApi.Providers
{
public class XDefaultAiService : XAIServiceBase, IXDefaultAiService
{
public XDefaultAiService(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
ILogger<XDefaultAiService> logger,
XAiApiConfiguration configuration
XAiApiConfiguration configuration,
XValidationProvider validationProvider,
IXFileContentExtractor fileContentExtractor
) : base(
fileProvider,
dataProvider,
logger,
configuration
configuration,
validationProvider,
fileContentExtractor,
XAiApiConstants.XAiDefaultModelName
)
{ }
}
+19 -8
View File
@@ -1,20 +1,31 @@
using Microsoft.Extensions.Logging;
using xAiApi.Configurations;
using xAiApi.Constants;
using xAiApi.Interfaces;
using xAiModels.Interfaces;
using xAiApi.Configurations;
using xFileService.Interfaces;
using Microsoft.Extensions.Logging;
using xAiApi.Interfaces.Extractors;
using xCommons.Providers;
namespace xAiApi.Providers
{
public class XDefaultEmbeddingService : XAiEmbeddingServiceBase, IXDefaultEmbeddingService
{
public XDefaultEmbeddingService(
IXAiDataProvider dataProvider,
XAiApiConfiguration configuration,
ILogger<XDefaultEmbeddingService> logger
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
XAiApiConfiguration configuration,
XValidationProvider validationProvider,
ILogger<XDefaultEmbeddingService> logger,
IXFileContentExtractor fileContentExtractor
) : base(
dataProvider,
configuration,
logger
fileProvider,
dataProvider,
configuration,
validationProvider,
logger,
fileContentExtractor,
XAiApiConstants.XAiEmbeddingModelName
)
{ }
}
+43
View File
@@ -0,0 +1,43 @@
using xAiApi.Constants;
using xAiApi.Interfaces;
using xCommons.Providers;
using xAiModels.Interfaces;
using xAiApi.Configurations;
using Microsoft.Extensions.AI;
using xFileService.Interfaces;
using Microsoft.Extensions.Logging;
using xAiApi.Interfaces.Extractors;
namespace xAiApi.Providers
{
public class XDefaultThinkingAiService : XAIServiceBase, IXDefaultThinkingAiService
{
public XDefaultThinkingAiService(
IXFileProvider fileProvider,
IXAiDataProvider dataProvider,
ILogger<XAIServiceBase> logger,
XAiApiConfiguration configuration,
XValidationProvider validationProvider,
IXFileContentExtractor fileContentExtractor,
ReasoningEffort effort = ReasoningEffort.Medium,
ReasoningOutput output = ReasoningOutput.Full
) : base(
fileProvider,
dataProvider,
logger,
configuration,
validationProvider,
fileContentExtractor,
XAiApiConstants.XAiDefaultThinkingModelName
)
{
//
// Preparing Reasoning ...
Options.Reasoning = new ReasoningOptions
{
Output = output,
Effort = effort,
};
}
}
}
+21 -5
View File
@@ -1,6 +1,8 @@
using System;
using System.IO;
using xAiApi.DI;
using System.Linq;
using xAiModels.DI;
using xTagService.DI;
using System.Net.Http;
using xAiApi.Database;
@@ -9,27 +11,27 @@ using xPushService.DI;
using xFileService.DI;
using xDataService.DI;
using Newtonsoft.Json;
using xAiApi.Providers;
using System.Reflection;
using xAiApi.Extensions;
using xStringService.DI;
using xAiApi.Interfaces;
using xStorageService.DI;
using xIdentityService.DI;
using xDataService.Models;
using xCommons.Extensions;
using xPushService.Helpers;
using xCommons.Configurations;
using System.Collections.Generic;
using xAiApi.Providers.Extractors;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using xAiApi.Interfaces.Extractors;
using Newtonsoft.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using IdentityModel.AspNetCore.OAuth2Introspection;
using xPushService.Helpers;
using xAiApi.DI;
using xAiApi.Interfaces;
using xAiApi.Providers;
using xAiModels.DI;
namespace xAiApi
{
@@ -185,10 +187,24 @@ namespace xAiApi
// Register XAiApi Configuration ...
services.AddXAiApiConfiguration(Configuration);
//
// Register File Content Extractors ...
services.AddSingleton<IXFileContentExtractorBase, XPdfFileContentExtractor>();
services.AddSingleton<IXFileContentExtractorBase, XDocxFileContentExtractor>();
services.AddSingleton<IXFileContentExtractorBase, XImageFileContentExtractor>();
services.AddSingleton<IXFileContentExtractorBase, XAudioFileContentExtractor>();
services.AddSingleton<IXFileContentExtractorBase, XExcelFileContentExtractor>();
services.AddSingleton<IXFileContentExtractorBase, XVisionFileContentExtractor>();
services.AddSingleton<IXFileContentExtractorBase, XPlainTextFileContentExtractor>();
//
services.AddSingleton<IXFileContentExtractor, XFileContentExtractor>();
//
// Register Default Ai Service ...
services.AddScoped<IXDefaultAiService, XDefaultAiService>();
services.AddScoped<IXDefaultEmbeddingService, XDefaultEmbeddingService>();
services.AddScoped<IXDefaultThinkingAiService, XDefaultThinkingAiService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+27 -8
View File
@@ -35,27 +35,40 @@
"ThumbQuality": 72,
"MaxFileSize": 41943040
},
"AiAPiConfiguration": {
"AiApiConfiguration": {
"Models": [
{
"ApiKey": "",
"Name": "Gemma",
"LLM": "gemma3:1b",
"Provider": "Ollama",
"Url": "http://localhost:11434"
"Provider": "OpenAI",
"Url": "http://178.131.88.93:1112/v1"
},
{
"ApiKey": "",
"Name": "Qwen",
"LLM": "qwen-9b",
"Provider": "OpenAI",
"Url": "http://178.131.88.93:1114/v1"
},
{
"ApiKey": "",
"Name": "Embed",
"LLM": "bge-m3",
"Provider": "Ollama",
"Url": "http://localhost:11434"
"Provider": "OpenAI",
"Url": "http://178.131.88.93:1113/v1"
}
],
"Prompts": [
{
"Name": "Introduction",
"Template": "your name is xSaherElmAi, you are a professional ai assistant which develped by SaherElm IT Center",
"Template": "your name is xSaherElmAi, you are a professional ai assistant which develped by SaherElm IT Center, answer each questions by it's language.",
"Description": "",
"Params": []
},
{
"Name": "ContentExtraction",
"Template": "You are an Expert Powerful OCR Engine. your job isReading all Contents in Given Document and Retrieve them as Raw Text by following these rules: 1. Converst all tables to Markdown format. 2. extract numbers, date and times and names exactly as is. 3. do not add any extra descriptions, introduction or summarization from yourself and just return extracted Content.",
"Description": "",
"Params": []
},
@@ -65,12 +78,18 @@
"Description": "",
"Params": []
}
]
],
"OCR": {
"DataPath": "Models",
"EngineMode": "LstmOnly",
"EnableOcrFallback": true,
"DefaultLanguage": "fas+eng"
}
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
+21 -2
View File
@@ -24,10 +24,16 @@
<!-- Icon Handling -->
<ItemGroup>
<None Include="../../Resources/Images/favicon.png" Link="icon.png" Pack="true"
PackagePath="\icon.png" />
<None Include="../../Resources/Images/favicon.png" Link="icon.png" Pack="true" PackagePath="\icon.png" />
</ItemGroup>
<!-- Native Libraries -->
<!-- <ItemGroup>
<None Include="runtimes/linux-x64/native/libpdfium.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> -->
<!-- Local Modules -->
<ItemGroup>
</ItemGroup>
@@ -47,9 +53,22 @@
<!-- Dependencies -->
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="ExcelDataReader" Version="3.9.0" />
<PackageReference Include="ExcelDataReader.DataSet" Version="3.9.0" />
<PackageReference Include="NAudio" Version="2.4.0" />
<PackageReference Include="OllamaSharp" Version="5.4.25" />
<PackageReference Include="PdfiumViewer" Version="2.13.0" />
<PackageReference Include="PdfiumViewer.Native.x86.no_v8-no_xfa" Version="2018.4.8.256" />
<PackageReference Include="PdfiumViewer.Native.x86.v8-xfa" Version="2018.4.8.256" />
<PackageReference Include="PdfiumViewer.Native.x86_64.no_v8-no_xfa" Version="2018.4.8.256" />
<PackageReference Include="PdfiumViewer.Native.x86_64.v8-xfa" Version="2018.4.8.256" />
<PackageReference Include="Tesseract" Version="5.2.0" />
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.11" />
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="5.0.0" />
<PackageReference Include="Whisper.net" Version="1.9.1" />
<PackageReference Include="Whisper.net.Runtime" Version="1.9.1" />
</ItemGroup>
<!-- For XML Documentation Support -->