707 lines
22 KiB
C#
707 lines
22 KiB
C#
using System;
|
|
using xModels.Dtos;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using xAiModels.Models;
|
|
using xAiApi.Interfaces;
|
|
using xCommons.Providers;
|
|
using xCommons.Extensions;
|
|
using xAiModels.Extensions;
|
|
using xAiModels.Models.Dtos;
|
|
using xExceptions.Constants;
|
|
using System.Threading.Tasks;
|
|
using xCommons.Configurations;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Http;
|
|
using System.Collections.Generic;
|
|
using xIdentityService.Interfaces;
|
|
using Microsoft.Extensions.Logging;
|
|
using xIdentityService.Controllers;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
namespace xAiApi.Controllers
|
|
{
|
|
[Authorize]
|
|
[Route("[controller]")]
|
|
public abstract class XAiServiceControllerBase<TAiService> : XBaseIdentityApiV1Controller, IXAiServiceControllerBase
|
|
where TAiService : IXAiServiceBase
|
|
{
|
|
private readonly TAiService aiService;
|
|
|
|
protected XAiServiceControllerBase(
|
|
ILogger logger,
|
|
TAiService aiService,
|
|
XAppConfiguration appConfiguration,
|
|
IXIdentityProvider identityProvider,
|
|
XValidationProvider validationProvider
|
|
) : base(
|
|
logger,
|
|
appConfiguration,
|
|
identityProvider,
|
|
validationProvider
|
|
)
|
|
{
|
|
this.aiService = aiService;
|
|
}
|
|
|
|
//
|
|
#region Actions ...
|
|
/// <summary>
|
|
/// Ask Question and Get Response ...
|
|
/// </summary>
|
|
/// <param name="question"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[AllowAnonymous]
|
|
[HttpGet("AskText")]
|
|
public virtual async Task<ActionResult<string>> AskText(
|
|
[FromQuery] string question,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
ValidationProvider.NotEmpty(question);
|
|
|
|
//
|
|
var result = await aiService.AskTextAsync(
|
|
prompt: question,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ask Question and Get Response Stream ...
|
|
/// </summary>
|
|
/// <param name="question"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[AllowAnonymous]
|
|
[HttpGet("AskTextStream")]
|
|
public virtual async Task AskTextStream(
|
|
[FromQuery] string question,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
ValidationProvider.NotEmpty(question);
|
|
|
|
//
|
|
// Configure SSE Response Headers ...
|
|
Response.Headers["X-Accel-Buffering"] = "no";
|
|
Response.Headers["Connection"] = "keep-alive";
|
|
Response.Headers["Content-Type"] = "text/event-stream";
|
|
Response.Headers["Cache-Control"] = "no-cache, no-store";
|
|
|
|
//
|
|
// Disable response buffering ...
|
|
var responseBodyFeature = HttpContext.Features
|
|
.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
|
|
responseBodyFeature?.DisableBuffering();
|
|
|
|
//
|
|
// Stream Response ...
|
|
var jsonOptions = new JsonSerializerOptions
|
|
{
|
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
|
};
|
|
|
|
//
|
|
var enumerable = aiService.AskTextAsEnumerable(
|
|
prompt: question,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
await foreach (var chunk in enumerable)
|
|
{
|
|
//
|
|
// Skip empty chunks ...
|
|
if (string.IsNullOrEmpty(chunk))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// Build SSE message ...
|
|
// Format: data: <json>\n\n
|
|
// var payload = JsonSerializer.Serialize(
|
|
// new { text = chunk },
|
|
// jsonOptions
|
|
// );
|
|
var payload = chunk;
|
|
|
|
//
|
|
// Write SSE Event ...
|
|
await Response.WriteAsync($"{payload}\n\n", cancellationToken);
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
|
|
//
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
await Response.Body.FlushAsync();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
//
|
|
#region Loaders ...
|
|
/// <summary>
|
|
/// Retrieve AI Projects ...
|
|
/// </summary>
|
|
/// <param name="includeDefaults"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("Projects")]
|
|
public async Task<ActionResult<IEnumerable<XAiProjectDto>>> LoadProjects(
|
|
[FromQuery] bool includeDefaults = false,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
var result = await aiService.LoadProjects(
|
|
ownerId: userInfo.UserId,
|
|
connectionId: connectionId,
|
|
includeDefaults: includeDefaults,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve Specified AI Project's Conversations ...
|
|
/// </summary>
|
|
/// <param name="projectId"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("Projects/{projectId}/Conversations")]
|
|
public async Task<ActionResult<IEnumerable<XAiConversationDto>>> LoadConversations(
|
|
[FromRoute] Guid projectId,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (projectId.IsDefaultGuid())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
var result = await aiService.LoadConversations(
|
|
projectId: projectId,
|
|
connectionId: connectionId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve Specified AI Conversation's Messages ...
|
|
/// </summary>
|
|
/// <param name="conversationId"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("Conversations/{conversationId}/Messages")]
|
|
public async Task<ActionResult<IEnumerable<XAiMessageDto>>> LoadMessages(
|
|
[FromRoute] Guid conversationId,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (conversationId.IsDefaultGuid())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var result = await aiService.LoadMessages(
|
|
conversationId: conversationId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
//
|
|
#region Queries ...
|
|
/// <summary>
|
|
/// Query Projects ...
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// </remarks>
|
|
/// <param name="query"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("Projects/Query")]
|
|
public async Task<ActionResult<XQueryResult<XAiProjectDto>>> QueryProjects(
|
|
[FromQuery] XQuery query,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
var result = await aiService.QueryProjects(
|
|
query: query,
|
|
ownerId: userInfo.UserId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Query Conversations ...
|
|
/// </summary>
|
|
/// <param name="projectId"></param>
|
|
/// <param name="query"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("Projects/{projectId}/Conversations/Query")]
|
|
public async Task<ActionResult<XQueryResult<XAiConversationDto>>> QueryConversations(
|
|
[FromRoute] Guid projectId,
|
|
[FromQuery] XQuery query,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
var result = await aiService.QueryConversations(
|
|
query: query,
|
|
projectId: projectId,
|
|
ownerId: userInfo.UserId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Query Messages ...
|
|
/// </summary>
|
|
/// <param name="conversationId"></param>
|
|
/// <param name="query"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpGet("Conversations/{conversationId}/Messages/Query")]
|
|
public async Task<ActionResult<XQueryResult<XAiMessageDto>>> QueryMessages(
|
|
[FromRoute] Guid conversationId,
|
|
[FromQuery] XQuery query,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
var result = await aiService.QueryMessages(
|
|
query: query,
|
|
ownerId: userInfo.UserId,
|
|
conversationId: conversationId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
//
|
|
#region Creators ...
|
|
/// <summary>
|
|
/// Create an AI Project ...
|
|
/// </summary>
|
|
/// <param name="item"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("Projects")]
|
|
public async Task<ActionResult<XAiProjectDto>> CreateProject(
|
|
[FromBody] XAiProjectDto item,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (item.IsNullOrDefault())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
// Update Required Params ...
|
|
item.OwnerId = userInfo.UserId;
|
|
if (!item.IsValidForAddOrUpdate())
|
|
{
|
|
XException.InvalidData.Throw();
|
|
}
|
|
|
|
//
|
|
var result = await aiService.CreateProject(
|
|
item: item,
|
|
ownerId: userInfo.UserId,
|
|
connectionId: connectionId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an AI Conversation ...
|
|
/// </summary>
|
|
/// <param name="projectId"></param>
|
|
/// <param name="item"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("Projects/{projectId}/Conversations")]
|
|
public async Task<ActionResult<XAiConversationDto>> CreateConversation(
|
|
[FromRoute] Guid projectId,
|
|
[FromBody] XAiConversationDto item,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (projectId.IsDefaultGuid() ||
|
|
item.IsNullOrDefault())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
// Update Required Params ...
|
|
item.OwnerId = userInfo.UserId;
|
|
if (!item.IsValidForAddOrUpdate())
|
|
{
|
|
XException.InvalidData.Throw();
|
|
}
|
|
|
|
//
|
|
var result = await aiService.CreateConversation(
|
|
item: item,
|
|
projectId: projectId,
|
|
ownerId: userInfo.UserId,
|
|
connectionId: connectionId,
|
|
cancellationToken: cancellationToken
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
//
|
|
#region Data Actions ...
|
|
/// <summary>
|
|
/// Ask Question and Get Response ...
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("Ask")]
|
|
public async Task<ActionResult<string>> Ask(
|
|
[FromBody] XAiResponseRequest request,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (!request.IsValid())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
// TODO: Reading Fiels Form Collection and Attach it ...
|
|
var result = await aiService.AskAsync(
|
|
files: null,
|
|
prompt: request.Prompt,
|
|
ownerId: userInfo.UserId,
|
|
connectionId: connectionId,
|
|
projectId: request.ProjectId,
|
|
cancellationToken: cancellationToken,
|
|
conversationId: request.ConversationId
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ask Question and Get Response Stream ...
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("AskStream")]
|
|
public async Task AskStream(
|
|
[FromBody] XAiResponseRequest request,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (!request.IsValid())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
// Configure SSE Response Headers ...
|
|
Response.Headers["X-Accel-Buffering"] = "no";
|
|
Response.Headers["Connection"] = "keep-alive";
|
|
Response.Headers["Content-Type"] = "text/event-stream";
|
|
Response.Headers["Cache-Control"] = "no-cache, no-store";
|
|
|
|
//
|
|
// Disable response buffering ...
|
|
var responseBodyFeature = HttpContext.Features
|
|
.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>();
|
|
responseBodyFeature?.DisableBuffering();
|
|
|
|
//
|
|
// TODO: Reading Fiels Form Collection and Attach it ...
|
|
var enumerable = aiService.AskAsEnumerable(
|
|
files: null,
|
|
prompt: request.Prompt,
|
|
ownerId: userInfo.UserId,
|
|
connectionId: connectionId,
|
|
projectId: request.ProjectId,
|
|
cancellationToken: cancellationToken,
|
|
conversationId: request.ConversationId
|
|
);
|
|
|
|
//
|
|
await foreach (var resp in enumerable)
|
|
{
|
|
//
|
|
// Skip Empty Responses ...
|
|
if (resp.IsNullOrDefault())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
//
|
|
var payload = resp.ToJSON(camelCase: true);
|
|
|
|
//
|
|
// Write SSE Event ...
|
|
await Response.WriteAsync($"{payload}", cancellationToken);
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
|
|
//
|
|
await Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
await Response.Body.FlushAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ask Question with Files and Get Response ...
|
|
/// </summary>
|
|
/// <param name="request"></param>
|
|
/// <param name="cancellationToken"></param>
|
|
/// <returns></returns>
|
|
[HttpPost("AskWithFiles")]
|
|
[Consumes("multipart/form-data")]
|
|
public async Task<ActionResult<string>> AskWithFiles(
|
|
[FromForm] XAiResponseRequest request,
|
|
[FromForm] IFormFileCollection files,
|
|
CancellationToken cancellationToken = default
|
|
)
|
|
{
|
|
//
|
|
try
|
|
{
|
|
//
|
|
// Validate ...
|
|
if (!request.IsValid())
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
|
|
//
|
|
var userInfo = await GetUserInfo();
|
|
var connectionId = GetConnectionId();
|
|
|
|
//
|
|
var result = await aiService.AskAsync(
|
|
files: files,
|
|
userInfo: userInfo,
|
|
prompt: request.Prompt,
|
|
ownerId: userInfo.UserId,
|
|
connectionId: connectionId,
|
|
projectId: request.ProjectId,
|
|
cancellationToken: cancellationToken,
|
|
conversationId: request.ConversationId
|
|
);
|
|
|
|
//
|
|
return Ok(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var result = GetExceptionActionResult(ex);
|
|
return result;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
} |