Skip to content

msflib.ai_core

Public modules of the msflib-ai-core package.

msflib.ai_core.actions

PromptVersionAction

Bases: ModelAction[PromptVersion, PromptVersionCreate, PromptVersionUpdate]

CRUD action for PromptVersion records.

get_active_by_scope(session: Session, *, prompt_key: str, workspace_id: int | None = None, account_id: int | None = None) -> PromptVersion | None

Return the active prompt using user->workspace->global precedence.

get_active_by_scope_for_keys(session: Session, *, prompt_keys: Sequence[str], workspace_id: int | None = None, account_id: int | None = None) -> dict[str, PromptVersion]

Return active prompts for keys using user->workspace->global precedence.

list_by_scope(session: Session, *, prompt_key: str, workspace_id: int | None = None, account_id: int | None = None, offset: int = 0, limit: int | None = 100) -> list[PromptVersion]

Return prompt versions at an exact scope with optional pagination.

activate_version(session: Session, target: PromptVersion) -> PromptVersion

Activate target, deactivate peers in scope via single atomic UPDATE.

Uses a CASE expression to set is_active = (id == target.id) for all rows in the scope, ensuring concurrency-safe activation even under simultaneous set_active calls.

AIProviderProfileAction(*, cipher: ProviderSecretCipher, settings: SettingsBase, ai_settings: AICoreSettings | None = None)

Bases: ModelAction[AIProviderProfile, AIProviderProfileCreate, AIProviderProfileUpdate]

list_all_profiles(session: Session, *, scope_type: ProviderScope | None = None, include_inactive: bool = True) -> list[AIProviderProfile]

List profiles across every scope (superuser administration).

VectorStoreProfileAction(*, cipher: ProviderSecretCipher, settings: SettingsBase, ai_settings: AICoreSettings | None = None)

Bases: ModelAction[VectorStoreProfile, VectorStoreProfileCreate, VectorStoreProfileUpdate]

list_all_profiles(session: Session, *, scope_type: ProviderScope | None = None, include_inactive: bool = True) -> list[VectorStoreProfile]

List profiles across every scope (superuser administration).

prompt

PromptVersionAction

Bases: ModelAction[PromptVersion, PromptVersionCreate, PromptVersionUpdate]

CRUD action for PromptVersion records.

get_active_by_scope(session: Session, *, prompt_key: str, workspace_id: int | None = None, account_id: int | None = None) -> PromptVersion | None

Return the active prompt using user->workspace->global precedence.

get_active_by_scope_for_keys(session: Session, *, prompt_keys: Sequence[str], workspace_id: int | None = None, account_id: int | None = None) -> dict[str, PromptVersion]

Return active prompts for keys using user->workspace->global precedence.

list_by_scope(session: Session, *, prompt_key: str, workspace_id: int | None = None, account_id: int | None = None, offset: int = 0, limit: int | None = 100) -> list[PromptVersion]

Return prompt versions at an exact scope with optional pagination.

activate_version(session: Session, target: PromptVersion) -> PromptVersion

Activate target, deactivate peers in scope via single atomic UPDATE.

Uses a CASE expression to set is_active = (id == target.id) for all rows in the scope, ensuring concurrency-safe activation even under simultaneous set_active calls.

provider

AIProviderProfileAction(*, cipher: ProviderSecretCipher, settings: SettingsBase, ai_settings: AICoreSettings | None = None)

Bases: ModelAction[AIProviderProfile, AIProviderProfileCreate, AIProviderProfileUpdate]

list_all_profiles(session: Session, *, scope_type: ProviderScope | None = None, include_inactive: bool = True) -> list[AIProviderProfile]

List profiles across every scope (superuser administration).

vector_store

VectorStoreProfileAction(*, cipher: ProviderSecretCipher, settings: SettingsBase, ai_settings: AICoreSettings | None = None)

Bases: ModelAction[VectorStoreProfile, VectorStoreProfileCreate, VectorStoreProfileUpdate]

list_all_profiles(session: Session, *, scope_type: ProviderScope | None = None, include_inactive: bool = True) -> list[VectorStoreProfile]

List profiles across every scope (superuser administration).

msflib.ai_core.config

msflib.ai_core.deps

get_provider_dependencies(settings: SettingsBase, *, get_session: Callable, get_current_workspace: Callable | None = None, get_current_active_user: Callable | None = None, get_current_account: Callable | None = None, get_policy_resolver: Callable | None = None) -> DependencyNamespace

Return reusable FastAPI dependencies for the provider registry surfaces.

One factory takes the site's callables and returns a DependencyNamespace of request-scoped dependencies (the module deps convention, see auth.get_account_dependencies). Wiring the dependencies together here rather than in the router guarantees every consumer shares the same dependency callables, so FastAPI's per-request cache resolves settings once per request no matter how many dependencies consume them.

Which identity callables to pass reduces to one question (same split as :func:msflib.scope.get_scope_dependencies): does this site have a membership entity distinct from the account?

  • Yes — pass get_current_workspace + get_current_active_user. Resolution runs over the workspace/user tiers, and the /manage members (workspace-scoped, no user tier) are exposed alongside them.
  • No — pass get_current_account. Whether a workspace tier exists at all is then a second, independent choice:

  • No workspace concept: omit get_current_workspace. Resolution runs over the account tier alone, and the /manage members are omitted — there's no workspace-scoped state distinct from the account's own for them to serve.

  • Workspace exists as an attribute of the account (e.g. an Account.current_workspace_id field), not a separate membership row: also pass get_current_workspace — not get_current_active_user. Resolution runs over the account/ workspace tiers, and the /manage members are exposed since a workspace tier is present.

Returns:

