Skip to content

LLM

Every name on this page is importable from sci_etl_core.llm.

sci_etl_core.llm.async_base

AsyncLLMClient

Bases: ABC

Contract for a chat-completion backend that answers with a JSON object.

The relevance filter and the entity extractor depend only on this interface, so a provider, a cache such as :class:~sci_etl_core.llm.cache_async.CachingLLMClient, or a test double can be injected in its place.

usage property

usage: TokenUsage | None

Tokens this client has used so far, or None when it does not track usage.

complete_json abstractmethod async

complete_json(
    system_prompt: str,
    user_content: str,
    timeout: int | None = None,
) -> dict[str, Any]

Send a chat completion request and return the parsed JSON object.

Raises:

Type Description
LLMError

The request failed, or the body is not a JSON object.

sci_etl_core.llm.openai_compatible_async

AsyncOpenAICompatibleClient

AsyncOpenAICompatibleClient(
    api_key: str | SecretStr,
    base_url: str,
    model: str,
    default_timeout: int = 120,
    temperature: float = 0.0,
    max_retries: int = 3,
    backoff_factor: float = 2.0,
    sleep: Any = sleep,
    max_retry_after: float = 60.0,
    rate_limiter: RateLimiting | None = None,
)

Bases: AsyncLLMClient

JSON-mode chat completions from any OpenAI-compatible endpoint, with retries and token accounting.

The client owns its HTTP connection pool; close it with :meth:aclose.

Configure the client.

This client is the only retry layer: the OpenAI SDK's own retries are turned off, so max_retries is the total number of attempts. Between attempts it waits backoff_factor ** attempt seconds, or longer when the server's retry-after-ms or Retry-After header asks for it, up to max_retry_after seconds.

Every attempt first enters rate_limiter, an :class:~sci_etl_core.rate_limiter.AsyncRateLimiter or a :class:~sci_etl_core.rate_limiter.HostRateLimiter matched against base_url, and releases it once the response arrives. Share one limiter between a chat client and an embedder that call the same provider to keep both inside one budget.

Raises:

Type Description
ValueError

max_retries is less than 1, which would fail every completion without making a single attempt, or max_retry_after is negative.

model property

model: str

The model completions are requested from.

usage property

usage: TokenUsage

Tokens reported across every response received so far, as a snapshot.

from_config classmethod

from_config(
    llm: LLMConfig, **options: Any
) -> AsyncOpenAICompatibleClient

Build a client from the llm config section.

The section supplies api_key, base_url, model, and timeout as default_timeout. options pass any other constructor argument, such as rate_limiter, and override a value taken from the config.

complete_json async

complete_json(
    system_prompt: str,
    user_content: str,
    timeout: int | None = None,
) -> dict[str, Any]

Request a JSON-mode completion and return the parsed object.

An empty completion reads as {}. Every response the API returns counts toward :attr:usage, including one whose body is then rejected.

Raises:

Type Description
LLMError

The request failed after retries, or the completion is not valid JSON or is JSON other than an object.

aclose async

aclose() -> None

Close the underlying HTTP connection pool.

sci_etl_core.llm.relevance_async

AsyncRelevanceFilter

Bases: ABC

Contract for the gate that decides, from a listing entry alone, whether a record is worth its full text.

is_relevant abstractmethod async

is_relevant(record: RawRecord) -> bool

Decide whether a record should proceed through the pipeline.

AsyncLLMRelevanceFilter

AsyncLLMRelevanceFilter(
    llm_client: AsyncLLMClient,
    system_prompt: str,
    timeout: int = 20,
    default_on_empty_abstract: bool = True,
    default_on_error: bool = True,
)

Bases: AsyncRelevanceFilter

Ask an LLM whether a record's title and abstract match system_prompt.

Configure the filter.

system_prompt must ask for a JSON object with a relevant key. timeout is passed to every completion. A record without an abstract is not sent and reads as default_on_empty_abstract; an :class:~sci_etl_core.exceptions.LLMError or an unclear verdict reads as default_on_error. Both default to True, so a fault costs an extraction call rather than a record.

