This commit is contained in:
2026-10-03 17:42:36 +03:30
parent c30d93be0b
commit 7cad889cd0
31 changed files with 1346 additions and 212 deletions
@@ -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;
}
}
}
+94 -43
View File
@@ -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
View File
@@ -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
+8 -8
View File
@@ -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