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
38 changes: 38 additions & 0 deletions examples/memories/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
import sys
import uuid
import logging

from gradient_labs import (
Client,
BulkUploadMemoriesParams,
)

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

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

# Each memory is an arbitrary JSON object stored verbatim for the AI agent to
# search over on demand. created_at_keys lists the payload keys tried in order
# to read each memory's timestamp; when none match, the upload time is used.
rsp = client.bulk_upload_conversation_memories(
conversation_id="conv_01ham6bzcdeja9xzqhjf6daq30",
params=BulkUploadMemoriesParams(
idempotency_key=str(uuid.uuid4()),
memories=[
{
"kind": "preference",
"channel": "email",
"occurred_at": "2026-01-02T10:00:00Z",
},
{"kind": "order", "order_id": "order-456", "total": 42.0},
],
created_at_keys=["occurred_at"],
),
)
logging.info(
f"✅ Memories uploaded: {rsp.memories_inserted} inserted (upload {rsp.upload_id})"
)
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
51 changes: 51 additions & 0 deletions src/gradient_labs/_conversation_memories_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Optional, List, Dict, Any

from dataclasses import dataclass
from dataclasses_json import dataclass_json

from ._http_client import HttpClient


@dataclass_json
@dataclass(frozen=True)
class BulkUploadMemoriesParams:
# idempotency_key de-duplicates retries of the same upload. Re-uploading with
# the same key returns the original upload instead of inserting again.
idempotency_key: str

# memories is the list of individual memories to store. Each element is an
# arbitrary JSON object stored verbatim as the memory's raw payload.
memories: List[Dict[str, Any]]

# created_at_keys optionally lists JSON keys tried in order to read each
# memory's timestamp from its payload. When none match, the upload time is used.
created_at_keys: Optional[List[str]] = None


@dataclass_json
@dataclass(frozen=True)
class BulkUploadMemoriesResponse:
# upload_id identifies the upload that stored these memories.
upload_id: str

# memories_inserted is the number of memories that were stored.
memories_inserted: int


def bulk_upload_conversation_memories(
*, client: HttpClient, conversation_id: str, params: BulkUploadMemoriesParams
) -> BulkUploadMemoriesResponse:
"""bulk_upload_conversation_memories stores a batch of memories scoped to a
conversation, for the AI agent to search over on demand."""
body: Dict[str, Any] = {
"idempotency_key": params.idempotency_key,
"memories": params.memories,
}
if params.created_at_keys is not None:
body["created_at_keys"] = params.created_at_keys

rsp = client.post(
path=f"conversations/{conversation_id}/memories",
body=body,
)
return BulkUploadMemoriesResponse.from_dict(rsp)
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,11 @@
return_async_tool_result,
ReturnAsyncToolResultParams,
)
from ._conversation_memories_upload import (
bulk_upload_conversation_memories,
BulkUploadMemoriesParams,
BulkUploadMemoriesResponse,
)

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

def bulk_upload_conversation_memories(
self,
*,
conversation_id: str,
params: BulkUploadMemoriesParams,
) -> BulkUploadMemoriesResponse:
"""bulk_upload_conversation_memories stores a batch of memories scoped to a
conversation, for the AI agent to search over on demand.

Each memory is stored verbatim as an arbitrary JSON object. Re-uploading
with the same idempotency_key returns the original upload instead of
inserting again."""
return bulk_upload_conversation_memories(
client=self.http_client,
conversation_id=conversation_id,
params=params,
)

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

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

from gradient_labs import Client, BulkUploadMemoriesParams

CONVERSATION_ID = "conv_01ham6bzcdeja9xzqhjf6daq30"


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


def test_bulk_upload_conversation_memories():
client, post = _client(
{"upload_id": "upl_123", "memories_inserted": 2},
)

rsp = client.bulk_upload_conversation_memories(
conversation_id=CONVERSATION_ID,
params=BulkUploadMemoriesParams(
idempotency_key="key-1",
memories=[{"note": "prefers email"}, {"tier": "premium"}],
),
)

_, kwargs = post.call_args
assert kwargs["path"] == f"conversations/{CONVERSATION_ID}/memories"
body = kwargs["body"]
assert body["idempotency_key"] == "key-1"
assert body["memories"] == [{"note": "prefers email"}, {"tier": "premium"}]
assert "created_at_keys" not in body

assert rsp.upload_id == "upl_123"
assert rsp.memories_inserted == 2


def test_bulk_upload_conversation_memories_includes_created_at_keys():
client, post = _client(
{"upload_id": "upl_456", "memories_inserted": 1},
)

client.bulk_upload_conversation_memories(
conversation_id=CONVERSATION_ID,
params=BulkUploadMemoriesParams(
idempotency_key="key-2",
memories=[{"event": "signup"}],
created_at_keys=["occurred_at", "created_at"],
),
)

_, kwargs = post.call_args
body = kwargs["body"]
assert body["created_at_keys"] == ["occurred_at", "created_at"]
Loading