Skip to content

Commit 8586291

Browse files
committed
feat(weave): store call views in CallViewSpec objects
Changes view storage from summary.weave.views blob to a dedicated CallViewSpec object referenced by view_spec_ref. This enables content-addressed deduplication of identical view configurations across calls. Changes: - Add CallViewSpec builtin object class with typed view item schemas - Register CallViewSpec in builtin_object_registry - Add _pending_views field to Call for collecting views during execution - Update set_call_view to store in _pending_views instead of summary - Add build_and_save_call_view_spec to create and save CallViewSpec at call_end - Update weave_client to call build_and_save_call_view_spec and set view_spec_ref - Update tests to verify new storage format
1 parent 1ce2762 commit 8586291

File tree

6 files changed

+316
-54
lines changed

6 files changed

+316
-54
lines changed

tests/trace/test_current_call.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ def my_op():
7373

7474

7575
def test_set_view_inside_op(client):
76-
"""Verify weave.set_view stores serialized content on op call summary."""
76+
"""Verify weave.set_view creates a CallViewSpec object and sets view_spec_ref."""
7777
markdown_view = weave.Content.from_text("# Report", mimetype="text/markdown")
7878
html_view = weave.Content.from_text("<p>Hi</p>", mimetype="text/html")
7979

8080
@weave.op
8181
def my_op_with_views() -> str:
82-
"""Op that attaches two views to its call summary.
82+
"""Op that attaches two views via CallViewSpec.
8383
8484
Returns:
8585
str: Constant string to keep output simple.
@@ -98,20 +98,23 @@ def my_op_with_views() -> str:
9898
calls = list(client.get_calls())
9999
assert len(calls) == 1
100100
call = calls[0]
101-
assert call.summary["weave"] is not None
102-
assert "views" in call.summary["weave"]
103-
views = call.summary["weave"]["views"]
104-
assert len(views) == 2
105-
assert views["markdown"] == to_json(
106-
markdown_view,
107-
client._project_id(),
108-
client,
109-
)
110-
assert views["html"] == to_json(
111-
html_view,
112-
client._project_id(),
113-
client,
114-
)
101+
102+
# Views are now stored in view_spec_ref, not summary
103+
assert call.view_spec_ref is not None
104+
assert "weave:///" in call.view_spec_ref
105+
assert "CallViewSpec" in call.view_spec_ref
106+
107+
# Retrieve the CallViewSpec object
108+
from weave.trace.refs import ObjectRef
109+
110+
ref = ObjectRef.parse_uri(call.view_spec_ref)
111+
view_spec = ref.get()
112+
113+
# Verify the views are present
114+
assert "markdown" in view_spec.views
115+
assert "html" in view_spec.views
116+
assert view_spec.views["markdown"].type == "content"
117+
assert view_spec.views["html"].type == "content"
115118

116119

117120
def test_set_view_string_content(client):
@@ -125,10 +128,18 @@ def op_with_string_view() -> None:
125128
op_with_string_view()
126129

127130
call = client.get_calls()[0]
128-
assert call.summary["weave"] is not None
129-
views = call.summary["weave"]["views"]
130-
stored = dict(views["md"])
131131

132-
assert stored["_type"] == "CustomWeaveType"
133-
assert stored["weave_type"]["type"] == "weave.type_wrappers.Content.content.Content"
134-
assert stored["files"]["content"]
132+
# Views are stored in view_spec_ref
133+
assert call.view_spec_ref is not None
134+
135+
# Retrieve the CallViewSpec object
136+
from weave.trace.refs import ObjectRef
137+
138+
ref = ObjectRef.parse_uri(call.view_spec_ref)
139+
view_spec = ref.get()
140+
141+
# Verify the markdown view is present
142+
assert "md" in view_spec.views
143+
view_item = view_spec.views["md"]
144+
assert view_item.type == "content"
145+
assert view_item.mimetype == "text/markdown"

weave/trace/call.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ class Call:
8282
_children: list[Call] = dataclasses.field(default_factory=list)
8383
_feedback: RefFeedbackQuery | None = None
8484

85+
# Pending views to be saved as CallViewSpec object at call_end
86+
_pending_views: dict[str, Any] = dataclasses.field(default_factory=dict)
87+
8588
# Size of metadata storage for this call
8689
storage_size_bytes: int | None = None
8790

weave/trace/view_utils.py

Lines changed: 157 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,31 @@
22

33
from __future__ import annotations
44

5-
from typing import Any
5+
import base64
6+
from typing import TYPE_CHECKING, Any
67

