Skip to content
Merged
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
106 changes: 73 additions & 33 deletions cardbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,85 @@
# Expose main data structures from the structures module
from cardbox.structures import (
Card,
CardBox,
TextContent,
JsonContent,
FieldSchema,
ToolContent,
ToolCallContent,
ToolResultContent,
TextFileContent,
FileContent,
PreviewImage,
FieldsSchemaContent,
ImageFileContent,
PdfFileContent,
VideoFileContent,
AudioFileContent,
MultiFileContent,
)
from cardbox.services import CardStore, CardHistory, CardBoxHistory
from cardbox.engine import ContextEngine
from cardbox.strategies import (
ExtractCodeStrategy,
PdfToTextStrategy,
Input,
InlineTextFileContentStrategy,
PdfToTextStrategyInput,
)
from cardbox.a2aclient import A2AHelperClient
from cardbox.adapters import (
AsyncPostgresStorageAdapter,
FileStorageAdapter,
LocalFileStorageAdapter,
InMemoryMappingAdapter,
LocalFileStorageAdapter,
)
from cardbox.config import configure
from cardbox.engine import ContextEngine
from cardbox.external import (
ExternalObjectError,
ExternalObjectNotFoundError,
ExternalObjectPointer,
ExternalObjectReader,
ExternalObjectError,
InvalidExternalURIError,
ExternalObjectNotFoundError,
S3ObjectReader,
)
from cardbox.config import configure
from cardbox.services import CardBoxHistory, CardHistory, CardStore
from cardbox.strategies import (
ExtractCodeStrategy,
InlineTextFileContentStrategy,
Input,
PdfToTextStrategy,
PdfToTextStrategyInput,
)
from cardbox.structures import (
AudioFileContent,
Card,
CardBox,
FieldSchema,
FieldsSchemaContent,
FileContent,
ImageFileContent,
JsonContent,
MultiFileContent,
PdfFileContent,
PreviewImage,
TextContent,
TextFileContent,
ToolCallContent,
ToolContent,
ToolResultContent,
VideoFileContent,
)

__all__ = [
"A2AHelperClient",
"AsyncPostgresStorageAdapter",
"AudioFileContent",
"Card",
"CardBox",
"CardBoxHistory",
"CardHistory",
"CardStore",
"ContextEngine",
"ExternalObjectError",
"ExternalObjectNotFoundError",
"ExternalObjectPointer",
"ExternalObjectReader",
"ExtractCodeStrategy",
"FieldSchema",
"FieldsSchemaContent",
"FileContent",
"FileStorageAdapter",
"ImageFileContent",
"InMemoryMappingAdapter",
"InlineTextFileContentStrategy",
"Input",
"InvalidExternalURIError",
"JsonContent",
"LocalFileStorageAdapter",
"MultiFileContent",
"PdfFileContent",
"PdfToTextStrategy",
"PdfToTextStrategyInput",
"PreviewImage",
"S3ObjectReader",
"TextContent",
"TextFileContent",
"ToolCallContent",
"ToolContent",
"ToolResultContent",
"VideoFileContent",
"configure",
]
49 changes: 27 additions & 22 deletions cardbox/adapters/async_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,28 +348,20 @@ async def save_card_box(self, box: "CardBox", tenant_id: str, *, conn: Any = Non
async with self._connection(conn) as db_conn:
if box.box_id is None:
box.box_id = str(uuid6.uuid7())
result = await db_conn.execute(
"""
INSERT INTO card_boxes (box_id, tenant_id, card_ids, parent_ids, updated_at)
VALUES (%s, %s, %s, %s, current_timestamp)
RETURNING box_id
""",
(box.box_id, tenant_id, card_ids_json, parent_ids_json),
)
row = await result.fetchone()
else:
result = await db_conn.execute(
"""
UPDATE card_boxes
SET card_ids = %s, parent_ids = %s, updated_at = current_timestamp
WHERE box_id = %s AND tenant_id = %s
RETURNING box_id
""",
(card_ids_json, parent_ids_json, box.box_id, tenant_id),
)
row = await result.fetchone()
if not row:
raise ValueError(f"card_box with id {box.box_id} not found for tenant {tenant_id}")
result = await db_conn.execute(
"""
INSERT INTO card_boxes (box_id, tenant_id, card_ids, parent_ids, updated_at)
VALUES (%s, %s, %s, %s, current_timestamp)
ON CONFLICT (box_id) DO UPDATE SET
tenant_id = EXCLUDED.tenant_id,
card_ids = EXCLUDED.card_ids,
parent_ids = EXCLUDED.parent_ids,
updated_at = current_timestamp
RETURNING box_id
""",
(box.box_id, tenant_id, card_ids_json, parent_ids_json),
)
row = await result.fetchone()

box_id = str(row.get("box_id"))
box.box_id = box_id
Expand Down Expand Up @@ -419,6 +411,19 @@ async def load_card_box(self, box_id: str, tenant_id: str, *, conn: Any = None)
box.card_ids = card_ids
return box

async def hydrate_card_box(self, box_id: str, tenant_id: str, *, conn: Any = None) -> Optional[tuple["CardBox", List["Card"]]]:
box = await self.load_card_box(box_id, tenant_id, conn=conn)
if box is None:
return None

cards: List["Card"] = []
for card_id in box.card_ids:
card = await self.get_card(card_id, tenant_id, conn=conn)
if card is None:
return None
cards.append(card)
return box, cards

async def add_sync_task(self, card_id: str, tenant_id: str, operation: str, *, conn: Any = None) -> None:
async with self._connection(conn) as db_conn:
await db_conn.execute(
Expand Down
18 changes: 15 additions & 3 deletions cardbox/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,28 @@ class TextContent(Content):
text: str


def _is_valid_json_value(value: Any) -> bool:
if value is None or isinstance(value, (str, bool, int)):
return True
if isinstance(value, float):
return value == value and value not in (float("inf"), -float("inf"))
if isinstance(value, list):
return all(_is_valid_json_value(item) for item in value)
if isinstance(value, dict):
return all(isinstance(key, str) and _is_valid_json_value(item) for key, item in value.items())
return False


class JsonContent(Content):
"""Represents structured JSON content (dict/list)."""

data: Union[Dict[str, Any], List[Any]]
data: Any

@field_validator("data")
@classmethod
def _validate_json_data(cls, value: Any) -> Any:
if not isinstance(value, (dict, list)):
raise InvalidCardContentError("JsonContent.data must be a dict or list.")
if not _is_valid_json_value(value):
raise InvalidCardContentError("JsonContent.data must be a valid JSON value.")
return value


Expand Down
Loading