From a3b3e370f2c064d74fd924a88d4dc350676a4a8c Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 9 Oct 2025 10:47:54 -0500 Subject: [PATCH] PM-26208 updated api endpoint (#6431) (cherry picked from commit a6726d2e04b14b7cd35a8d55dcca83d7ab3c1719) --- src/Billing/BillingSettings.cs | 9 +++ .../Controllers/FreshdeskController.cs | 80 +++++++++++-------- .../OnyxAnswerWithCitationRequestModel.cs | 51 ++++++++---- .../OnyxAnswerWithCitationResponseModel.cs | 33 -------- src/Billing/Models/OnyxResponseModel.cs | 15 ++++ src/Billing/appsettings.json | 8 +- .../Controllers/FreshdeskControllerTests.cs | 2 +- 7 files changed, 115 insertions(+), 83 deletions(-) delete mode 100644 src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs create mode 100644 src/Billing/Models/OnyxResponseModel.cs diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 32630e4a4a..3dc3e3e808 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -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; } } diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 66d4f47d92..38ed05cfdf 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -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(org.PlanType).Name.Split(" ").FirstOrDefault(); + var displayAttribute = GetAttribute(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(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(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(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(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(onyxSimpleRequest); + return (request, onyxSimpleResponse); + } + + private async Task CallOnyxApi(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(responseStr, options: new JsonSerializerOptions @@ -373,11 +384,12 @@ public class FreshdeskController : Controller PropertyNameCaseInsensitive = true, }); - return (response, responseJson); + return responseJson ?? new T(); } - private TAttribute GetAttribute(Enum enumValue) where TAttribute : Attribute + private TAttribute? GetAttribute(Enum enumValue) where TAttribute : Attribute { - return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute(); + var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault(); + return memberInfo != null ? memberInfo.GetCustomAttribute() : null; } } diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index ba3b89e297..9a753be4bc 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -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 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; + } +} + +/// +/// This is used with the onyx endpoint /query/answer-with-citation +/// which has been deprecated. This can be removed once later +/// +public class OnyxAnswerWithCitationRequestModel : OnyxRequestModel +{ + [JsonPropertyName("messages")] + public List Messages { get; set; } = new List(); + + public OnyxAnswerWithCitationRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) { message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); Messages = new List() { new Message() { MessageText = message } }; - RetrievalOptions = new RetrievalOptions(); - PersonaId = personaId; + } +} + +/// +/// This is used with the onyx endpoint /chat/send-message-simple-api +/// +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"; diff --git a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs deleted file mode 100644 index 5f67cd51d2..0000000000 --- a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs +++ /dev/null @@ -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 Citations { get; set; } - - [JsonPropertyName("llm_selected_doc_indices")] - public List 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; } -} diff --git a/src/Billing/Models/OnyxResponseModel.cs b/src/Billing/Models/OnyxResponseModel.cs new file mode 100644 index 0000000000..96fa134c40 --- /dev/null +++ b/src/Billing/Models/OnyxResponseModel.cs @@ -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; +} diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 0074b5aafe..6c90c22686 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -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 + } } } } diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index 8fd0769a02..5c9199d29a 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -169,7 +169,7 @@ public class FreshdeskControllerTests [BitAutoData(WebhookKey)] public async Task PostWebhookOnyxAi_success( string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - OnyxAnswerWithCitationResponseModel onyxResponse, + OnyxResponseModel onyxResponse, SutProvider sutProvider) { var billingSettings = sutProvider.GetDependency>().Value;