1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

PM-26208 updated api endpoint (#6431)

This commit is contained in:
Vijay Oommen
2025-10-09 10:47:54 -05:00
committed by GitHub
parent fe32e885c6
commit a6726d2e04
7 changed files with 115 additions and 83 deletions

View File

@@ -44,6 +44,15 @@ public class BillingSettings
{
public virtual string ApiKey { get; set; }
public virtual string BaseUrl { get; set; }
public virtual string Path { get; set; }
public virtual int PersonaId { get; set; }
public virtual bool UseAnswerWithCitationModels { get; set; } = true;
public virtual SearchSettings SearchSettings { get; set; } = new SearchSettings();
}
public class SearchSettings
{
public virtual string RunSearch { get; set; } = "auto"; // "always", "never", "auto"
public virtual bool RealTime { get; set; } = true;
}
}

View File

@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
@@ -35,7 +32,7 @@ public class FreshdeskController : Controller
GlobalSettings globalSettings,
IHttpClientFactory httpClientFactory)
{
_billingSettings = billingSettings?.Value;
_billingSettings = billingSettings?.Value ?? throw new ArgumentNullException(nameof(billingSettings));
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_logger = logger;
@@ -101,7 +98,8 @@ public class FreshdeskController : Controller
customFields[_billingSettings.FreshDesk.OrgFieldName] += $"\n{orgNote}";
}
var planName = GetAttribute<DisplayAttribute>(org.PlanType).Name.Split(" ").FirstOrDefault();
var displayAttribute = GetAttribute<DisplayAttribute>(org.PlanType);
var planName = displayAttribute?.Name?.Split(" ").FirstOrDefault();
if (!string.IsNullOrWhiteSpace(planName))
{
tags.Add(string.Format("Org: {0}", planName));
@@ -159,28 +157,22 @@ public class FreshdeskController : Controller
return Ok();
}
// create the onyx `answer-with-citation` request
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
};
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
// Get response from Onyx AI
var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model);
// the CallOnyxApi will return a null if we have an error response
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg))
{
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
JsonSerializer.Serialize(model),
JsonSerializer.Serialize(onyxRequestModel),
JsonSerializer.Serialize(onyxJsonResponse));
JsonSerializer.Serialize(onyxRequest),
JsonSerializer.Serialize(onyxResponse));
return Ok(); // return ok so we don't retry
}
// add the answer as a note to the ticket
await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
await AddAnswerNoteToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId);
return Ok();
}
@@ -206,27 +198,21 @@ public class FreshdeskController : Controller
}
// create the onyx `answer-with-citation` request
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
};
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model);
// the CallOnyxApi will return a null if we have an error response
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg))
{
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
JsonSerializer.Serialize(model),
JsonSerializer.Serialize(onyxRequestModel),
JsonSerializer.Serialize(onyxJsonResponse));
JsonSerializer.Serialize(onyxRequest),
JsonSerializer.Serialize(onyxResponse));
return Ok(); // return ok so we don't retry
}
// add the reply to the ticket
await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
await AddReplyToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId);
return Ok();
}
@@ -356,7 +342,32 @@ public class FreshdeskController : Controller
return await CallFreshdeskApiAsync(request, retriedCount++);
}
private async Task<(HttpResponseMessage, T)> CallOnyxApi<T>(HttpRequestMessage request)
async Task<(OnyxRequestModel onyxRequest, OnyxResponseModel onyxResponse)> GetAnswerFromOnyx(FreshdeskOnyxAiWebhookModel model)
{
// TODO: remove the use of the deprecated answer-with-citation models after we are sure
if (_billingSettings.Onyx.UseAnswerWithCitationModels)
{
var onyxRequest = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx);
var onyxAnswerWithCitationRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
{
Content = JsonContent.Create(onyxRequest, mediaType: new MediaTypeHeaderValue("application/json")),
};
var onyxResponse = await CallOnyxApi<OnyxResponseModel>(onyxAnswerWithCitationRequest);
return (onyxRequest, onyxResponse);
}
var request = new OnyxSendMessageSimpleApiRequestModel(model.TicketDescriptionText, _billingSettings.Onyx);
var onyxSimpleRequest = new HttpRequestMessage(HttpMethod.Post,
string.Format("{0}{1}", _billingSettings.Onyx.BaseUrl, _billingSettings.Onyx.Path))
{
Content = JsonContent.Create(request, mediaType: new MediaTypeHeaderValue("application/json")),
};
var onyxSimpleResponse = await CallOnyxApi<OnyxResponseModel>(onyxSimpleRequest);
return (request, onyxSimpleResponse);
}
private async Task<T> CallOnyxApi<T>(HttpRequestMessage request) where T : class, new()
{
var httpClient = _httpClientFactory.CreateClient("OnyxApi");
var response = await httpClient.SendAsync(request);
@@ -365,7 +376,7 @@ public class FreshdeskController : Controller
{
_logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}",
response.StatusCode, JsonSerializer.Serialize(response));
return (null, default);
return new T();
}
var responseStr = await response.Content.ReadAsStringAsync();
var responseJson = JsonSerializer.Deserialize<T>(responseStr, options: new JsonSerializerOptions
@@ -373,11 +384,12 @@ public class FreshdeskController : Controller
PropertyNameCaseInsensitive = true,
});
return (response, responseJson);
return responseJson ?? new T();
}
private TAttribute GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
private TAttribute? GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
{
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<TAttribute>();
var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault();
return memberInfo != null ? memberInfo.GetCustomAttribute<TAttribute>() : null;
}
}