78
from weave.trace.call import Call
9+
from weave.trace.refs import ObjectRef
810
from weave.trace.serialization.serialize import to_json
911
from weave.trace.table import Table
10-
from weave.trace.weave_client import WeaveClient
11-
from weave.trace.widgets import Widget
12+
from weave.trace.widgets import ChildPredictionsWidget, ScoreSummaryWidget, Widget
13+
from weave.trace_server.interface.builtin_object_classes.call_view_spec import (
14+
CallViewSpec,
15+
ChildPredictionsWidgetItem,
16+
ContentViewItem,
17+
ObjectRefViewItem,
18+
ScoreSummaryWidgetItem,
19+
TableRefViewItem,
20+
)
21+
from weave.trace_server.interface.builtin_object_classes.call_view_spec import (
22+
ViewItem as CallViewSpecItem,
23+
)
1224
from weave.type_wrappers.Content.content import Content
1325

14-
ViewItem = Content | str | Widget | Table
26+
if TYPE_CHECKING:
27+
from weave.trace.weave_client import WeaveClient
28+
29+
ViewItem = Content | str | Widget | Table | ObjectRef
1530
ViewSpec = ViewItem | list[ViewItem]
1631

1732

@@ -148,6 +163,101 @@ def serialize_view_spec(
148163
)
149164

150165

