Вынести wire-ключи и операции ИИ-сервиса в каталоги
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Deal.Ai;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Имена операций AiService для аудита
|
||||||
|
/// </summary>
|
||||||
|
public static class AiOperations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Операция ИИ-фильтра входящих сообщений.
|
||||||
|
/// </summary>
|
||||||
|
public const string Filter = "filter";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Операция полного разбора лида.
|
||||||
|
/// </summary>
|
||||||
|
public const string Classify = "classify";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Операция генерации ключевых слов discovery-задачи.
|
||||||
|
/// </summary>
|
||||||
|
public const string GenerateKeywords = "generate_keywords";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Операция оценки соответствия сообщения задаче поиска.
|
||||||
|
/// </summary>
|
||||||
|
public const string EvaluateFit = "evaluate_fit";
|
||||||
|
}
|
||||||
@@ -150,7 +150,7 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
|||||||
}
|
}
|
||||||
catch (LlmCallException callError)
|
catch (LlmCallException callError)
|
||||||
{
|
{
|
||||||
LogAiUnavailable("filter", tenantId, config, callError);
|
LogAiUnavailable(AiOperations.Filter, tenantId, config, callError);
|
||||||
throw ToUnavailable(callError);
|
throw ToUnavailable(callError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
|||||||
}
|
}
|
||||||
catch (LlmCallException callError)
|
catch (LlmCallException callError)
|
||||||
{
|
{
|
||||||
LogAiUnavailable("classify", tenantId, config, callError);
|
LogAiUnavailable(AiOperations.Classify, tenantId, config, callError);
|
||||||
throw ToUnavailable(callError);
|
throw ToUnavailable(callError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
|||||||
}
|
}
|
||||||
catch (LlmCallException callError)
|
catch (LlmCallException callError)
|
||||||
{
|
{
|
||||||
LogAiUnavailable("generate_keywords", tenantId, config, callError);
|
LogAiUnavailable(AiOperations.GenerateKeywords, tenantId, config, callError);
|
||||||
throw ToUnavailable(callError);
|
throw ToUnavailable(callError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
|
|||||||
}
|
}
|
||||||
catch (LlmCallException callError)
|
catch (LlmCallException callError)
|
||||||
{
|
{
|
||||||
LogAiUnavailable("evaluate_fit", tenantId, config, callError);
|
LogAiUnavailable(AiOperations.EvaluateFit, tenantId, config, callError);
|
||||||
throw ToUnavailable(callError);
|
throw ToUnavailable(callError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Deal.Ai.Llm;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Роли сообщений чата в wire-формате ИИ-провайдеров
|
||||||
|
/// </summary>
|
||||||
|
public static class LlmChatRoles
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Роль системного сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public const string System = "system";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Роль пользовательского сообщения.
|
||||||
|
/// </summary>
|
||||||
|
public const string User = "user";
|
||||||
|
}
|
||||||
@@ -100,12 +100,12 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
|
|
||||||
var body = new JsonObject
|
var body = new JsonObject
|
||||||
{
|
{
|
||||||
["model"] = config.Model,
|
[LlmWireKeys.Model] = config.Model,
|
||||||
["messages"] = new JsonArray(
|
[LlmWireKeys.Messages] = new JsonArray(
|
||||||
ChatMessage("system", systemPrompt),
|
ChatMessage(LlmChatRoles.System, systemPrompt),
|
||||||
ChatMessage("user", userText)),
|
ChatMessage(LlmChatRoles.User, userText)),
|
||||||
["temperature"] = Temperature,
|
[LlmWireKeys.Temperature] = Temperature,
|
||||||
["max_tokens"] = MaxResponseTokens,
|
[LlmWireKeys.MaxTokens] = MaxResponseTokens,
|
||||||
};
|
};
|
||||||
|
|
||||||
request.Content = JsonBody(body);
|
request.Content = JsonBody(body);
|
||||||
@@ -123,10 +123,10 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
|
|
||||||
var body = new JsonObject
|
var body = new JsonObject
|
||||||
{
|
{
|
||||||
["model"] = config.Model,
|
[LlmWireKeys.Model] = config.Model,
|
||||||
["max_tokens"] = MaxResponseTokens,
|
[LlmWireKeys.MaxTokens] = MaxResponseTokens,
|
||||||
["system"] = systemPrompt,
|
[LlmWireKeys.System] = systemPrompt,
|
||||||
["messages"] = new JsonArray(ChatMessage("user", userText)),
|
[LlmWireKeys.Messages] = new JsonArray(ChatMessage(LlmChatRoles.User, userText)),
|
||||||
};
|
};
|
||||||
|
|
||||||
request.Content = JsonBody(body);
|
request.Content = JsonBody(body);
|
||||||
@@ -156,7 +156,7 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
private static ProviderChatResult ReadOpenAiBody(string body)
|
private static ProviderChatResult ReadOpenAiBody(string body)
|
||||||
{
|
{
|
||||||
JsonObject? payload = ParseObjectOrThrow(body);
|
JsonObject? payload = ParseObjectOrThrow(body);
|
||||||
JsonArray? choices = payload["choices"] as JsonArray;
|
JsonArray? choices = payload[LlmWireKeys.Choices] as JsonArray;
|
||||||
if (choices is null || choices.Count == 0)
|
if (choices is null || choices.Count == 0)
|
||||||
{
|
{
|
||||||
// Тип сбоя без содержимого тела (замечание code-review: тело ответа наружу/в лог не уходит).
|
// Тип сбоя без содержимого тела (замечание code-review: тело ответа наружу/в лог не уходит).
|
||||||
@@ -164,14 +164,14 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
// choices[i] — объект выбора {message, finish_reason, …}; текст — в message.content.
|
// choices[i] — объект выбора {message, finish_reason, …}; текст — в message.content.
|
||||||
JsonObject? message = (choices[0] as JsonObject)?["message"] as JsonObject;
|
JsonObject? message = (choices[0] as JsonObject)?[LlmWireKeys.Message] as JsonObject;
|
||||||
string? content = ReadStringField(message, "content");
|
string? content = ReadStringField(message, LlmWireKeys.Content);
|
||||||
if (string.IsNullOrEmpty(content) && !string.IsNullOrEmpty(ReadStringField(message, "reasoning_content")))
|
if (string.IsNullOrEmpty(content) && !string.IsNullOrEmpty(ReadStringField(message, LlmWireKeys.ReasoningContent)))
|
||||||
{
|
{
|
||||||
throw new LlmHttpException("Модель вернула только reasoning без ответа");
|
throw new LlmHttpException("Модель вернула только reasoning без ответа");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProviderChatResult(content ?? string.Empty, ReadOpenAiUsage(payload["usage"]));
|
return new ProviderChatResult(content ?? string.Empty, ReadOpenAiUsage(payload[LlmWireKeys.Usage]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProviderChatResult ReadAnthropicBody(string body)
|
private static ProviderChatResult ReadAnthropicBody(string body)
|
||||||
@@ -179,18 +179,18 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
JsonObject? payload = ParseObjectOrThrow(body);
|
JsonObject? payload = ParseObjectOrThrow(body);
|
||||||
|
|
||||||
var text = new StringBuilder();
|
var text = new StringBuilder();
|
||||||
if (payload["content"] is JsonArray contentBlocks)
|
if (payload[LlmWireKeys.Content] is JsonArray contentBlocks)
|
||||||
{
|
{
|
||||||
foreach (JsonNode? blockNode in contentBlocks)
|
foreach (JsonNode? blockNode in contentBlocks)
|
||||||
{
|
{
|
||||||
if (blockNode is JsonObject block)
|
if (blockNode is JsonObject block)
|
||||||
{
|
{
|
||||||
text.Append(ReadStringField(block, "text"));
|
text.Append(ReadStringField(block, LlmWireKeys.Text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProviderChatResult(text.ToString(), ReadAnthropicUsage(payload["usage"]));
|
return new ProviderChatResult(text.ToString(), ReadAnthropicUsage(payload[LlmWireKeys.Usage]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Возвращает usage OpenAI-совместимого ответа (prompt/completion/total_tokens) либо null.
|
// Возвращает usage OpenAI-совместимого ответа (prompt/completion/total_tokens) либо null.
|
||||||
@@ -202,9 +202,9 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? promptTokens = ReadIntField(usage, "prompt_tokens");
|
int? promptTokens = ReadIntField(usage, LlmWireKeys.PromptTokens);
|
||||||
int? completionTokens = ReadIntField(usage, "completion_tokens");
|
int? completionTokens = ReadIntField(usage, LlmWireKeys.CompletionTokens);
|
||||||
int? totalTokens = ReadIntField(usage, "total_tokens");
|
int? totalTokens = ReadIntField(usage, LlmWireKeys.TotalTokens);
|
||||||
return promptTokens is null || completionTokens is null || totalTokens is null
|
return promptTokens is null || completionTokens is null || totalTokens is null
|
||||||
? null
|
? null
|
||||||
: new ProviderUsage(promptTokens.Value, completionTokens.Value, totalTokens.Value);
|
: new ProviderUsage(promptTokens.Value, completionTokens.Value, totalTokens.Value);
|
||||||
@@ -219,8 +219,8 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? inputTokens = ReadIntField(usage, "input_tokens");
|
int? inputTokens = ReadIntField(usage, LlmWireKeys.InputTokens);
|
||||||
int? outputTokens = ReadIntField(usage, "output_tokens");
|
int? outputTokens = ReadIntField(usage, LlmWireKeys.OutputTokens);
|
||||||
return inputTokens is null || outputTokens is null
|
return inputTokens is null || outputTokens is null
|
||||||
? null
|
? null
|
||||||
: new ProviderUsage(inputTokens.Value, outputTokens.Value, inputTokens.Value + outputTokens.Value);
|
: new ProviderUsage(inputTokens.Value, outputTokens.Value, inputTokens.Value + outputTokens.Value);
|
||||||
@@ -254,8 +254,8 @@ public sealed class LlmHttpClient : IProviderClient
|
|||||||
private static JsonObject ChatMessage(string role, string content)
|
private static JsonObject ChatMessage(string role, string content)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
["role"] = role,
|
[LlmWireKeys.Role] = role,
|
||||||
["content"] = content,
|
[LlmWireKeys.Content] = content,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Создаёт JSON-содержимое запроса (application/json).
|
// Создаёт JSON-содержимое запроса (application/json).
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
namespace Deal.Ai.Llm;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON-ключи wire-формата запросов и ответов ИИ-провайдеров
|
||||||
|
/// </summary>
|
||||||
|
public static class LlmWireKeys
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ имени модели.
|
||||||
|
/// </summary>
|
||||||
|
public const string Model = "model";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ списка сообщений чата.
|
||||||
|
/// </summary>
|
||||||
|
public const string Messages = "messages";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ системного промпта (Anthropic — отдельным полем).
|
||||||
|
/// </summary>
|
||||||
|
public const string System = "system";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ температуры генерации (OpenAI-совместимый формат).
|
||||||
|
/// </summary>
|
||||||
|
public const string Temperature = "temperature";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ лимита токенов ответа.
|
||||||
|
/// </summary>
|
||||||
|
public const string MaxTokens = "max_tokens";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ списка вариантов ответа (OpenAI-совместимый формат).
|
||||||
|
/// </summary>
|
||||||
|
public const string Choices = "choices";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ выбранного варианта ответа (OpenAI-совместимый формат).
|
||||||
|
/// </summary>
|
||||||
|
public const string Message = "message";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ текстового содержимого.
|
||||||
|
/// </summary>
|
||||||
|
public const string Content = "content";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ текста рассуждений модели (reasoning).
|
||||||
|
/// </summary>
|
||||||
|
public const string ReasoningContent = "reasoning_content";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ оценки токенов вызова.
|
||||||
|
/// </summary>
|
||||||
|
public const string Usage = "usage";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ текста блока контента (Anthropic).
|
||||||
|
/// </summary>
|
||||||
|
public const string Text = "text";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ числа токенов запроса (OpenAI-совместимый формат).
|
||||||
|
/// </summary>
|
||||||
|
public const string PromptTokens = "prompt_tokens";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ числа токенов ответа (OpenAI-совместимый формат).
|
||||||
|
/// </summary>
|
||||||
|
public const string CompletionTokens = "completion_tokens";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ суммарного числа токенов (OpenAI-совместимый формат).
|
||||||
|
/// </summary>
|
||||||
|
public const string TotalTokens = "total_tokens";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ числа входных токенов (Anthropic).
|
||||||
|
/// </summary>
|
||||||
|
public const string InputTokens = "input_tokens";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ числа выходных токенов (Anthropic).
|
||||||
|
/// </summary>
|
||||||
|
public const string OutputTokens = "output_tokens";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ключ роли сообщения чата.
|
||||||
|
/// </summary>
|
||||||
|
public const string Role = "role";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user