View File

@@ -1,35 +1,58 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using static Bit.Billing.BillingSettings;
namespace Bit.Billing.Models;
public class OnyxAnswerWithCitationRequestModel
public class OnyxRequestModel
{
[JsonPropertyName("messages")]
public List<Message> Messages { get; set; }
[JsonPropertyName("persona_id")]
public int PersonaId { get; set; } = 1;
[JsonPropertyName("retrieval_options")]
public RetrievalOptions RetrievalOptions { get; set; }
public RetrievalOptions RetrievalOptions { get; set; } = new RetrievalOptions();
public OnyxAnswerWithCitationRequestModel(string message, int personaId = 1)
public OnyxRequestModel(OnyxSettings onyxSettings)
{
PersonaId = onyxSettings.PersonaId;
RetrievalOptions.RunSearch = onyxSettings.SearchSettings.RunSearch;
RetrievalOptions.RealTime = onyxSettings.SearchSettings.RealTime;
}
}
/// <summary>
/// This is used with the onyx endpoint /query/answer-with-citation
/// which has been deprecated. This can be removed once later
/// </summary>
public class OnyxAnswerWithCitationRequestModel : OnyxRequestModel
{
[JsonPropertyName("messages")]
public List<Message> Messages { get; set; } = new List<Message>();
public OnyxAnswerWithCitationRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings)
{
message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
Messages = new List<Message>() { new Message() { MessageText = message } };
RetrievalOptions = new RetrievalOptions();
PersonaId = personaId;
}
}
/// <summary>
/// This is used with the onyx endpoint /chat/send-message-simple-api
/// </summary>
public class OnyxSendMessageSimpleApiRequestModel : OnyxRequestModel
{
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
public OnyxSendMessageSimpleApiRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings)
{
Message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
}
}
public class Message
{
[JsonPropertyName("message")]
public string MessageText { get; set; }
public string MessageText { get; set; } = string.Empty;
[JsonPropertyName("sender")]
public string Sender { get; set; } = "user";

View File

@@ -1,33 +0,0 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json.Serialization;
namespace Bit.Billing.Models;
public class OnyxAnswerWithCitationResponseModel
{
[JsonPropertyName("answer")]
public string Answer { get; set; }
[JsonPropertyName("rephrase")]
public string Rephrase { get; set; }
[JsonPropertyName("citations")]
public List<Citation> Citations { get; set; }
[JsonPropertyName("llm_selected_doc_indices")]
public List<int> LlmSelectedDocIndices { get; set; }
[JsonPropertyName("error_msg")]
public string ErrorMsg { get; set; }
}
public class Citation
{
[JsonPropertyName("citation_num")]
public int CitationNum { get; set; }
[JsonPropertyName("document_id")]
public string DocumentId { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Bit.Billing.Models;
public class OnyxResponseModel
{
[JsonPropertyName("answer")]
public string Answer { get; set; } = string.Empty;
[JsonPropertyName("answer_citationless")]
public string AnswerCitationless { get; set; } = string.Empty;
[JsonPropertyName("error_msg")]
public string ErrorMsg { get; set; } = string.Empty;
}

View File

@@ -80,7 +80,13 @@
"onyx": {
"apiKey": "SECRET",
"baseUrl": "https://cloud.onyx.app/api",
"personaId": 7
"path": "/chat/send-message-simple-api",
"useAnswerWithCitationModels": true,
"personaId": 7,
"searchSettings": {
"runSearch": "always",
"realTime": true
}
}
}
}

View File

@@ -169,7 +169,7 @@ public class FreshdeskControllerTests
[BitAutoData(WebhookKey)]
public async Task PostWebhookOnyxAi_success(
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
OnyxAnswerWithCitationResponseModel onyxResponse,
OnyxResponseModel onyxResponse,
SutProvider<FreshdeskController> sutProvider)
{
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;