Name Type Description
DependencyNamespace exposing:
  • get_resolved_ai_settings — AICoreSettings resolved for the request's tenant/workspace/user tiers (workspace sites), tenant/ workspace/account tiers (account sites with a workspace tier), or tenant/account tiers (workspace-less sites).
  • get_manage_resolved_ai_settings — same with the user/account tier skipped, for surfaces that only touch workspace-scoped state (e.g. the /manage listing). Only present when a workspace tier exists.
  • get_registry_service / get_manage_registry_service — ProviderRegistryService bound to the corresponding settings. get_manage_registry_service is only present when a workspace tier exists.
  • user_profiles_enabled — gate raising 403 when the workspace disables self-service BYO profiles.
The caller's ScopeEnvelope is not built here: that is a core primitive,
see func:`msflib.scope.get_scope_dependencies`.

get_vector_store_dependencies(settings: SettingsBase, *, get_session: Callable, get_current_workspace: Callable, get_current_active_user: Callable, get_current_tenant: Callable | None = None, get_policy_resolver: Callable | None = None) -> DependencyNamespace

Return reusable FastAPI dependencies for the vector store profile surfaces.

Same module deps convention as :func:get_provider_dependencies (one factory takes the site's callables and returns a DependencyNamespace of request-scoped dependencies), applied to vector store profiles. Vector store profiles are always workspace-membership sites, so unlike get_provider_dependencies there is no account-only variant to choose between.

Unlike :func:get_provider_dependencies — where the caller's ScopeEnvelope is left to :func:msflib.scope.get_scope_dependencies — scope creation is included here, since every vector store service dependency needs one to resolve tiered settings anyway. Endpoints pull get_scope/get_manage_scope directly rather than rebuilding a ScopeEnvelope from workspace/current_user themselves.

Returns:

Type Description
DependencyNamespace exposing:
  • get_scope — ScopeEnvelope for the caller's tenant/workspace/ account tiers: for member-facing reads and writes where user-scoped rows participate.
  • get_manage_scope — tenant/workspace only, no account tier: for the /manage listing, which only surfaces workspace-scoped rows (an account_id there would fetch user-scoped rows only to discard them).
  • get_registry_service / get_manage_registry_service — VectorStoreRegistryService resolved for the corresponding scope, reusing the shared base service when tiered policy resolution changes nothing (same base-service-reuse / fallback shape as get_provider_dependencies).

get_ai_dependencies(settings: SettingsBase) -> DependencyNamespace

Return AI building blocks other modules call programmatically.

Unlike :func:get_provider_dependencies, these are plain callables (several take runtime arguments), not request-scoped FastAPI dependencies.

msflib.ai_core.memory_types

msflib.ai_core.models

PromptVersion

Bases: PromptVersionBase, ModelBase

Persisted prompt version with scoped override support.

Resolution priority (highest to lowest): 1. User-scoped active (workspace_id + account_id) 2. Workspace-scoped active (workspace_id only) 3. Global active (workspace_id=None, account_id=None)

Uniqueness is enforced via scope_key, a non-null computed string that encodes (prompt_key, version, workspace_id, account_id). This avoids the problem where most SQL engines allow multiple NULLs inside a UNIQUE constraint.

redact_url_credentials(url: str | None) -> str | None

Strip embedded userinfo (user:pass@) from a connection URL.

Several backends (pgvector, weaviate, milvus, chroma over http) accept connection strings that may embed credentials directly in the URL. Read schemas must never echo those back.

prompt

PromptVersionBase

Bases: SQLModel

Shared fields for prompt version models.

PromptVersion

Bases: PromptVersionBase, ModelBase

Persisted prompt version with scoped override support.

Resolution priority (highest to lowest): 1. User-scoped active (workspace_id + account_id) 2. Workspace-scoped active (workspace_id only) 3. Global active (workspace_id=None, account_id=None)

Uniqueness is enforced via scope_key, a non-null computed string that encodes (prompt_key, version, workspace_id, account_id). This avoids the problem where most SQL engines allow multiple NULLs inside a UNIQUE constraint.

vector_store

redact_url_credentials(url: str | None) -> str | None

Strip embedded userinfo (user:pass@) from a connection URL.

Several backends (pgvector, weaviate, milvus, chroma over http) accept connection strings that may embed credentials directly in the URL. Read schemas must never echo those back.

msflib.ai_core.policy

AI Core policy models.

Module author contract for policy discovery: 1. Keep policy models in sibling policy.py next to config.py. 2. Name models as <FieldBase>PolicySchema for policy fields such as MIDDLEWARE_POLICY and CHUNKING_POLICY. 3. In config.py, annotate settings fields with these model classes where possible. 4. If plain mapping annotations are used, ModuleSettingsBase will attempt discovery by these names from this module.

MiddlewarePolicySchema

Bases: PolicyEnvelope

High-level middleware policy envelope consumed by runtime bridges.

ChunkingPolicySchema

Bases: PolicyEnvelope

Provider-first chunking policy envelope.

msflib.ai_core.providers

ProviderTypeRegistry()

register_alias(alias: str, target: str) -> None

Map alias to target for custom provider registrations.

normalize(name: str, *, capability: str | None = None) -> str

Validate name and return its lowercase form.

Raises ValueError when the provider is unknown or lacks the requested capability.

ProviderTypeSpec(name: str, capabilities: frozenset[str] = frozenset((CHAT,)), langchain_provider: str | None = None, base_url_key: str = 'base_url', api_key_key: str | None = 'api_key', default_base_url: str | None = None, chat_factory: Callable[..., Any] | None = None, embedding_factory: Callable[..., Any] | None = None, extra: dict[str, Any] = dict()) dataclass

Describes one provider type and how to build models for it.

register_provider_type(spec: ProviderTypeSpec, aliases: tuple[str, ...] = ()) -> None

Register a custom provider type on the process-wide registry.

registry

Provider type registry: single source of truth for model provider types.

Seeds itself from langchain's built-in provider maps so every provider supported by init_chat_model / init_embeddings is available without code changes here. Sites and modules can register custom provider types (with their own factories) via :func:register_provider_type at startup.

ProviderTypeSpec(name: str, capabilities: frozenset[str] = frozenset((CHAT,)), langchain_provider: str | None = None, base_url_key: str = 'base_url', api_key_key: str | None = 'api_key', default_base_url: str | None = None, chat_factory: Callable[..., Any] | None = None, embedding_factory: Callable[..., Any] | None = None, extra: dict[str, Any] = dict()) dataclass

Describes one provider type and how to build models for it.

ProviderTypeRegistry()

register_alias(alias: str, target: str) -> None

Map alias to target for custom provider registrations.

normalize(name: str, *, capability: str | None = None) -> str

Validate name and return its lowercase form.

Raises ValueError when the provider is unknown or lacks the requested capability.

register_provider_type(spec: ProviderTypeSpec, aliases: tuple[str, ...] = ()) -> None

Register a custom provider type on the process-wide registry.

build_connection_kwargs(spec: ProviderTypeSpec, *, api_key: Any = None, base_url: Any = None) -> dict[str, Any]

Build the api_key/base_url kwargs shared by chat and embedding builds.

msflib.ai_core.router

ai_provider

router(*, get_session: Callable, settings: SettingsBase, get_current_workspace: Callable, get_current_workspace_anonymous: Callable, get_current_active_user: Callable, workspace_role_check: Callable, get_current_tenant: Callable | None = None, get_policy_resolver: Callable | None = None, prefix: str = '/ai/providers', tags: list[str] | None = None) -> APIRouter

get_current_tenant: resolved once at the endpoint boundary, the same way get_current_workspace/get_current_active_user are (see msflib.tenancy.deps.get_tenant_dependencies) -- yields a Tenant row. When omitted, scope building falls back to resolve_default_tenant_id, same as before this parameter existed.

admin_router(*, get_session: Callable, settings: SettingsBase, get_current_active_superuser: Callable, get_current_tenant: Callable | None = None, prefix: str = '/ai/admin/providers', tags: list[str] | None = None) -> APIRouter

Platform-level provider profile administration (superuser only).

Owns the scopes the workspace router cannot touch: global and tenant profiles are created here, and the by-id routes can manage profiles of any scope for operational support.

A superuser isn't acting "as" any one tenant, so unlike the workspace router there is no per-request tenant to resolve -- tenant-scoped profiles created here are always scoped to the single default tenant (see get_current_tenant/resolve_default_tenant_id below). Targeting a specific other tenant is a later phase's concern (an explicit tenant selector on these endpoints).

vector_store

router(*, get_session: Callable, settings: SettingsBase, get_current_workspace: Callable, get_current_active_user: Callable, workspace_role_check: Callable, get_current_tenant: Callable | None = None, get_policy_resolver: Callable | None = None, prefix: str = '/ai/vector-stores', tags: list[str] | None = None) -> APIRouter

get_current_tenant: resolved once at the endpoint boundary, the same way get_current_workspace/get_current_active_user are (see msflib.tenancy.deps.get_tenant_dependencies) -- yields a Tenant row. When omitted, scope building falls back to msflib.scope.build_context_scope's own default (today's single-tenant resolution), same as before this parameter existed.

