Public library (sparkth.lib)¶
sparkth/lib/ is the curated, stable API that application code and plugins import
from, instead of reaching into sparkth.core.* (or sparkth.llm.* / sparkth.rag.*)
directly (see #379). Everything below is
generated from the module docstrings.
The plugin authoring surface and the permissions API have their own pages: Plugin authoring and Permissions.
Database sessions¶
sparkth.lib.db ¶
Database session access for Sparkth — the curated public session API.
This is the single public entry point for obtaining a database session. All code,
including plugins, should acquire sessions from here rather than reaching for the
raw SQLAlchemy engine in :mod:sparkth.core.db or :mod:sparkth.core.analytics.db.
The engine itself stays in :mod:sparkth.core.db (app DB) and :mod:sparkth.core.analytics.db
(analytics DB); this module is only the public face that hands out sessions over them.
analytics_session_scope
async
¶
analytics_session_scope(
expire_on_commit: bool = False,
) -> AsyncGenerator[AsyncSession, None]
Open an async session against the analytics database as a managed context.
Identical in contract to :func:session_scope, but bound to the analytics
engine (the separate analytics database). Use this for background/non-request
analytics code (event ingestion, rollup maintenance). Inside request handlers,
prefer the :func:get_analytics_session dependency.
Delegates to analytics_db.open_analytics_session — the same seam that the
test suite overrides to inject a throwaway engine.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expire_on_commit
|
bool
|
Whether ORM objects are expired after |
False
|
Yields:
| Name | Type | Description |
|---|---|---|
An |
AsyncGenerator[AsyncSession, None]
|
class: |
get_analytics_session
async
¶
get_analytics_session() -> AsyncGenerator[
AsyncSession, None
]
FastAPI dependency providing an :class:AsyncSession bound to the analytics database.
Delegates to :func:analytics_session_scope. Parameterless for the same
reason as :func:get_async_session.
get_async_session
async
¶
get_async_session() -> AsyncGenerator[AsyncSession, None]
FastAPI dependency that provides an :class:AsyncSession to async def handlers.
Delegates to :func:session_scope. It is intentionally parameterless:
FastAPI turns a dependency's parameters into request (query) parameters, so
the expire_on_commit knob must not be exposed here — use
:func:session_scope directly when you need to override it.
session_scope
async
¶
session_scope(
expire_on_commit: bool = False,
) -> AsyncGenerator[AsyncSession, None]
Open an async database session as a managed context.
Yields an :class:AsyncSession — a unit-of-work that borrows a connection
from the shared engine (sparkth.core.db.get_engine), tracks the ORM objects you load and
mutate, and returns the connection to the pool when the async with block
exits (whether normally or via an exception). Always use it as a context
manager so the connection is never leaked::
from sparkth.lib.db import session_scope
async with session_scope() as session:
session.add(obj)
await session.commit()
Use this for code that runs outside an HTTP request — background tasks,
plugin bootstrap, CLI jobs, cleanup routines — where FastAPI's dependency
injection is not available. Inside request handlers, prefer the
:func:get_async_session dependency instead.
Transaction semantics: the caller is responsible for committing
(await session.commit()); any un-committed work is rolled back when the
context exits.
The expire_on_commit parameter defaults to False, which is the
async-safe choice. By default SQLAlchemy expires every ORM object after
commit(), so the next attribute access lazily re-issues a SELECT to
reload it. In synchronous code that reload is a transparent (if wasteful)
blocking query, but in async code it is implicit I/O that cannot be awaited
and fails once the session has closed. Keeping expire_on_commit=False
leaves already-loaded attributes valid after the commit and after the block.
Pass expire_on_commit=True only if you specifically want post-commit
objects to refresh on next access.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expire_on_commit
|
bool
|
Whether ORM objects are expired after |
False
|
Yields:
| Name | Type | Description |
|---|---|---|
An |
AsyncGenerator[AsyncSession, None]
|
class: |
Logging¶
sparkth.lib.log ¶
Centralized logging for Sparkth.
The single public entry point for logging. All modules — application code and
plugins alike — must obtain loggers via :func:get_logger, never
logging.getLogger directly. Logging is configured exactly once per process
via :func:configure_logging, which is the only logging.basicConfig call in
the codebase.
Example
from sparkth.lib.log import get_logger
logger = get_logger(__name__)
logger.info("This is a log message")
configure_logging ¶
configure_logging(level: int = logging.INFO) -> None
Configure root logging once for the whole process.
This is the only place in the codebase that calls logging.basicConfig.
It is idempotent: basicConfig is a no-op if the root logger already has
handlers, so calling it from multiple entrypoints is safe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
int
|
Root logging level (defaults to |
INFO
|
get_logger ¶
get_logger(name: str) -> logging.Logger
Return the Sparkth logger for name (typically __name__).
Loggers are namespaced under sparkth and propagate to the root logger
configured by :func:configure_logging. Application modules pass __name__,
which already starts with sparkth (the package name), so it is used as-is;
any other name is prefixed with sparkth. to keep every logger under the
single sparkth root.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Logger name, typically |
required |
Returns:
| Type | Description |
|---|---|
Logger
|
Logger instance. |
Settings¶
sparkth.lib.settings ¶
Application settings public API for Sparkth.
The single public entry point for application settings. All modules — application
code and plugins alike — must access settings via :func:get_settings, never by
importing from sparkth.core.config directly.
Example
from sparkth.lib.settings import get_settings
settings = get_settings()
print(settings.SECRET_KEY)
Encryption¶
sparkth.lib.encryption ¶
Public API for the symmetric-encryption service.
Import get_encryption_service (and EncryptionService) from here instead of
reaching into sparkth.core.encryption directly. It encrypts stored secrets — such
as LLM API keys — at rest with Fernet. Implementation lives in
sparkth/core/encryption.py.
Data models¶
sparkth.lib.models ¶
Public API for the SQLModel data models that plugins consume.
Plugins import model classes and the shared mixins from here instead of reaching
into sparkth.core.models.* directly — every internal symbol a plugin imports
becomes an implicit public API and blocks refactoring (see issue #379).
Implementation lives in sparkth/core/models/.
Authentication¶
sparkth.lib.auth ¶
Authentication dependency for resolving the current user from a bearer token.
The single canonical home for get_current_user: every caller (routes, the permission
gate, plugins, and the test harness) imports it from here. It is deliberately not
re-exported from sparkth.api.v1.auth — one object keeps FastAPI dependency overrides working.
RAG¶
sparkth.lib.rag ¶
Public API for the RAG library.
All plugins and external modules import RAG functionality from here.
DocumentNotFoundError ¶
Bases: RAGError
Raised when a document does not exist or is not accessible to the user.
DocumentSection
dataclass
¶
DocumentSection(
source_name: str,
chapter: str | None,
section: str | None,
subsection: str | None,
chunk_count: int,
position_index: int,
)
RAGNotReadyError ¶
RAGNotReadyError(document_id: int, status: str)
Bases: RAGError
Raised when the document exists but status is not READY.
RAGRetrievalError ¶
Bases: RAGError
Raised when agent retrieval or section-chunk fetch fails.
RetrievedChunk
dataclass
¶
RetrievedChunk(
source_name: str,
chapter: str | None,
section: str | None,
subsection: str | None,
content: str,
)
A chunk returned by retrieval, with its document/section attribution.
ScannedPDFError ¶
ScannedPDFError(source_name: str)
Bases: RAGError
Raised when a PDF appears to be scanned/image-only.
UnsupportedFileTypeError ¶
Bases: RAGIngestionError
Raised when a file's type cannot be handled by the RAG extractors.
Callers typically treat this as a benign skip (the file is fine, it just isn't RAG-ingestible).
agentic_retrieve_context
async
¶
agentic_retrieve_context(
query: str, document_ids: list[int], llm: BaseChatModel
) -> list[RetrievedChunk]
Retrieve relevant document chunks for a query across the given documents.
Validates that every document exists and is READY before retrieval — callers do not need to perform this check themselves. Uses agentic section retrieval per document and returns a flat list of RetrievedChunk. Opens its own database sessions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document_ids
|
list[int]
|
Documents to search. All must exist and be READY. |
required |
query
|
str
|
The user's natural-language query. |
required |
llm
|
BaseChatModel
|
LangChain chat model used by the retrieval agent. |
required |
Returns:
| Type | Description |
|---|---|
list[RetrievedChunk]
|
Flat list of RetrievedChunk across all documents (empty if no matches). |
Raises:
| Type | Description |
|---|---|
DocumentNotFoundError
|
a document is missing or soft-deleted. |
RAGNotReadyError
|
a document exists but is not READY. |
RAGRetrievalError
|
retrieval failed. |
copy_document_chunk_links
async
¶
copy_document_chunk_links(
session: AsyncSession,
source_document_id: int,
target_document_id: int,
) -> None
Copy all chunk links from source_document_id to target_document_id.
format_document_chunks_as_llm_context ¶
format_document_chunks_as_llm_context(
retrieved_chunks: list[RetrievedChunk],
) -> str
Group retrieved chunks by document and render them as one LLM context string.
Retrieval can return chunks from several documents interleaved in relevance order. They are grouped by the document each chunk came from (keyed by the document name the chunk carries, in first-seen order); each document's chunks are rendered as a labelled context block, and the blocks are joined with a blank line into a single string. An empty input yields an empty string.
get_rag_ingested_document_structure
async
¶
get_rag_ingested_document_structure(
document_id: int,
) -> list[DocumentSection]
Return ordered section metadata generated from the ingested RAG chunks.
Sections are ordered by the minimum chunk id within each (chapter, section, subsection) group. This preserves document insertion order, so position_index reliably reflects the section's position in the original document.
Returns an empty list when the document does not exist.
ingest_document
async
¶
ingest_document(
filename: str, file_bytes: bytes, document_id: int
) -> IngestionResult
Ingest a document's bytes into the RAG store.
Pipeline: eligibility check -> extract -> chunk -> store (with cross-document content-hash dedup) -> link chunks to document_id. Opens and commits its own database session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
document_id
|
int
|
Document.id recorded in the chunk-link table. |
required |
file_bytes
|
bytes
|
Raw file content. |
required |
filename
|
str
|
Original filename (drives extension dispatch). |
required |
Returns:
| Type | Description |
|---|---|
IngestionResult
|
IngestionResult with new/reused chunk counts. |
Raises:
| Type | Description |
|---|---|
UnsupportedFileTypeError
|
type the extractors cannot handle. |
ScannedPDFError
|
PDF appears scanned/image-only. |
LLM¶
sparkth.lib.llm ¶
Public API for the LLM library.
All plugins and external modules import LLM functionality from here. Nothing
outside sparkth/llm/ should import from sparkth.llm.* directly.
BaseChatProvider ¶
BaseChatProvider(
api_key: str,
model: str,
system_prompt: str | None = None,
temperature: float = 0.7,
max_tool_executions: int = 50,
max_retries: int = 2,
)
Bases: ABC
create_llm ¶
create_llm(
streaming: bool = False,
callbacks: list[Any] | None = None,
) -> Any
Public interface for creating a configured LangChain LLM instance.
send_message
async
¶
send_message(
messages: list[dict[str, Any]],
max_tokens: int | None = None,
tools: list[Any] | None = None,
) -> dict[str, Any]
Send a message and get a response, with optional tool usage.
stream_message
async
¶
stream_message(
messages: list[dict[str, Any]],
max_tokens: int | None = None,
tools: list[Any] | None = None,
) -> AsyncIterator[dict[str, Any]]
Stream a message response, with optional tool usage.
Yields typed event dicts
{"type": "token", "content": str} — text to display {"type": "tool_start","name": str} — tool execution beginning {"type": "tool_end", "name": str} — tool execution finished
LLMConfigAdapter ¶
Base adapter for plugins that hold an optional llm_config_id reference.
preprocess_config: validates the referenced LLMConfig is owned by the user. postprocess_config: resolves llm_config_id to name/provider/model for the frontend.
LLMConfigDuplicateNameError ¶
LLMConfigDuplicateNameError(name: str)
Bases: ValueError
Raised when an LLM config with the same name already exists for the user.
LLMConfigInactiveError ¶
LLMConfigInactiveError()
Bases: ValueError
Raised when an LLM config exists but is inactive.
LLMConfigModelNotSetError ¶
Bases: ValueError
Raised when the model field is empty on an LLM config.
LLMConfigNotFoundError ¶
LLMConfigNotFoundError(config_id: int, user_id: int)
Bases: ValueError
Raised when an LLM config is not found.
LLMConfigService ¶
LLMConfigService(
encryption: SupportsEncryption, cache: SupportsCache
)
Initialize with encryption and cache services.
create
async
¶
create(
session: AsyncSession,
user_id: int,
name: str,
provider: str,
model: str,
api_key: str,
) -> LLMConfig
Create a new LLM config, encrypting the API key. Raises LLMConfigDuplicateNameError on duplicate name.
delete
async
¶
delete(
session: AsyncSession, user_id: int, config_id: int
) -> bool
Soft-delete a config and evict its cache entry. Returns False if not found.
get
async
¶
get(
session: AsyncSession, user_id: int, config_id: int
) -> LLMConfig | None
Fetch a single non-deleted LLM config by ID and user, or None if not found.
list
async
¶
list(
session: AsyncSession,
user_id: int,
include_inactive: bool = False,
) -> list[LLMConfig]
Return all non-deleted LLM configs for a user, newest first.
By default only active configs are returned. Pass include_inactive=True to also include deactivated configs (e.g. for the settings page).
mask_key
staticmethod
¶
mask_key(api_key: str) -> str
Return a masked version of the API key, preserving the provider prefix and last 4 chars.
resolve
async
¶
resolve(
session: AsyncSession, user_id: int, config_id: int
) -> tuple[LLMConfig, str]
Return (config, decrypted_api_key). Caches by (user_id, config_id). Updates last_used_at.
rotate_key
async
¶
rotate_key(
session: AsyncSession,
user_id: int,
config_id: int,
api_key: str,
) -> LLMConfig
Replace the API key for a config, updating encryption and invalidating cache.
set_active
async
¶
set_active(
session: AsyncSession,
user_id: int,
config_id: int,
is_active: bool,
) -> LLMConfig
Activate or deactivate a config. Bypasses the is_active filter so inactive configs can be re-activated.
update
async
¶
update(
session: AsyncSession,
user_id: int,
config_id: int,
name: str | None = None,
model: str | None = None,
) -> LLMConfig
Update name and/or model of an existing config. Raises ValueError if not found or duplicate name.
LLMConfigValidationError ¶
Bases: ValueError
Raised when a field value is invalid (e.g. model not allowed for provider).
get_provider ¶
get_provider(
provider_name: str,
api_key: str,
model: str,
system_prompt: str | None = None,
temperature: float = 0.7,
max_tool_executions: int = 50,
max_retries: int = 2,
) -> BaseChatProvider
Get a chat provider instance.
get_provider_catalog ¶
get_provider_catalog() -> list[ProviderCatalogEntry]
Return all providers with their available models.
Analytics¶
sparkth.lib.analytics ¶
Public API for the analytics emission gateway.
All application code and plugins import analytics functionality from here rather
than reaching into sparkth.core.analytics.* directly. Implementation lives in
sparkth/core/analytics/.
Plugins
- subclass
AnalyticsEventSchemato define an event payload schema (declaringevent_type/version), - register it from their
__init__viaregister_event_schema(self, MyEvent), - emit it through
ingest_event.
AnalyticsEventSchema ¶
Bases: BaseModel
Base class for analytics event payload schemas.
Subclasses set event_type and version as class attributes and declare
the payload as ordinary Pydantic fields. Those two are ClassVar — identity
metadata, not part of the validated payload — and are required: a subclass that
omits either fails at definition time (__init_subclass__).
Extra fields are forbidden so a producer sending unexpected keys gets a
422 rather than having those fields silently dropped from the stored row.
ContinuousAggregateNotFound ¶
ContinuousAggregateNotFound(name: str)
Bases: Exception
Raised when a backfill targets a continuous aggregate that does not exist.
DuplicateEventTypeError ¶
DuplicateEventTypeError(event_type: str, version: int)
Bases: Exception
Raised when a schema claims an already-registered (event_type, version).
Registration is not idempotent: this fires for a colliding different class and for re-registering the same class. Either is a startup-fatal programming error — a producer's payload could silently validate against the wrong schema.
EventNamespaceError ¶
EventNamespaceError(plugin_name: str, event_type: str)
Bases: Exception
Raised when a plugin contributes an event not namespaced under its own name.
LoginActivityPoint ¶
Bases: BaseModel
One day's login count. day is an ISO YYYY-MM-DD string.
UnknownEventTypeError ¶
UnknownEventTypeError(event_type: str, version: int)
Bases: Exception
Raised when an (event_type, version) pair has no registered schema.
backfill_continuous_aggregates
async
¶
backfill_continuous_aggregates(
name: str | None = None,
) -> list[str] | None
Materialize the full history of continuous aggregates (all, or one by name).
Continuous aggregates are created WITH NO DATA (so creation does not backfill
inside Alembic's transaction), and their refresh policies only cover a trailing
window (start_offset). Without a one-off full refresh, buckets older than that
window fall below the materialization watermark once the first policy run advances it
and disappear from the view — so any pre-migration history is silently lost. Run this
once after applying an aggregate's migration on PostgreSQL/TimescaleDB; it is
idempotent and safe to re-run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Refresh only this aggregate. When |
None
|
Returns:
| Type | Description |
|---|---|
list[str] | None
|
The list of aggregate names refreshed (possibly empty if none are registered), or |
list[str] | None
|
|
list[str] | None
|
(e.g. SQLite in tests/e2e, where continuous aggregates do not exist and the read |
list[str] | None
|
path aggregates |
Raises:
| Type | Description |
|---|---|
ContinuousAggregateNotFound
|
if |
get_event_schema ¶
get_event_schema(
event_type: str, version: int
) -> type[AnalyticsEventSchema]
Return the schema for (event_type, version), or raise UnknownEventTypeError.
get_login_activity
async
¶
get_login_activity(
session: AsyncSession, days: int = 30
) -> list[LoginActivityPoint]
Return daily login counts, newest first, for the last days calendar days.
The window is bounded by a date floor (now - days), so gap days never let
the series reach back beyond the requested window. Days with no logins are
omitted entirely — the series is not zero-filled; callers must tolerate gaps.
ingest_event
async
¶
ingest_event(
session: AsyncSession,
event_type: str,
version: int,
payload: dict[str, Any],
actor_id: str | None = None,
occurred_at: datetime | None = None,
) -> None
Validate payload against the registered schema and land it in raw_events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
AsyncSession
|
An async session bound to the analytics database. |
required |
event_type
|
str
|
The base event name, e.g. |
required |
version
|
int
|
The schema version, e.g. |
required |
payload
|
dict[str, Any]
|
The raw event body to validate. |
required |
actor_id
|
str | None
|
The authenticated user id, stored for provenance. |
None
|
occurred_at
|
datetime | None
|
When the event happened; defaults to |
None
|
Raises:
| Type | Description |
|---|---|
UnknownEventTypeError
|
No schema is registered for |
ValidationError
|
The payload does not satisfy the schema. |
SQLAlchemyError
|
The insert failed. |
register_event_schema ¶
register_event_schema(
plugin: SparkthPlugin,
schema: type[AnalyticsEventSchema],
) -> None
Register a plugin's event schema on the ANALYTICS_EVENTS hook.
Call this from a plugin's __init__. Registration happens at
import time, straight into the ANALYTICS_EVENTS hook the gateway resolves
against.
Two startup-fatal guards, enforced here so a misconfigured plugin crashes the
process at import rather than at first emit (a third — that the schema declares
event_type/version — is enforced on AnalyticsEventSchema itself, at
class-definition time, via __init_subclass__):
- Namespace.
event_typemust be prefixed with the contributing plugin's name (e.g. pluginslack→"slack.*"), elseEventNamespaceError. This stops a plugin squatting a core or another plugin's event name. - Collision. Any class claiming an already-registered
(event_type, version)raisesDuplicateEventTypeError.
HTTP client¶
sparkth.lib.http ¶
BaseHttpClient ¶
BaseHttpClient(base_url: str, auth: Auth = Auth.BEARER)
Shared base for HTTP API clients that authenticate with a token.
Subclasses provide a token property and call _request() instead of
duplicating the token-check + URL-join logic.
The optional base_url override on _request supports clients whose
verb methods accept a per-call base URL (e.g. OpenEdxClient).
Initialise the client with a base URL and authentication scheme.
token
property
¶
token: str | None
Return the bearer token for the current session; override in subclasses.
__aexit__
async
¶
__aexit__(
_exc_type: type[BaseException] | None,
_exc_val: BaseException | None,
_exc_tb: TracebackType | None,
) -> None
Close the underlying session on context manager exit.
Documents¶
sparkth.lib.documents ¶
Public API for the Document registry.
All plugins and external modules manage document identity and status from here.
Document ¶
Bases: TimestampedModel, SoftDeleteModel
Plugin-agnostic registry of documents submitted for RAG ingestion.
Plugins create one row per source document they want to ingest. RAG uses document_id as its primary reference — it never imports from any plugin model.
create_document
async
¶
create_document(
session: AsyncSession,
user_id: int,
name: str,
mime_type: Optional[str],
) -> Document
Create a new Document row with QUEUED status and flush (no commit).
get_document
async
¶
get_document(
session: AsyncSession, document_id: int, user_id: int
) -> Optional[Document]
Return the Document if owned by user_id and not soft-deleted; else None.
list_ready_documents
async
¶
list_ready_documents(
session: AsyncSession, user_id: int
) -> list[Document]
Return non-deleted READY documents owned by user_id, ordered for display.
soft_delete_document
async
¶
soft_delete_document(
session: AsyncSession, document_id: int
) -> None
Soft-delete a Document by id. Does not commit.
update_document_status
async
¶
update_document_status(
session: AsyncSession,
document_id: int,
status: DocumentStatus,
error: Optional[str] = None,
) -> None
Update status (and optional error) on an existing Document. Does not commit.
Enums¶
sparkth.lib.enums ¶
Exceptions¶
sparkth.lib.exceptions ¶
Legacy HTTP-carrying exceptions, kept for existing LMS-client callers.
These classes predate the exception→HTTP mapping standard (CLAUDE.md,
"Domain exceptions → HTTP responses"): they carry status_code on the exception
itself. New code must NOT follow this pattern — raise an HTTP-agnostic domain exception
and map it to a status via register_exception_handler (sparkth.lib.exceptions.handlers)
instead.