/
afanasevn
/
PdfEncoder
Обзор
Документация
Войти
/
afanasevn
/
PdfEncoder
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/PdfEncoder.IntegrationTests/Support/MockLlmClient.cs
224 строки
7 KB
IBS\NAfanasev
Parse results functional update
07 июл 2026, 00:06
07 июл 2026, 00:06
8e97443
Код
Авторство
О чём код?
using PdfEncoder.Application.Abstractions; using PdfEncoder.Application.Models; using PdfEncoder.Domain.Models; using PdfEncoder.IntegrationTests.Fixtures; namespace PdfEncoder.IntegrationTests.Support; /// <summary> /// Mock LLM для integration-тестов: фиксированные classify/extract по типу документа. /// </summary> public class MockLlmClient : ILlmClient { private readonly object _sync = new(); private bool _shouldThrow; /// <summary> /// Число вызовов LLM (classify + extract). /// </summary> public int CallCount { get; private set; } /// <summary> /// Имена шагов в порядке вызова. /// </summary> public IReadOnlyList<string> StepNames { get { lock (_sync) { return _stepNames.ToList(); } } } private readonly List<string> _stepNames = []; /// <summary> /// Заставляет следующие вызовы бросать <see cref="LlmException"/>. /// </summary> public void SetThrowOnNextCalls(bool shouldThrow) { _shouldThrow = shouldThrow; } /// <inheritdoc /> public Task<LlmCompletionResult<T>> CompleteStructuredAsync<T>( LlmStructuredRequest request, CancellationToken ct) { lock (_sync) { CallCount++; _stepNames.Add(request.StepName); } if (_shouldThrow) { throw new Domain.Errors.LlmException( "llm-unavailable", "Mock LLM failure for integration test.", 503); } if (typeof(T) == typeof(LlmClassifyResponseDto)) { var documentType = ResolveDocumentType(request); return Task.FromResult(new LlmCompletionResult<T>( (T)(object)new LlmClassifyResponseDto { DocumentType = documentType, Confidence = 0.92, Reasoning = "mock-classify" }, 12, 8, 5)); } if (typeof(T) == typeof(LlmExtractResponseDto)) { var documentType = ResolveDocumentType(request); var fixture = DocumentFixtureCatalog.All .FirstOrDefault(item => item.DocumentType == documentType); var fields = fixture?.ExpectedFields .Select(field => new LlmExtractedField { Key = field.Key, Value = field.Value, Confidence = 0.88, Source = "pdf-text" }) .ToList() ?? []; return Task.FromResult(new LlmCompletionResult<T>( (T)(object)new LlmExtractResponseDto { Fields = fields, Tables = [], Warnings = [] }, 20, 15, 8)); } if (typeof(T) == typeof(LlmContentHintResponseDto)) { var pageText = ExtractPageTextSection(request.UserPrompt); var hint = pageText.Contains("банан", StringComparison.OrdinalIgnoreCase) || pageText.Contains("оладуш", StringComparison.OrdinalIgnoreCase) || pageText.Contains("RECIPE", StringComparison.OrdinalIgnoreCase) || pageText.Contains("STEP", StringComparison.OrdinalIgnoreCase) ? "Вероятно, это рецепт банановых оладушек с ингредиентами и инструкцией." : "Вероятно, это документ с неструктурированным содержимым."; return Task.FromResult(new LlmCompletionResult<T>( (T)(object)new LlmContentHintResponseDto { Hint = hint, Confidence = 0.82 }, 10, 12, 4)); } throw new NotSupportedException($"Mock LLM does not support type {typeof(T).Name}."); } /// <summary> /// Сбрасывает счётчики вызовов. /// </summary> public void Reset() { lock (_sync) { CallCount = 0; _stepNames.Clear(); _shouldThrow = false; } } private static string ResolveDocumentType(LlmStructuredRequest request) { if (request.StepName == "llm-extract") { var marker = "\"detectedType\":\""; var start = request.UserPrompt.IndexOf(marker, StringComparison.Ordinal); if (start >= 0) { start += marker.Length; var end = request.UserPrompt.IndexOf('"', start); if (end > start) { return request.UserPrompt[start..end]; } } } return InferDocumentTypeFromText(ExtractPageTextSection(request.UserPrompt)); } private static string ExtractPageTextSection(string prompt) { const string openMarker = "---\n"; const string closeMarker = "\n---"; var start = prompt.IndexOf(openMarker, StringComparison.Ordinal); if (start < 0) { return prompt; } start += openMarker.Length; var end = prompt.IndexOf(closeMarker, start, StringComparison.Ordinal); return end > start ? prompt[start..end] : prompt[start..]; } private static string InferDocumentTypeFromText(string text) { if (text.Contains("40817810000000001234", StringComparison.Ordinal) || text.Contains("Opening balance", StringComparison.OrdinalIgnoreCase) || text.Contains("Closing balance", StringComparison.OrdinalIgnoreCase)) { return "bank_statement"; } if (text.Contains("567890", StringComparison.Ordinal) || text.Contains("Ivan Testov", StringComparison.OrdinalIgnoreCase) || text.Contains("PASSPORT", StringComparison.OrdinalIgnoreCase)) { return "passport"; } if (text.Contains("CONTRACT DEMO", StringComparison.OrdinalIgnoreCase) || text.Contains("DEMO-003", StringComparison.OrdinalIgnoreCase)) { return "contract"; } if (text.Contains("ACT OF WORK", StringComparison.OrdinalIgnoreCase) || text.Contains("DEMO-002", StringComparison.OrdinalIgnoreCase)) { return "act"; } if (text.Contains("INVOICE DEMO", StringComparison.OrdinalIgnoreCase) || text.Contains("DEMO-001", StringComparison.OrdinalIgnoreCase)) { return "invoice"; } if (text.Contains("оладуш", StringComparison.OrdinalIgnoreCase) || text.Contains("банан", StringComparison.OrdinalIgnoreCase) || text.Contains("STEP 1", StringComparison.OrdinalIgnoreCase) || text.Contains("RECIPE DEMO", StringComparison.OrdinalIgnoreCase)) { return "unknown"; } return "unknown"; } }