Skip to content

Feat/nullwatch python sdk#1

Open
Viroslav wants to merge 14 commits intomainfrom
feat/nullwatch-python-sdk
Open

Feat/nullwatch python sdk#1
Viroslav wants to merge 14 commits intomainfrom
feat/nullwatch-python-sdk

Conversation

@Viroslav
Copy link
Copy Markdown
Collaborator

@Viroslav Viroslav commented May 8, 2026

Контекст

Этот PR подготовлен командой TNS_MODERATION в рамках WB × OpenSource Hackathon по треку nullclaw.

Команда:


Что добавляет этот PR

nullwatch-py — Python SDK для nullwatch с нулевыми обязательными зависимостями в ядре.

У nullwatch уже есть хороший HTTP API и CLI, но нет SDK ни для одного языка — каждая интеграция требует ручного написания raw HTTP-запросов или вызовов CLI. Этот SDK закрывает этот gap и добавляет три eval scorer-а для реальных классов ошибок агентов.

Почему это хороший вклад

  • Первый SDK для nullwatch в любом языке
  • ядро клиента использует только stdlib (urllib, json, dataclasses) — никаких зависимостей по умолчанию
  • RAGHallucinationScorer закрывает конкретный observability gap: агенты с RAG теперь могут автоматически проверять, корректен ли ответ на основании контекста, и сразу отправлять результат как nullwatch eval
  • ToolCallScorer ловит распространённый класс ошибок агентов (выдуманные названия инструментов, опечатки в аргументах, неправильные типы) и формирует структурированные evals без ML-модели
  • ToolCallGroundingScorer — семантическое дополнение к ToolCallScorer: проверяет, что значения аргументов взяты из реального контекста, а не выдуманы моделью. Два бэкенда: zero-deps keyword-эвристика и LLM-судья (любой OpenAI-совместимый API, например локальный Ollama)
  • все scorer-ы возвращают объекты Eval и подключаются напрямую к существующему эндпоинту /v1/evals

Состав

1. NullwatchClient — HTTP-клиент

  • ingest_span(span) / ingest_spans(spans) — POST в /v1/spans и /v1/spans/bulk
  • ingest_eval(eval_) — POST в /v1/evals
  • list_spans(), list_evals(), list_runs(), get_run() — GET с фильтрами
  • client.span(run_id, operation, ...) — контекст-менеджер: автоматически завершает span и отправляет его, при исключении ставит status="error"
  • client.trace(operation) / client.atrace(operation) — декораторы для sync и async функций
  • is_alive() / health() / capabilities() — проверка доступности сервера
  • режим raise_on_error=False для fire-and-forget инструментации
  • buffered=True + flush_at=N — батчевая отправка через /v1/spans/bulk
  • redact= — хук для зачистки секретов перед отправкой

2. RAGHallucinationScorer

Использует LettuceDetect (lettucedect-large-modernbert-en-v1, ModernBERT token classifier, F1=79.2% на RAGTruth) для определения, какие части ответа LLM не подтверждены извлечённым контекстом.

Возвращает nullwatch Eval с:

  • eval_key = "rag_hallucination"
  • score — степень уверенности ответа (1.0 = полностью подтверждён контекстом)
  • verdict"pass" / "fail" на основе доли «галлюцинированных» символов и настраиваемого fail_threshold
  • notes — конкретные span-ы с указанием уверенности модели
  • meta — структурированный словарь для downstream-анализа

Модель загружается при первом вызове, последующие вызовы переиспользуют детектор.

3. ToolCallScorer

Схемный валидатор tool call, сгенерированных LLM. ML-модель не нужна.

Ловит:

  • несуществующие названия инструментов (с подсказкой через расстояние Левенштейна ≤ 2)
  • отсутствующие обязательные аргументы
  • опечатки в именах аргументов (расстояние Левенштейна ≤ 2, с подсказкой правильного варианта)
  • несоответствие типов аргументов
  • нарушения enum и числовых диапазонов (minimum / maximum)
  • malformed JSON в function.arguments (формат OpenAI)

Поддерживает форматы OpenAI, Anthropic tool_use и компактную nullwatch-схему. Score = доля валидных вызовов в тёрне.

4. ToolCallGroundingScorer

Семантическое дополнение к ToolCallScorer — проверяет, что значения аргументов есть в предоставленном контексте, а не придуманы моделью.

Два бэкенда:

  • keyword (по умолчанию, zero deps) — извлекает слова из значений аргументов и проверяет их по контексту; операционные аргументы (пути, URL, shell-команды, timeout, max_results и т.п.) пропускаются автоматически
  • llm — отправляет структурированный промпт в любой OpenAI-совместимый API (например локальный Ollama с qwen3:0.6b) для глубокой семантической проверки

Вместе с ToolCallScorer покрывает как структурные, так и семантические галлюцинации в tool call.

5. BaseScorer

Абстрактный базовый класс для кастомных scorer-ов. Реализуй eval_key, scorer_name и score() — и любая логика оценки подключается к nullwatch.

6. Provider helpers

Best-effort адаптеры для извлечения метрик из ответов провайдеров без импорта их SDK:

with client.span("run-123", "llm.call", model="gpt-4o") as span:
    response = openai_client.chat.completions.create(...)
    span.record_openai_usage(response)   # prompt_tokens, completion_tokens, cost

with client.span("run-123", "llm.call", model="claude-3-5-sonnet") as span:
    response = anthropic_client.messages.create(...)
    span.record_anthropic_usage(response)  # input_tokens, output_tokens

7. Testing utilities

