Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions examples/memories/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
import sys
import logging
from datetime import datetime

from gradient_labs import (
Client,
CustomerMemory,
)

logging.basicConfig(stream=sys.stdout, level=logging.INFO)

client = Client(
api_key=os.environ["GLABS_API_KEY"],
base_url=os.environ.get("GRADIENT_LABS_BASE_URL", "http://localhost:4000"),
)

# Each memory is scoped to a customer and stores an arbitrary JSON object in
# `data`, kept verbatim for the AI agent to search over on demand. The call is
# asynchronous ("fire and forget") and returns nothing; it raises a 409 Conflict
# if a batch is already being created for the same customer.
client.batch_create_customer_memories(
customer_id="cust_01ham6bzcdeja9xzqhjf6daq30",
memories=[
CustomerMemory(
external_id="order_456",
custom_type="order",
created_at=datetime(2026, 1, 2, 10, 0, 0),
data={"order_id": "order-456", "total": 42.0},
),
CustomerMemory(
external_id="pref_email",
custom_type="preference",
created_at=datetime(2026, 1, 3, 9, 30, 0),
data={"channel": "email"},
),
],
)
logging.info("✅ Customer memories batch accepted")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "gradient-labs"
version = "0.12.1"
version = "0.13.0"
description = "Python bindings for the Gradient Labs API"
readme = "README.md"
requires-python = ">=3.9,<4.0"
Expand Down
59 changes: 59 additions & 0 deletions src/gradient_labs/_customer_memories_batch_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Optional, List, Dict, Any
from datetime import datetime

from dataclasses import dataclass, field
from dataclasses_json import dataclass_json, config
from marshmallow import fields

from ._http_client import HttpClient


@dataclass_json
@dataclass(frozen=True)
class CustomerMemory:
# external_id is your own identifier for this memory.
external_id: str

# created_at is when the event this memory describes occurred.
created_at: datetime = field(
metadata=config(
encoder=datetime.isoformat,
decoder=datetime.fromisoformat,
mm_field=fields.DateTime(format="iso"),
)
)

# data is an arbitrary JSON object stored verbatim as the memory's payload.
data: Dict[str, Any]

# custom_type is an optional free-form label for the memory.
custom_type: Optional[str] = None


def batch_create_customer_memories(
*, client: HttpClient, customer_id: str, memories: List[CustomerMemory]
) -> None:
"""batch_create_customer_memories stores a batch of memories scoped to a
customer, for the AI agent to search over on demand.

Each memory's data is stored verbatim as an arbitrary JSON object. The call
is asynchronous: it returns as soon as the batch is accepted. It returns a
409 Conflict if a batch is already being created for the same customer."""
body: Dict[str, Any] = {
"memories": [_memory_to_dict(memory) for memory in memories],
}
_ = client.post(
path=f"customers/{customer_id}/memories",
body=body,
)


def _memory_to_dict(memory: CustomerMemory) -> Dict[str, Any]:
item: Dict[str, Any] = {
"external_id": memory.external_id,
"created_at": HttpClient.localize(memory.created_at),
"data": memory.data,
}
if memory.custom_type is not None:
item["custom_type"] = memory.custom_type
return item
23 changes: 23 additions & 0 deletions src/gradient_labs/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
return_async_tool_result,
ReturnAsyncToolResultParams,
)
from ._customer_memories_batch_create import (
batch_create_customer_memories,
CustomerMemory,
)

from ._outbound_conversation_start import (
start_outbound_conversation,
Expand Down Expand Up @@ -395,6 +399,25 @@ def return_async_tool_result(
params=params,
)

def batch_create_customer_memories(
self,
*,
customer_id: str,
memories: List[CustomerMemory],
) -> None:
"""batch_create_customer_memories stores a batch of memories scoped to a
customer, for the AI agent to search over on demand.

Each memory's data is stored verbatim as an arbitrary JSON object. The
call is asynchronous: it returns as soon as the batch is accepted, and
does not return an upload id or inserted count. It returns a 409 Conflict
if a batch is already being created for the same customer."""
batch_create_customer_memories(
client=self.http_client,
customer_id=customer_id,
memories=memories,
)

def upsert_hand_off_target(self, *, params: UpsertHandOffTargetParams) -> None:
"""upsert_hand_off_target inserts or updates a hand-off target.

Expand Down
68 changes: 68 additions & 0 deletions tests/test_customer_memories_batch_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from datetime import datetime
from unittest.mock import MagicMock

from gradient_labs import Client, CustomerMemory

CUSTOMER_ID = "cust_01ham6bzcdeja9xzqhjf6daq30"


def _client(response=None):
client = Client(api_key="test-key")
post = MagicMock(return_value=response)
client.http_client.post = post
return client, post


def test_batch_create_customer_memories():
client, post = _client()

rsp = client.batch_create_customer_memories(
customer_id=CUSTOMER_ID,
memories=[
CustomerMemory(
external_id="order_123",
custom_type="order",
created_at=datetime(2026, 7, 1, 10, 0, 0),
data={"total": 42.0},
),
CustomerMemory(
external_id="pref_1",
created_at=datetime(2026, 7, 2, 11, 30, 0),
data={"channel": "email"},
),
],
)

assert rsp is None

_, kwargs = post.call_args
assert kwargs["path"] == f"customers/{CUSTOMER_ID}/memories"
memories = kwargs["body"]["memories"]
assert len(memories) == 2

assert memories[0]["external_id"] == "order_123"
assert memories[0]["custom_type"] == "order"
assert memories[0]["created_at"] == "2026-07-01T10:00:00.000000Z"
assert memories[0]["data"] == {"total": 42.0}


def test_batch_create_customer_memories_omits_optional_custom_type():
client, post = _client()

client.batch_create_customer_memories(
customer_id=CUSTOMER_ID,
memories=[
CustomerMemory(
external_id="pref_1",
created_at=datetime(2026, 7, 2, 11, 30, 0),
data={"channel": "email"},
),
],
)

_, kwargs = post.call_args
memory = kwargs["body"]["memories"][0]
assert memory["external_id"] == "pref_1"
assert memory["created_at"] == "2026-07-02T11:30:00.000000Z"
assert memory["data"] == {"channel": "email"}
assert "custom_type" not in memory
Loading