API: Inference Layer¶
Semantic front-end: Novel → Contract via Reader Response inference.
The inference layer converts parsed Module objects into fully populated
ModuleContract objects. It can operate via LLM API calls or any
ContractInferencer implementation.
Abstract interface¶
Inference interfaces for generating narrative contracts automatically.
The inference layer converts parsed Modules into ModuleContracts that include inferred ReaderState and module intent (expected_changes).
ContractInferencer ¶
Bases: ABC
Strategy interface for contract inference.
Implementations may use: - LLM inference (OpenAI API) - deterministic NLP features (NLTK/spaCy + rules) - hybrid approaches
infer
abstractmethod
¶
infer(modules, *, novel_title)
Infer a full contract for a set of modules.
:param modules: Parsed modules from a Novel. :param novel_title: Novel title for context. :return: Fully populated list of ModuleContract objects.
LLM client¶
OpenAI LLM client adapter for contract inference and segmentation.
This module isolates API integration so the rest of the codebase does not depend on OpenAI SDK details.
Two call patterns are supported:
- :meth:
complete— Free-form text completion (used by segmentation). - :meth:
infer_json— JSON-structured inference (used by contract inference).
LLMClientConfig
dataclass
¶
Configuration for the LLM client.
:param model: Model name used for inference. :param timeout_s: Request timeout (seconds).
OpenAILLMClient ¶
OpenAI-backed LLM client using the official Python SDK.
Requires OPENAI_API_KEY to be set in the environment.
This client is the single integration point with the OpenAI API. It supports two calling modes:
- :meth:
completefor raw text responses (e.g. segmentation) - :meth:
infer_jsonfor JSON-structured inference (e.g. contract inference)
__init__ ¶
__init__(config=None)
Initialize the OpenAI client.
:param config: Optional :class:LLMClientConfig. Defaults are used if
not provided.
:raises RuntimeError: If OPENAI_API_KEY is not set or the OpenAI
SDK is not installed.
complete ¶
complete(prompt)
Call the model with a plain text prompt and return the raw text response.
Use this method when the caller expects free-form text output rather than structured JSON — for example, in LLM-based segmentation where the model returns annotated Markdown prose.
:param prompt: Full prompt string sent as the user message. :return: Raw text response from the model.
infer_json ¶
infer_json(*, user_prompt)
Call the model and return parsed JSON.
The system prompt instructs the model to output only valid JSON.
:param user_prompt: Prompt content for the module. :return: Parsed JSON dict. :raises ValueError: If the model output is not valid JSON.
The client exposes two call patterns:
complete(prompt)— raw text response (used byLLMSegmenter)infer_json(user_prompt=...)— structured JSON response (used byOpenAIContractInferencer)
LLM inferencer¶
LLM-based :class:ContractInferencer implementation.
This module provides :class:OpenAIContractInferencer, which uses
:class:~novel_testbed.inference.llm_client.OpenAILLMClient to populate
narrative contracts one module at a time.
Reader state is chained across modules so that each module's pre_state
equals the previous module's post_state, preserving narrative continuity.
OpenAIContractInferencer ¶
Bases: ContractInferencer
Contract inference using OpenAI LLM calls.
This inferencer treats the novel as a sequential state machine:
pre_stateof module N equalspost_stateof module N-1.post_stateis inferred from the current module's text via the LLM.
This chaining ensures that the reader's accumulating state across the novel is represented faithfully in the generated contracts.
__init__ ¶
__init__(client)
Initialize the inferencer.
:param client: An :class:~novel_testbed.inference.llm_client.OpenAILLMClient
instance used to call the LLM API.
infer ¶
infer(modules, *, novel_title)
Infer a full contract for a sequence of modules.
Each module is sent to the LLM individually. The post_state returned
by the LLM becomes the pre_state of the next module, chaining reader
state across the entire novel.
:param modules: Parsed modules from a :class:~novel_testbed.models.Novel.
:param novel_title: Title of the novel, passed to the LLM for context.
:return: List of fully populated :class:~novel_testbed.models.ModuleContract
objects in the same order as the input modules.
:raises ValueError: If the LLM response is missing required keys or
contains invalid field values.
Reader state is chained across modules: the post_state of module N
becomes the pre_state of module N+1, so the reader's accumulated state is
preserved across the full novel.
High-level pipeline helper¶
High-level helpers to infer a narrative contract from annotated Markdown.
infer_contract_from_markdown ¶
infer_contract_from_markdown(markdown_text, *, title, inferencer)
Infer a complete narrative contract from annotated Markdown.
Pipeline
Annotated Markdown → Parse → Infer
This is the recommended entry point for the infer CLI command. It handles
parse → infer orchestration in one call.
Prompt construction¶
Prompt templates for LLM-based contract inference.
build_module_prompt ¶
build_module_prompt(*, novel_title, chapter, module_title, module_text)
Build the user prompt for a module inference request.
:param novel_title: Title of the novel. :param chapter: Chapter name. :param module_title: Module title. :param module_text: Full module body text. :return: Prompt string.
Prompts are deterministic templates. The SYSTEM_PROMPT instructs the model
to return only valid JSON. build_module_prompt constructs the per-module
user prompt from structural metadata and prose text.
Inference types¶
Types and validation helpers for inference outputs.
We keep validation lightweight and explicit to avoid introducing heavy schema dependencies in v1.
These frozen dataclasses define the validated intermediate representation
between the raw LLM response and the final ModuleContract. They are not
exposed in the public API but are used internally by OpenAIContractInferencer.
These modules allow the system to read a novel and produce a contract without manual author annotation.
They do not replace authorship. They expose it.