Skip to content
This repository was archived by the owner on May 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
47243b9
feat(worker): migrate to .NET 10 and install Microsoft.Agents.AI
cherninkiy May 13, 2026
aa5dfac
feat(shared): add workflow checkpoint and agent definition models
cherninkiy May 13, 2026
961f696
feat(shared): define agent abstractions (IAgent, IAgentOrchestrator, …
cherninkiy May 13, 2026
17af237
feat(worker): implement PostgreSQL checkpoint store for MAF
cherninkiy May 13, 2026
53d9b0d
feat(worker): implement DocumentProcessingAgent with MAF checkpoints
cherninkiy May 13, 2026
ddb7bfe
feat(worker): refactor consumer to use MAF DocumentProcessingAgent
cherninkiy May 13, 2026
aec2cf2
feat(worker): register MAF services in DI container
cherninkiy May 13, 2026
a14c351
test(worker): add DocumentProcessingAgent unit tests with checkpoint …
cherninkiy May 13, 2026
6400e39
test(worker): add DocumentProcessingAgent unit tests with checkpoint …
cherninkiy May 13, 2026
e51c127
docs: add agentic architecture documentation and readiness report
cherninkiy May 13, 2026
664f69b
feat(worker): wrap Prometheus MetricServer in IHostedService
cherninkiy May 14, 2026
21d90cc
refactor(api-gateway): extract DB configuration into AddDatabase() ex…
cherninkiy May 14, 2026
ffde14b
feat(api-gateway): add JWT authentication with dev token endpoint
cherninkiy May 14, 2026
583d74e
feat(infra): replace default ILogger with Serilog structured logging
cherninkiy May 14, 2026
42eff53
fix(worker): add typed exception, CancellationToken checks, docker-co…
cherninkiy May 14, 2026
2f6d150
test(worker,api-gateway): add OutboxPublisher, retry→DLQ, idempotency…
cherninkiy May 14, 2026
aa8459f
docs(readme): add badges, SOTA link, update to current state
cherninkiy May 14, 2026
aa9eca0
fix(ci): update .NET version from 8.0 to 10.0 in CI workflow and Dock…
cherninkiy May 14, 2026
fb2b83d
fix(review): address code review feedback — JWT secret validation, de…
cherninkiy May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 10.0.x

- name: Install system dependencies (Tesseract OCR + poppler-utils)
run: |
Expand Down
256 changes: 170 additions & 86 deletions README.md

Large diffs are not rendered by default.

41 changes: 40 additions & 1 deletion db/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,43 @@ CREATE TABLE IF NOT EXISTS processed_messages (
);

CREATE INDEX IF NOT EXISTS idx_processed_messages_message_id ON processed_messages(message_id);
CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status);
CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status);