is_relevant async

is_relevant(record: RawRecord) -> bool

Ask the LLM for a verdict read from the relevant key.

A boolean is used as-is; 0/1 and the strings "true", "false", "yes", "no", "1" and "0" (any case) are accepted too. A failed call, a response that is not a JSON object, and a missing or unrecognized verdict all return default_on_error, so a garbled answer such as the string "false" read as truthy can never pass for a confident verdict.

sci_etl_core.llm.relevance_embedding_async

AsyncEmbeddingRelevanceFilter

AsyncEmbeddingRelevanceFilter(
    embedder: AsyncEmbedder,
    reference_texts: Sequence[str],
    threshold: float = 0.35,
    record_to_text: Callable[
        [RawRecord], str
    ] = _default_record_text,
    default_on_empty_abstract: bool = True,
    default_on_error: bool = True,
)

Bases: AsyncRelevanceFilter

Keep records whose meaning is close to a set of reference concepts.

Reference concepts are embedded once, lazily, on first use. Each record is then embedded and kept when its cosine similarity to the nearest reference reaches threshold, so semantically related work is matched even when it shares no keywords with the query.

Only a real similarity score can reject a record. A failed embedding call, a record vector that is missing, all zero or non-finite, and a vector whose dimension differs from the references all return default_on_error: none of them is evidence that the record is irrelevant, and a negative verdict would mark it processed for good.

is_relevant async

is_relevant(record: RawRecord) -> bool

sci_etl_core.llm.extraction_async

AsyncEntityExtractor

Bases: ABC

Contract for turning a record's full text into structured entities.

extract abstractmethod async

extract(text: str | bytes) -> list[dict[str, Any]]

Extract structured entities from raw text.

An empty list means the text holds no entities, and the pipeline marks the record processed. Raising any exception instead fails the record, which stays unmarked and is retried on the next run.

AsyncLLMEntityExtractor

AsyncLLMEntityExtractor(
    llm_client: AsyncLLMClient,
    system_prompt: str,
    html_parser: Parser | None = None,
    result_key: str = "items",
    max_chars: int = 120000,
    timeout: int = 120,
    max_tokens: int | None = None,
    encoding_name: str = "cl100k_base",
    validator: RecordValidator | None = None,
    logger: Callable[[str], None] | None = None,
    label_field: str | None = None,
)

Bases: AsyncEntityExtractor

Extract entities from full text with one JSON-mode LLM call per record.

Text that starts with markup is stripped with html_parser. The text is then truncated to max_tokens tokens when that is set and tiktoken is installed, or to max_chars characters otherwise.

Configure the extractor.

With a validator, each extracted entity is checked before it is returned, and an entity the validator rejects is dropped and logged as Entity rejected by validation: <label>. The label is the entity's label_field value when that is given, and its position in the response otherwise. A record whose every entity is rejected exports nothing and is still marked processed, as for a response with no entities.

extract async

extract(text: str | bytes) -> list[dict[str, Any]]

Extract entities from text with a single LLM call.

The entity list is read from result_key, or from the only value when the response has exactly one key; any other shape yields no entities. null reads as no entities and a lone object as a one-entity list. HTML stripping and truncation run in a worker thread, since both are CPU-bound and token counting may load encoding data on first use.

Raises:

Type Description
LLMError

The completion failed, or the entity list is not a list of objects (for example a string or a list of strings). The error propagates instead of reading as "no entities", so the pipeline leaves the record unmarked and retries it on the next run rather than recording it as processed with nothing exported.

sci_etl_core.llm.cache_async

AsyncLLMResponseCache

Bases: ABC

Storage for parsed completions, keyed by :func:response_cache_key.

get abstractmethod async

get(key: str) -> dict[str, Any] | None

Return the cached response for key, or None when there is none.

set abstractmethod async

set(key: str, response: dict[str, Any]) -> None

Store response under key, replacing any earlier one.

clear abstractmethod async

clear() -> None

Remove every cached response.

InMemoryLLMResponseCache

InMemoryLLMResponseCache(max_entries: int | None = None)

