Metadata-Version: 2.4
Name: slm-ai-court-verification
Version: 0.2.0
Summary: Agentic judicial verification loop (plaintiff -> critic -> librarian -> jury -> judge) for any SLM behind an OpenAI-compatible API
Author: Research
License: MIT
Keywords: llm,agents,verification,fact-checking,openai
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai>=1.35
Requires-Dist: httpx>=0.24
Provides-Extra: search
Requires-Dist: ddgs>=6; extra == "search"
Provides-Extra: gbm
Requires-Dist: numpy; extra == "gbm"
Requires-Dist: scikit-learn>=1.3; extra == "gbm"
Dynamic: license-file

# slm-ai-court-verification — агентский судебный цикл верификации для любых SLM

Python-библиотека, переписанная из исследовательского фреймворка Multi-Agent
Verification System. Подключает **любую модель по OpenAI-совместимому API**
(LM Studio, Ollama, vLLM, llama.cpp-server, OpenRouter, Together, OpenAI…) и
прогоняет вопрос через полный судебный цикл агентов внутри себя.

Обязательные зависимости — только `openai>=1.35` и `httpx`. Никаких
langchain/langgraph.

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

```
plaintiff (истец) ──► screen (придирчивый фактчекер)
                         │  «атака не нашла проблем» ──► final (итог)   ← дешёвый выход: ~3 вызова LLM
                         │  «проблема есть» (эскалация)
                         ▼
        critic ──► librarian ──► fact_check ──► jury (N специалистов-присяжных, параллельно)
                                                    │ [опционально: GBM-оценщик]
                                                    ▼
                                                 judge ──► memory ─┐
                                                          ▲        │ STOP/CONTINUE/
                                                          └────────┘ max_iter/reward
```

- **plaintiff** — генерирует структурированный ответ с учётом памяти прошлых итераций.
- **screen** — состязательная сортировка: единственная задача — найти ошибку в черновике;
  дешёвый выход разрешён только если атака честно ничего не нашла.
- **critic** — выявляет логические разрывы и галлюцинации.
- **librarian** — формулирует нейтральный поисковый запрос, ищет веб-источники.
- **fact_check** — сверяет утверждение ТОЛЬКО с текстом источников (разрывает общее слепое пятно LLM).
- **jury** — присяжные со специализированными «линзами»: логика, определения,
  контрпримеры, сверка с источниками, скрытые допущения.
- **judge** — оценка 0–10 и решение STOP/CONTINUE; цикл повторяется минимум
  `min_iterations` раз и максимум `max_iterations`, с ранней остановкой по reward.
- **final** — синтез итогового ответа с машиночитаемым вердиктом `ВЕРНО|НЕВЕРНО|НЕОДНОЗНАЧНО`.

## Установка

```bash
pip install -e .                          # из корня репозитория
# после публикации на PyPI:
pip install slm-ai-court-verification

pip install "slm-ai-court-verification[search]"  # + DuckDuckGo поиск (ddgs)
pip install "slm-ai-court-verification[gbm]"     # + observational GBM-оценщик жюри
```

## Быстрый старт

### Конфигурация через переменные окружения

Библиотека берёт модель, endpoint и ключ из env (явные аргументы `Court(...)`
имеют приоритет). Проще всего заполнить файл `.env` в корне репозитория
(он не коммитится, шаблон — `.env.example`; его же подхватывает `test_run.py`):

| Переменная | Фолбэк | По умолчанию | Смысл |
|---|---|---|---|
| `AI_COURT_MODEL` | — | обязателен* | id модели на сервере |
| `AI_COURT_BASE_URL` | `OPENAI_BASE_URL` | `http://localhost:1234/v1` | корень OpenAI-совместимого API |
| `AI_COURT_API_KEY` | `OPENAI_API_KEY` | `not-needed` | ключ (локальным серверам подходит любая строка) |
| `AI_COURT_TEMPERATURE` | — | `0.6` | температура сэмплирования |
| `AI_COURT_TOP_P` | — | не задан | nucleus sampling (`top_p`) |
| `AI_COURT_SEED` | — | не задан | seed генерации, если endpoint поддерживает |
| `AI_COURT_MAX_TOKENS` | — | без лимита | потолок completion-токенов |
| `LANGSEARCH_API_KEY` | — | — | веб-поиск LangSearch в CLI |

