mirror of
https://github.com/bitwarden/server
synced 2025-12-10 05:13:48 +00:00
PM-26208 updated api endpoint (#6431)
(cherry picked from commit a6726d2e04)
This commit is contained in:
committed by
Conner Turnbull
parent
d26e290cf4
commit
a3b3e370f2
@@ -44,6 +44,15 @@ public class BillingSettings
|
|||||||
{
|
{
|
||||||
public virtual string ApiKey { get; set; }
|
public virtual string ApiKey { get; set; }
|
||||||
public virtual string BaseUrl { get; set; }
|
public virtual string BaseUrl { get; set; }
|
||||||
|
public virtual string Path { get; set; }
|
||||||
public virtual int PersonaId { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
using System.ComponentModel.DataAnnotations;
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -35,7 +32,7 @@ public class FreshdeskController : Controller
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IHttpClientFactory httpClientFactory)
|
IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_billingSettings = billingSettings?.Value;
|
_billingSettings = billingSettings?.Value ?? throw new ArgumentNullException(nameof(billingSettings));
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -101,7 +98,8 @@ public class FreshdeskController : Controller
|
|||||||
customFields[_billingSettings.FreshDesk.OrgFieldName] += $"\n{orgNote}";
|
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))
|
if (!string.IsNullOrWhiteSpace(planName))
|
||||||
{
|
{
|
||||||
tags.Add(string.Format("Org: {0}", planName));
|
tags.Add(string.Format("Org: {0}", planName));
|
||||||
@@ -159,28 +157,22 @@ public class FreshdeskController : Controller
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the onyx `answer-with-citation` request
|
// Get response from Onyx AI
|
||||||
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
|
var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model);
|
||||||
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);
|
|
||||||
|
|
||||||
// the CallOnyxApi will return a null if we have an error response
|
// 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}. ",
|
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
|
||||||
JsonSerializer.Serialize(model),
|
JsonSerializer.Serialize(model),
|
||||||
JsonSerializer.Serialize(onyxRequestModel),
|
JsonSerializer.Serialize(onyxRequest),
|
||||||
JsonSerializer.Serialize(onyxJsonResponse));
|
JsonSerializer.Serialize(onyxResponse));
|
||||||
|
|
||||||
return Ok(); // return ok so we don't retry
|
return Ok(); // return ok so we don't retry
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the answer as a note to the ticket
|
// 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();
|
return Ok();
|
||||||
}
|
}
|
||||||
@@ -206,27 +198,21 @@ public class FreshdeskController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create the onyx `answer-with-citation` request
|
// create the onyx `answer-with-citation` request
|
||||||
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
|
var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model);
|
||||||
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);
|
|
||||||
|
|
||||||
// the CallOnyxApi will return a null if we have an error response
|
// 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}. ",
|
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
|
||||||
JsonSerializer.Serialize(model),
|
JsonSerializer.Serialize(model),
|
||||||
JsonSerializer.Serialize(onyxRequestModel),
|
JsonSerializer.Serialize(onyxRequest),
|
||||||
JsonSerializer.Serialize(onyxJsonResponse));
|
JsonSerializer.Serialize(onyxResponse));
|
||||||
|
|
||||||
return Ok(); // return ok so we don't retry
|
return Ok(); // return ok so we don't retry
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the reply to the ticket
|
// add the reply to the ticket
|
||||||
await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
|
await AddReplyToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId);
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
@@ -356,7 +342,32 @@ public class FreshdeskController : Controller
|
|||||||
return await CallFreshdeskApiAsync(request, retriedCount++);
|
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 httpClient = _httpClientFactory.CreateClient("OnyxApi");
|
||||||
var response = await httpClient.SendAsync(request);
|
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}",
|
_logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}",
|
||||||
response.StatusCode, JsonSerializer.Serialize(response));
|
response.StatusCode, JsonSerializer.Serialize(response));
|
||||||
return (null, default);
|
return new T();
|
||||||
}
|
}
|
||||||
var responseStr = await response.Content.ReadAsStringAsync();
|
var responseStr = await response.Content.ReadAsStringAsync();
|
||||||
var responseJson = JsonSerializer.Deserialize<T>(responseStr, options: new JsonSerializerOptions
|
var responseJson = JsonSerializer.Deserialize<T>(responseStr, options: new JsonSerializerOptions
|
||||||
@@ -373,11 +384,12 @@ public class FreshdeskController : Controller
|
|||||||
PropertyNameCaseInsensitive = true,
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,58 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
using System.Text.Json.Serialization;
|
||||||
#nullable disable
|
using static Bit.Billing.BillingSettings;
|
||||||
|
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Bit.Billing.Models;
|
namespace Bit.Billing.Models;
|
||||||
|
|
||||||
public class OnyxAnswerWithCitationRequestModel
|
public class OnyxRequestModel
|
||||||
{
|
{
|
||||||
[JsonPropertyName("messages")]
|
|
||||||
public List<Message> Messages { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("persona_id")]
|
[JsonPropertyName("persona_id")]
|
||||||
public int PersonaId { get; set; } = 1;
|
public int PersonaId { get; set; } = 1;
|
||||||
|
|
||||||
[JsonPropertyName("retrieval_options")]
|
[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', ' ');
|
message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
|
||||||
Messages = new List<Message>() { new Message() { MessageText = message } };
|
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
|
public class Message
|
||||||
{
|
{
|
||||||
[JsonPropertyName("message")]
|
[JsonPropertyName("message")]
|
||||||
public string MessageText { get; set; }
|
public string MessageText { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("sender")]
|
[JsonPropertyName("sender")]
|
||||||
public string Sender { get; set; } = "user";
|
public string Sender { get; set; } = "user";
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
15
src/Billing/Models/OnyxResponseModel.cs
Normal file
15
src/Billing/Models/OnyxResponseModel.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -80,7 +80,13 @@
|
|||||||
"onyx": {
|
"onyx": {
|
||||||
"apiKey": "SECRET",
|
"apiKey": "SECRET",
|
||||||
"baseUrl": "https://cloud.onyx.app/api",
|
"baseUrl": "https://cloud.onyx.app/api",
|
||||||
"personaId": 7
|
"path": "/chat/send-message-simple-api",
|
||||||
|
"useAnswerWithCitationModels": true,
|
||||||
|
"personaId": 7,
|
||||||
|
"searchSettings": {
|
||||||
|
"runSearch": "always",
|
||||||
|
"realTime": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ public class FreshdeskControllerTests
|
|||||||
[BitAutoData(WebhookKey)]
|
[BitAutoData(WebhookKey)]
|
||||||
public async Task PostWebhookOnyxAi_success(
|
public async Task PostWebhookOnyxAi_success(
|
||||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||||
OnyxAnswerWithCitationResponseModel onyxResponse,
|
OnyxResponseModel onyxResponse,
|
||||||
SutProvider<FreshdeskController> sutProvider)
|
SutProvider<FreshdeskController> sutProvider)
|
||||||
{
|
{
|
||||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||||
|
|||||||
Reference in New Issue
Block a user