-- ── Workflow Checkpoints ──
-- Stores agent execution state for durable workflows (MAF).
-- If a worker crashes mid-processing, the agent resumes from the last checkpoint.
CREATE TABLE IF NOT EXISTS workflow_checkpoints (
id UUID PRIMARY KEY,
agent_name VARCHAR(128) NOT NULL,
document_id UUID NOT NULL,
current_activity VARCHAR(128) NOT NULL,
state_data JSONB,
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
is_failed BOOLEAN NOT NULL DEFAULT FALSE,
error_message TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_workflow_checkpoints_agent_document ON workflow_checkpoints(agent_name, document_id);
CREATE INDEX IF NOT EXISTS idx_workflow_checkpoints_completed ON workflow_checkpoints(is_completed);

-- ── Agent Definitions ──
-- Registry of available agents for dynamic discovery and orchestration.
-- New agents (Translation, NER, Summarization) are added here.
CREATE TABLE IF NOT EXISTS agent_definitions (
id UUID PRIMARY KEY,
name VARCHAR(128) NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
handler_type VARCHAR(512) NOT NULL,
activities JSONB NOT NULL DEFAULT '[]',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_agent_definitions_name ON agent_definitions(name);

-- Seed the default DocumentProcessing agent
INSERT INTO agent_definitions (id, name, description, handler_type, activities) VALUES
(gen_random_uuid(), 'DocumentProcessing', 'Downloads, parses, extracts text from PDF documents', 'Worker.Agents.DocumentProcessingAgent', '["DownloadDocument","ParseDocument","ExtractText","SaveResult","UpdateStatus"]')
ON CONFLICT (name) DO NOTHING;
270 changes: 270 additions & 0 deletions docs/AGENTIC_READINESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# Agentic Readiness Report

## Статус: ✅ Завершено

Все 9 этапов миграции на MAF выполнены. Система готова к расширению через новых AI-агентов.

## Результат миграции

| Метрика | До (MVP) | После (Agentic) |
|---------|----------|-----------------|
| Платформа | .NET 8 | .NET 10 |
| Архитектура воркера | Линейная цепочка вызовов | MAF Agent с чекпоинтами |
| Resume после падения | Нет (retry с нуля) | Да (resume с последнего чекпоинта) |
| Тесты Worker | 7 | 16 (+9 MAF/checkpoint тестов) |
| Тесты всего | 19 | 28 |
| Добавление нового агента | Изменение Consumer | Новый класс `IAgent` + DI |

## Архитектура

### Компоненты

```
┌─────────────────────────────────────────────────────────────┐
│ MassTransit Consumer │
│ (приём сообщений из RabbitMQ, retry/DLQ — без изменений) │
└──────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DocumentProcessingAgent (MAF) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Download │──▶│ Parse │──▶│ Extract │ │
│ │ Document │ │ Document │ │ Text │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ checkpoint │ checkpoint │ checkpoint │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Save │──▶│ Update │ │
│ │ Result │ │ Status │ │
│ └──────────────┘ └──────────────┘ │
│ │ checkpoint │ checkpoint │
└─────────────────────────────────────────────────────────────┘
┌────────────────────────┐
│ CheckpointStore │
│ (PostgreSQL + EF Core)│
└────────────────────────┘
```

### Ключевые интерфейсы (Shared)

| Интерфейс | Назначение |
|-----------|------------|
| `IAgent` | Контракт агента: `AgentName`, `Activities`, `ExecuteAsync` |
| `IAgentOrchestrator` | Оркестрация пайплайна из нескольких агентов |
| `ICheckpointStore` | Сохранение/загрузка чекпоинтов |
| `AgentContext` | Контекст выполнения: `DocumentId`, `FilePath`, `CurrentActivity` |
| `AgentResult` | Результат шага: `IsSuccess`, `OutputData`, `ErrorMessage` |

### Модели данных (Shared)

| Модель | Назначение |
|--------|------------|
| `WorkflowCheckpoint` | Запись чекпоинта: `AgentName`, `DocumentId`, `CurrentActivity`, `StateData`, `IsCompleted`, `IsFailed` |
| `AgentDefinition` | Определение агента для оркестратора |

### Таблицы PostgreSQL

| Таблица | Назначение |
|---------|------------|
| `documents` | Метаданные документов (статус, текст, путь) |
| `outbox` | Транзакционный outbox |
| `processed_messages` | Идемпотентность потребителя |
| `workflow_checkpoints` | Чекпоинты MAF-агентов |
| `agent_definitions` | Определения агентов |

## Особенности миграции на .NET 10 + MAF

### Требования MAF к платформе

> [**System Requirements: .NET 10.0+**](https://github.com/microsoft/semantic-kernel/pkgs/nuget/Microsoft.SemanticKernel.Connectors.Memory.Kusto#system-requirements)

Релиз MAF не поддерживает .NET 8 и более ранние версии. По этой причине была выполнена миграция всего решения с `.net8.0` на `.net10.0` (Этап 1).

### Проблема совместимости пакетов

При переходе с `net8.0` на `net10.0` обнаружена несовместимость версий NuGet-пакетов:

- **MassTransit 8.5.9** объявляет зависимость от `Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.0)` для .NET 10
- В проектах было `Version="8.0.*"` — это вызывало `NU1605: Detected package downgrade`
- **Решение**: обновить все пакеты `Microsoft.Extensions.*` и `Microsoft.EntityFrameworkCore.*` до версий 10.x

### Миграция на Microsoft Agent Framework

**Ключевые решения:**

1. **Гибридная архитектура**: MassTransit остаётся на границе сервисов (RabbitMQ, retry/DLQ), MAF работает внутри воркера как движок workflow.

2. **Чекпоинты в PostgreSQL**: Вместо in-memory хранилища используется PostgreSQL через EF Core. Это позволяет переживать перезапуски воркера.

3. **Resume логика**: При старте `ExecuteAsync` загружает завершённые чекпоинты и пропускает уже выполненные шаги, восстанавливая состояние из `StateData`.

4. **Base64 для бинарных данных**: PDF байты сохраняются в чекпоинте как Base64-строка. При resume — декодируются обратно в `byte[]`.

5. **Failure checkpoint**: При исключении сохраняется чекпоинт с `IsFailed = true` и сообщением ошибки. Это позволяет анализировать причины сбоев.

6. **Расширяемость через IAgent**: Новый агент — это просто новый класс, реализующий `IAgent`. Не требует изменения существующего кода.

## Пример: создание нового агента (TranslationAgent)

### Шаг 1: Создать класс агента

```csharp
using Shared.Interfaces;
using Shared.Models;
using Microsoft.Extensions.Logging;

namespace Worker.Agents;

public class TranslationAgent : IAgent
{
public string AgentName => "Translation";

public IReadOnlyList<string> Activities => new List<string>
{
"DetectLanguage",
"TranslateText",
"SaveTranslation"
}.AsReadOnly();

private readonly ITranslationService _translationService;
private readonly IDocumentRepository _repository;
private readonly ILogger<TranslationAgent> _logger;

public TranslationAgent(
ITranslationService translationService,
IDocumentRepository repository,
ILogger<TranslationAgent> logger)
{
_translationService = translationService;
_repository = repository;
_logger = logger;
}

public async Task<AgentResult> ExecuteAsync(
AgentContext context,
ICheckpointStore checkpointStore,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Starting Translation for {DocumentId}", context.DocumentId);

// Загрузить завершённые чекпоинты (resume support)
var completed = await checkpointStore.LoadCompletedCheckpointsAsync(
AgentName, context.DocumentId, cancellationToken);
var completedActivities = completed
.Where(c => c.IsCompleted && !c.IsFailed)
.Select(c => c.CurrentActivity)
.ToHashSet();

try
{
// Шаг 1: Определить язык
string detectedLanguage;
if (completedActivities.Contains("DetectLanguage"))
{
var cp = completed.First(c => c.CurrentActivity == "DetectLanguage");
detectedLanguage = cp.StateData ?? "en";
}
else
{
var text = context.GetPreviousResult<string>("ExtractText");
detectedLanguage = await _translationService.DetectLanguageAsync(
text, cancellationToken);
await checkpointStore.SaveCheckpointAsync(
AgentName, context.DocumentId, "DetectLanguage",
AgentResult.Success(detectedLanguage), cancellationToken);
}

// Шаг 2: Перевести
string translatedText;
if (completedActivities.Contains("TranslateText"))
{
var cp = completed.First(c => c.CurrentActivity == "TranslateText");
translatedText = cp.StateData ?? string.Empty;
}
else
{
var text = context.GetPreviousResult<string>("ExtractText");
translatedText = await _translationService.TranslateAsync(
text, detectedLanguage, context.TargetLanguage, cancellationToken);
await checkpointStore.SaveCheckpointAsync(
AgentName, context.DocumentId, "TranslateText",
AgentResult.Success(translatedText), cancellationToken);
}

// Шаг 3: Сохранить
if (!completedActivities.Contains("SaveTranslation"))
{
await _repository.SaveTranslationAsync(
context.DocumentId, translatedText, cancellationToken);
await checkpointStore.SaveCheckpointAsync(
AgentName, context.DocumentId, "SaveTranslation",
AgentResult.Success(), cancellationToken);
}

// Очистить чекпоинты
await checkpointStore.DeleteCheckpointsAsync(
AgentName, context.DocumentId, cancellationToken);

return AgentResult.Success(translatedText);
}
catch (Exception ex)
{
_logger.LogError(ex, "Translation failed for {DocumentId}", context.DocumentId);
await checkpointStore.SaveCheckpointAsync(
AgentName, context.DocumentId, "Failure",
AgentResult.Failure(ex.Message), cancellationToken);
throw;
}
}
}
```

### Шаг 2: Зарегистрировать в DI

```csharp
// В Program.cs воркера
builder.Services.AddScoped<TranslationAgent>();
builder.Services.AddScoped<ITranslationService, AzureTranslationService>();
```

### Шаг 3: Подключить к оркестратору

```csharp
// Пайплайн: PDF → текст → перевод
var pipeline = agentOrchestrator
.AddAgent<DocumentProcessingAgent>()
.AddAgent<TranslationAgent>()
.Build();
```

## Тесты

### Покрытие чекпоинт-сценариев

| Тест | Сценарий |
|------|----------|
| `ExecuteAsync_FullWorkflow_CompletesSuccessfully` | Первый запуск, все 5 шагов |
| `ExecuteAsync_ResumeAfterCrash_SkipsCompletedActivities` | Resume после 2 шагов |
| `ExecuteAsync_ResumeFromMiddle_SkipsFirstThreeActivities` | Resume после 3 шагов |
| `ExecuteAsync_ResumeFromLastActivity_SkipsFirstFourActivities` | Resume после 4 шагов |
| `ExecuteAsync_AllActivitiesCompleted_OnlyCleansUp` | Все 5 шагов уже выполнены |
| `ExecuteAsync_CheckpointStateData_RoundtripsBase64Bytes` | Roundtrip бинарных данных через Base64 |
| `ExecuteAsync_FailureCheckpoint_PreservesErrorMessage` | Сообщение ошибки в failure checkpoint |

## Метрики

### До миграции (MVP)

- 19 тестов (8 ApiGateway + 7 Worker + 4 Integration)
- .NET 8
- Линейная обработка без resume

### После миграции (Agentic)

- 28 тестов (8 ApiGateway + 16 Worker + 4 Integration)
- .NET 10
- MAF Agent с чекпоинтами и resume
Loading
Loading