Bases: AsyncLLMResponseCache

A cache that lives as long as the process, optionally bounded.

With max_entries, the least recently used response is evicted once the cache is full. Responses are copied in and out, so a caller that changes a returned dict cannot change what is cached.

Create an empty cache.

Raises:

Type Description
ValueError

max_entries is less than 1.

get async

get(key: str) -> dict[str, Any] | None

set async

set(key: str, response: dict[str, Any]) -> None

clear async

clear() -> None

AsyncSqliteLLMResponseCache

AsyncSqliteLLMResponseCache(
    path: str | Path,
    now: Callable[[], datetime] | None = None,
)

Bases: AsyncLLMResponseCache

A cache kept in a SQLite file, so responses survive between runs.

The file and its parent folder are created on first use, and responses are stored as JSON. Every SQLite failure, including a file that is not a database, raises :class:~sci_etl_core.exceptions.LLMCacheError. Close it with :meth:aclose, for example by listing it in the pipeline's closeables. Use each instance from one event loop.

get async

get(key: str) -> dict[str, Any] | None

set async

set(key: str, response: dict[str, Any]) -> None

clear async

clear() -> None

count async

count() -> int

Return how many responses are cached.

aclose async

aclose() -> None

Close the connection; a later call reopens it.

CacheStats dataclass

CacheStats(hits: int = 0, misses: int = 0, faults: int = 0)

How many completions a :class:CachingLLMClient served from its cache.

hits class-attribute instance-attribute

hits: int = 0

misses class-attribute instance-attribute

misses: int = 0

faults class-attribute instance-attribute

faults: int = 0

CachingLLMClient

CachingLLMClient(
    client: AsyncLLMClient,
    cache: AsyncLLMResponseCache,
    model: str | None = None,
    logger: Callable[[str], None] | None = None,
)

Bases: AsyncLLMClient

Serve repeated completions from a cache instead of calling the LLM again.

A request is keyed on model and both prompts (:func:response_cache_key). The timeout is not part of the key. Only successful responses are cached, so a failed call is retried the next time it is made. Change model when anything else that shapes the response changes, such as the temperature, for example "gpt-4o-mini@t0.2".

The cache never fails a completion: an exception from the cache is logged as LLM cache <get|set> failed: <error>, counted in :attr:stats, and the call goes to the LLM as on a miss. :attr:usage is the wrapped client's, so cache hits cost no tokens.

Wrap client.

model defaults to the wrapped client's model attribute.

Raises:

Type Description
ValueError

model is not given and the client has no model string to key the cache by.

model property

model: str

The model name responses are cached under.

stats property

stats: CacheStats

Cache hits, misses, and faults so far, as a snapshot.

usage property

usage: TokenUsage | None

complete_json async

complete_json(
    system_prompt: str,
    user_content: str,
    timeout: int | None = None,
) -> dict[str, Any]

Return the cached response, or ask the wrapped client and cache its answer.

Raises:

Type Description
LLMError

The wrapped client failed; nothing is cached.

response_cache_key

response_cache_key(
    model: str, system_prompt: str, user_content: str
) -> str

Return the cache key for a completion: a SHA-256 hex digest of its model and both prompts.

sci_etl_core.llm.base

LLMClient

Bases: ABC

Blocking counterpart of :class:~sci_etl_core.llm.async_base.AsyncLLMClient.

complete_json abstractmethod

complete_json(
    system_prompt: str,
    user_content: str,
    timeout: int | None = None,
) -> dict[str, Any]

Send a chat completion request and parse the JSON response body.

RelevanceFilter

Bases: ABC

Blocking relevance filter; wrap it in SyncRelevanceFilterAdapter.

is_relevant abstractmethod

is_relevant(record: RawRecord) -> bool

Decide whether a record should proceed through the pipeline.

EntityExtractor

Bases: ABC

Blocking entity extractor; wrap it in SyncEntityExtractorAdapter.

extract abstractmethod

extract(text: str | bytes) -> list[dict[str, Any]]

Extract structured entities from raw text, raising on failure.