Skip to content

Commit 5c3b4d7

Browse files
committed
wip DE checkpointing, add entities models
1 parent 21f00e3 commit 5c3b4d7

5 files changed

Lines changed: 119 additions & 30 deletions

File tree

src/agent/components/chains.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Description: Contains the chains for the main agent system
2-
from langchain import hub
32
from langchain_core.prompts import SystemMessagePromptTemplate, PromptTemplate, ChatPromptTemplate, MessagesPlaceholder
43
from langchain_core.messages import HumanMessage, SystemMessage
54
from langchain_core.output_parsers import StrOutputParser

src/checkpointer/entities.py

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from typing import Optional
33
from pydantic import BaseModel, Field
4+
from src.client.obp_client import OBPClient
45

56
class OpeyCheckpointEntity(BaseModel):
67
"""OBP Dynamic Entity representation of a LangGraph checkpoint."""
@@ -19,19 +20,15 @@ def obp_entity_name(cls) -> str:
1920

2021
@classmethod
2122
def to_obp_schema(cls) -> dict:
23+
json_schema = cls.model_json_schema()
2224
return {
2325
"hasPersonalEntity": True,
2426
cls.obp_entity_name(): {
25-
"description": "LangGraph checkpoint snapshots for Opey AI conversations",
26-
"required": ["thread_id", "checkpoint_id", "checkpoint_ns", "checkpoint_data"],
27+
"description": cls.__doc__.strip(),
28+
"required": json_schema["required"],
2729
"properties": {
28-
"thread_id": {"type": "string", "description": "Thread identifier"},
29-
"checkpoint_id": {"type": "string", "description": "Unique checkpoint ID (UUID)"},
30-
"checkpoint_ns": {"type": "string", "description": "Checkpoint namespace"},
31-
"parent_checkpoint_id": {"type": "string", "description": "Parent checkpoint ID for history"},
32-
"checkpoint_data": {"type": "string", "description": "JSON-serialized checkpoint (channel_values, versions, etc.)"},
33-
"metadata": {"type": "string", "description": "JSON-serialized metadata (step, source, writes)"},
34-
"created_at": {"type": "string", "description": "ISO timestamp"}
30+
k: {"type": v.get("type", "string"), "description": v.get("description", "")}
31+
for k, v in json_schema["properties"].items()
3532
}
3633
}
3734
}
@@ -54,24 +51,48 @@ def obp_entity_name(cls) -> str:
5451

5552
@classmethod
5653
def to_obp_schema(cls) -> dict:
57-
"""OBP Dynamic Entity schema definition."""
54+
json_schema = cls.model_json_schema()
5855
return {
5956
"hasPersonalEntity": True,
6057
cls.obp_entity_name(): {
61-
"description": "Pending writes for LangGraph checkpoints",
62-
"required": ["thread_id", "checkpoint_id", "checkpoint_ns", "task_id", "channel", "value", "idx"],
58+
"description": cls.__doc__.strip(),
59+
"required": json_schema["required"],
6360
"properties": {
64-
"thread_id": {"type": "string"},
65-
"checkpoint_id": {"type": "string"},
66-
"checkpoint_ns": {"type": "string"},
67-
"task_id": {"type": "string", "description": "Task that produced this write"},
68-
"idx": {"type": "integer", "description": "Write index within task"},
69-
"channel": {"type": "string", "description": "Channel name"},
70-
"value": {"type": "string", "description": "Serialized write value"}
61+
k: {"type": v.get("type", "string"), "description": v.get("description", "")}
62+
for k, v in json_schema["properties"].items()
7163
}
7264
}
7365
}
7466

67+
class DynamicEntitiesManager:
68+
"""Wrapper for DynamicEntities CRUD endpoints."""
69+
def __init__(self, obp_client: OBPClient, endpoint_url: str):
70+
71+
self.endpoint_url = endpoint_url
72+
self.client = obp_client
73+
74+
75+
async def create(self, entity: OpeyCheckpointEntity | OpeyCheckpointWriteEntity):
76+
await self.client.async_obp_requests(
77+
method="POST",
78+
body=str(entity.model_dump()),
79+
path=self.endpoint_url
80+
)
81+
82+
async def delete(self, entity_id: str):
83+
await self.client.async_obp_requests(
84+
method="DELETE",
85+
body="",
86+
path=f"{self.endpoint_url}/{entity_id}"
87+
)
88+
89+
async def read(self, entity_id: str) -> dict:
90+
response = await self.client.async_obp_requests(
91+
method="GET",
92+
body="",
93+
path=f"{self.endpoint_url}/{entity_id}"
94+
)
95+
return response
7596

