diff --git a/cardbox/__init__.py b/cardbox/__init__.py index ad5f45a..14ed9c8 100644 --- a/cardbox/__init__.py +++ b/cardbox/__init__.py @@ -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", +] diff --git a/cardbox/adapters/async_storage.py b/cardbox/adapters/async_storage.py index ec6392e..c760772 100644 --- a/cardbox/adapters/async_storage.py +++ b/cardbox/adapters/async_storage.py @@ -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 @@ -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( diff --git a/cardbox/structures.py b/cardbox/structures.py index 03a8521..46bed36 100644 --- a/cardbox/structures.py +++ b/cardbox/structures.py @@ -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