166+
def _sdk_view_item_to_call_view_spec_item(
167+
item: ViewItem,
168+
client: WeaveClient,
169+
*,
170+
extension: str | None = None,
171+
mimetype: str | None = None,
172+
metadata: dict[str, Any] | None = None,
173+
encoding: str = "utf-8",
174+
) -> CallViewSpecItem:
175+
"""Convert an SDK view item to a CallViewSpec item for storage.
176+
177+
Args:
178+
item: A Content, string, Widget, or Table to convert.
179+
client: The WeaveClient for saving tables.
180+
extension: Optional file extension for string content.
181+
mimetype: Optional MIME type for string content.
182+
metadata: Optional metadata for string content.
183+
encoding: Encoding for string content.
184+
185+
Returns:
186+
A CallViewSpec item (ContentViewItem, WidgetItem, or TableRefViewItem).
187+
"""
188+
if isinstance(item, ScoreSummaryWidget):
189+
return ScoreSummaryWidgetItem()
190+
elif isinstance(item, ChildPredictionsWidget):
191+
return ChildPredictionsWidgetItem()
192+
elif isinstance(item, Widget):
193+
# Future widgets - default to score_summary for now
194+
return ScoreSummaryWidgetItem()
195+
elif isinstance(item, Table):
196+
# Publish the table and return its ref URI
197+
table_ref = client._save_table(item)
198+
return TableRefViewItem(uri=table_ref.uri())
199+
elif isinstance(item, ObjectRef):
200+
# Store object references (e.g., SavedView) as URI strings
201+
return ObjectRefViewItem(uri=item.uri())
202+
elif isinstance(item, (Content, str)):
203+
content_obj = resolve_view_content(
204+
item,
205+
extension=extension,
206+
mimetype=mimetype,
207+
metadata=metadata,
208+
encoding=encoding,
209+
)
210+
# Encode content data as base64
211+
data_bytes = (
212+
content_obj.data
213+
if isinstance(content_obj.data, bytes)
214+
else content_obj.data.encode(content_obj.encoding or "utf-8")
215+
)
216+
return ContentViewItem(
217+
mimetype=content_obj.mimetype,
218+
encoding=content_obj.encoding,
219+
data=base64.b64encode(data_bytes).decode("ascii"),
220+
metadata=content_obj.metadata,
221+
)
222+
else:
223+
raise TypeError(f"Unsupported view item type: {type(item)}")
224+
225+
226+
def _sdk_view_spec_to_call_view_spec_item(
227+
spec: ViewSpec,
228+
client: WeaveClient,
229+
*,
230+
extension: str | None = None,
231+
mimetype: str | None = None,
232+
metadata: dict[str, Any] | None = None,
233+
encoding: str = "utf-8",
234+
) -> CallViewSpecItem | list[CallViewSpecItem]:
235+
"""Convert an SDK view spec to CallViewSpec item(s).
236+
237+
Args:
238+
spec: A single view item or list of view items.
239+
client: The WeaveClient for saving tables.
240+
extension: Optional file extension for string content.
241+
mimetype: Optional MIME type for string content.
242+
metadata: Optional metadata for string content.
243+
encoding: Encoding for string content.
244+
245+
Returns:
246+
A CallViewSpec item or list of items.
247+
"""
248+
if isinstance(spec, list):
249+
return [_sdk_view_item_to_call_view_spec_item(item, client) for item in spec]
250+
else:
251+
return _sdk_view_item_to_call_view_spec_item(
252+
spec,
253+
client,
254+
extension=extension,
255+
mimetype=mimetype,
256+
metadata=metadata,
257+
encoding=encoding,
258+
)
259+
260+
151261
def set_call_view(
152262
*,
153263
call: Call,
@@ -159,12 +269,16 @@ def set_call_view(
159269
metadata: dict[str, Any] | None = None,
160270
encoding: str = "utf-8",
161271
) -> None:
162-
"""Attach serialized content to the provided call's summary under `weave.views`.
272+
"""Attach a view to the call's pending views for later storage as CallViewSpec.
273+
274+
Views are stored in the call's _pending_views dict and will be converted to
275+
a CallViewSpec object when the call is finished. This allows deduplication
276+
of identical view configurations across calls.
163277
164278
Args:
165-
call: The call whose summary should receive the view entry.
279+
call: The call whose pending views should receive the view entry.
166280
client: The active ``WeaveClient`` used for serialization.
167-
name: Key to store the view under within ``summary.weave.views``.
281+
name: Key to store the view under.
168282
content: A ``weave.Content``, raw text, ``Widget``, ``Table``, or list of these
169283
to serialize into the view.
170284
extension: Optional file extension used when converting text content.
@@ -189,35 +303,46 @@ def set_call_view(
189303
... extension="html",
190304
... )
191305
"""
192-
if call.summary is None:
193-
call.summary = {}
194-
195-
summary = call.summary
196-
197-
weave_bucket = summary.get("weave")
198-
if not isinstance(weave_bucket, dict):
199-
weave_bucket = {}
200-
summary["weave"] = weave_bucket
201-
202-
legacy_views = summary.get("views")
203-
if isinstance(legacy_views, dict):
204-
summary.pop("views", None)
205-
views = weave_bucket.get("views")
206-
207-
if not isinstance(views, dict):
208-
views = {}
209-
weave_bucket["views"] = views
210-
211-
if isinstance(legacy_views, dict):
212-
views.update(legacy_views)
213-
214-
project_id = client._project_id()
215-
views[name] = serialize_view_spec(
306+
# Store the converted view item in _pending_views
307+
call._pending_views[name] = _sdk_view_spec_to_call_view_spec_item(
216308
content,
217-
project_id,
218309
client,
219310
extension=extension,
220311
mimetype=mimetype,
221312
metadata=metadata,
222313
encoding=encoding,
223314
)
315+
316+
317+
def build_and_save_call_view_spec(
318+
call: Call,
319+
client: WeaveClient,
320+
) -> str | None:
321+
"""Build a CallViewSpec from pending views and save it as an object.
322+
323+
This function collects all pending views from the call, creates a CallViewSpec
324+
object, and saves it to the project. The returned ref URI can be used as the
325+
view_spec_ref in the call_end request.
326+
327+
Args:
328+
call: The call with pending views to save.
329+
client: The WeaveClient for saving the object.
330+
331+
Returns:
332+
The view_spec_ref URI string, or None if no views are pending.
333+
"""
334+
if not call._pending_views:
335+
return None
336+
337+
# Convert pending views dict values to the proper union type
338+
views_dict: dict[str, CallViewSpecItem | list[CallViewSpecItem]] = {}
339+
for name, item in call._pending_views.items():
340+
views_dict[name] = item
341+
342+
# Create the CallViewSpec object
343+
view_spec = CallViewSpec(views=views_dict)
344+
345+
# Save as a versioned object - content-addressed storage will deduplicate
346+
ref: ObjectRef = client._save_object(view_spec, name="CallViewSpec")
347+
348+
return ref.uri()

weave/trace/weave_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from weave.trace.table_upload_chunking import ChunkingConfig, TableChunkManager
8585
from weave.trace.util import log_once
8686
from weave.trace.vals import WeaveObject, WeaveTable, make_trace_obj
87+
from weave.trace.view_utils import build_and_save_call_view_spec
8788
from weave.trace.wandb_run_context import (
8889
WandbRunContext,
8990
check_wandb_run_matches,
@@ -981,6 +982,9 @@ def send_end_call() -> None:
981982
wb_run_context_end.step if wb_run_context_end else None
982983
)
983984

985+
# Build and save CallViewSpec if there are pending views
986+
view_spec_ref = build_and_save_call_view_spec(call, self)
987+
984988
call_end_req = CallEndReq(
985989
end=EndedCallSchemaForInsertWithStartedAt(
986990
project_id=project_id,
@@ -991,6 +995,7 @@ def send_end_call() -> None:
991995
summary=merged_summary,
992996
exception=exception_str,
993997
wb_run_step_end=current_wb_run_step_end,
998+
view_spec_ref=view_spec_ref,
994999
)
9951000
)
9961001
bytes_size = len(call_end_req.model_dump_json())

weave/trace_server/interface/builtin_object_classes/builtin_object_registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from weave.trace_server.interface.builtin_object_classes.base_object_def import (
66
BaseObject,
77
)
8+
from weave.trace_server.interface.builtin_object_classes.call_view_spec import (
9+
CallViewSpec,
10+
)
811
from weave.trace_server.interface.builtin_object_classes.comparison_view import (
912
ComparisonView,
1013
)
@@ -50,3 +53,4 @@ def register_base_object(cls: type[BaseObject]) -> None:
5053
register_base_object(ComparisonView)
5154
register_base_object(LLMStructuredCompletionModel)
5255
register_base_object(ChartConfig)
56+
register_base_object(CallViewSpec)

0 commit comments

Comments
 (0)