refactor for better support of streaming and etc ...

This commit is contained in:
2026-09-26 10:46:14 +03:30
parent 9afa413c6f
commit ed8a66f432
6 changed files with 132 additions and 136 deletions
+86 -4
View File
@@ -1,7 +1,9 @@
using System; using System;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using xAiApi.Interfaces; using xAiApi.Interfaces;
@@ -12,7 +14,7 @@ using xCommons.Providers;
namespace xAiApi.Controllers namespace xAiApi.Controllers
{ {
[Authorize] [Authorize]
[Route ("[controller]")] [Route("[controller]")]
public abstract class XAiServiceControllerBase<TAiService> : XBaseController, IXAiServiceControllerBase public abstract class XAiServiceControllerBase<TAiService> : XBaseController, IXAiServiceControllerBase
where TAiService : IXAiServiceBase where TAiService : IXAiServiceBase
{ {
@@ -39,8 +41,8 @@ namespace xAiApi.Controllers
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
[AllowAnonymous] [AllowAnonymous]
[HttpGet("Ask")] [HttpGet("AskText")]
public virtual async Task<ActionResult<string>> Ask( public virtual async Task<ActionResult<string>> AskText(
[FromQuery] string question, [FromQuery] string question,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
@@ -53,7 +55,7 @@ namespace xAiApi.Controllers
ValidationProvider.NotEmpty(question); ValidationProvider.NotEmpty(question);
// //
var result = await aiService.AskAsync( var result = await aiService.AskTextAsync(
prompt: question, prompt: question,
cancellationToken: cancellationToken cancellationToken: cancellationToken
); );
@@ -68,5 +70,85 @@ namespace xAiApi.Controllers
return result; return result;
} }
} }
/// <summary>
/// Ask Question and Get Response Stream ...
/// </summary>
/// <param name="question"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("AskTextStream")]
public virtual async Task AskTextStream(
[FromQuery] string question,
CancellationToken cancellationToken = default
)
{
//
try
{
//
// Validate ...
ValidationProvider.NotEmpty(question);
//
// Configure SSE Response Headers ...
Response.Headers["X-Accel-Buffering"] = "no";
Response.Headers["Connection"] = "keep-alive";
Response.Headers["Content-Type"] = "text/event-stream";
Response.Headers["Cache-Control"] = "no-cache, no-store";
//
// Disable response buffering ...
var responseBodyFeature = HttpContext.Features
.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
responseBodyFeature?.DisableBuffering();
//
// Stream Response ...
var jsonOptions = new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
//
var enumerable = aiService.AskTextAsEnumerable(
prompt: question,
cancellationToken: cancellationToken
);
//
await foreach (var chunk in enumerable)
{
//
// Skip empty chunks ...
if (string.IsNullOrEmpty(chunk))
{
continue;
}
//
// Build SSE message ...
// Format: data: <json>\n\n
// var payload = JsonSerializer.Serialize(
// new { text = chunk },
// jsonOptions
// );
var payload = chunk;
//
// Write SSE Event ...
await Response.WriteAsync($"{payload}\n\n", cancellationToken);
await Response.Body.FlushAsync(cancellationToken);
}
//
await Response.Body.FlushAsync(cancellationToken);
}
catch (Exception)
{
await Response.Body.FlushAsync();
}
}
} }
} }
+1 -3
View File
@@ -34,11 +34,9 @@ namespace xAiApi.Interfaces
); );
/// <summary> /// <summary>
/// Access Embedding Client ... /// Embedding Generator ...
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
OllamaApiClient GetEmbeddingClient();
IEmbeddingGenerator<string, Embedding<float>> GetEmbeddingGenerator(); IEmbeddingGenerator<string, Embedding<float>> GetEmbeddingGenerator();
} }
} }
+2 -2
View File
@@ -18,7 +18,7 @@ namespace xAiApi.Interfaces
/// <param name="prompt"></param> /// <param name="prompt"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
Task<string> AskAsync( Task<string> AskTextAsync(
string prompt, string prompt,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );
@@ -29,7 +29,7 @@ namespace xAiApi.Interfaces
/// <param name="prompt"></param> /// <param name="prompt"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
IAsyncEnumerable<string> AskAsEnumerable( IAsyncEnumerable<string> AskTextAsEnumerable(
string prompt, string prompt,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );
+14 -2
View File
@@ -12,8 +12,20 @@ namespace xAiApi.Interfaces
/// <param name="question"></param> /// <param name="question"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("Ask")] [HttpGet("AskText")]
Task<ActionResult<string>> Ask( Task<ActionResult<string>> AskText(
[FromQuery] string question,
CancellationToken cancellationToken = default
);
/// <summary>
/// Ask Question and Get Response Stream ...
/// </summary>
/// <param name="question"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[HttpGet("AskTextStream")]
Task AskTextStream(
[FromQuery] string question, [FromQuery] string question,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );
+10 -41
View File
@@ -47,7 +47,7 @@ namespace xAiApi.Providers
/// <param name="prompt"></param> /// <param name="prompt"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public virtual async Task<string> AskAsync( public virtual async Task<string> AskTextAsync(
string prompt, string prompt,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
@@ -81,10 +81,6 @@ namespace xAiApi.Providers
// Retrieve Response Text ... // Retrieve Response Text ...
var result = response.Text; var result = response.Text;
//
// Dispose Client ...
client.Dispose();
// //
return result; return result;
} }
@@ -95,7 +91,7 @@ namespace xAiApi.Providers
/// <param name="prompt"></param> /// <param name="prompt"></param>
/// <param name="cancellationToken"></param> /// <param name="cancellationToken"></param>
/// <returns></returns> /// <returns></returns>
public virtual async IAsyncEnumerable<string> AskAsEnumerable( public virtual async IAsyncEnumerable<string> AskTextAsEnumerable(
string prompt, string prompt,
[EnumeratorCancellation] [EnumeratorCancellation]
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
@@ -117,7 +113,6 @@ namespace xAiApi.Providers
); );
// //
var result = "";
await foreach (var res in enumerable) await foreach (var res in enumerable)
{ {
// //
@@ -128,15 +123,8 @@ namespace xAiApi.Providers
} }
// //
result += res.Text; yield return res.Text;
} }
//
// Dispose Client ...
client.Dispose();
//
yield return result;
} }
/// <summary> /// <summary>
@@ -172,6 +160,7 @@ namespace xAiApi.Providers
var ollamaClient = new OllamaApiClient(httpClient, model); var ollamaClient = new OllamaApiClient(httpClient, model);
result = result =
new ChatClientBuilder(ollamaClient) new ChatClientBuilder(ollamaClient)
.UseFunctionInvocation()
.Build(); .Build();
break; break;
@@ -188,9 +177,12 @@ namespace xAiApi.Providers
} }
); );
result = result =
new ChatClientBuilder(
openAiClient openAiClient
.GetChatClient(model) .GetChatClient(model)
.AsIChatClient(); .AsIChatClient())
.UseFunctionInvocation()
.Build();
break; break;
// //
@@ -206,29 +198,6 @@ namespace xAiApi.Providers
break; break;
} }
// //
// if (isOllamaBased)
// {
// }
// else if (isDeepSeekBased)
// {
// //
// if (apiKey is null ||
// string.IsNullOrWhiteSpace(apiKey))
// {
// throw new Exception("Inavlid Args ...");
// }
// //
// var deepseekClient = new DeepSeekChatClient(apiKey: apiKey);
// result = deepseekClient;
// }
// else
// {
// //
// // TODO: Implement this ...
// }
// //
if (result.IsNullOrDefault()) if (result.IsNullOrDefault())
{ {
@@ -330,7 +299,7 @@ namespace xAiApi.Providers
// Preparing Introduction Prompt ... // Preparing Introduction Prompt ...
var introductionPrompt = new ChatMessage( var introductionPrompt = new ChatMessage(
ChatRole.System, ChatRole.System,
configuration.GetPrompt() configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName)
); );
// //
@@ -339,7 +308,7 @@ namespace xAiApi.Providers
introductionPrompt, introductionPrompt,
]; ];
if (forceMessage && if (forceMessage &&
message is not null) !message.IsNullOrDefault())
{ {
result.Add(message); result.Add(message);
} }
+17 -82
View File
@@ -37,11 +37,7 @@ namespace xAiApi.Providers
XAiApiConfiguration configuration, XAiApiConfiguration configuration,
ILogger<XAiEmbeddingServiceBase> logger, ILogger<XAiEmbeddingServiceBase> logger,
string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName string embeddingModelName = XAiApiConstants.XAiEmbeddingModelName
) : base( ) : base(dataProvider, logger, configuration)
dataProvider,
logger,
configuration
)
{ {
// //
// Prepare Embedding Model Descriptor ... // Prepare Embedding Model Descriptor ...
@@ -75,15 +71,13 @@ namespace xAiApi.Providers
// //
using var generator = GetEmbeddingGenerator(); using var generator = GetEmbeddingGenerator();
//
GeneratedEmbeddings<Embedding<float>> response = null; GeneratedEmbeddings<Embedding<float>> response = null;
try response = await generator.GenerateAsync(
{ values: [content],
response = await generator.GenerateAsync([content]); cancellationToken: cancellationToken
} );
catch (Exception ex)
{
logger.LogError(ex.Message);
}
// //
if (response.IsNullOrDefault()) if (response.IsNullOrDefault())
@@ -103,40 +97,6 @@ namespace xAiApi.Providers
XException.ActionFailed.Throw(); XException.ActionFailed.Throw();
} }
//
// Retrieve Ollama Client ...
// using var client = GetEmbeddingClient();
// //
// // Create Embedding Request ....
// var request = new EmbedRequest
// {
// Input = [content],
// Model = descriptor.LLM,
// };
// //
// // Make Request and Recieve Response ...
// var response = await client.EmbedAsync(
// request: request,
// cancellationToken: cancellationToken
// );
// if (response.IsNullOrDefault())
// {
// XException.ActionFailed.Throw();
// }
// //
// // Generae Result ...
// var result = response?
// .Embeddings?
// .FirstOrDefault()
// ?? [];
// if (result.Length == 0)
// {
// XException.ActionFailed.Throw();
// }
// //
// Normalize ... // Normalize ...
VectorHelper.Normalize(result); VectorHelper.Normalize(result);
@@ -166,41 +126,25 @@ namespace xAiApi.Providers
} }
// //
// Retrieve Ollama Client ... using var generator = GetEmbeddingGenerator();
using var client = GetEmbeddingClient();
// //
// Create Embedding Request .... GeneratedEmbeddings<Embedding<float>> response = null;
var request = new EmbedRequest response = await generator.GenerateAsync(
{ values: [.. batch],
Input = [.. batch],
Model = descriptor.LLM,
};
//
// Make Request and Recieve Response ...
var response = await client.EmbedAsync(
request: request,
cancellationToken: cancellationToken cancellationToken: cancellationToken
); );
//
if (response.IsNullOrDefault()) if (response.IsNullOrDefault())
{ {
XException.ActionFailed.Throw(); XException.ActionFailed.Throw();
} }
// //
// Generae Result ... var result = response
var result = response? .Select(e => e.Vector.ToArray())
.Embeddings .ToList();
.ToArray()
?? [];
//
// Validate Result ...
if (result.Length == 0)
{
XException.ActionFailed.Throw();
}
// //
// Normalize ... // Normalize ...
@@ -214,18 +158,9 @@ namespace xAiApi.Providers
} }
/// <summary> /// <summary>
/// Access Embedding Client ... /// Embedding Generator ...
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public OllamaApiClient GetEmbeddingClient()
{
//
var result = new OllamaApiClient(new Uri(descriptor.Url));
//
return result;
}
public IEmbeddingGenerator<string, Embedding<float>> GetEmbeddingGenerator() public IEmbeddingGenerator<string, Embedding<float>> GetEmbeddingGenerator()
{ {
// //