Skip to content

Utilities API

Supporting utilities for theme discovery, LLM management, and graph operations.

Theme Registry

scinr.newton.utils.theme_registry.ThemeRegistry

Scans models/ recursively to discover theme folders and build a theme tree.

A folder is a theme iff it contains a catalog.py. Nested theme folders (sub-themes) are supported to any depth.

build_catalog_block

build_catalog_block(theme: ThemeNode) -> str

Build the plain-text model catalog block to inject into LLM decision prompts.

Format

Available annotation models:

  1. ClassName — First line of docstring Fields: field1: str, field2: int | None

  2. ContainerClass [list container] — Description Fields: items: list[ItemClass] Each item (ItemClass): field1: str, field2: str

This format is readable by all LLM families (Claude, OpenAI, Kimi, GLM, etc.) and replaces the previous XML format.

build_theme_section_for_extraction_prompt

build_theme_section_for_extraction_prompt() -> str

Builds the XML block to inject into the extraction prompt.

Called once at extraction startup; the result is static for the duration of the run (themes do not change at runtime).

find_best_theme

find_best_theme(detected_path: str | None) -> ThemeNode

Return the most specific available ThemeNode for detected_path.

Resolution order (most to least specific): 1. Exact match for the full detected path 2. Progressively shorter path prefixes 3. "default" theme (if available) 4. First registered theme (emergency fallback)

Examples:

detected_path="structural_specs/pharmaceutical/ema" → tries "structural_specs/pharmaceutical/ema" → not found → tries "structural_specs/pharmaceutical" → not found → tries "structural_specs" → FOUND, returns it

detected_path="unknown_theme" → tries "unknown_theme" → not found → falls back to "default"

get_all_theme_paths

get_all_theme_paths() -> list[str]

Returns all registered theme paths sorted alphabetically.

Used by the extraction pipeline to build a dynamic Literal type for structured output validation.

get_all_valid_model_classes

get_all_valid_model_classes() -> list[str]

Return sorted list of ALL valid model class names across ALL themes.

get_neo4j_theme_structure

get_neo4j_theme_structure() -> list[dict[str, Any]]

Return the full theme tree as a list of dicts for Neo4j ingestion.

Each dict has

path — str, e.g. "structural_specs/nta" name — str, e.g. "nta" parent_path — str | None (parent theme path, or None if top-level) model_names — list[str] of selectable class names

get_theme_list_for_prompt

get_theme_list_for_prompt() -> str

Build a formatted list of available themes for injection into the classification prompt.

Example output
  • default: Generic fallback for content that does not fit a specific domain
  • pharmaceutical: Pharmaceutical drug development documents following ICH CTD Module 3
  • structural_specs: Documents that prescribe how other documents must be structured

get_valid_model_classes

get_valid_model_classes(theme: ThemeNode) -> list[str]

Return list of valid model class names for the repair prompt (current theme only).

LLM Utilities

scinr.newton.utils.llm_factory

utils/llm_factory.py — Deprecated. Use scinr_config instead.

This module exists for backward compatibility only.

Retry Utilities

scinr.newton.utils.llm_retry

utils/llm_retry.py — Generic LLM retry with exponential backoff.

Handles rate-limiting and transient errors from any LangChain-compatible LLM provider (AWS Bedrock, OpenAI, Anthropic, Ollama, etc.).

with_bedrock_retry async

with_bedrock_retry(
    coro_fn: Callable[[], Awaitable[Any]],
    max_retries: int = _MAX_RETRIES,
) -> Any

Deprecated alias for with_llm_retry. Use with_llm_retry instead.

with_llm_retry async

with_llm_retry(
    coro_fn: Callable[[], Awaitable[Any]],
    max_retries: int = _MAX_RETRIES,
) -> Any

Execute an async LLM call with exponential backoff on retryable errors.

Parameters

coro_fn: Zero-argument async callable that invokes the LLM. max_retries: Maximum number of retry attempts (default 6).

Returns

Any The return value of coro_fn() on success.

Raises

The last exception if all retries are exhausted.

scinr.newton.utils.neo4j_retry

utils/neo4j_retry.py — Two-phase retry wrapper for transient Neo4j errors.

Neo4j can raise transient errors (e.g. DeadlockDetected, LockClientStopped, ServiceUnavailable / defunct connection) under write contention or parallel read overload. Without a retry mechanism, concurrent document ingestion would fail immediately on the first lock conflict or socket reset.

Public API

result = await with_neo4j_retry(lambda: session.run(...))
result = with_neo4j_retry_sync(lambda: session.run(...))  # sync mirror,
    # for use inside asyncio.to_thread() worker threads

The wrapper uses a two-phase retry strategy:

Phase 1 — Exponential backoff with full jitter, capped at 60 s (7 attempts). Phase 2 — Fixed 60 s plateau, 5 additional attempts.

Total: 12 retry attempts maximum (~6.8 min worst-case wait). Non-transient exceptions are re-raised immediately without retrying.

BASE_DELAY module-attribute

BASE_DELAY: float = 1.0

Initial wait in seconds; doubles each exponential attempt.

EXP_RETRIES module-attribute

EXP_RETRIES: int = 7