7697
opey_checkpoint_entity = {
7798
"hasPersonalEntity": True,
@@ -105,4 +126,4 @@ def to_obp_schema(cls) -> dict:
105126
"value": {"type": "string", "description": "JSON-serialized write value"}
106127
}
107128
}
108-
}
129+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from typing import Iterator, Optional, Sequence
2+
from langchain_core.runnables import RunnableConfig
3+
from langgraph.checkpoint.base import (
4+
BaseCheckpointSaver,
5+
Checkpoint,
6+
CheckpointMetadata,
7+
CheckpointTuple,
8+
ChannelVersions,
9+
PendingWrite
10+
)
11+
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
12+
13+
from src.client import obp_client
14+
from src.checkpointer.entities import OpeyCheckpointEntity, OpeyCheckpointWriteEntity
15+
16+
class OBPCheckpointSaver(BaseCheckpointSaver):
17+
"""Checkpoint saver using OBP Dynamic Entitites as a storage backend."""
18+
19+
def __init__(self, client: OBPClient):
20+
super().__init__()
21+
self.client = client
22+
self.is_setup = False
23+
self.serde = JsonPlusSerializer()
24+
25+
def setup(self) -> None:
26+
"""
27+
Setup the Dynamic Entities in OBP if they do not exist.
28+
Needs to use an admin OBP client to create the system level entities.
29+
"""
30+
if self.is_setup:
31+
return
32+
33+
def put(
34+
self,
35+
config: RunnableConfig,
36+
checkpoint: Checkpoint,
37+
metadata: CheckpointMetadata,
38+
new_versions: ChannelVersions,
39+
) -> RunnableConfig:
40+
"""Store a checkpoint on OBP."""

test/checkpointer/test_entities.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from src.checkpointer.entities import OpeyCheckpointEntity, OpeyCheckpointWriteEntity
2+
3+
4+
def test_opey_checkpoint_entity_schema():
5+
schema = OpeyCheckpointEntity.to_obp_schema()
6+
7+
assert schema["hasPersonalEntity"] is True
8+
assert schema["OpeyCheckpoint"]["description"] == "OBP Dynamic Entity representation of a LangGraph checkpoint."
9+
assert set(schema["OpeyCheckpoint"]["required"]) == {
10+
"thread_id", "checkpoint_id", "checkpoint_ns",
11+
"parent_checkpoint_id", "checkpoint_data", "metadata"
12+
}
13+
assert "created_at" in schema["OpeyCheckpoint"]["properties"]
14+
assert schema["OpeyCheckpoint"]["properties"]["checkpoint_data"]["description"] == "Serialized checkpoint TypedDict"
15+
16+
17+
def test_opey_checkpoint_write_entity_schema():
18+
schema = OpeyCheckpointWriteEntity.to_obp_schema()
19+
20+
assert schema["hasPersonalEntity"] is True
21+
assert schema["OpeyCheckpointWrite"]["description"] == "OBP Dynamic Entity representation of a pending LangGraph checkpoint write."
22+
assert set(schema["OpeyCheckpointWrite"]["required"]) == {
23+
"thread_id", "checkpoint_id", "checkpoint_ns",
24+
"task_id", "idx", "channel", "value"
25+
}
26+
assert all(
27+
prop in schema["OpeyCheckpointWrite"]["properties"]
28+
for prop in ["thread_id", "checkpoint_id", "task_id", "idx", "channel", "value"]
29+
)

test/conftest.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
from httpx import AsyncClient, ASGITransport
1313
from asgi_lifespan import LifespanManager
1414

15-
@pytest_asyncio.fixture(scope="session", loop_scope="session")
16-
def anyio_backend():
17-
return "asyncio"
18-
19-
@pytest_asyncio.fixture(scope="session", loop_scope="session")
20-
async def client():
21-
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://localhost:5000") as client:
22-
print("Client is ready")
23-
yield client
15+
# @pytest_asyncio.fixture(scope="session", loop_scope="session")
16+
# def anyio_backend():
17+
# return "asyncio"
18+
19+
# @pytest_asyncio.fixture(scope="session", loop_scope="session")
20+
# async def client():
21+
# async with AsyncClient(transport=ASGITransport(app=app), base_url="http://localhost:5000") as client:
22+
# print("Client is ready")
23+
# yield client
2424

2525
@pytest.fixture(scope="session")
2626
def get_obp_consent():

0 commit comments

Comments
 (0)