Metadata-Version: 2.2
Name: gllm-memory-binary
Version: 0.2.2
Summary: A flexible memory system for Gen AI applications
Author-email: Budi Kurniawan <budi.kurniawan1@gdplabs.id>
Requires-Python: <3.14,>=3.11
Description-Content-Type: text/markdown
Requires-Dist: filetype<2.0.0,>=1.2.0
Requires-Dist: gllm-core-binary>=0.4.0
Requires-Dist: gllm-datastore-binary<0.6.0,>=0.5.0
Requires-Dist: gllm-inference-binary<0.7.0,>=0.6.0
Requires-Dist: httpx<1.0.0,>=0.28.0
Requires-Dist: langchain-community<2.0.0,>=0.4.0
Requires-Dist: langchain-core<2.0.0,>=1.0.0
Requires-Dist: pydantic<3.0.0,>=2.11.4
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
Requires-Dist: python-json-logger<5.0.0,>=3.3.0
Requires-Dist: python-magic<0.5.0,>=0.4.27; sys_platform != "win32"
Requires-Dist: python-magic-bin<0.5.0,>=0.4.14; sys_platform == "win32"
Requires-Dist: pyyaml<7.0.0,>=6.0
Requires-Dist: scipy<2.0.0,>=1.15.1
Requires-Dist: typing-extensions<5.0.0,>=4.8.0
Requires-Dist: cryptography>=46.0.5
Provides-Extra: dev
Requires-Dist: coverage<8.0.0,>=7.4.4; extra == "dev"
Requires-Dist: mypy<2.0.0,>=1.15.0; extra == "dev"
Requires-Dist: pre-commit<5.0.0,>=3.7.0; extra == "dev"
Requires-Dist: pytest<10.0.0,>=9.0.0; extra == "dev"
Requires-Dist: pytest-asyncio<2.0.0,>=1.3.0; extra == "dev"
Requires-Dist: pytest-cov<8.0.0,>=6.0.0; extra == "dev"
Requires-Dist: ruff<1.0.0,>=0.6.7; extra == "dev"
Provides-Extra: mem0ai
Requires-Dist: mem0ai<3.0.0,>=0.1.117; extra == "mem0ai"
Requires-Dist: spacy<4.0.0,>=3.7.0; extra == "mem0ai"
Provides-Extra: openai
Requires-Dist: gllm-inference-binary[openai]<0.7.0,>=0.6.0; extra == "openai"
Provides-Extra: kg
Requires-Dist: gllm-datastore-binary[kg]<0.6.0,>=0.5.0; extra == "kg"
Requires-Dist: gllm-misc-binary<0.10.0,>=0.9.0; extra == "kg"
Provides-Extra: vector-stores
Requires-Dist: elasticsearch<10.0.0,>=9.0.0; extra == "vector-stores"

# GLLM Memory

## Description

Memory layer for AI agents. The public API is `MemoryManager`. You can use it in two ways:

1. **HTTP mode**: use `api_key` and optional `host`
2. **SDK mode**: use `MemoryManagerConfig` and pass `config=...`

In SDK mode, you can register your own LLM, embedding model, memory store, and optional reranker without exposing backend-specific config to application code.

## Prerequisites

### Mandatory

