# MaMAI v4.0.0

MaMAI is a stateless Python RAG library.

It is designed for service-oriented systems where the caller owns state and storage decisions.

## Core principles
- No internal `db.json` or hidden persistence.
- No implicit global runtime configuration.
- Every operation receives a full `ExecutionContext`.
- Vector store backend and storage location are selected at runtime by the caller.

## Installation

```bash
python3 -m venv .venv
. .venv/bin/activate
pip install -U pip
pip install -e .
```

## Quick start

```python
from Mama import (
    ExecutionContext,
    LlmConfig,
    RetrievalConfig,
    StorageConfig,
    default_runtime,
    ingest_pdf_directory,
    query,
)

ctx = ExecutionContext(
    tenant_id="acme",
    user_id="user-1",
    session_id="session-1",
    kb_id="kb-support",
    llm=LlmConfig(provider="dummy", model="dummy"),
    retrieval=RetrievalConfig(search_type="similarity", k=3),
    storage=StorageConfig(
        backend="faiss_local",
        params={
            "base_path": "./data/vectorstores",
            "embeddings_backend": "simple",
        },
    ),
)

runtime = default_runtime()
ingest_pdf_directory(ctx, "./docs_to_index", runtime)
result = query(
    ctx,
    "What is this document about?",
    runtime,
    prompt=(
        "You are the KB assistant. Follow global KB rules.\n"
        "Apply instructions from the active knowledge branch."
    ),
)

print(result.answer)
print(result.documents)

# Caller-owned state persistence
ctx.conversation = result.updated_conversation_state
```

## ExecutionContext

`ExecutionContext` is the contract between your wrapper/service and MaMAI core.

Required identity fields:
- `tenant_id`
- `user_id`
- `session_id`
- `kb_id`

Config sections:
- `llm`: provider, model, parameters
- `retrieval`: search type and `k`
- `storage`: backend + dynamic backend params
- `conversation`: in-memory message state passed in/out

Per-query prompt input (required by `query(...)`):
- `prompt`: fully resolved prompt string for the current query

You can build it programmatically or from JSON payload with:

```python
from Mama.config import build_execution_context
ctx = build_execution_context(payload)
```

## Identity Model (`tenant_id`, `user_id`, `session_id`, `kb_id`)

These 4 fields are not interchangeable. They model different scopes.

### `tenant_id` (organization boundary)
- Purpose: hard isolation boundary between customers/workspaces.
- Typical value: company/workspace slug or UUID (`acme`, `tenant-42`).
- Effect in MaMAI:
  - used by storage naming for `faiss_local` (`<base_path>/<tenant_id>/<kb_id>`)
  - should also be propagated to metadata and external stores.
- Rule: never mix data from different tenants in one index.

### `user_id` (end-user identity)
- Purpose: actor identity (who is asking / who ingested).
- Typical value: authenticated user id (`user-123`, email hash, UUID).
- Effect in MaMAI:
  - not used for index partitioning by default
  - available for metadata, auditing, policy decisions in wrappers.
- Rule: treat as identity/audit field, not as storage key unless your design needs per-user KBs.

### `session_id` (conversation instance)
- Purpose: one chat/conversation thread.
- Typical value: request/session UUID (`sess-2026-...`).
- Effect in MaMAI:
  - identifies the conversation memory passed in/out via `ctx.conversation`
  - does not create separate vector indexes by default.
- Rule: rotate `session_id` per new chat thread; persist chat state in your own store.

### `kb_id` (knowledge base identity)
- Purpose: logical knowledge corpus id.
- Typical value: domain/corpus key (`kb-support`, `kb-legal-v2`).
- Effect in MaMAI:
  - primary key for vector index selection together with `tenant_id`
  - same `kb_id` means same index namespace (inside tenant).
- Rule: change `kb_id` when you want a different corpus/index.

### Practical composition

Think in scopes:
- Tenant scope: `tenant_id`
- Corpus scope: `kb_id`
- User scope: `user_id`
- Conversation scope: `session_id`

