Skip to content

Commit 19eb9da

Browse files
committed
feat: initial implementation for typed accessor methods
1 parent 08667a5 commit 19eb9da

6 files changed

Lines changed: 647 additions & 60 deletions

File tree

models/conversation.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

3+
import logging
34
from dataclasses import dataclass, field
45
from typing import Any
56

7+
_logger = logging.getLogger(__name__)
8+
69
from models.errors import SchemaError
710
from models.from_dict_validation import (
811
require_dict,
@@ -67,6 +70,84 @@ def from_dict(cls, raw: dict[str, Any], *, composer_id: str) -> "Composer":
6770
raw=raw,
6871
)
6972

73+
@property
74+
def newly_created_files(self) -> list[Any]:
75+
value = self.raw.get("newlyCreatedFiles")
76+
if value is None:
77+
return []
78+
if not isinstance(value, list):
79+
_logger.warning(
80+
"Schema drift in Composer %s: invalid type for newlyCreatedFiles (expected list, got %s)",
81+
self.composer_id,
82+
type(value).__name__,
83+
)
84+
return []
85+
return value
86+
87+
@property
88+
def code_block_data(self) -> dict[str, Any] | None:
89+
value = self.raw.get("codeBlockData")
90+
if value is None:
91+
return None
92+
if not isinstance(value, dict):
93+
_logger.warning(
94+
"Schema drift in Composer %s: invalid type for codeBlockData (expected dict, got %s)",
95+
self.composer_id,
96+
type(value).__name__,
97+
)
98+
return None
99+
return value
100+
101+
@property
102+
def usage_data(self) -> dict[str, Any]:
103+
"""Composer cost rollup; empty dict when absent (common)."""
104+
value = self.raw.get("usageData")
105+
if value is None:
106+
return {}
107+
if not isinstance(value, dict):
108+
suffix = f" {self.composer_id}" if self.composer_id else ""
109+
_logger.warning(
110+
"Schema drift in Composer%s: invalid type for usageData (expected dict, got %s)",
111+
suffix,
112+
type(value).__name__,
113+
)
114+
return {}
115+
return value
116+
117+
def _optional_counter(self, key: str) -> int | float:
118+
value = self.raw.get(key, 0)
119+
if isinstance(value, bool) or not isinstance(value, (int, float)):
120+
if key in self.raw:
121+
suffix = f" {self.composer_id}" if self.composer_id else ""
122+
_logger.warning(
123+
"Schema drift in Composer%s: invalid type for %s (expected number, got %s)",
124+
suffix,
125+
key,
126+
type(value).__name__,
127+
)
128+
return 0
129+
return value
130+
131+
@property
132+
def total_lines_added(self) -> int | float:
133+
return self._optional_counter("totalLinesAdded")
134+
135+
@property
136+
def total_lines_removed(self) -> int | float:
137+
return self._optional_counter("totalLinesRemoved")
138+
139+
@property
140+
def added_files(self) -> int | float:
141+
return self._optional_counter("addedFiles")
142+
143+
@property
144+
def removed_files(self) -> int | float:
145+
return self._optional_counter("removedFiles")
146+
147+
def model_name_from_config(self) -> str | None:
148+
name = self.model_config.get("modelName")
149+
return name if isinstance(name, str) and name else None
150+
70151