1. **Python 3.11+** — [Install here](https://www.python.org/downloads/)
2. **pip** — [Install here](https://pip.pypa.io/en/stable/installation/)
3. **uv** — [Install here](https://docs.astral.sh/uv/getting-started/installation/)
4. **gcloud CLI** (for authentication) — [Install here](https://cloud.google.com/sdk/docs/install), then log in using:
   ```bash
   gcloud auth login
   ```

### Mem0 Configuration

- **Mem0 API key** (HTTP client): from [Mem0 dashboard](https://app.mem0.ai/dashboard/api-keys).
- **Self-hosted URL**: set `MEM0_HOST` if the API is not Mem0 cloud.

**Environment variables** (typical):

| Variable          | Role                                                                                      |
|-------------------|-------------------------------------------------------------------------------------------|
| `MEM0_API_KEY`    | Required for the HTTP client when not passed in code.                                     |
| `MEM0_HOST`       | Optional; base URL for self-hosted Mem0 API.                                              |
| `MEMORY_PROVIDER` | Optional; default is Mem0 (`mem0`).                                                       |
| `TIMEOUT_SEC`     | Optional; request timeout in seconds (default `30`). Used when building clients from env. |

**Two ways to connect**

1. **HTTP API** — pass `api_key` and optionally `host` to `MemoryManager`. Same as setting `MEM0_API_KEY` / `MEM0_HOST` and using defaults.
2. **SDK mode** — pass `config=MemoryManagerConfig(...)` to `MemoryManager`. This path uses the local SDK integration and lets you register LLM, embedding, memory store, and reranker through the builder API. See `examples/example_mem0_sdk_client.py`.

Do not commit secrets to git.

---

## 📦 Installation

### Install from Artifact Registry

This requires authentication via the `gcloud` CLI.

```bash
uv pip install \
  --extra-index-url "https://oauth2accesstoken:$(gcloud auth print-access-token)@glsdk.gdplabs.id/gen-ai-internal/simple/" \
  gllm-memory
```

---

## 🔧 Local Development Setup

### Prerequisites

1. **Python 3.11+** — [Install here](https://www.python.org/downloads/)
2. **pip** — [Install here](https://pip.pypa.io/en/stable/installation/)
3. **uv** — [Install here](https://docs.astral.sh/uv/getting-started/installation/)
4. **gcloud CLI** — [Install here](https://cloud.google.com/sdk/docs/install), then log in using:
   ```bash
   gcloud auth login
   ```
5. **Git** — [Install here](https://git-scm.com/downloads)
6. **Access** to the [GDP Labs SDK GitHub repository](https://github.com/GDP-ADMIN/gl-sdk)

---

### 1. Clone Repository

```bash
git clone git@github.com:GDP-ADMIN/gl-sdk.git
cd gl-sdk/libs/gllm-memory
```

---

### 2. Setup Authentication

Set the following environment variables to authenticate with internal package indexes:

```bash
export UV_INDEX_GEN_AI_INTERNAL_USERNAME=oauth2accesstoken
export UV_INDEX_GEN_AI_INTERNAL_PASSWORD="$(gcloud auth print-access-token)"
export UV_INDEX_GEN_AI_USERNAME=oauth2accesstoken
export UV_INDEX_GEN_AI_PASSWORD="$(gcloud auth print-access-token)"
```

---

### 3. Quick Setup

Run:

```bash
make setup
```

---

### 4. Activate Virtual Environment

```bash
source .venv/bin/activate
```

---

## 🚀 Quick Start

### For Using the Library

1. **Install the package**:
   ```bash
   uv pip install gllm-memory
   ```

2. **Set your Mem0 API key**:
   ```bash
   export MEM0_API_KEY="your_api_key_here"
   ```

3. **For Self-Hosted Mem0** (Optional):
   ```bash
   export MEM0_API_KEY="your_api_key_here"
   export MEM0_HOST="https://your-mem0-server.com"
   ```

### For Development

1. **Complete setup** (this will install all dependencies, setup pre-commit, and activate the environment):
   ```bash
   make setup
   source .venv/bin/activate
   ```

2. **Set your Mem0 API key**:
   ```bash
   export MEM0_API_KEY="your_api_key_here"
   ```

3. **Run an example**:
   ```bash
   # HTTP API (add, search, list, delete_by_user_query, delete)
   python examples/simple_usage.py
   # SDK mode with MemoryManagerConfig
   python examples/example_mem0_sdk_client.py
   ```

## Architecture

The system follows a layered architecture below:

```
┌──────────────────────────────────────────────────────────────┐
│                    Application Layer                         │
├──────────────────────────────────────────────────────────────┤
│                    Memory Manager                            │
├──────────────────────────────────────────────────────────────┤
│                    Memory Client (Base)                      │
├──────────────────────────────────────────────────────────────┤
│                    Provider Layer (Mem0)                     │
├──────────────────────────────────────────────────────────────┤
│                    Mem0 Platform (HTTP client or Python SDK) │
└──────────────────────────────────────────────────────────────┘
```

## 🌐 HTTP Mode

Use this mode if you want to connect to the HTTP API directly. Point the client at your own server:

```python
from gllm_memory import MemoryManager

manager = MemoryManager(
    api_key="your-api-key",
    host="https://your-mem0-server.com",
)
```

If you want local SDK mode, use `MemoryManager(config=...)` instead of `api_key` and `host`.

## 🧩 SDK Mode With `MemoryManagerConfig`

Use this mode if you want to:

1. register your own `LMComponent` for memory LLM execution
2. register your own EM Invoker
3. choose the memory store from config
4. configure an optional reranker
5. keep application code independent from backend-specific config shape

For the memory LLM, there are two registration paths:

1. **Preferred**: `llm.register_component(lm_component, model=...)`
   Use this when your application already owns the `LMComponent`, prompt setup, and optional `fallback_lms`.
2. **Compatibility**: `llm.register(lm_invoker, model=...)`
   Use this when you want to pass a direct LM Invoker without a caller-built component.

`gllm-memory` does not create provider-specific runtime objects for you in normal SDK usage. Your application creates the `LMComponent` or LM Invoker, then registers it in `MemoryManagerConfig`.

### SDK Mode Example

Recommended LLM registration:

```python
config = (
    MemoryManagerConfig.builder()
    .memory_store.elasticsearch(
        host="localhost",
        port=9200,
        collection_name="memories",
        embedding_model_dims=1536,
    )
    .embedding.register(
        em_invoker,
        model="text-embedding-3-small",
        embedding_dims=1536,
    )
    .llm.register_component(lm_component, model="gpt-5-nano")
    .build()
)
```

In this path, `lm_component` is created by your application. It may use one primary LM Invoker and optional fallback LMs internally.

Compatibility example with a direct LM Invoker:

```python
from gllm_memory import MemoryManager, MemoryManagerConfig
from gllm_inference.lm_invoker.openai_lm_invoker import OpenAILMInvoker

lm_invoker = OpenAILMInvoker(
    model_name="gpt-5-nano",
    api_key="your_openai_api_key",
)


def build_em_invoker():
    from gllm_inference.em_invoker.openai_em_invoker import OpenAIEMInvoker

    return OpenAIEMInvoker(
        model_name="text-embedding-3-small",
        api_key="your_openai_api_key",
    )


em_invoker = build_em_invoker()

config = (
    MemoryManagerConfig.builder()
    .memory_store.elasticsearch(
        host="localhost",
        port=9200,
        collection_name="memories",
        embedding_model_dims=1536,
    )
    .embedding.register(
        em_invoker,
        model="text-embedding-3-small",
        embedding_dims=1536,
    )
    .llm.register(lm_invoker, model="gpt-5-nano")
    .reranker.llm_reranker(
        model="gpt-5-nano",
        api_key="your_openai_api_key",
        top_k=5,
    )
    .build()
)

memory_manager = MemoryManager(config=config)
```

`gllm-memory` does not require a provider-specific helper import for this step. You only need to pass either:

1. an `LMComponent` instance for the preferred path, or
2. an LM Invoker instance for the compatibility path

The reranker is optional; when configured with `llm_reranker`, the builder emits the Mem0-compatible reranker section for SDK search calls that use `rerank=True`. If your installed `gllm_inference` version still has a circular import on `OpenAIEMInvoker`, instantiate the EM invoker with a local lazy import like the example above.

### SDK Mode With Default Config

If you want to use the default SDK setup, you can build an empty config:

```python
from gllm_memory import MemoryManager, MemoryManagerConfig

config = MemoryManagerConfig.builder().build()
memory_manager = MemoryManager(config=config)
```

Default SDK behavior:

1. memory store uses Elasticsearch
2. embedding uses `gllm-inference: EM Invoker` with OpenAI defaults
3. llm uses `gllm-inference: LM Invoker` with OpenAI defaults
4. reranker is omitted unless configured explicitly

Required environment variables for the default SDK config:

1. `ELASTICSEARCH_HOST`
2. `ELASTICSEARCH_PORT`
3. `ELASTICSEARCH_COLLECTION_NAME`
4. `ELASTICSEARCH_EMBEDDING_MODEL_DIMS`
5. `OPENAI_API_KEY`

Optional environment variables:

1. `ELASTICSEARCH_USER`
2. `ELASTICSEARCH_PASSWORD`
3. `OPENAI_BASE_URL`
4. `OPENAI_MODEL_NAME` (default SDK LLM model override)
5. `OPENAI_EMBEDDING_MODEL` (used by `examples/example_mem0_sdk_client.py`)

### SDK Mode With Another Memory Store

You can register another memory store with the same builder style:

```python
config = (
    MemoryManagerConfig.builder()
    .memory_store.register(
        "pgvector",
        {
            "host": "localhost",
            "port": 5432,
            "dbname": "postgres",
            "user": "postgres",
            "password": "postgres",
            "collection_name": "memories",
        },
    )
    .embedding.register(em_invoker, embedding_dims=1536)
    .llm.register(lm_invoker)
    .build()
)
```

Notes:

1. `memory_store` is the public config name
2. you do not need to know the backend-native config structure for the built-in builder helpers
3. non-Elasticsearch stores use the backend's native behavior unless `gllm-memory` adds custom handling for them

## 🕸️ Knowledge Graph in GLLM Memory

`gllm-memory` can optionally use a Knowledge Graph (KG). This is useful when you want two kinds of memory at the same time:

1. **normal memory search**
2. **graph-based facts** such as people, places, companies, and relationships

Simple example:

- text memory:
  - `"Larry Page co-founded Google."`
- graph memory:
  - `Larry Page -[CO_FOUNDED]-> Google`

### How it works

When `use_knowledge_graph=True`:

1. `add(...)` stores the memory in `gllm-memory`
2. the same messages are also sent to a text-to-graph extractor
3. extracted nodes and relationships are saved to the graph store
4. `update(...)` keeps `mem0` as the source of truth, then refreshes the related KG contribution
5. `delete(...)` and `delete_by_user_query(...)` remove matched memory rows, then clean up the related KG contribution
6. `search(...)` returns a hybrid result:
   - normal memory chunks
   - graph-based chunks from KG traversal

This means you can keep the normal memory experience, while also getting graph-style knowledge.

### KG storage isolation

Current KG storage uses an isolation boundary so graph data from `gllm-memory` is easier to
separate from other applications that may share the same Neo4j instance.

In practice:

1. KG nodes keep their semantic labels such as `Person`, `Company`, or `Topic`
2. every KG node and relationship is stored with `graph_namespace="memory"`
3. graph node storage IDs use a `memory:` prefix for internal persistence, while human-readable
   entity values are preserved in `name`, for example:
   - `memory:9f12ab34:person:john`
   - `memory:9f12ab34:company:google`

### Simple flow

**Add flow**

```text
┌──────────────────┐
│ application      │
│ calls add(...)   │
└──────────────────┘
          ↓
┌──────────────────────────────────────┐
│ gllm-memory                          │
│ starts 2 add paths                   │
└──────────────────────────────────────┘
          ↓                     ↓
┌──────────────────┐    ┌──────────────────┐
│ memory provider  │    │ text-to-graph    │
│ stores memory    │    │ extraction       │
└──────────────────┘    └──────────────────┘
                                ↓
                        ┌──────────────────┐
                        │ graph store      │
                        │ saves nodes and  │
                        │ relationships    │
                        └──────────────────┘
```

**Retrieve flow**

```text
┌──────────────────────┐
│ application          │
│ calls search(...)    │
└──────────────────────┘
            ↓
┌──────────────────────────────────────────────────────────┐
│ gllm-memory                                              │
│ starts 2 retrieve paths                                  │
└──────────────────────────────────────────────────────────┘
            ↓
┌──────────────────────┐              ┌──────────────────────┐
│ memory provider      │              │ text-to-graph        │
│ searches memory      │              │ finds graph seeds    │
└──────────────────────┘              └──────────────────────┘
            │                                       ↓
            │                            ┌──────────────────────┐
            │                            │ graph store          │
            │                            │ returns related      │
            │                            │ facts                │
            │                            └──────────────────────┘
            │                                       │
            └───────────────┐         ┌─────────────┘
                            ↓         ↓
┌──────────────────────────────────────────────────────────┐
│ gllm-memory                                              │
│ merges memory result and KG result into final chunks     │
└──────────────────────────────────────────────────────────┘
```

**Update flow**

```text
┌──────────────────────┐
│ application          │
│ calls update(...)    │
└──────────────────────┘
            ↓
┌──────────────────────────────────────────────────────────┐
│ gllm-memory                                              │
│ updates mem0 first                                       │
└──────────────────────────────────────────────────────────┘
            ↓
┌──────────────────────┐
│ memory provider      │
│ updates 1 memory row │
└──────────────────────┘
            ↓
┌──────────────────────────────────────────────────────────┐
│ gllm-memory                                              │
│ rebuilds KG contribution from the updated memory row     │
└──────────────────────────────────────────────────────────┘
            ↓
┌──────────────────────┐    ┌──────────────────────┐
│ text-to-graph        │    │ graph store          │
│ extracts new facts   │ -> │ replaces old facts   │
└──────────────────────┘    └──────────────────────┘
```

**Delete flow**

```text
┌──────────────────────────────┐
│ application                  │
│ calls delete(...)            │
│ or delete_by_user_query(...) │
└──────────────────────────────┘
               ↓
┌──────────────────────────────────────────────────────────┐
│ gllm-memory                                              │
│ resolves the target memory rows                          │
└──────────────────────────────────────────────────────────┘
               ↓
┌──────────────────────┐
│ memory provider      │
│ deletes matched rows │
└──────────────────────┘
               ↓
┌──────────────────────────────────────────────────────────┐
│ gllm-memory                                              │
│ cleans up KG contribution for deleted memory rows        │
└──────────────────────────────────────────────────────────┘
               ↓
┌──────────────────────┐
│ graph store          │
│ removes orphan facts │
│ when no owner remains│
└──────────────────────┘
```

### KG update behavior

When `use_knowledge_graph=True`, `update(...)` works like this:

1. `mem0` is updated first
2. the updated memory row becomes the new source for KG extraction
3. KG performs a logical replace of the old contribution for that memory row
4. new nodes and relationships are written with graph upsert operations
5. shared graph facts stay safe because KG tracks ownership across memory rows

Notes:

1. KG update is contribution-based, not a direct patch on raw nodes or edges
2. one memory row maps to one KG contribution
3. repeated example runs can still create multiple `mem0` rows if the example keeps calling `add(...)`

### KG delete behavior

When `use_knowledge_graph=True`, delete works like this:

1. target memory rows are resolved first
2. `mem0` deletes the matched memory rows
3. KG removes the contribution owned by each deleted memory row
4. shared graph facts stay safe because KG tracks ownership across memory rows
5. orphan nodes and relationships are removed when no owner remains

Notes:

1. `delete(...)` is useful when you already know the target memory IDs
2. `delete_by_user_query(...)` resolves targets from memory search, then deletes those memory rows and their KG contribution
3. KG delete is contribution-based, not a raw `DETACH DELETE` of every matched graph node

### How to enable it

The public API is still the same:

```python
memory_manager = MemoryManager(
    config=config,
    use_knowledge_graph=True,
)
```

You can configure KG in two ways:

1. **recommended**: use `MemoryManagerConfig`
2. **fallback**: use environment variables

### Recommended setup

The recommended setup is to create a separate LM invoker for KG and register it explicitly.

This gives you a clean setup because:

1. the memory LLM and KG LLM can be different
2. the config stays clear and easy to manage
3. you can change each part independently later

Example:

```python
from gllm_inference.lm_invoker.openai_lm_invoker import OpenAILMInvoker
from gllm_memory import MemoryManager, MemoryManagerConfig

memory_lm_invoker = OpenAILMInvoker(
    model_name="gpt-5-nano",
    api_key="your_openai_api_key",
)

kg_lm_invoker = OpenAILMInvoker(
    model_name="gpt-4o-mini",
    api_key="your_openai_api_key",
)

config = (
    MemoryManagerConfig.builder()
    .memory_store.elasticsearch(
        host="localhost",
        port=9200,
        collection_name="memories",
        embedding_model_dims=1536,
    )
    .embedding.openai(model="text-embedding-3-small", api_key="your_openai_api_key")
    .llm.register(memory_lm_invoker, model="gpt-5-nano")
    .knowledge_graph.text_to_graph.register(
        kg_lm_invoker,
        strict_mode=True,
        use_structured_output=False,
    )
    .knowledge_graph.graph_data_store.neo4j(
        uri="bolt://localhost:7687",
        user="neo4j",
        password="password",
    )
    .build()
)

memory_manager = MemoryManager(config=config, use_knowledge_graph=True)
```

For a complete `add -> search -> update -> delete` example, see:

```text
examples/example_memory_manager_with_knowledge_graph.py
```

### Environment-based setup

If you do not pass `MemoryManagerConfig`, KG can also read its settings from environment variables.

Common required env vars:

1. `KNOWLEDGE_GRAPH_LLM_MODEL_ID`
2. `OPENAI_API_KEY`
3. `NEO4J_URI`
4. `NEO4J_USER`
5. `NEO4J_PASSWORD`

### When to use it

Use KG when you want the memory system to understand connections between things.

Examples:

1. who founded a company
2. where a company is located
3. what relationship exists between two entities

In short:

- if you only need normal memory, use `MemoryManager` as usual
- if you also want graph-based knowledge, enable `use_knowledge_graph=True`

## Core API methods

`MemoryManager` exposes async methods; `query` is required where noted.

### Methods

- **`add(user_id, agent_id, messages, scopes, metadata, infer, is_important)`** - Add new memories from message objects
- **`search(query, user_id, agent_id, scopes, metadata, threshold, top_k, include_important, rerank)`** - Search and
  retrieve memories by query (query is required)
- **`list_memories(user_id, agent_id, scopes, metadata, keywords, page, page_size)`** - Get all memories with pagination
  and keywords filtering
- **`update(memory_id, new_content, metadata, user_id, agent_id, scopes, is_important)`** - Update an existing memory by
  ID
- **`delete(memory_ids, user_id, agent_id, scopes, metadata)`** - Delete memories by IDs or by user/agent identifiers.
  When KG is enabled, the related KG contribution is also cleaned up for deleted rows.
- **`delete_by_user_query(query, user_id, agent_id, scopes, metadata, threshold, top_k)`** - Delete memories by query (
  query is required). When KG is enabled, the related KG contribution is also cleaned up for deleted rows.

### Example (HTTP API)

```python
from gllm_memory import MemoryManager
from gllm_inference.schema.message import Message
from gllm_memory.enums import MemoryScope

memory_manager = MemoryManager(api_key="...", host="...")  # host optional

messages = [
    Message.user("I love pizza"),
    Message.assistant("Noted."),
]
await memory_manager.add(
    user_id="user_123",
    agent_id="agent_456",
    messages=messages,
    scopes={MemoryScope.USER},
    metadata={"conversation_id": "chat_001"},  # Optional
    infer=True,  # Optional, defaults to True
    is_important=False,  # Optional, defaults to False
)

memories = await memory_manager.search(
    query="What does the user like?",
    user_id="user_123",
    scopes={MemoryScope.USER},
    metadata=None,  # Optional
    threshold=0.3,  # Optional, defaults to 0.3
    top_k=10,  # Optional, defaults to 10
    include_important=False,  # Optional, defaults to False
    rerank=False,  # Optional, defaults to False; if True, applies re-ranking to results
)

await memory_manager.list_memories(
    user_id="user_123",
    scopes={MemoryScope.USER},
    metadata=None,  # Optional
    keywords="food",  # Optional
    page=1,  # Optional, defaults to 1
    page_size=100  # Optional, defaults to 100
)

await memory_manager.update(
    memory_id="memory_uuid_123",
    new_content="Updated text",
    user_id="user_123",
    agent_id="agent_456",
    scopes={MemoryScope.USER, MemoryScope.ASSISTANT},  # Optional
    is_important=None,  # Optional; None leaves existing flag unchanged
)

await memory_manager.delete_by_user_query(
    query="food preferences",
    user_id="user_123",
    scopes={MemoryScope.USER, MemoryScope.ASSISTANT},
    metadata=None,  # Optional
    threshold=0.3,  # Optional, defaults to 0.3
    top_k=10  # Optional, defaults to 10
)

# Delete memories by identifiers
delete_result = await memory_manager.delete(
    memory_ids=None,  # Optional
    user_id="user_123",
    scopes={MemoryScope.USER, MemoryScope.ASSISTANT},
    metadata=None  # Optional
)
# Then use await manager.add(...), search(...), etc.
```

### Example (SDK Mode)

```python
from gllm_memory import MemoryManager, MemoryManagerConfig
from gllm_memory.enums import MemoryScope
from gllm_inference.lm_invoker.openai_lm_invoker import OpenAILMInvoker
from gllm_inference.schema.message import Message

lm_invoker = OpenAILMInvoker(model_name="gpt-5-nano", api_key="...")


def build_em_invoker():
    from gllm_inference.em_invoker.openai_em_invoker import OpenAIEMInvoker

    return OpenAIEMInvoker(model_name="text-embedding-3-small", api_key="...")


em_invoker = build_em_invoker()

config = (
    MemoryManagerConfig.builder()
    .memory_store.elasticsearch(
        host="localhost",
        port=9200,
        collection_name="memories",
        embedding_model_dims=1536,
    )
    .embedding.register(em_invoker, embedding_dims=1536)
    .llm.register(lm_invoker)
    .reranker.llm_reranker(model="gpt-5-nano", api_key="...", top_k=5)
    .build()
)

memory_manager = MemoryManager(config=config)

messages = [
    Message.user("I love pizza"),
    Message.assistant("Noted."),
]

await memory_manager.add(
    user_id="user_123",
    agent_id="agent_456",
    messages=messages,
    scopes={MemoryScope.USER},
)
```

### 🔧 Code Quality

```bash
# Format code with ruff
ruff format gllm_memory/ tests/

# Check code quality
ruff check gllm_memory/ tests/

# Fix auto-fixable issues
ruff check gllm_memory/ tests/ --fix
```

---

## Local Development Utilities

The following Makefile commands are available for quick operations:

### Install uv

```bash
make install-uv
```

### Install Pre-Commit

```bash
make install-pre-commit
```

### Install Dependencies

```bash
make install
```

### Update Dependencies

```bash
make update
```

### Run Tests

```bash
make test
```

---

## Contributing

Please refer to the [Python Style Guide](https://docs.google.com/document/d/1uRggCrHnVfDPBnG641FyQBwUwLoFw0kTzNqRm92vUwM/edit?usp=sharing) for information about code style, documentation standards, and SCA requirements.

### Contributing Steps

1. **Fork and clone** the repository
2. **Set up development environment**:
   ```bash
   # Complete setup: installs uv, configures auth, installs packages, sets up pre-commit
   make setup
   ```

3. **Activate virtual environment**:
   ```bash
   source .venv/bin/activate
   ```

4. **Run tests** to ensure everything works:
   ```bash
   make test
   ```

5. **Make your changes** and ensure tests pass:
   ```bash
   # Make your changes
   # Ensure tests pass
   make test
   ```

6. **Submit a pull request**:
   ```bash
   # Submit a pull request
   git push origin your-branch
   ```