admin_router(*, get_session: Callable, settings: SettingsBase, get_current_active_superuser: Callable, get_current_tenant: Callable | None = None, prefix: str = '/ai/admin/vector-stores', tags: list[str] | None = None) -> APIRouter

Platform-level vector store profile administration (superuser only).

Owns the scopes the workspace router cannot touch: global and tenant profiles are created here, and the by-id routes can manage profiles of any scope for operational support.

A superuser isn't acting "as" any one tenant, so unlike the workspace router there is no per-request tenant to resolve -- tenant-scoped profiles created here are always scoped to the single default tenant (see get_current_tenant/resolve_default_tenant_id below). Targeting a specific other tenant is a later phase's concern (an explicit tenant selector on these endpoints).

msflib.ai_core.scope_profiles

msflib.ai_core.services

ProviderRegistryService(settings: SettingsBase, provider_action: AIProviderProfileAction | None = None, ai_settings: AICoreSettings | None = None)

ai_settings overrides the AI_CORE scope of settings.

Pass a tiered-resolved AICoreSettings (e.g. from ScopedConfigService.resolve_module_settings) so per-tenant, per-workspace or per-user TASK_PROFILES bindings take effect.

import_optional(module_name: str, *, feature: str, pip_package: str, extra: str, distribution: str) -> ModuleType

Import an optional module, raising a Poetry install hint on failure.

Use this instead of a bare importlib.import_module call at any site where a missing optional dependency should fail with actionable guidance rather than a bare ModuleNotFoundError. For availability probes (is_available()-style checks), use :func:is_importable instead.

distribution is the Poetry package that declares extra (e.g. "msflib-ai-core", "msflib-documents") — the caller's own package, not necessarily this one.

is_importable(*module_names: str) -> bool cached

True if every named module can be imported (cached for this process).

A cheap availability probe for optional-dependency gating (is_available()-style checks). Returns False only when the named module itself (or a parent package of it) is missing. Other failures — a genuine bug in an installed module (e.g. a SyntaxError), or an ImportError from one of its dependencies rather than the module being probed — still propagate, since misreporting those as "not installed" would hide the real failure instead of surfacing it.

Pass multiple names when a feature needs more than one optional module present.

optional_install_hint(*, pip_package: str, extra: str, distribution: str) -> str

Poetry-flavored install instructions for a missing optional package.

checkpoint_policy

CheckpointScope(scope: ScopeEnvelope) dataclass

Thin scope wrapper that validates and produces LangGraph checkpoint keys.

All key serialization is delegated to :class:~msflib.scope.ScopeCompiler. The envelope must carry at minimum tenant_id. sub_thread_id is optional and scopes the checkpoint key to a sub-thread within a conversation.

from_envelope(*, scope: ScopeEnvelope) -> CheckpointScope classmethod

Validate scope against the checkpoint profile and return a new instance.

scoped_config(config: Mapping[str, Any] | None = None) -> dict[str, Any]