Number of exponential-backoff attempts (attempts 0–6, delays 1 s → 60 s).

MAX_DELAY_EXP module-attribute

MAX_DELAY_EXP: float = 60.0

Upper cap on the exponential back-off delay and fixed plateau delay (seconds).

PLATEAU_RETRIES module-attribute

PLATEAU_RETRIES: int = 5

Number of fixed-plateau attempts after the exponential phase (60 s each).

TOTAL_RETRIES module-attribute

TOTAL_RETRIES: int = EXP_RETRIES + PLATEAU_RETRIES

Maximum total retry attempts after the initial failure (12).

with_neo4j_retry async

with_neo4j_retry(
    coro_fn: Callable[[], Coroutine[Any, Any, T]],
    *,
    exp_retries: int = EXP_RETRIES,
    plateau_retries: int = PLATEAU_RETRIES,
    base_delay: float = BASE_DELAY,
    max_delay_exp: float = MAX_DELAY_EXP,
) -> T

Await coro_fn() with two-phase retry on transient Neo4j errors.

Parameters

coro_fn: A zero-argument callable that returns an awaitable (e.g. lambda: session.run(...)). It is called fresh on every attempt so that a new coroutine object is created each time. exp_retries: Number of exponential-backoff retry attempts (Phase 1). plateau_retries: Number of fixed-plateau retry attempts after the exponential phase (Phase 2). base_delay: Base wait time in seconds for the exponential phase. max_delay_exp: Cap on the exponential delay and the fixed plateau delay (seconds).

Returns

T Whatever coro_fn() returns on success.

Raises

Exception Re-raises the last transient exception once all retries are exhausted, or immediately re-raises any non-transient exception.

with_neo4j_retry_sync

with_neo4j_retry_sync(
    fn: Callable[[], T],
    *,
    exp_retries: int = EXP_RETRIES,
    plateau_retries: int = PLATEAU_RETRIES,
    base_delay: float = BASE_DELAY,
    max_delay_exp: float = MAX_DELAY_EXP,
) -> T

Synchronous mirror of with_neo4j_retry(): uses time.sleep() instead of asyncio.sleep(). Designed to run inside an asyncio.to_thread() worker thread (no event loop of its own) — must never await.

Parameters

fn: A zero-argument callable that returns a value synchronously (e.g. lambda: session.run(...)). It is called fresh on every attempt. exp_retries: Number of exponential-backoff retry attempts (Phase 1). plateau_retries: Number of fixed-plateau retry attempts after the exponential phase (Phase 2). base_delay: Base wait time in seconds for the exponential phase. max_delay_exp: Cap on the exponential delay and the fixed plateau delay (seconds).

Returns

T Whatever fn() returns on success.

Raises

Exception Re-raises the last transient exception once all retries are exhausted, or immediately re-raises any non-transient exception.

Concurrency

scinr.newton.utils.neo4j_concurrency

utils/neo4j_concurrency.py — Compatibility shim for Neo4j concurrency semaphore.

The semaphore logic has moved to config.py alongside get_llm_semaphore() and reset_llm_semaphore(). This module re-exports those functions for backward compatibility so that existing callers (annotation/nodes.py, entity_extraction/nodes.py) do not need to change their imports.

To configure concurrency, call configure(neo4j_concurrency=N) followed by reset_neo4j_semaphore() before running annotation or entity extraction stages.

get_neo4j_semaphore

get_neo4j_semaphore()

Return (creating if needed) the global asyncio.Semaphore for Neo4j session concurrency.

The semaphore bounds the number of concurrent Neo4j sessions during annotation (Stage 3) and entity extraction (Stage 4). It is created lazily on the first call using the neo4j_concurrency value from ScinrConfig.

To change the concurrency at runtime, call configure(neo4j_concurrency=N) followed by reset_neo4j_semaphore() before running the relevant stages.

reset_neo4j_semaphore

reset_neo4j_semaphore() -> None

Reset the Neo4j semaphore (used after configure() changes neo4j_concurrency).

Call this after configure(neo4j_concurrency=N) to ensure the new value takes effect on the next call to get_neo4j_semaphore().

Logging

scinr.newton.utils.logging_config

utils/logging_config.py — Logging setup for scinr-ingest.

Library mode (default)

When called without arguments, only a console handler is added — no files are created, no directories are touched. This is the correct behaviour for a library: the application that uses scinr-ingest is responsible for configuring file logging if it wants it.

CLI mode

When log_dir is provided (the CLI passes Path("logs")), two rotating daily-folder file handlers are added in addition to the console handler:

<log_dir>/
└── YYYY-MM-DD/
    ├── scinr.log          ← INFO+, retained 30 days
    └── scinr.errors.log   ← ERROR+ only, retained 90 days

Usage::

# Library — console only, no files:
from scinr.newton.utils.logging_config import setup_logging
setup_logging()

# CLI — console + daily file rotation under ./logs/:
setup_logging(log_dir=Path("logs"))

setup_logging

setup_logging(log_dir: Path | None = None) -> None

Configure logging for scinr-ingest.

Parameters

log_dir: Directory under which dated sub-folders and log files are created. When None (the default), only a console handler is configured — no files or directories are created. Pass an explicit path (e.g. Path("logs")) to enable file logging.