|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import logging |
3 | 4 | from dataclasses import dataclass, field |
4 | 5 | from typing import Any |
5 | 6 |
|
| 7 | +_logger = logging.getLogger(__name__) |
| 8 | + |
6 | 9 | from models.errors import SchemaError |
7 | 10 | from models.from_dict_validation import ( |
8 | 11 | require_dict, |
@@ -67,6 +70,84 @@ def from_dict(cls, raw: dict[str, Any], *, composer_id: str) -> "Composer": |
67 | 70 | raw=raw, |
68 | 71 | ) |
69 | 72 |
|
| 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 | + |
70 | 151 |
|
71 | 152 | @dataclass(frozen=True) |
72 | 153 | class WorkspaceLocalComposer: |
@@ -101,3 +182,141 @@ def from_dict(cls, raw: dict[str, Any], *, bubble_id: str) -> "Bubble": |
101 | 182 | raw = require_dict(raw, model="Bubble", field="bubble") |
102 | 183 | require_non_empty_str(bubble_id, model="Bubble", field="bubbleId") |
103 | 184 | 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