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
+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