Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions dev_docs/BuiltinObjectClasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ While many Weave Objects are free-form and user-defined, there is often a need f

Here's how to define and use a validated base object:

1. **Define your schema** (in `weave/trace_server/interface/builtin_object_classes/your_schema.py`):
1. **Define your schema** (in `weave/shared/builtin_object_classes/your_schema.py`):

```python
from pydantic import BaseModel
Expand Down Expand Up @@ -160,10 +160,10 @@ Run `make synchronize-base-object-schemas` to ensure the frontend TypeScript typ

### Architecture Flow

1. Define your schema in a python file in the `weave/trace_server/interface/builtin_object_classes/test_only_example.py` directory. See `weave/trace_server/interface/builtin_object_classes/test_only_example.py` as an example.
2. Make sure to register your schemas in `weave/trace_server/interface/builtin_object_classes/builtin_object_registry.py` by calling `register_base_object`.
1. Define your schema in a python file in the `weave/shared/builtin_object_classes/test_only_example.py` directory. See `weave/shared/builtin_object_classes/test_only_example.py` as an example.
2. Make sure to register your schemas in `weave/shared/builtin_object_classes/builtin_object_registry.py` by calling `register_base_object`.
3. Run `make synchronize-base-object-schemas` to generate the frontend types.
- The first step (`make generate_base_object_schemas`) will run `scripts/generate_base_object_schemas.py` to generate a JSON schema in `weave/trace_server/interface/builtin_object_classes/generated/generated_builtin_object_class_schemas.json`.
- The first step (`make generate_base_object_schemas`) will run `scripts/generate_base_object_schemas.py` to generate a JSON schema in `weave/shared/builtin_object_classes/generated/generated_builtin_object_class_schemas.json`.
- The second step (yarn `generate-schemas`) will read this file and use it to generate the frontend types located in `frontends/weave/src/components/PagePanelComponents/Home/Browse3/pages/wfReactInterface/generatedBuiltinObjectClasses.zod.ts`.
4. Now, each use case uses different parts:
1. `Python Writing`. Users can directly import these classes and use them as normal Pydantic models, which get published with `weave.publish`. The python client correct builds the requisite payload.
Expand Down
5 changes: 2 additions & 3 deletions scripts/generate_base_object_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

from pydantic import create_model

from weave.trace_server.interface.builtin_object_classes.builtin_object_registry import (
from weave.shared.builtin_object_classes.builtin_object_registry import (
BUILTIN_OBJECT_REGISTRY,
)

OUTPUT_DIR = (
Path(__file__).parent.parent
/ "weave"
/ "trace_server"
/ "interface"
/ "shared"
/ "builtin_object_classes"
/ "generated"
)
Expand Down
14 changes: 7 additions & 7 deletions tests/trace/test_llm_structured_completion_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def test_llm_structured_completion_model_filtering(client: WeaveClient):


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_predict_text_response(mock_get_client):
"""Test the predict function with mocked LLM API response for text format."""
Expand Down Expand Up @@ -245,7 +245,7 @@ def test_llm_structured_completion_model_predict_text_response(mock_get_client):


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_predict_json_response(mock_get_client):
"""Test the predict function with mocked LLM API response for JSON format."""
Expand Down Expand Up @@ -284,7 +284,7 @@ def test_llm_structured_completion_model_predict_json_response(mock_get_client):


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_predict_with_template(mock_get_client):
"""Test the predict function with message templates and template variables."""
Expand Down Expand Up @@ -343,7 +343,7 @@ def test_llm_structured_completion_model_predict_with_template(mock_get_client):


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_predict_with_config_override(mock_get_client):
"""Test the predict function with config parameter overriding defaults."""
Expand Down Expand Up @@ -395,7 +395,7 @@ def test_llm_structured_completion_model_predict_with_config_override(mock_get_c


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_predict_error_handling(mock_get_client):
"""Test the predict function error handling."""
Expand Down Expand Up @@ -656,7 +656,7 @@ def test_cast_to_message():


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_predict_with_prompt(
mock_get_client, client: WeaveClient
Expand Down Expand Up @@ -733,7 +733,7 @@ def test_llm_structured_completion_model_predict_with_prompt(


@patch(
"weave.trace_server.interface.builtin_object_classes.llm_structured_model.get_weave_client"
"weave.shared.builtin_object_classes.llm_structured_model.get_weave_client"
)
def test_llm_structured_completion_model_prompt_takes_precedence(
mock_get_client, client: WeaveClient
Expand Down
2 changes: 1 addition & 1 deletion weave/flow/annotation_spec.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from weave.trace_server.interface.builtin_object_classes import annotation_spec
from weave.shared.builtin_object_classes import annotation_spec

# Re-export:
AnnotationSpec = annotation_spec.AnnotationSpec
2 changes: 1 addition & 1 deletion weave/flow/leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from dataclasses import dataclass
from typing import Any

from weave.shared.builtin_object_classes import leaderboard
from weave.trace.refs import OpRef
from weave.trace.weave_client import WeaveClient, get_ref
from weave.trace_server.interface.builtin_object_classes import leaderboard
from weave.trace_server.trace_server_interface import CallsFilter
from weave.utils.project_id import from_project_id

Expand Down
8 changes: 4 additions & 4 deletions weave/flow/saved_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from pydantic import BaseModel
from typing_extensions import Self

from weave.shared.builtin_object_classes.saved_view import Column, Pin
from weave.shared.builtin_object_classes.saved_view import (
SavedView as SavedViewBase,
)
from weave.trace import urls
from weave.trace.api import publish as weave_publish
from weave.trace.api import ref as weave_ref
Expand All @@ -20,10 +24,6 @@
from weave.trace_server import trace_server_interface as tsi
from weave.trace_server.common_interface import SortBy
from weave.trace_server.interface import query as tsi_query
from weave.trace_server.interface.builtin_object_classes.saved_view import Column, Pin
from weave.trace_server.interface.builtin_object_classes.saved_view import (
SavedView as SavedViewBase,
)

KNOWN_COLUMNS = [
"id",
Expand Down
6 changes: 3 additions & 3 deletions weave/scorers/llm_as_a_judge_scorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

from weave.flow.scorer import Scorer
from weave.prompt.prompt import MessagesPrompt
from weave.shared.builtin_object_classes.llm_structured_model import (
LLMStructuredCompletionModel,
)
from weave.trace.context.weave_client_context import get_weave_client
from weave.trace.objectify import maybe_objectify, register_object
from weave.trace.op import op
from weave.trace.vals import make_trace_obj
from weave.trace_server.interface.builtin_object_classes.llm_structured_model import (
LLMStructuredCompletionModel,
)


@register_object
Expand Down
75 changes: 75 additions & 0 deletions weave/shared/builtin_object_classes/alert_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field

from weave.shared.builtin_object_classes import base_object_def


class WeaveMetricThresholdSpec(BaseModel):
"""Alert specification for weave metric threshold alerts.

Fields align with gorilla's WeaveMetricThresholdFilter and the
alert_worker's WeaveMetricFilter. Extra keys are permitted so that
the schema can evolve without breaking existing stored objects.
"""

model_config = ConfigDict(extra="allow")

alert_condition: str = Field(
default="THRESHOLD",
description="Trigger condition type (e.g. 'THRESHOLD')",
)
comparison_operator: Literal["GREATER_THAN", "LESS_THAN", "EQUAL"] = Field(
default="GREATER_THAN",
description="Direction of the threshold comparison",
)
metric_path: str = Field(
default="",
description="Dot-notation path to the metric value in the call (e.g. 'output.score')",
)
op_str: str = Field(
default="",
description="Op or scorer ref to scope this alert to; empty means all ops",
)
threshold: float = Field(
default=0.0,
description="Threshold value that triggers the alert",
)
window_size: int | None = Field(
default=None,
description="Max number of recent calls to include in the window",
)
window_duration: int | None = Field(
default=None,
description="Time window in seconds for historical calls",
)
monitor_ref: str | None = Field(
default=None,
description="Monitor ref; presence indicates alert was created from a monitor",
)
scorer_ref: str | None = Field(
default=None,
description="Scorer object ref identifying which scorer instance within a monitor to alert on. "
"Disambiguates scorers that share the same op class (e.g. multiple LLMAsAJudgeScorer instances). "
"Filterable via inputs.self on scorer calls or input_refs on CallsFilter.",
)
aggregation_function: Literal["mean", "median", "min", "max", "mode"] = Field(
default="mean",
description="Aggregation function applied to metric values within the window before threshold comparison",
)


class AlertSpec(base_object_def.BaseObject):
op_scope: list[str] | None = Field(
default=None,
description="If provided, this alert only applies to calls from the given op refs",
examples=[
["weave:///entity/project/op/name:digest"],
["weave:///entity/project/op/name:*"],
],
)

spec: WeaveMetricThresholdSpec = Field(
default_factory=WeaveMetricThresholdSpec,
description="Alert specification (threshold config, window config, etc.)",
)
126 changes: 126 additions & 0 deletions weave/shared/builtin_object_classes/annotation_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import Any

from pydantic import BaseModel, Field, create_model, field_validator, model_validator
from pydantic.fields import FieldInfo

from weave.shared.builtin_object_classes import base_object_def

SUPPORTED_PRIMITIVES = (int, float, bool, str)


class AnnotationSpec(base_object_def.BaseObject):
field_schema: dict[str, Any] = Field(
default={},
description="Expected to be valid JSON Schema. Can be provided as a dict, a Pydantic model class, a tuple of a primitive type and a Pydantic Field, or primitive type",
examples=[
# String feedback
{"type": "string", "maxLength": 100},
# Number feedback
{"type": "number", "minimum": 0, "maximum": 100},
# Integer feedback
{"type": "integer", "minimum": 0, "maximum": 100},
# Boolean feedback
{"type": "boolean"},
# Categorical feedback
{"type": "string", "enum": ["option1", "option2"]},
],
)

# TODO
# If true, all unique creators will have their
# own value for this feedback type. Otherwise,
# by default, the value is shared and can be edited.
unique_among_creators: bool = False

# TODO
# If provided, this feedback type will only be shown
# when a call is generated from the given op ref
op_scope: list[str] | None = Field(
default=None,
examples=[
["weave:///entity/project/op/name:digest"],
["weave:///entity/project/op/name:*"],
],
)

@model_validator(mode="before")
@classmethod
def preprocess_field_schema(cls, data: dict[str, Any]) -> dict[str, Any]:
if "field_schema" not in data:
return data

field_schema = data["field_schema"]

temp_field_tuple = None
# Handle Pydantic Field
if isinstance(field_schema, tuple):
if len(field_schema) != 2:
raise ValueError("Expected a tuple of length 2")
annotation, field = field_schema
if (
not isinstance(annotation, type)
) or annotation not in SUPPORTED_PRIMITIVES:
raise TypeError("Expected annotation to be a primitive type")
if not isinstance(field, FieldInfo):
raise TypeError("Expected field to be a Pydantic Field")
temp_field_tuple = (annotation, field)
elif field_schema in SUPPORTED_PRIMITIVES:
temp_field_tuple = (field_schema, Field())

if temp_field_tuple is not None:
# Create a temporary model to leverage Pydantic's schema generation
TempModel = create_model("TempModel", field=temp_field_tuple) # noqa: N806

schema = TempModel.model_json_schema()["properties"]["field"]

if (
"title" in schema and schema["title"] == "Field"
): # default title for Field
schema.pop("title")

data["field_schema"] = schema
return data

# Handle Pydantic model
if isinstance(field_schema, type) and issubclass(field_schema, BaseModel):
# Read back through `data` (typed `Any`): pydantic is invisible to
# the type-check env, so `issubclass` cannot narrow off bare `type`.
data["field_schema"] = data["field_schema"].model_json_schema()
return data

return data

@field_validator("field_schema")
def validate_field_schema(cls, schema: dict[str, Any]) -> dict[str, Any]: # noqa: N805
# Imported lazily: `import jsonschema` eagerly loads every installed
# format-checker lib (rfc3987_syntax builds a Lark grammar, ~0.45s),
# so keeping it out of module scope avoids paying that on `import weave`.
import jsonschema

# Validate the schema
try:
jsonschema.validate(None, schema)
except jsonschema.exceptions.SchemaError:
raise
except jsonschema.exceptions.ValidationError:
pass # we don't care that `None` does not conform
return schema

def value_is_valid(self, payload: Any) -> bool:
"""Validates a payload against this annotation spec's schema.

Args:
payload: The data to validate against the schema

Returns:
bool: True if validation succeeds, False otherwise
"""
# Lazy import: see note in validate_field_schema (avoids ~0.45s of
# jsonschema format-checker imports at `import weave` time).
import jsonschema

try:
jsonschema.validate(payload, self.field_schema)
except jsonschema.exceptions.ValidationError:
return False
return True
10 changes: 10 additions & 0 deletions weave/shared/builtin_object_classes/base_object_def.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pydantic

RefStr = str


# This is just an alternative to weave.Object for the server side.
# I _think_ this will go away once we have the full weave system on the server
class BaseObject(pydantic.BaseModel):
name: str | None = None
description: str | None = None
Loading
Loading