Вынести wire-ключи и операции ИИ-сервиса в каталоги

This commit is contained in:
2026-09-13 20:51:42 +03:00
parent 1fbe29a838
commit 04d7a0f6fb
5 changed files with 165 additions and 29 deletions
+27
View File
@@ -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";
}
+4 -4
View File
@@ -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";
}
+25 -25
View File
@@ -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).
+92
View File
@@ -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";
}