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_idfield), not a separate membership row: also passget_current_workspace— notget_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:
|
|
|
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_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.
msflib.ai_core.providers
¶
ProviderTypeRegistry()
¶
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_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
¶
providers
¶
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. |
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'sKnowledgeToolRegistry): 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 viaagent.invoke(..., context=...)— see ai_api'sAgentRuntimeContext) 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
¶
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
¶
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 optionalddgsdependency (ddgsextra)."tavily"— Tavily, a paid search API purpose-built for LLM agents. Needs anapi_keyand the optionaltavily-pythondependency (tavilyextra).
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=...).
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.