Return a RunnableConfig dict with scoped checkpoint keys injected.

When conversation_id is absent from the scope envelope, no thread_id or checkpoint_ns is injected — LangGraph runs ephemerally with no state persistence, preventing different users from sharing a sentinel slot.

Raises :exc:ValueError if config already carries thread_id or checkpoint_ns in configurable.

validate(config: Mapping[str, Any]) -> None

Raise if config's checkpoint keys do not match this scope.

In ephemeral mode (no conversation_id), neither thread_id nor checkpoint_ns should be present — their presence indicates the config was produced by a different scope or injected externally.

The expected thread_id is recompiled from the envelope each time, so cross-scope access is caught deterministically.

lifecycle_hooks() -> dict[str, Any]

Define lifecycle hooks for checkpoint management.

create_langgraph_checkpointer(*, backend: str = 'memory', connection_string: str | None = None, async_mode: bool = False, scope: CheckpointScope | None = None, run_setup: bool = False) -> LangGraphCheckpointerFactoryResult

Create framework-native LangGraph checkpointer.

This factory returns a managed context manager. Use with for sync flows and async with when async_mode=True.

For postgres backends, run_setup defaults to False to avoid repeating DDL checks on every context entry. Run setup() once during application startup, then keep run_setup=False in request paths.

Backends: - memory: langgraph.checkpoint.memory.InMemorySaver - postgres: PostgresSaver or AsyncPostgresSaver from langgraph-checkpoint-postgres when installed.

config_bridge

ContextSource(id: str, content: str, token_count: int, metadata: dict[str, Any]) dataclass

estimate_tokens(content: str) -> int staticmethod

Estimate token count for the given content string.

Attempts to use tiktoken (provided by langchain-openai) with the cl100k_base encoding (GPT-4 / GPT-3.5-compatible) for an accurate count. Falls back to a conservative word-count heuristic (words × 1.3) when tiktoken is not installed.

This estimate is a pre-filter hint only. Callers must enforce the final token budget through LangChain's trim_messages() or an equivalent framework primitive after assembling the message list.

LangChainConfigBridge(settings: AICoreSettings, prompt_registry: PromptRegistryService | None = None)

Bridge resolved AICore settings to LangChain-friendly runtime policy.

to_langchain_middleware_config(tools: Sequence[Any] | None = None, *, session: Session | None = None, workspace_id: int | None = None, account_id: int | None = None, context_sources: Sequence[dict[str, Any]] | None = None) -> dict[str, Any]

Build a LangChain-compatible middleware configuration dict.

The returned context payload is a pre-filter hint: it contains ordered and budget-gated source IDs and token metadata, but not content. Callers must retrieve content from their source and enforce the final token budget via trim_messages() or an equivalent LangChain/LangGraph primitive.

The returned prompt_name_mapping.resolved dict maps target names to prompt content strings. Apply these through a ChatPromptTemplate or PromptTemplate — not by direct string concatenation.

context_windows

Static provider:model -> context-window (token) lookup, with a fallback.

Hand-maintained; unknown/new models fall back to a conservative default rather than failing. override lets a caller correct either case.

resolve_context_window(provider: str, model: str, override: int | None) -> tuple[int, bool]

Returns (context_window_tokens, used_fallback).

extraction

LoaderRegistry()

Lean fallback registry with local mime/extension mappings.

RegistryNotHandledError

Bases: Exception

Raised when a registry cannot handle a given payload.

LangChainMimeRegistry()

Delegates extraction to LangChain's MIME-based parser registry.

LangChainRegistry

Delegates extraction to LangChain document loaders when installed.

LlamaIndexRegistry

Delegates extraction to LlamaIndex readers when installed.

UnifiedLoaderRegistry(registries: list[ExtractorRegistry])

Unified access that tries multiple registries in priority order.

registries: tuple[ExtractorRegistry, ...] property

Configured registries in priority order (read-only view).

fallbacks

RegistryNotHandledError

Bases: Exception

Raised when a registry cannot handle a given payload.

LoaderRegistry()

Lean fallback registry with local mime/extension mappings.

providers

LlamaIndexRegistry

Delegates extraction to LlamaIndex readers when installed.

LangChainMimeRegistry()

Delegates extraction to LangChain's MIME-based parser registry.

LangChainRegistry

Delegates extraction to LangChain document loaders when installed.

registry

UnifiedLoaderRegistry(registries: list[ExtractorRegistry])

Unified access that tries multiple registries in priority order.

registries: tuple[ExtractorRegistry, ...] property

Configured registries in priority order (read-only view).

ingestion_pipeline

llm_json

Parse an LLM's attempted-JSON output, tolerating the syntax slips real chat models produce over long structured-output responses.

Not knowledge-specific -- any caller sending a chat model raw JSON-producing instructions (extraction, tool-call payload reconstruction, etc.) hits the same failure class, so this lives in ai_core rather than any one consumer module, mirroring markdown.unwrap_markdown_code_block.

LLMJSONParseError

Bases: Exception

No recoverable JSON structure in non-blank LLM output.

Distinct from a legitimate empty-content response (see parse_llm_json): this means the model produced something, but json_repair couldn't find any JSON structure in it at all. Raised rather than returned so a caller mid-pipeline (e.g. the knowledge extraction worker) surfaces it as a failed extraction instead of silently recording a zero-entity success.

Deliberately not a ValueError: a garbled response is plausibly a one-off generation glitch rather than a deterministic failure, so msflib.core.errors.is_terminal_ingestion_error (which treats ValueError as terminal) should let the job retry instead of giving up permanently.

normalize_llm_punctuation(text: str) -> str

Fix stray non-ASCII punctuation an LLM sometimes drifts into for JSON structural characters (colons, brackets, quotes) over a long response -- echoing source-text typography or its own inconsistent formatting -- which a JSON parser rejects as invalid syntax.

Curly single quotes and fullwidth punctuation are translated unconditionally -- they can't prematurely terminate an ASCII-quoted JSON string, so there's no structural risk to normalizing them everywhere. Curly double quotes and doubled straight quotes are only collapsed where they look like a delimiter pair (see the module-level regexes above), since collapsing those unconditionally can inject a real, unescaped quote into otherwise-legitimate string content.

parse_llm_json(content: str) -> Any

Parse an LLM's attempted-JSON output.

Fixes curly/smart quotes and other stray non-ASCII punctuation first (see normalize_llm_punctuation), then hands off to json_repair, which recovers from the broader class of malformed-JSON output real chat models have produced: mismatched brackets, unquoted keys, unterminated strings, trailing commas, single-quoted Python-dict-literal syntax. Normalization must run first -- json_repair alone mishandles the curly-quote cases, leaving a stray " fused into key names (e.g. 'relations"' instead of 'relations').

json_repair.loads degrades gracefully instead of raising: content it can't find any JSON structure in at all comes back as "" rather than a JSONDecodeError. For blank/whitespace-only content that's a legitimate result (e.g. a reasoning model's output-budget collapse), so it's returned as-is. For non-blank content -- the model said something but none of it was recoverable JSON -- silently turning "the model produced garbage" into "zero results, no error" would let a genuinely failed extraction masquerade as a successful empty one, so this raises LLMJSONParseError instead so the caller can retry/classify it as a failure explicitly.

markdown

Shared helper for stripping markdown code-fence wrapping from LLM output.

Chat models routinely wrap structured output (JSON, etc.) in a markdown code fence even when asked not to. Anything that parses raw LLM content should unwrap it first; this lives here rather than in any one consumer so ingestion_pipeline's JSON loader and knowledge's LLM extractor (and any future caller) share one implementation instead of drifting.

memory_store_policy

MemoryStoreScope(scope: ScopeEnvelope) dataclass

Thin scope wrapper that validates and produces LangGraph memory namespace keys.

The envelope carries the cross-cutting dimensions (tenant, workspace). Memory-type routing (semantic, episodic, etc.) is an ai_core-level concern and is supplied at namespace-construction time via :meth:scoped_namespace.

from_envelope(*, scope: ScopeEnvelope) -> MemoryStoreScope classmethod

Validate scope against the memory profile and return a new instance.

scoped_namespace(*, memory_type: str, sub_namespace: str | None = None) -> tuple[str, ...]

Full namespace for a specific memory type.

Parameters:

Name Type Description Default
memory_type str

Kind of memory store (e.g. "semantic", "episodic").

required
sub_namespace str | None

Optional additional segment (e.g. an agent identifier).

None
validate(namespace: tuple[str, ...] | list[str]) -> None

Raise if namespace does not fall within this scope's base prefix.

lifecycle_hooks() -> dict[str, Any]

Define lifecycle hooks for memory store management.

put_scoped_memory(store: Any, scope: MemoryStoreScope, *, key: str, value: Any, memory_type: str, sub_namespace: str | None = None) -> tuple[str, ...]

Store value under key in the scoped namespace for memory_type.

aput_scoped_memory(store: Any, scope: MemoryStoreScope, *, key: str, value: Any, memory_type: str, sub_namespace: str | None = None) -> tuple[str, ...] async

Async store value under key in the scoped namespace for memory_type.

get_scoped_memory(store: Any, scope: MemoryStoreScope, *, key: str, memory_type: str, sub_namespace: str | None = None) -> Any

Retrieve key from the scoped namespace for memory_type.

aget_scoped_memory(store: Any, scope: MemoryStoreScope, *, key: str, memory_type: str, sub_namespace: str | None = None) -> Any async

Async retrieve key from the scoped namespace for memory_type.

cleanup_scoped_memories(store: Any, scope: MemoryStoreScope, *, memory_type: str, sub_namespace: str | None = None) -> int

Delete all records stored at the scoped namespace for memory_type.

Returns the number of deleted records. Safe to call when no entries exist.

acleanup_scoped_memories(store: Any, scope: MemoryStoreScope, *, memory_type: str, sub_namespace: str | None = None) -> int async

Async delete all records at the scoped namespace for memory_type.

create_langgraph_store(*, backend: str = 'memory', connection_string: str | None = None, index: dict[str, Any] | None = None, async_mode: bool = False, scope: MemoryStoreScope | None = None, run_setup: bool = False) -> LangGraphStoreFactoryResult

Create framework-native LangGraph long-term memory store.

This factory returns a managed context manager. Use with for sync flows and async with when async_mode=True.

For postgres backends, run_setup defaults to False to avoid repeating DDL checks on every context entry. Run setup() once during application startup, then keep run_setup=False in request paths.

prompt_registry

PromptNotFoundError

Bases: Exception

Raised when a requested prompt version does not exist.

PromptVersionConflictError

Bases: Exception

Raised when registering a version that already exists at the same scope.

PromptRegistryService(action: PromptVersionAction | None = None)

Manages prompt versioning, active version resolution, and scoped overrides.

Resolution priority for get_active_version: 1. User-scoped active (workspace_id + account_id match) 2. Workspace-scoped active (workspace_id match, account_id=None) 3. Global active (workspace_id=None, account_id=None) 4. None if none found.

This follows the tiered precedence chain user -> workspace -> global.

register(session: Session, prompt_key: str, version: str, content: str, *, variables: list[str] | None = None, description: str | None = None, workspace_id: int | None = None, account_id: int | None = None, set_active: bool = False) -> PromptVersion

Register a new prompt version at the given scope.

Raises PromptVersionConflictError if the same key/version/scope already exists. Pass set_active=True to immediately activate this version within its scope.

get_version(session: Session, prompt_key: str, version: str, *, workspace_id: int | None = None, account_id: int | None = None) -> PromptVersion | None

Return the exact version at the given scope, or None.

get_active_version(session: Session, prompt_key: str, *, workspace_id: int | None = None, account_id: int | None = None) -> PromptVersion | None

Resolve the active prompt version using the tiered priority chain.

Searches in order: 1. User scope (workspace_id + account_id) 2. Workspace scope (workspace_id, account_id=None) 3. Global scope (workspace_id=None, account_id=None)

Returns the first active match, or None if none is found.

get_active_versions(session: Session, prompt_keys: Sequence[str], *, workspace_id: int | None = None, account_id: int | None = None) -> dict[str, PromptVersion]

Resolve active prompt versions for many keys using tiered precedence.

list_versions(session: Session, prompt_key: str, *, workspace_id: int | None = None, account_id: int | None = None, offset: int = 0, limit: int | None = 100) -> list[PromptVersion]

Return versions for a prompt key at the given scope.

Uses paginated defaults (offset=0, limit=100). Pass limit=None to fetch all matching rows.

set_active(session: Session, prompt_key: str, version: str, *, workspace_id: int | None = None, account_id: int | None = None) -> PromptVersion

Make version the active prompt at the given scope.

Deactivates all other versions at the same scope first, then activates the target version.

Raises PromptNotFoundError if the version does not exist at the given scope.

provider_registry

ProviderRegistryService(settings: SettingsBase, provider_action: AIProviderProfileAction | None = None, ai_settings: AICoreSettings | None = None)

ai_settings overrides the AI_CORE scope of settings.

Pass a tiered-resolved AICoreSettings (e.g. from ScopedConfigService.resolve_module_settings) so per-tenant, per-workspace or per-user TASK_PROFILES bindings take effect.

provider_scope

extract_scope_ids(scope: ScopeEnvelope) -> tuple[int | None, int | None, int | None]

Extract (tenant_id, workspace_id, account_id) from a caller ScopeEnvelope.

All three are read from scope dimensions (see msflib.scope.build_context_scope) and decomposed to int -- never resolved or defaulted here: a caller's tenant_id dimension is only ever populated with an already-resolved real id (by a get_current_tenant-backed dependency at the endpoint boundary) or left unset, in which case this returns None rather than guessing a default. account_id checks the account_id dimension first, then falls back to scope.principal.account_id for envelopes built elsewhere (e.g. ai_api's per-request scope) that carry account identity via PrincipalContext rather than a dimension.

normalize_scope_envelope(scope: ScopeEnvelope, *, scope_type: ProviderScope | str) -> ScopeEnvelope

Return a scope envelope carrying only the dimensions relevant to scope_type.

Shared by AIProviderProfile and VectorStoreProfile creation: only the profile's own scope_type gets a populated dimension (mirrors ScopedConfigEntry), the others stay explicitly null. scope carries the caller's ambient tenant/workspace/account context, already resolved to real ids by the endpoint boundary -- a tenant-scoped profile requires a resolved tenant_id the same way workspace/user scope require their own ambient dimensions below. The returned envelope is what callers should pass straight through to :func:build_provider_scope_key / :func:build_vector_store_scope_key and, decomposed via get_dim + :func:dim_as_int, to the DB persistence call -- there's no need to unpack it into scalars in between.

Raises :exc:ValueError when tenant/workspace/user scope is requested without the required ambient dimension(s).

dim_as_int(value: str | None) -> int | None

Decompose a scope dimension back to int for an int-typed DB column.

build_provider_scope_key(scope: ScopeEnvelope, *, name: str, scope_type: ProviderScope | str) -> str

Return a non-null unique string encoding provider name + scope tuple.

scope should already carry only the tenant_id/workspace_id/account_id dimensions relevant to scope_type (see :func:normalize_scope_envelope); it's passed straight through here, merged with the two profile-specific dimensions (name, scope_type) that aren't part of the ambient scope. Serialization is delegated to :class:~msflib.scope.ScopeCompiler via the aicore.provider profile. Raises :exc:ValueError (mapped to 422 by the router) when a value cannot be serialized, e.g. a name containing the / delimiter or equal to the - null sentinel.

build_vector_store_scope_key(scope: ScopeEnvelope, *, name: str, scope_type: ProviderScope | str) -> str

Return a non-null unique string encoding vector store profile name + scope.

Mirrors :func:build_provider_scope_key via the aicore.vector_store profile. Raises :exc:ValueError (mapped to 422 by the router) when a value cannot be serialized.

scope_metadata

retrieval_scope_profile_dimensions() -> set[str]

Return scope dimensions reserved for retrieval filtering.

compile_retrieval_scope_metadata(scope: ScopeEnvelope) -> dict[str, Any]

Compile scope metadata for retrieval filtering using one canonical projection path.

tenant_id (required) and workspace_id (required_nullable) use exact match, unchanged. conversation_id/sub_thread_id/private_to_account_id use hierarchical matching (see _HIERARCHICAL_RETRIEVAL_DIMENSIONS and _requesting_account_id). account_id is excluded from the filter entirely.

build_retrieval_scope_metadata_from_dimensions(*, tenant_id: int | str, workspace_id: int | str | None, account_id: int | str | None = None, conversation_id: int | str | None = None, sub_thread_id: int | str | None = None, private_to_account_id: int | str | None = None) -> dict[str, Any]

Build retrieval/storage metadata from raw dimensions via ScopeEnvelope.

tool_registry

ToolRegistry

Bases: Protocol

Provider of agent tools, resolved once per process/router lifetime.

Two recognized categories of implementation, by convention (not a separate code path — both go through the same list_tools(context=)):

  • Static (e.g. StaticToolRegistry, MCPToolRegistry): output never depends on the invoking request.
  • Scope-aware (e.g. msflib.knowledge's KnowledgeToolRegistry): tools that must enforce per-request authorization. These MUST NOT accept scope/tenant identity via __init__ — the registry itself is still built once. Instead, each tool reads the live per-invocation identity from the LangGraph runtime context (langgraph.runtime.get_runtime().context, populated per call via agent.invoke(..., context=...) — see ai_api's AgentRuntimeContext) inside its own function body, and fails closed if that context is absent. This is what lets one registry instance safely serve every request with the correct caller's scope, with no per-scope rebuild or cache.

vector_store_registry

VectorStoreRegistryService(settings: SettingsBase, vector_store_action: VectorStoreProfileAction | None = None, ai_settings: AICoreSettings | None = None)

ai_settings overrides the AI_CORE scope of settings.

Pass a tiered-resolved AICoreSettings so per-tenant/workspace/user VECTOR_STORE_* bindings take effect in the static-fallback path.

resolve_embeddings(session: Session, *, scope: ScopeEnvelope, config: dict[str, Any]) -> Any

Resolve embeddings paired with a vector store's index-time embedding space.

Precedence: an explicit embedding_profile_name pins a specific AIProviderProfile; explicit embedding_provider/embedding_model select a provider directly; otherwise falls back to the scope's default embeddings. This ordering keeps search-time embeddings consistent with whatever embedded the documents already in the store.

msflib.ai_core.tools

evaluate_arithmetic(expression: str) -> float

Evaluate a restricted arithmetic expression without calling eval.

basic

Simple, dependency-free tools: current_datetime and calculator.

Part of the zero-config toolkit under msflib.ai_core.tools — see tools/__init__.py. Grouped here because both are self-contained (no external packages, no network access, no scope/tenant concerns); tools with more going on (an optional dependency, SSRF guarding, scope-awareness) get their own module instead — see web_search.py, fetch_url.py, rag_search.py.

evaluate_arithmetic(expression: str) -> float

Evaluate a restricted arithmetic expression without calling eval.

fetch_url

fetch_url tool: fetch a public http(s) URL, SSRF-guarded.

Part of the zero-config toolkit under msflib.ai_core.tools — see tools/__init__.py. Needs the optional httpx dependency (web extra).

rag_search tool: semantic search over the configured vector store.

Part of the zero-config toolkit under msflib.ai_core.tools — see tools/__init__.py. Independent of msflib.knowledge's knowledge-graph tools: this is a plain similarity search over whatever vector store the host app has configured (the same one ai_api's /search endpoint uses via similarity_search_scoped).

Like msflib.knowledge's KnowledgeToolRegistry (see that module's docstring), this tool never accepts scope/tenant identity at construction: each call reads the live invocation's scope from the LangGraph runtime context (langgraph.runtime.get_runtime()), set per-request via agent.invoke(..., context=...), and fails closed if that context or its scope is absent.

The vector store itself is not re-resolved per call by default — it's bound once at construction time (see vector_store below), since a single static backend is the common case. A caller wanting per-request backend routing (e.g. ai_api's agent router, when different tenants/workspaces use different vector store backends) can additionally set a vector_store_resolver callable on that same runtime context; if present, it's called on every invocation and its return value used instead of the bound vector_store.

web_search tool: search the web via a pluggable backend.

Part of the zero-config toolkit under msflib.ai_core.tools — see tools/__init__.py. Two backends are supported:

  • "ddgs" (default) — ddgs, an unofficial multi-engine metasearch scraper. No API key needed, but no ToS guarantees or SLA either; needs the optional ddgs dependency (ddgs extra).
  • "tavily" — Tavily, a paid search API purpose-built for LLM agents. Needs an api_key and the optional tavily-python dependency (tavily extra).