71152
@dataclass(frozen=True)
72153
class WorkspaceLocalComposer:
@@ -101,3 +182,141 @@ def from_dict(cls, raw: dict[str, Any], *, bubble_id: str) -> "Bubble":
101182
raw = require_dict(raw, model="Bubble", field="bubble")
102183
require_non_empty_str(bubble_id, model="Bubble", field="bubbleId")
103184
return cls(bubble_id=bubble_id, raw=raw)
185+
186+
@property
187+
def text(self) -> str | None:
188+
"""Plain ``text`` field; richText is handled by :func:`extract_text_from_bubble`."""
189+
value = self.raw.get("text")
190+
return value if isinstance(value, str) else None
191+
192+
@property
193+
def metadata(self) -> dict[str, Any]:
194+
value = self.raw.get("metadata")
195+
if value is None:
196+
return {}
197+
if not isinstance(value, dict):
198+
_logger.warning(
199+
"Schema drift in Bubble %s: invalid type for metadata (expected dict, got %s)",
200+
self.bubble_id,
201+
type(value).__name__,
202+
)
203+
return {}
204+
return value
205+
206+
@property
207+
def relevant_files(self) -> list[Any]:
208+
value = self.raw.get("relevantFiles")
209+
if value is None:
210+
return []
211+
if not isinstance(value, list):
212+
_logger.warning(
213+
"Schema drift in Bubble %s: invalid type for relevantFiles (expected list, got %s)",
214+
self.bubble_id,
215+
type(value).__name__,
216+
)
217+
return []
218+
return value
219+
220+
@property
221+
def attached_file_code_chunks_uris(self) -> list[Any]:
222+
value = self.raw.get("attachedFileCodeChunksUris")
223+
if value is None:
224+
return []
225+
if not isinstance(value, list):
226+
_logger.warning(
227+
"Schema drift in Bubble %s: invalid type for attachedFileCodeChunksUris (expected list, got %s)",
228+
self.bubble_id,
229+
type(value).__name__,
230+
)
231+
return []
232+
return value
233+
234+
@property
235+
def context(self) -> dict[str, Any]:
236+
value = self.raw.get("context")
237+
if value is None:
238+
return {}
239+
if not isinstance(value, dict):
240+
_logger.warning(
241+
"Schema drift in Bubble %s: invalid type for context (expected dict, got %s)",
242+
self.bubble_id,
243+
type(value).__name__,
244+
)
245+
return {}
246+
return value
247+
248+
@property
249+
def token_count(self) -> Any | None:
250+
return self.raw.get("tokenCount")
251+
252+
@property
253+
def tool_former_data(self) -> dict[str, Any] | None:
254+
value = self.raw.get("toolFormerData")
255+
if value is None:
256+
return None
257+
if not isinstance(value, dict):
258+
_logger.warning(
259+
"Schema drift in Bubble %s: invalid type for toolFormerData (expected dict, got %s)",
260+
self.bubble_id,
261+
type(value).__name__,
262+
)
263+
return None
264+
return value
265+
266+
@property
267+
def model_info(self) -> dict[str, Any]:
268+
value = self.raw.get("modelInfo")
269+
if value is None:
270+
return {}
271+
if not isinstance(value, dict):
272+
_logger.warning(
273+
"Schema drift in Bubble %s: invalid type for modelInfo (expected dict, got %s)",
274+
self.bubble_id,
275+
type(value).__name__,
276+
)
277+
return {}
278+
return value
279+
280+
@property
281+
def thinking(self) -> Any | None:
282+
return self.raw.get("thinking")
283+
284+
@property
285+
def thinking_duration_ms(self) -> Any | None:
286+
return self.raw.get("thinkingDurationMs")
287+
288+
@property
289+
def context_window_status_at_creation(self) -> dict[str, Any]:
290+
value = self.raw.get("contextWindowStatusAtCreation")
291+
if value is None:
292+
return {}
293+
if not isinstance(value, dict):
294+
_logger.warning(
295+
"Schema drift in Bubble %s: invalid type for contextWindowStatusAtCreation (expected dict, got %s)",
296+
self.bubble_id,
297+
type(value).__name__,
298+
)
299+
return {}
300+
return value
301+
302+
@property
303+
def tool_results(self) -> list[Any] | None:
304+
value = self.raw.get("toolResults")
305+
if value is None:
306+
return None
307+
if not isinstance(value, list):
308+
_logger.warning(
309+
"Schema drift in Bubble %s: invalid type for toolResults (expected list, got %s)",
310+
self.bubble_id,
311+
type(value).__name__,
312+
)
313+
return None
314+
return value
315+
316+
def bubble_timestamp_ms(self) -> int | float | None:
317+
"""``createdAt`` or ``timestamp`` in milliseconds when present."""
318+
for key in ("createdAt", "timestamp"):
319+
value = self.raw.get(key)
320+
if isinstance(value, (int, float)) and not isinstance(value, bool):
321+
return value
322+
return None

0 commit comments

Comments
 (0)