MemoryTransport — in-memory замена HTTP-слоя для тестов без запущенного nullwatch:

from nullwatch import NullwatchClient, MemoryTransport

transport = MemoryTransport()
client = NullwatchClient(transport=transport)

with client.span("run-123", "tool.execute", tool_name="search"):
    pass

transport.assert_span_recorded(operation="tool.execute")
transport.assert_no_failed_evals()

8. CLI

nullwatch-py ping                    # проверка доступности сервера
nullwatch-py ingest-span span.json   # отправить span из JSON-файла
nullwatch-py ingest-eval eval.json   # отправить eval из JSON-файла
nullwatch-py run <run-id>            # вывести summary по run-у

Что сознательно не входит в этот PR

  • async-клиент (можно добавить поверх того же API)
  • retry / backoff
  • streaming spans
  • аутентификация (не требуется текущим API nullwatch)

Использование

from nullwatch import NullwatchClient
from nullwatch.scorers import RAGHallucinationScorer, ToolCallScorer, ToolCallGroundingScorer

client = NullwatchClient()

# span с автоматическим таймером
with client.span("run-123", "llm.call", model="gpt-4o") as s:
    response = call_llm(prompt)
    s.record_openai_usage(response)

# eval на галлюцинации в RAG
rag_scorer = RAGHallucinationScorer()
client.ingest_eval(rag_scorer.score("run-123", contexts=docs, question=q, answer=response.text))

# eval на валидность tool call-ов (структурная проверка)
tool_scorer = ToolCallScorer(tools=MY_TOOLS)
client.ingest_eval(tool_scorer.score("run-123", tool_calls=response.tool_calls))

# eval на заземлённость аргументов (семантическая проверка)
grounding_scorer = ToolCallGroundingScorer(context=user_message)
client.ingest_eval(grounding_scorer.score("run-123", tool_call=response.tool_calls[0]))

Установка

pip install nullwatch-py          # только ядро клиента
pip install "nullwatch-py[rag]"   # с детекцией галлюцинаций в RAG

Также доступно на Test PyPI как ранний preview.


Валидация

make install
make lint   # ruff — 0 ошибок
make test   # 161 тест, внешние сервисы не нужны

Все тесты используют локальный mock HTTP-сервер — запущенный nullwatch для прогона не требуется.


Пример реального использования — nullclaw-python-tg-bot

В рамках хакатона мы также собрали nullclaw-python-tg-bot — Telegram-бот поверх nullclaw, который использует nullwatch-py как единственный способ взаимодействия с nullwatch.

Бот демонстрирует полный стек в действии:

Сервис Роль
nullwatch observability backend — принимает spans и evals (порт 7710)
nullclaw AI agent gateway — memory, tools, A2A протокол (порт 3000)
ollama локальный LLM runtime (порт 11434)
bot Telegram-бот с nullwatch-py SDK внутри

Что демонстрирует бот:

  • /rag <question> — отвечает на вопрос из контекста и сразу прогоняет RAGHallucinationScorer, результат уходит в nullwatch как eval
  • /tool <request> — отправляет запрос в nullclaw-агента, валидирует сгенерированные tool calls через ToolCallScorer + ToolCallGroundingScorer
  • /status — health check всех сервисов через client.is_alive()
  • plain text сообщения идут напрямую в nullclaw через A2A протокол, каждый вызов оборачивается в client.span(...) — span автоматически отправляется в nullwatch

Это живое подтверждение того, что SDK работает с реальным nullclaw-агентом и выполняет заданный функционал без ошибок.

Текущий статус

Готов к ревью.

Nikolay.Ivanov and others added 14 commits May 8, 2026 15:24
…egration tests

ToolCallScorer:
- normalize_tool_call(): OpenAI and Anthropic format support
- enum validation
- minimum/maximum range validation
- fix: bool no longer passes as integer
- fix: tool_call={} no longer silently dropped
- typo hints for misspelled tool names

Client bug fix (found by reading nullwatch source):
- list_spans/list_evals/list_runs now unwrap {"items": [...]} correctly
  (was always returning [] against real nullwatch)

Tests: 33 → 60 + 19 integration tests (auto-skip without live server)
New: examples/live_demo.py — end-to-end demo with ollama + lettucedetect
- make RAG hallucination verdict respect fail_threshold
- improve nullclaw/nullwatch run correlation in Telegram bot
… and CLI

- NullwatchClient: env vars (NULLWATCH_URL, NULLWATCH_API_KEY), api_key/Bearer auth,
  redact hook, capabilities(), flush(), close(), context-manager support
- Buffered mode: flush_at threshold, thread-safe buffer, bulk ingest on flush
- Decorators: @client.trace and @client.atrace for sync/async functions
- Provider helpers: Span.record_openai_usage(), record_anthropic_usage(),
  record_tokens(), record_cost()
- nullwatch/testing.py: MemoryTransport with assert_span_recorded(),
  assert_eval_recorded(), assert_no_failed_evals()
- nullwatch/cli.py: ping, ingest-span, ingest-eval, run commands
- pyproject.toml: CLI entrypoint nullwatch-py = nullwatch.cli:main
- tests/test_new_features.py: 46 new tests, all 114 pass
…reshold

- Rewrote README to document all implemented features:
  ToolCallGroundingScorer, decorators (@trace/@atrace), buffered mode,
  provider helpers, MemoryTransport testing utils, CLI, redaction
- Fixed test_short_hallucinated_span_fails_by_default: added explicit
  fail_threshold=0.05 so the 11% hallucinated-char ratio triggers a fail
  (the scorer correctly uses ratio-based threshold, not any-span-fails logic)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant