last ...
This commit is contained in:
@@ -9,11 +9,15 @@ namespace xAiApi.Constants
|
||||
|
||||
//
|
||||
public const string XDILogTag = "XAiApi";
|
||||
|
||||
//
|
||||
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";
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ using xIdentityService.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using xIdentityService.Controllers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Linq;
|
||||
|
||||
namespace xAiApi.Controllers
|
||||
{
|
||||
@@ -522,6 +523,61 @@ namespace xAiApi.Controllers
|
||||
}
|
||||
}
|
||||
#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 ...
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
using System;
|
||||
using OpenAI;
|
||||
using System.Linq;
|
||||
using OllamaSharp;
|
||||
using System.Net.Http;
|
||||
using xAiApi.Constants;
|
||||
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
|
||||
{
|
||||
@@ -176,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
|
||||
{ }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
public interface IXDocxFileContentExtractor : IXFileContentExtractor
|
||||
public interface IXDocxFileContentExtractor : IXFileContentExtractorBase
|
||||
{ }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
public interface IXExcelFileContentExtractor : IXFileContentExtractor
|
||||
public interface IXExcelFileContentExtractor : IXFileContentExtractorBase
|
||||
{ }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -6,9 +8,9 @@ using xAiModels.Models;
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
/// <summary>
|
||||
/// a Service for Extracting File Content ...
|
||||
/// a Base Service for Extracting File Content ...
|
||||
/// </summary>
|
||||
public interface IXFileContentExtractor
|
||||
public interface IXFileContentExtractorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if this extractor supports the specified MIME type ...
|
||||
@@ -39,4 +41,54 @@ namespace xAiApi.Interfaces.Extractors
|
||||
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
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
public interface IXImageFileContentExtractor : IXFileContentExtractor
|
||||
public interface IXImageFileContentExtractor : IXFileContentExtractorBase
|
||||
{ }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
public interface IXPdfFileContentExtractor : IXFileContentExtractor
|
||||
public interface IXPdfFileContentExtractor : IXFileContentExtractorBase
|
||||
{ }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
public interface IXPlainTextFileContentExtractor : IXFileContentExtractor
|
||||
public interface IXPlainTextFileContentExtractor : IXFileContentExtractorBase
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace xAiApi.Interfaces.Extractors
|
||||
{
|
||||
public interface IXVisionFileContentExtractor : IXFileContentExtractorBase
|
||||
{ }
|
||||
}
|
||||
@@ -51,19 +51,6 @@ namespace xAiApi.Interfaces
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get LLM Client instance for Communicating with LLM ...
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IChatClient GetClient();
|
||||
|
||||
/// <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>
|
||||
@@ -209,6 +196,22 @@ namespace xAiApi.Interfaces
|
||||
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 ...
|
||||
|
||||
@@ -5,8 +5,8 @@ using xAiModels.Models;
|
||||
using xAiModels.Models.Dtos;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace xAiApi.Interfaces
|
||||
{
|
||||
@@ -152,6 +152,24 @@ namespace xAiApi.Interfaces
|
||||
);
|
||||
#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>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -75,5 +76,39 @@ namespace xAiApi.Providers.Extractors
|
||||
//
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using ExcelDataReader;
|
||||
using System.Threading;
|
||||
using xAiModels.Models;
|
||||
using System.Threading.Tasks;
|
||||
using xAiApi.Interfaces.Extractors;
|
||||
|
||||
@@ -89,5 +90,39 @@ namespace xAiApi.Providers.Extractors
|
||||
//
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Reflection;
|
||||
using xAiModels.Models;
|
||||
using xCommons.Extensions;
|
||||
using xExceptions.Constants;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using xAiApi.Interfaces.Extractors;
|
||||
|
||||
namespace xAiApi.Providers
|
||||
namespace xAiApi.Providers.Extractors
|
||||
{
|
||||
/// <summary>
|
||||
/// Composite extractor that delegates to appropriate extractor
|
||||
@@ -18,67 +17,50 @@ namespace xAiApi.Providers
|
||||
public class XFileContentExtractor : IXFileContentExtractor
|
||||
{
|
||||
//
|
||||
private readonly IList<IXFileContentExtractor> extractors = [];
|
||||
private readonly IList<IXFileContentExtractorBase> _extractors = [];
|
||||
|
||||
public XFileContentExtractor()
|
||||
public XFileContentExtractor(
|
||||
IEnumerable<IXFileContentExtractorBase> availableExtractors
|
||||
)
|
||||
{
|
||||
//
|
||||
var path = Path.Combine(AppContext.BaseDirectory);
|
||||
var files = Directory.GetFiles(path, "*.dll");
|
||||
foreach (var dll in files)
|
||||
{
|
||||
//
|
||||
// Retrieve DLL Assembly ...
|
||||
var assembly = Assembly.LoadFrom(dll);
|
||||
|
||||
//
|
||||
// Extract Registerar Types ...
|
||||
var extractorTypes = assembly
|
||||
.GetExportedTypes()
|
||||
.Where(t =>
|
||||
!t.IsAbstract &&
|
||||
typeof(IXFileContentExtractor).IsAssignableFrom(t));
|
||||
foreach (var extractorType in extractorTypes)
|
||||
{
|
||||
//
|
||||
// Ignore XFileContentExtractor ...
|
||||
if (extractorType.FullName.Contains(nameof(XFileContentExtractor)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// Create Instance of Service ...
|
||||
var instance = (IXFileContentExtractor)Activator.CreateInstance(extractorType);
|
||||
if (!instance.IsNull())
|
||||
{
|
||||
//
|
||||
// Add Registerar Instance to Collection ...
|
||||
extractors.Add(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
_extractors = [.. availableExtractors
|
||||
.Where(e => e.GetType() != typeof(XFileContentExtractor))];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if this extractor supports the specified MIME type ...
|
||||
/// </summary>
|
||||
public bool CanExtract(string mimeType)
|
||||
/// <param name="mimeType"></param>
|
||||
/// <param name="extractors"></param>
|
||||
/// <returns></returns>
|
||||
public bool CanExtract(
|
||||
string mimeType,
|
||||
string[] extractors = null
|
||||
)
|
||||
{
|
||||
return extractors.Any(e => e.CanExtract(mimeType));
|
||||
//
|
||||
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 = extractors
|
||||
var extractor = GetExtractors(extractors)
|
||||
.FirstOrDefault(e => e.CanExtract(mimeType));
|
||||
if (extractor.IsNull())
|
||||
{
|
||||
@@ -99,5 +81,74 @@ namespace xAiApi.Providers
|
||||
//
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ namespace xAiApi.Providers.Extractors
|
||||
/// </summary>
|
||||
private readonly bool _enableOcrFallback;
|
||||
|
||||
public XImageFileContentExtractor() : this(enableOcrFallback: true)
|
||||
{ }
|
||||
|
||||
public XImageFileContentExtractor(bool enableOcrFallback = true)
|
||||
{
|
||||
_enableOcrFallback = enableOcrFallback;
|
||||
@@ -132,9 +135,7 @@ namespace xAiApi.Providers.Extractors
|
||||
)
|
||||
{
|
||||
//
|
||||
// TODO:
|
||||
// پیادهسازی با Tesseract یا هر کتابخانه OCR دیگر
|
||||
// در حال حاضر یک placeholder است
|
||||
// TODO: Complete this ...
|
||||
return await Task.FromResult(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UglyToad.PdfPig;
|
||||
using PdfiumViewer;
|
||||
using System.Threading;
|
||||
using xAiModels.Models;
|
||||
using System.Threading.Tasks;
|
||||
using xAiApi.Interfaces.Extractors;
|
||||
|
||||
namespace xAiApi.Providers
|
||||
namespace xAiApi.Providers.Extractors
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts content from PDF files using PdfPig ...
|
||||
@@ -22,6 +23,26 @@ namespace xAiApi.Providers
|
||||
"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>
|
||||
@@ -41,21 +62,100 @@ namespace xAiApi.Providers
|
||||
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 = PdfDocument.Open(fileStream);
|
||||
using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream);
|
||||
foreach (var page in document.GetPages())
|
||||
{
|
||||
//
|
||||
var text = page.Text;
|
||||
|
||||
//
|
||||
sb.AppendLine(text);
|
||||
sb.AppendLine();
|
||||
var text = page.Text?.Trim();
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
//
|
||||
sb.AppendLine(text);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -65,5 +165,129 @@ namespace xAiApi.Providers
|
||||
//
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@ 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
|
||||
namespace xAiApi.Providers.Extractors
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts content from plain text files (txt, md, csv, json, xml) ...
|
||||
@@ -50,5 +51,39 @@ namespace xAiApi.Providers
|
||||
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
|
||||
}
|
||||
}
|
||||
+155
-126
@@ -107,7 +107,7 @@ namespace xAiApi.Providers
|
||||
}
|
||||
|
||||
//
|
||||
using var client = GetClient();
|
||||
using var client = Descriptor.GetClient();
|
||||
var history = GetHistory(prompt);
|
||||
var response = await client.GetResponseAsync(
|
||||
messages: history,
|
||||
@@ -152,7 +152,7 @@ namespace xAiApi.Providers
|
||||
}
|
||||
|
||||
//
|
||||
using var client = GetClient();
|
||||
using var client = Descriptor.GetClient();
|
||||
var history = GetHistory(prompt);
|
||||
var enumerable = client.GetStreamingResponseAsync(
|
||||
options: null,
|
||||
@@ -174,106 +174,6 @@ namespace xAiApi.Providers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get LLM Client instance for Communicating with LLM ...
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual IChatClient GetClient()
|
||||
{
|
||||
//
|
||||
// Try to Initialize LLm ...
|
||||
var model = Descriptor.LLM;
|
||||
var apiKey = Descriptor.ApiKey;
|
||||
var url = new Uri(Descriptor.Url);
|
||||
var httpClient = GetHttpClient(Descriptor.Url);
|
||||
|
||||
//
|
||||
IChatClient result = null;
|
||||
switch (Descriptor.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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Custom HttpClient for Communicating with LLM API ...
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
public virtual HttpClient GetHttpClient(string url = null)
|
||||
{
|
||||
//
|
||||
var result = new HttpClient
|
||||
{
|
||||
//
|
||||
// Disable timeout completely (not recommended for production)
|
||||
Timeout = Timeout.InfiniteTimeSpan,
|
||||
|
||||
//
|
||||
BaseAddress = url.IsNullOrEmpty()
|
||||
? null
|
||||
: new Uri(url),
|
||||
|
||||
//
|
||||
// OR set a very long timeout (e.g., 30 minutes for large file uploads)
|
||||
// Timeout = TimeSpan.FromMinutes(30)
|
||||
};
|
||||
|
||||
//
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepare a Message History List for LLM Communication based on Prompt ...
|
||||
/// </summary>
|
||||
@@ -366,7 +266,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="includeDefaults"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<XAiProjectDto>> LoadProjects(
|
||||
public virtual async Task<IEnumerable<XAiProjectDto>> LoadProjects(
|
||||
string ownerId,
|
||||
string connectionId = null,
|
||||
bool includeDefaults = false,
|
||||
@@ -439,7 +339,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="connectionId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<XAiConversationDto>> LoadConversations(
|
||||
public virtual async Task<IEnumerable<XAiConversationDto>> LoadConversations(
|
||||
Guid projectId,
|
||||
string connectionId = null,
|
||||
CancellationToken cancellationToken = default
|
||||
@@ -470,7 +370,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="conversationId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<XAiMessageDto>> LoadMessages(
|
||||
public virtual async Task<IEnumerable<XAiMessageDto>> LoadMessages(
|
||||
Guid conversationId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
@@ -502,7 +402,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="query"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<XQueryResult<XAiProjectDto>> QueryProjects(
|
||||
public virtual async Task<XQueryResult<XAiProjectDto>> QueryProjects(
|
||||
string ownerId,
|
||||
XQuery query,
|
||||
CancellationToken cancellationToken = default
|
||||
@@ -534,7 +434,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="query"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<XQueryResult<XAiConversationDto>> QueryConversations(
|
||||
public virtual async Task<XQueryResult<XAiConversationDto>> QueryConversations(
|
||||
string ownerId,
|
||||
Guid projectId,
|
||||
XQuery query,
|
||||
@@ -589,7 +489,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="query"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<XQueryResult<XAiMessageDto>> QueryMessages(
|
||||
public virtual async Task<XQueryResult<XAiMessageDto>> QueryMessages(
|
||||
string ownerId,
|
||||
Guid conversationId,
|
||||
XQuery query,
|
||||
@@ -646,7 +546,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="connectionId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<XAiProjectDto> CreateProject(
|
||||
public virtual async Task<XAiProjectDto> CreateProject(
|
||||
string ownerId,
|
||||
XAiProjectDto item,
|
||||
string connectionId = null,
|
||||
@@ -674,7 +574,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="connectionId"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<XAiConversationDto> CreateConversation(
|
||||
public virtual async Task<XAiConversationDto> CreateConversation(
|
||||
string ownerId,
|
||||
Guid projectId,
|
||||
XAiConversationDto item,
|
||||
@@ -696,6 +596,91 @@ namespace xAiApi.Providers
|
||||
}
|
||||
#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>
|
||||
public async Task<string> ExtractContent(
|
||||
IFormFileCollection files,
|
||||
string[] extractors = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//
|
||||
if (!files.HasChild())
|
||||
{
|
||||
XException.InvalidArgs.Throw();
|
||||
}
|
||||
|
||||
//
|
||||
var extractionResults = new List<XFileExtractionResult>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
//
|
||||
if (file.IsNull() || file.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty;
|
||||
if (!fileContentExtractor.CanExtract(mimeType, extractors))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
try
|
||||
{
|
||||
//
|
||||
using var stream = file.OpenReadStream();
|
||||
|
||||
//
|
||||
var fr = await fileContentExtractor.ExtractRichAsync(
|
||||
fileStream: stream,
|
||||
mimeType: mimeType,
|
||||
extractors: extractors,
|
||||
fileName: file.FileName,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
//
|
||||
if (fr.IsValid())
|
||||
{
|
||||
extractionResults.Add(fr);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
var result = string.Empty;
|
||||
if (extractionResults.HasChild())
|
||||
{
|
||||
//
|
||||
extractionResults.ForEach(er =>
|
||||
{
|
||||
result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----";
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
//
|
||||
#region Data Actions ...
|
||||
/// <summary>
|
||||
@@ -710,7 +695,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="userInfo"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<XAiMessageDto> AskAsync(
|
||||
public virtual async Task<XAiMessageDto> AskAsync(
|
||||
string prompt,
|
||||
string ownerId,
|
||||
Guid projectId,
|
||||
@@ -861,25 +846,69 @@ namespace xAiApi.Providers
|
||||
|
||||
//
|
||||
// Since Prompt Message Has Id now, we can Attach Uploaded Files to it ...
|
||||
IList<AIContent> fileContents = null;
|
||||
IList<AIContent> fileContents = [];
|
||||
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
|
||||
);
|
||||
}
|
||||
try
|
||||
{
|
||||
//
|
||||
// Add Reference File to Prompt Message ...
|
||||
await fileProvider.AddReference(
|
||||
id: uf.Id,
|
||||
userInfo: userInfo,
|
||||
connectionId: connectionId,
|
||||
forProvidedId: promptMessage.Id,
|
||||
cancellationToken: cancellationToken,
|
||||
providedFor: XAiServiceConstants.XAiMessageFilesIdentifier
|
||||
);
|
||||
|
||||
//
|
||||
// TODO: Extract File Contents ...
|
||||
//
|
||||
// Handle File Content Extraction and Attach to Prompt Message ...
|
||||
var fileDescriptor = await fileProvider.GetFileDescriptor(
|
||||
id: uf.Id,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
if (fileDescriptor == null || fileDescriptor.Stream == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
var extractionResult = await fileContentExtractor.ExtractRichAsync(
|
||||
fileStream: fileDescriptor.Stream,
|
||||
fileName: uf.FileName ?? uf.Name,
|
||||
mimeType: fileDescriptor.MIMEType,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
if (extractionResult.IsValid())
|
||||
{
|
||||
//
|
||||
var contents = extractionResult.ToAIContents();
|
||||
fileContents = [.. fileContents, .. contents];
|
||||
|
||||
//
|
||||
// ذخیره Metadata برای ردیابی
|
||||
logger.LogInformation(
|
||||
"File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}",
|
||||
uf.FileName,
|
||||
!string.IsNullOrEmpty(extractionResult.Text),
|
||||
extractionResult.Images.Count,
|
||||
extractionResult.UsedImageFallback
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
fileDescriptor.Stream.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -928,7 +957,7 @@ namespace xAiApi.Providers
|
||||
/// <param name="files"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async IAsyncEnumerable<XAiResonseMessage> AskAsEnumerable(
|
||||
public virtual async IAsyncEnumerable<XAiResonseMessage> AskAsEnumerable(
|
||||
string prompt,
|
||||
string ownerId,
|
||||
Guid projectId,
|
||||
@@ -1055,7 +1084,7 @@ namespace xAiApi.Providers
|
||||
|
||||
//
|
||||
// Ask Questions From LLM ...
|
||||
using var client = GetClient();
|
||||
using var client = Descriptor.GetClient();
|
||||
var enumerable = client.GetStreamingResponseAsync(
|
||||
options: null,
|
||||
messages: history
|
||||
@@ -1202,7 +1231,7 @@ namespace xAiApi.Providers
|
||||
|
||||
//
|
||||
history.Add(prompt);
|
||||
using var client = GetClient();
|
||||
using var client = Descriptor.GetClient();
|
||||
var response = await client.GetResponseAsync(
|
||||
messages: history,
|
||||
cancellationToken: cancellationToken
|
||||
|
||||
@@ -9,6 +9,7 @@ using xAiApi.Extensions;
|
||||
using xAiApi.Interfaces;
|
||||
using xAiService.Helpers;
|
||||
using System.ClientModel;
|
||||
using xCommons.Providers;
|
||||
using xCommons.Extensions;
|
||||
using xAiModels.Constants;
|
||||
using xAiModels.Extensions;
|
||||
@@ -22,7 +23,6 @@ using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using xAiApi.Interfaces.Extractors;
|
||||
using System.ClientModel.Primitives;
|
||||
using xCommons.Providers;
|
||||
|
||||
namespace xAiApi.Providers
|
||||
{
|
||||
@@ -44,12 +44,12 @@ namespace xAiApi.Providers
|
||||
IXFileContentExtractor fileContentExtractor,
|
||||
string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName
|
||||
) : base(
|
||||
fileProvider,
|
||||
dataProvider,
|
||||
logger,
|
||||
configuration,
|
||||
validationProvider,
|
||||
fileContentExtractor,
|
||||
fileProvider,
|
||||
dataProvider,
|
||||
logger,
|
||||
configuration,
|
||||
validationProvider,
|
||||
fileContentExtractor,
|
||||
null
|
||||
)
|
||||
{
|
||||
@@ -199,7 +199,7 @@ namespace xAiApi.Providers
|
||||
new OpenAIClientOptions
|
||||
{
|
||||
Endpoint = new Uri(descriptor.Url),
|
||||
Transport = new HttpClientPipelineTransport(GetHttpClient(descriptor.Url))
|
||||
Transport = new HttpClientPipelineTransport(descriptor.Url.GetHttpClient())
|
||||
}
|
||||
);
|
||||
result = openAiClient
|
||||
|
||||
+18
-2
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -188,9 +189,24 @@ namespace xAiApi
|
||||
|
||||
//
|
||||
// Register File Content Extractors ...
|
||||
// services.AddSingleton<IXFileContentExtractor, XPdfContentExtractor>();
|
||||
// services.AddSingleton<IXFileContentExtractor, XPlainTextContentExtractor>();
|
||||
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>();
|
||||
// services.AddSingleton(sp =>
|
||||
// {
|
||||
// //
|
||||
// var extractors = sp.GetServices<IXFileContentExtractor>();
|
||||
// return new XFileContentExtractor(extractors);
|
||||
// });
|
||||
// services.AddSingleton<IXFileContentExtractor>(sp =>
|
||||
// sp.GetRequiredService<XFileContentExtractor>());
|
||||
|
||||
//
|
||||
// Register Default Ai Service ...
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
"Name": "Embedding",
|
||||
"Template": "your name is xSaherElmAi, you are a professional ai assistant which develped by SaherElm IT Center specified for Embedding Documents",
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
<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>
|
||||
@@ -49,10 +56,19 @@
|
||||
<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 -->
|
||||
|
||||
Reference in New Issue
Block a user