backend is fixed at construction time, but a caller running under LangGraph (e.g. ai_api's agent router) can override it per invocation via AgentRuntimeContext.web_search_backend on the runtime context — see _runtime_web_search_backend below.

msflib.ai_core.tracking

msflib.ai_core.vector_store

ScopedVectorStore(raw_store: Any, spec: VectorStoreBackendSpec, *, collection_name: str, config: dict[str, Any] | None = None)

Backend-agnostic handle returned by vector_store.factory.get_vector_store.

close() -> None

Close the underlying client connections if supported by the backend.

VectorStoreBackendRegistry()

normalize(name: str) -> str

Validate name and return its canonical lowercase form.

Raises ValueError when the backend is unknown.

VectorStoreBackendSpec(name: str, builder: Callable[[dict[str, Any], str, Any], Any], capabilities: frozenset[str] = frozenset((METADATA_FILTERING, SERVER_SIDE)), required_connection_keys: frozenset[str] = frozenset(('url',)), filter_translator: Callable[[dict[str, Any]], Any] | None = None, delete_by_filter: Callable[[Any, dict[str, Any]], bool] | None = None, sanitize_collection_name: Callable[[str], str] | None = None, connectivity_check: Callable[[Any], None] | None = None, capabilities_for_config: Callable[[dict[str, Any]], frozenset[str]] | None = None, pip_package: str = '', extra_name: str = '') dataclass

Describes one vector store backend and how to build/operate it.

build_retrieval_filters(*, scope: ScopeEnvelope, document_type: str | None = None, source_id: str | None = None, source_metadata: dict[str, Any] | None = None) -> dict[str, Any]

Build backend-agnostic metadata filters from a canonical scope envelope.

Delegates to shared scope metadata projection used by ingestion storage, then merges document/source metadata.

get_vector_store(settings: AICoreSettings, collection_name: str, embeddings: Any, *, backend_config: dict[str, Any] | None = None) -> Any

Build a :class:~msflib.ai_core.vector_store.handle.ScopedVectorStore.

backend_config overrides the static settings.VECTOR_STORE_* fields -- pass it the dict produced by VectorStoreRegistryService.profile_to_backend_config to build a store from a scoped VectorStoreProfile instead of static settings.

similarity_search_scoped(vector_store: Any, *, query: str, scope: ScopeEnvelope, k: int = 4, document_type: str | None = None, source_id: str | None = None, source_metadata: dict[str, Any] | None = None) -> list[Any]

Run similarity search with mandatory tenant filtering and optional workspace filtering.

Backends must support filter=... kwargs to guarantee scoped retrieval. If unsupported, this function raises an explicit error.

Backend-specific filter translation (e.g. Qdrant's native filter tree, Pinecone/Chroma's sentinel-based null handling) happens inside ScopedVectorStore when vector_store is one; this function always passes the canonical backend-agnostic filter dict through unchanged.

register_vector_store_backend(spec: VectorStoreBackendSpec, aliases: tuple[str, ...] = ()) -> None

Register a custom vector store backend on the process-wide registry.

chroma

factory

get_vector_store(settings: AICoreSettings, collection_name: str, embeddings: Any, *, backend_config: dict[str, Any] | None = None) -> Any

Build a :class:~msflib.ai_core.vector_store.handle.ScopedVectorStore.

backend_config overrides the static settings.VECTOR_STORE_* fields -- pass it the dict produced by VectorStoreRegistryService.profile_to_backend_config to build a store from a scoped VectorStoreProfile instead of static settings.

build_retrieval_filters(*, scope: ScopeEnvelope, document_type: str | None = None, source_id: str | None = None, source_metadata: dict[str, Any] | None = None) -> dict[str, Any]

Build backend-agnostic metadata filters from a canonical scope envelope.

Delegates to shared scope metadata projection used by ingestion storage, then merges document/source metadata.

similarity_search_scoped(vector_store: Any, *, query: str, scope: ScopeEnvelope, k: int = 4, document_type: str | None = None, source_id: str | None = None, source_metadata: dict[str, Any] | None = None) -> list[Any]

Run similarity search with mandatory tenant filtering and optional workspace filtering.

Backends must support filter=... kwargs to guarantee scoped retrieval. If unsupported, this function raises an explicit error.

Backend-specific filter translation (e.g. Qdrant's native filter tree, Pinecone/Chroma's sentinel-based null handling) happens inside ScopedVectorStore when vector_store is one; this function always passes the canonical backend-agnostic filter dict through unchanged.

faiss

get_faiss_store(config: dict[str, Any], collection_name: str, embeddings: Any) -> Any

Build (or load) a local, single-process FAISS index.

Not a SERVER_SIDE backend: the index lives on local disk under persist_dir, saved via temp-dir + os.replace per file and a per-persist_dir lock (see _lock_for) after every mutation, so a concurrent load can't observe a half-swapped index.faiss/index.pkl pair. Unsuitable for multi-worker/multi-process deployments -- the registry marks this explicitly so /validate can warn about it.

make_faiss_filter(filters: dict[str, Any]) -> Callable[[dict[str, Any]], bool]

Return a predicate implementing $exists/$or/$eq/scalar filter semantics.

FAISS's similarity_search(filter=callable) accepts a predicate over each candidate's metadata dict, so this predicate doubles as the executable reference implementation of the canonical filter shapes used by the cross-backend conformance tests.

handle

ScopedVectorStore: the backend-agnostic handle returned by the factory.

Wraps a raw (usually LangChain) vector store instance together with the :class:~msflib.ai_core.vector_store.registry.VectorStoreBackendSpec that built it, so every backend satisfies the same duck-typed surface the rest of the codebase already relies on: similarity_search(query, *, k, filter) (ScopedSearchable), add_texts/add_documents, and delete(filter=...)/delete(where=...).

ScopedVectorStore(raw_store: Any, spec: VectorStoreBackendSpec, *, collection_name: str, config: dict[str, Any] | None = None)

Backend-agnostic handle returned by vector_store.factory.get_vector_store.

close() -> None

Close the underlying client connections if supported by the backend.

local_path

resolve_local_persist_dir(base_dir: str, requested: str, collection_name: str) -> str

Resolve a local-disk backend's persist directory, contained under base_dir.

requested (a VectorStoreProfile's persist_path/url) is controlled by whoever can create/edit that profile -- a workspace admin/owner, not necessarily a superuser. Without containment, a crafted absolute path or .. traversal could point a local backend (FAISS's load_local, Chroma's PersistentClient) at an arbitrary filesystem location, including one holding attacker-planted files (see FAISS's allow_dangerous_deserialization=True pickle load). Anything that would resolve outside base_dir is rejected rather than silently clamped, so a misconfiguration fails loudly instead of reading/writing to an unexpected location.

milvus

pgvector

make_pgvector_retriever(settings: Any, *, collection_name: str = 'documents', k: int = 4, search_type: str = 'similarity') -> Any

Build an unscoped PGVector retriever. Caller is responsible for tenant filtering.

pinecone

qdrant

registry

Vector store backend registry: single source of truth for backend types.

Mirrors msflib.ai_core.providers.registry.ProviderTypeRegistry so adding a new vector store backend (or a site-specific one) follows the same pattern as adding a new LLM/embedding provider: build a VectorStoreBackendSpec and register it, either in :func:create_default_registry (built-in backends) or via :func:register_vector_store_backend (site extensions).

VectorStoreBackendSpec(name: str, builder: Callable[[dict[str, Any], str, Any], Any], capabilities: frozenset[str] = frozenset((METADATA_FILTERING, SERVER_SIDE)), required_connection_keys: frozenset[str] = frozenset(('url',)), filter_translator: Callable[[dict[str, Any]], Any] | None = None, delete_by_filter: Callable[[Any, dict[str, Any]], bool] | None = None, sanitize_collection_name: Callable[[str], str] | None = None, connectivity_check: Callable[[Any], None] | None = None, capabilities_for_config: Callable[[dict[str, Any]], frozenset[str]] | None = None, pip_package: str = '', extra_name: str = '') dataclass

Describes one vector store backend and how to build/operate it.

VectorStoreBackendRegistry()

normalize(name: str) -> str

Validate name and return its canonical lowercase form.

Raises ValueError when the backend is unknown.

register_vector_store_backend(spec: VectorStoreBackendSpec, aliases: tuple[str, ...] = ()) -> None

Register a custom vector store backend on the process-wide registry.

weaviate

sanitize_weaviate_class_name(name: str) -> str

Weaviate collection names must start with an uppercase letter and contain only alphanumerics/underscores.