For `faiss_local`, effective index path is:
- `<base_path>/<tenant_id>/<kb_id>`

So:
- changing `session_id` does **not** change index
- changing `user_id` does **not** change index (unless you encode it in `kb_id`)
- changing `kb_id` **does** change index
- changing `tenant_id` **does** change isolation boundary and index location

### Recommended patterns

Shared team KB + per-user chats:
- same `tenant_id` + `kb_id`
- different `user_id`/`session_id`

Versioned KB rollout:
- `kb_id=kb-support-v1`, then `kb-support-v2`
- run A/B or migration safely

Per-user private KB:
- set `kb_id` like `kb-user-<user_id>` (or explicit mapping in wrapper)

### Anti-patterns to avoid

- Using `session_id` as `kb_id` accidentally (creates fragmented indexes).
- Reusing one global `tenant_id` for all customers (breaks isolation).
- Expecting `user_id` alone to separate vector stores (it does not by default).
- Keeping `kb_id` constant across unrelated corpora.

## Caller-defined vector store storage

MaMAI does not decide where your index is stored. The caller does.

### `faiss_local`
Persistent FAISS index on caller-provided filesystem location.

Supported params:
- `base_path`: root directory; final path is `<base_path>/<tenant_id>/<kb_id>`
- `index_path`: full explicit path (overrides `base_path` logic)
- `embeddings_backend`: `simple`, `hf`, or `auto`

Example with explicit per-request storage path:

```python
ctx.storage = StorageConfig(
    backend="faiss_local",
    params={
        "index_path": "/mnt/customer-a/rag/kb-support-v1",
        "embeddings_backend": "simple",
    },
)
```

### `faiss_memory`
In-memory FAISS store (non-persistent), useful for ephemeral jobs and tests.

```python
ctx.storage = StorageConfig(
    backend="faiss_memory",
    params={"embeddings_backend": "simple"},
)
```

## Register your own vector store backend

You can plug a custom backend (Qdrant, Pinecone, Weaviate, Milvus, custom S3 adapter).

```python
from Mama.ports import RuntimePorts
from Mama.runtime import DefaultEmbeddingsProvider, DefaultLlmProvider
from Mama.vectorstores import DynamicVectorStoreFactory

factory = DynamicVectorStoreFactory()

# builder signature: (ctx, embeddings) -> VectorStorePort
factory.register_backend("qdrant", my_qdrant_builder)

runtime = RuntimePorts(
    llm_provider=DefaultLlmProvider(),
    embeddings_provider=DefaultEmbeddingsProvider(),
    vector_store_factory=factory,
)

ctx.storage.backend = "qdrant"
ctx.storage.params = {
    "url": "https://qdrant.example.com",
    "api_key": "...",
    "collection": "kb-support",
    "tenant": ctx.tenant_id,
}
```

## State ownership model

MaMAI core is stateless by design:
- it reads conversation state from `ctx.conversation`
- it returns updated state in `QueryResult.updated_conversation_state`
- it does not persist sessions/history internally

Typical wrapper responsibilities:
- load context from request + tenant config
- call MaMAI
- persist updated conversation state in your DB/cache
- enforce auth, rate limits, and policy

## Main modules

- `Mama/models.py`: data contracts (`ExecutionContext`, `QueryResult`, `IngestResult`)
- `Mama/ports.py`: extension contracts for runtime providers
- `Mama/runtime.py`: default runtime providers
- `Mama/vectorstores.py`: dynamic vector store factory and FAISS adapters
- `Mama/ingestion.py`: stateless ingestion use cases
- `Mama/query.py`: stateless RAG query pipeline
- `Mama/admin.py`: document listing and vector store stats
- `Mama/webscraper.py`: URL crawl/load/ingest helpers

## Testing

Run smoke test:

```bash
make test
```

Run web demo:

```bash
make web
```

Health endpoint:

```bash
curl http://127.0.0.1:5000/health
```

## Notes

The sample tests generate a minimal PDF file and may show `pypdf` parsing warnings. If the test ends with `TEST OK`, the pipeline worked as expected.