\* обязателен, если не передан аргументом `Court(model=...)`.

```bash
export AI_COURT_MODEL="google/gemma-3n-e4b"
export AI_COURT_BASE_URL="http://localhost:1234/v1"
export AI_COURT_API_KEY="lm-studio"

python examples/quickstart.py          # весь цикл на дефолтной модели
ai-court "Все простые числа нечётны?"  # то же через CLI
```

```python
from ai_court import Court

court = Court()                        # всё из окружения
result = court.run("Верно ли, что все простые числа нечётны?")
print(result.answer)            # итоговый ответ
print(result.claim_verdict)     # ВЕРНО | НЕВЕРНО | НЕОДНОЗНАЧНО
print(result.iterations, result.score, result.usage["llm_calls"])
result.save_json("trial.json")  # атомарно сохраняет полный след всех шагов
```

### Явное переопределение

```python
from ai_court import Court

court = Court(
    model="google/gemma-3n-e4b",
    base_url="http://localhost:1234/v1",
    api_key="lm-studio",
)
result = court.run("Верно ли, что все простые числа нечётны?")
```

### Локальный сервер (LM Studio)

```bash
# сервер LM Studio уже отдаёт OpenAI-совместимый API на :1234
export AI_COURT_BASE_URL="http://localhost:1234/v1"
```

### Ollama

```python
court = Court(model="qwen2.5:7b", base_url="http://localhost:11434/v1", api_key="ollama")
```

### OpenRouter / любой облачный OpenAI-совместимый API

```python
court = Court(
    model="google/gemma-3n-e4b-it:free",
    base_url="https://openrouter.ai/api/v1",
    api_key="sk-or-...",
)
```

### CLI

```bash
ai-court "Все простые числа нечётны?" \
  --base-url http://localhost:1234/v1 --model google/gemma-3n-e4b \
  --max-iterations 3 --json trial.json
```

## Разные модели на разные роли

Дешёвая модель сортирует, сильная судит:

```python
from ai_court import Court, ChatModel

court = Court("qwen2.5-3b", base_url="http://localhost:1234/v1")
court.set_role_model("screen", ChatModel("gemma-3n-e4b", base_url=...))   # дешёвый скрининг
court.set_role_model("juror",  ChatModel("qwen2.5-7b", base_url=...))
court.set_role_model("judge",  ChatModel("qwen2.5-32b", base_url=..., temperature=0.2))
```

Роли: `plaintiff, screen, critic, librarian, fact_check, juror, judge, memory, final`.

## Конфигурация (`CourtConfig`)

| Параметр | По умолчанию | Смысл |
|---|---|---|
| `max_iterations` | 3 | максимум полных проходов верификации |
| `min_iterations` | 2 | минимум проходов даже при SCORE ≥ 8 |
| `early_stop_reward` | 0.8 | ранняя остановка при среднем reward выше порога |
| `num_jurors` | 5 | явный размер жюри |
| `target_confidence` | 0.85 | только legacy-эвристика, если `num_jurors=None`; не является статистической гарантией |
| `use_screen` | True | каскадный скрининг перед полным пайплайном |
| `use_jury` | True | стадия присяжных |
| `use_search` | True | библиотекарь + сверка с внешними источниками |
| `use_jury_evaluator` | False | observational GBM-оценщик поверх мнений жюри |
| `verbose` | True | цветной поток решений в консоль |
| `save_logs` | False | автосохранение JSON-лога прогона |

