Skip to content

Commit 937dc91

Browse files
authored
Add typed raw accessors with drift logging; migrate workspace tabs and resolver (#94)
* feat: initial implementation for typed accessor methods * fix: typecheck fail error * fix: review comments by ai * fix: outside diff range comments * fix: reviews from the reviewer and ai * fix: remaining issues
1 parent 6f6143f commit 937dc91

8 files changed

Lines changed: 894 additions & 103 deletions

File tree

api/search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def search():
159159
bid = parts[2]
160160
try:
161161
bubble = Bubble.from_dict(json.loads(row["value"]), bubble_id=bid)
162-
text = extract_text_from_bubble(bubble.raw)
162+
text = extract_text_from_bubble(bubble)
163163
bubble_map[bid] = {"text": text, "raw": bubble.raw}
164164
except SchemaError as e:
165165
# Drift logged so the operator can see why a chat dropped

models/conversation.py

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

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

@@ -12,6 +13,8 @@
1213
require_type,
1314
)
1415

16+
_logger = logging.getLogger(__name__)
17+
1518

1619
@dataclass(frozen=True)
1720
class Composer:
@@ -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,171 @@ 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) -> dict[str, Any] | None:
250+
value = self.raw.get("tokenCount")
251+
if value is None:
252+
return None
253+
if not isinstance(value, dict):
254+
_logger.warning(
255+
"Schema drift in Bubble %s: invalid type for tokenCount (expected dict, got %s)",
256+
self.bubble_id,
257+
type(value).__name__,
258+
)
259+
return None
260+
return value
261+
262+
@property
263+
def tool_former_data(self) -> dict[str, Any] | None:
264+
value = self.raw.get("toolFormerData")
265+
if value is None:
266+
return None
267+
if not isinstance(value, dict):
268+
_logger.warning(
269+
"Schema drift in Bubble %s: invalid type for toolFormerData (expected dict, got %s)",
270+
self.bubble_id,
271+
type(value).__name__,
272+
)
273+
return None
274+
return value
275+
276+
@property
277+
def model_info(self) -> dict[str, Any]:
278+
value = self.raw.get("modelInfo")
279+
if value is None:
280+
return {}
281+
if not isinstance(value, dict):
282+
_logger.warning(
283+
"Schema drift in Bubble %s: invalid type for modelInfo (expected dict, got %s)",
284+
self.bubble_id,
285+
type(value).__name__,
286+
)
287+
return {}
288+
return value
289+
290+
@property
291+
def thinking(self) -> str | dict[str, Any] | None:
292+
value = self.raw.get("thinking")
293+
if value is None:
294+
return None
295+
if isinstance(value, (str, dict)):
296+
return value
297+
_logger.warning(
298+
"Schema drift in Bubble %s: invalid type for thinking (expected str or dict, got %s)",
299+
self.bubble_id,
300+
type(value).__name__,
301+
)
302+
return None
303+
304+
@property
305+
def thinking_duration_ms(self) -> int | float | None:
306+
value = self.raw.get("thinkingDurationMs")
307+
if value is None:
308+
return None
309+
if isinstance(value, bool) or not isinstance(value, (int, float)):
310+
_logger.warning(
311+
"Schema drift in Bubble %s: invalid type for thinkingDurationMs (expected number, got %s)",
312+
self.bubble_id,
313+
type(value).__name__,
314+
)
315+
return None
316+
return value
317+
318+
@property
319+
def context_window_status_at_creation(self) -> dict[str, Any]:
320+
value = self.raw.get("contextWindowStatusAtCreation")
321+
if value is None:
322+
return {}
323+
if not isinstance(value, dict):
324+
_logger.warning(
325+
"Schema drift in Bubble %s: invalid type for contextWindowStatusAtCreation (expected dict, got %s)",
326+
self.bubble_id,
327+
type(value).__name__,
328+
)
329+
return {}
330+
return value
331+
332+
@property
333+
def tool_results(self) -> list[Any] | None:
334+
value = self.raw.get("toolResults")
335+
if value is None:
336+
return None
337+
if not isinstance(value, list):
338+
_logger.warning(
339+
"Schema drift in Bubble %s: invalid type for toolResults (expected list, got %s)",
340+
self.bubble_id,
341+
type(value).__name__,
342+
)
343+
return None
344+
return value
345+
346+
def bubble_timestamp_ms(self) -> int | float | None:
347+
"""``createdAt`` or ``timestamp`` in milliseconds when present."""
348+
for key in ("createdAt", "timestamp"):
349+
value = self.raw.get(key)
350+
if isinstance(value, (int, float)) and not isinstance(value, bool):
351+
return value
352+
return None

0 commit comments

Comments
 (0)