Некорректные комбинации параметров (например, `min_iterations > max_iterations`)
отклоняются сразу, чтобы эксперимент не выполнялся в неожиданном режиме.

## Поиск источников

```python
from ai_court import LangSearchBackend, DuckDuckGoBackend, NullSearchBackend, ChainedSearchBackend

court = Court(
    "model-id",
    search=ChainedSearchBackend(
        LangSearchBackend(api_key="ls_...", fallback=DuckDuckGoBackend()),
    ),
)
# или полностью без сети:
court = Court("model-id", search=NullSearchBackend())
```

## Наблюдаемость: события и GBM-оценщик

```python
from ai_court import Court, PickleJuryEvaluator, set_event_sink

set_event_sink(lambda event: print(event["agent"], event["output_tokens"]))  # live-стрим шагов

court = Court(
    "model-id",
    jury_evaluator=PickleJuryEvaluator("jury_models/jury_judge_model.pkl"),
)
result = court.run("вопрос")
for step in result.steps:            # каждый вызов агента: промпт, ответ, токены
    print(step["agent"], step["iteration"])
```

`TrialResult.usage` относится только к текущему запуску, даже если один объект
`Court` используется для многих вопросов. В него входят успешные LLM-вызовы,
все попытки, retries и неуспешные попытки. Сохранённый JSON содержит `steps`,
`parse_errors`, историю жюри и fact-check, конфигурацию, mapping моделей по ролям,
seed и версию промптов. API-ключи в метаданные не записываются.

Дешёвый выход screen разрешён только тогда, когда структурированные вердикты
истца и независимого screen-агента совпали и screen не нашёл проблему. Ошибка
формата или конфликт вердиктов консервативно эскалирует вопрос в полный цикл.

### Длинные экспериментальные серии

```python
batch = court.run_batch(
    topics,
    checkpoint_path="results/run.jsonl",
    resume=True,
    continue_on_error=True,
)
print(len(batch.successes), len(batch.failures))
```

Checkpoint дописывается после каждого вопроса. При повторном запуске успешно
завершённые пары `(index, topic)` загружаются из JSONL, а упавшие элементы
повторяются. Ошибки сохраняются в том же журнале и не исчезают из аудита.

`PickleJuryEvaluator` загружает артефакты обученного оценщика жюри
(`{"model", "tfidf", "svd", "feat_names", "best_threshold"}`); его вердикт —
наблюдательный и не влияет на решение цикла. Требует extras `[gbm]`.

## Структура пакета

```
src/ai_court/
├── client.py    # ChatModel: OpenAI-совместимый клиент, retry/backoff, учёт токенов
├── config.py    # CourtConfig — параметры цикла
├── state.py     # TrialState — состояние дела
├── prompts.py   # все промпты агентов (перенесены из исходного фреймворка)
├── agents.py    # plaintiff/screen/critic/librarian/fact_check/juror/judge/memory/final
├── court.py     # Court — оркестратор судебного цикла, TrialResult
├── search.py    # SearchBackend: LangSearch / DuckDuckGo / цепочка / null
├── scoring.py   # PickleJuryEvaluator (GBM над мнениями жюри)
├── journal.py   # RunLogger + event sink
└── cli.py       # ai-court …
```

## Тесты

```bash
cd ai_court && python -m unittest discover -s tests -v   # офлайн-тесты библиотеки
```

Смоук-тесты гоняют весь цикл на скриптованной fake-модели (без сети):
каскадный дешёвый выход, эскалация в полный пайплайн, лимит итераций,
переопределение моделей ролей, отключение экрана/поиска.

### Проверка на живой модели

```bash
cp .env.example .env        # вписать AI_COURT_MODEL / AI_COURT_BASE_URL / AI_COURT_API_KEY
python test_run.py --check  # только связь с сервером
python test_run.py --dry-run# весь цикл без сети (заготовленные ответы)
python test_run.py          # полный судебный цикл на вашей модели
```
