diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6f86b504b29e..665f91779c1e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -235,6 +235,7 @@ jobs: "trace", "trace_server", "anthropic", + "bedrock", "cerebras", "cohere", "dspy", diff --git a/docs/docs/guides/integrations/bedrock.md b/docs/docs/guides/integrations/bedrock.md new file mode 100644 index 000000000000..0fe0db80c0c7 --- /dev/null +++ b/docs/docs/guides/integrations/bedrock.md @@ -0,0 +1,137 @@ +# Amazon Bedrock + +Weave automatically tracks and logs LLM calls made via Amazon Bedrock, AWS's fully managed service that offers foundation models from leading AI companies through a unified API. + +## Traces + +Weave will automatically capture traces for Bedrock API calls. You can use the Bedrock client as usual after initializing Weave and patching the client: + +```python +import weave +import boto3 +import json +from weave.integrations.bedrock.bedrock_sdk import patch_client + +weave.init("my_bedrock_app") + +# Create and patch the Bedrock client +client = boto3.client("bedrock-runtime") +patch_client(client) + +# Use the client as usual +response = client.invoke_model( + modelId="anthropic.claude-3-5-sonnet-20240620-v1:0", + body=json.dumps({ + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 100, + "messages": [ + {"role": "user", "content": "What is the capital of France?"} + ] + }), + contentType='application/json', + accept='application/json' +) +response_dict = json.loads(response.get('body').read()) +print(response_dict["content"][0]["text"]) +``` + +of using the `converse` API: + +```python +messages = [{"role": "user", "content": [{"text": "What is the capital of France?"}]}] + +response = client.converse( + modelId="anthropic.claude-3-5-sonnet-20240620-v1:0", + system=[{"text": "You are a helpful AI assistant."}], + messages=messages, + inferenceConfig={"maxTokens": 100}, +) +print(response["output"]["message"]["content"][0]["text"]) + +``` + +## Wrapping with your own ops + +You can create reusable operations using the `@weave.op()` decorator. Here's an example showing both the `invoke_model` and `converse` APIs: + +```python +@weave.op +def call_model_invoke( + model_id: str, + prompt: str, + max_tokens: int = 100, + temperature: float = 0.7 +) -> dict: + body = json.dumps({ + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": max_tokens, + "temperature": temperature, + "messages": [ + {"role": "user", "content": prompt} + ] + }) + + response = client.invoke_model( + modelId=model_id, + body=body, + contentType='application/json', + accept='application/json' + ) + return json.loads(response.get('body').read()) + +@weave.op +def call_model_converse( + model_id: str, + messages: str, + system_message: str, + max_tokens: int = 100, +) -> dict: + response = client.converse( + modelId=model_id, + system=[{"text": system_message}], + messages=messages, + inferenceConfig={"maxTokens": max_tokens}, + ) + return response +``` + +![](./imgs/bedrock_converse.png) + +## Create a `Model` for easier experimentation + +You can create a Weave Model to better organize your experiments and capture parameters. Here's an example using the `converse` API: + +```python +class BedrockLLM(weave.Model): + model_id: str + max_tokens: int = 100 + system_message: str = "You are a helpful AI assistant." + + @weave.op + def predict(self, prompt: str) -> str: + "Generate a response using Bedrock's converse API" + + messages = [{ + "role": "user", + "content": [{"text": prompt}] + }] + + response = client.converse( + modelId=self.model_id, + system=[{"text": self.system_message}], + messages=messages, + inferenceConfig={"maxTokens": self.max_tokens}, + ) + return response["output"]["message"]["content"][0]["text"] + +# Create and use the model +model = BedrockLLM( + model_id="anthropic.claude-3-5-sonnet-20240620-v1:0", + max_tokens=100, + system_message="You are an expert software engineer that knows a lot of programming. You prefer short answers." +) +result = model.predict("What is the best way to handle errors in Python?") +print(result) +``` + +This approach allows you to version your experiments and easily track different configurations of your Bedrock-based application. diff --git a/docs/docs/guides/integrations/imgs/bedrock_converse.png b/docs/docs/guides/integrations/imgs/bedrock_converse.png new file mode 100644 index 000000000000..1e1fb281baf0 Binary files /dev/null and b/docs/docs/guides/integrations/imgs/bedrock_converse.png differ diff --git a/docs/docs/guides/tracking/objects.md b/docs/docs/guides/tracking/objects.md index ffb26972f4cd..23e806d60b1f 100644 --- a/docs/docs/guides/tracking/objects.md +++ b/docs/docs/guides/tracking/objects.md @@ -58,6 +58,28 @@ Saving an object with a name will create the first version of that object if it +## Deleting an object + + + + To delete a version of an object, call `.delete()` on the object ref. + + ```python + weave.init('intro-example') + cat_names_ref = weave.ref('cat-names:v1') + cat_names_ref.delete() + ``` + + Trying to access a deleted object will result in an error. Resolving an object that has a reference to a deleted object will return a `DeletedRef` object in place of the deleted object. + + + + ```plaintext + This feature is not available in TypeScript yet. Stay tuned! + ``` + + + ## Ref styles A fully qualified weave object ref uri looks like this: diff --git a/docs/docs/guides/tracking/ops.md b/docs/docs/guides/tracking/ops.md index b5751ff57617..c78aacbb0efb 100644 --- a/docs/docs/guides/tracking/ops.md +++ b/docs/docs/guides/tracking/ops.md @@ -156,3 +156,25 @@ If you want to suppress the printing of call links during logging, you can set t ```bash export WEAVE_PRINT_CALL_LINK=false ``` + +## Deleting an op + + + + To delete a version of an op, call `.delete()` on the op ref. + + ```python + weave.init('intro-example') + my_op_ref = weave.ref('track_me:v1') + my_op_ref.delete() + ``` + + Trying to access a deleted op will result in an error. + + + + ```plaintext + This feature is not available in TypeScript yet. Stay tuned! + ``` + + diff --git a/docs/docs/guides/tracking/tracing.mdx b/docs/docs/guides/tracking/tracing.mdx index fbdf6bea96f9..e0efd4cd4453 100644 --- a/docs/docs/guides/tracking/tracing.mdx +++ b/docs/docs/guides/tracking/tracing.mdx @@ -5,6 +5,7 @@ import TracingCallsMacroImage from '@site/static/img/screenshots/calls_macro.png import TracingCallsFilterImage from '@site/static/img/screenshots/calls_filter.png'; import BasicCallImage from '@site/static/img/screenshots/basic_call.png'; + # Calls =1.0.0"] pandas-test = ["pandas>=2.2.3"] modal = ["modal", "python-dotenv"] vertexai = ["vertexai>=1.70.0"] +bedrock = ["boto3", "moto[bedrock]>=5.0.24"] test = [ "nox", "pytest>=8.2.0", @@ -230,7 +231,7 @@ module = "weave_query.*" ignore_errors = true [tool.bumpversion] -current_version = "0.51.29-dev0" +current_version = "0.51.30-dev0" parse = """(?x) (?P0|[1-9]\\d*)\\. (?P0|[1-9]\\d*)\\. diff --git a/scripts/benchmark_init.py b/scripts/benchmark_init.py new file mode 100644 index 000000000000..7cd54eb61081 --- /dev/null +++ b/scripts/benchmark_init.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 + +import argparse +import statistics +import subprocess +import sys +from datetime import datetime + +from rich.console import Console +from rich.progress import Progress, SpinnerColumn, TimeElapsedColumn +from rich.table import Table + +from weave.trace.autopatch import AutopatchSettings + + +def today() -> str: + return datetime.today().date().strftime("%Y-%m-%d") + + +def run_single_init(disable_autopatch: bool = False): + projname = today() + "_benchmark_init" + cmd = rf""" +import time +import weave +start = time.perf_counter() +weave.init("{projname}", autopatch_settings={{"disable_autopatch": {disable_autopatch}}}) +end = time.perf_counter() +print(end - start) +""" + result = subprocess.run([sys.executable, "-c", cmd], capture_output=True, text=True) + # Get the last line of output (Skipping "Logged in as Weights & Biases user...") + output = result.stdout.strip().split("\n")[-1] + print(output) + return float(output) + + +def benchmark(iterations: int = 10, disable_autopatch: bool = False): + projname = today() + "_benchmark_init" + # Ensure project exists + import weave + + weave.init(projname, autopatch_settings=AutopatchSettings(disable_autopatch=True)) + + console = Console() + times = [] + + with Progress( + SpinnerColumn(), + *Progress.get_default_columns(), + TimeElapsedColumn(), + console=console, + ) as progress: + task = progress.add_task("[cyan]Running init tests...", total=iterations) + + for _ in range(iterations): + times.append(run_single_init(disable_autopatch)) + progress.advance(task) + + # Display results in a nice table + table = Table(title="Init Time Benchmark Results") + table.add_column("Metric", style="cyan") + table.add_column("Value", style="green") + + table.add_row("Mean init time", f"{statistics.mean(times):.4f}s") + table.add_row("Median init time", f"{statistics.median(times):.4f}s") + table.add_row("Std dev", f"{statistics.stdev(times):.4f}s") + table.add_row("Min time", f"{min(times):.4f}s") + table.add_row("Max time", f"{max(times):.4f}s") + + console.print("\n") + console.print(table) + + # Show individual times + times_table = Table(title="Individual Init Times") + times_table.add_column("Run #", style="cyan") + times_table.add_column("Time (seconds)", style="green") + + for i, t in enumerate(times, 1): + times_table.add_row(str(i), f"{t:.4f}") + + console.print("\n") + console.print(times_table) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Benchmark weave.init performance") + parser.add_argument( + "--disable-autopatch", action="store_true", help="Disable autopatching" + ) + parser.add_argument( + "--iterations", + type=int, + default=10, + help="Number of iterations to run (default: 10)", + ) + args = parser.parse_args() + + benchmark(iterations=args.iterations, disable_autopatch=args.disable_autopatch) diff --git a/tests/integrations/bedrock/bedrock_test.py b/tests/integrations/bedrock/bedrock_test.py new file mode 100644 index 000000000000..79f7c743f307 --- /dev/null +++ b/tests/integrations/bedrock/bedrock_test.py @@ -0,0 +1,302 @@ +import io +import json +from unittest.mock import patch + +import boto3 +import botocore +import pytest +from moto import mock_aws + +import weave +from weave.integrations.bedrock import patch_client + +model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0" +system_message = "You are an expert software engineer that knows a lot of programming. You prefer short answers." +messages = [ + { + "role": "user", + "content": [ + { + "text": ( + "In Bash, how do I list all text files in the current directory " + "(excluding subdirectories) that have been modified in the last month?" + ) + } + ], + } +] + +invoke_prompt = ( + "In Bash, how do I list all text files in the current directory " + "(excluding subdirectories) that have been modified in the last month?" +) + +# Mock responses +MOCK_CONVERSE_RESPONSE = { + "ResponseMetadata": { + "RequestId": "917ceb8d-3a0a-4649-b3bb-527494c17a69", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "date": "Fri, 20 Dec 2024 16:44:08 GMT", + "content-type": "application/json", + "content-length": "323", + "connection": "keep-alive", + "x-amzn-requestid": "917ceb8d-3a0a-4649-b3bb-527494c17a69", + }, + "RetryAttempts": 0, + }, + "output": { + "message": { + "role": "assistant", + "content": [ + { + "text": "To list all text files in the current directory (excluding subdirectories) " + "that have been modified in the last month using Bash, you can use" + } + ], + } + }, + "stopReason": "max_tokens", + "usage": {"inputTokens": 40, "outputTokens": 30, "totalTokens": 70}, + "metrics": {"latencyMs": 838}, +} + +MOCK_STREAM_EVENTS = [ + {"messageStart": {"role": "assistant"}}, + {"contentBlockDelta": {"delta": {"text": "To"}, "contentBlockIndex": 0}}, + { + "contentBlockDelta": { + "delta": {"text": " list all text files"}, + "contentBlockIndex": 0, + } + }, + { + "contentBlockDelta": { + "delta": {"text": " in the current directory"}, + "contentBlockIndex": 0, + } + }, + {"contentBlockDelta": {"delta": {"text": " modifie"}, "contentBlockIndex": 0}}, + { + "contentBlockDelta": { + "delta": {"text": "d in the last month"}, + "contentBlockIndex": 0, + } + }, + {"contentBlockDelta": {"delta": {"text": ", use"}, "contentBlockIndex": 0}}, + {"contentBlockDelta": {"delta": {"text": ":"}, "contentBlockIndex": 0}}, + {"contentBlockDelta": {"delta": {"text": "\n\n```bash"}, "contentBlockIndex": 0}}, + {"contentBlockDelta": {"delta": {"text": "\nfind . -max"}, "contentBlockIndex": 0}}, + {"contentBlockDelta": {"delta": {"text": "depth "}, "contentBlockIndex": 0}}, + {"contentBlockDelta": {"delta": {"text": "1"}, "contentBlockIndex": 0}}, + {"contentBlockStop": {"contentBlockIndex": 0}}, + {"messageStop": {"stopReason": "max_tokens"}}, + { + "metadata": { + "usage": {"inputTokens": 55, "outputTokens": 30, "totalTokens": 85}, + "metrics": {"latencyMs": 926}, + } + }, +] + +MOCK_INVOKE_RESPONSE = { + "body": io.BytesIO( + json.dumps( + { + "id": "msg_bdrk_01WpFc3918C93ZG9ZMKVqzCd", + "type": "message", + "role": "assistant", + "model": "claude-3-5-sonnet-20240620", + "content": [ + { + "type": "text", + "text": "To list all text files in the current directory (excluding subdirectories) " + "that have been modified in the last month using Bash, you can use", + } + ], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 40, "output_tokens": 30, "total_tokens": 70}, + } + ).encode("utf-8") + ), + "ContentType": "application/json", +} + +# Original botocore _make_api_call function +orig = botocore.client.BaseClient._make_api_call + + +def mock_converse_make_api_call(self, operation_name, kwarg): + if operation_name == "Converse": + return MOCK_CONVERSE_RESPONSE + elif operation_name == "ConverseStream": + + class MockStream: + def __iter__(self): + yield from MOCK_STREAM_EVENTS + + return {"stream": MockStream()} + return orig(self, operation_name, kwarg) + + +def mock_invoke_make_api_call(self, operation_name, kwarg): + if operation_name == "InvokeModel": + return MOCK_INVOKE_RESPONSE + return orig(self, operation_name, kwarg) + + +@pytest.mark.skip_clickhouse_client +@mock_aws +def test_bedrock_converse(client: weave.trace.weave_client.WeaveClient) -> None: + bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1") + patch_client(bedrock_client) + + with patch( + "botocore.client.BaseClient._make_api_call", new=mock_converse_make_api_call + ): + response = bedrock_client.converse( + modelId=model_id, + system=[{"text": system_message}], + messages=messages, + inferenceConfig={"maxTokens": 30}, + ) + + # Existing checks + assert response is not None + assert "output" in response + assert "message" in response["output"] + assert "content" in response["output"]["message"] + + # Now verify that a trace was captured. + calls = list(client.calls()) + assert len(calls) == 1, "Expected exactly one trace call" + call = calls[0] + + assert call.exception is None + assert call.ended_at is not None + + # Inspect the captured output if desired + output = call.output + + # Confirm we see the same text as in the mock response + assert ( + output["output"]["message"]["content"][0]["text"] + == "To list all text files in the current directory (excluding subdirectories) that have been modified in the last month using Bash, you can use" + ) + + # Check usage in a style similar to mistral tests + summary = call.summary + assert summary is not None, "Summary should not be None" + # We'll reference usage by the model_id + model_usage = summary["usage"][model_id] + assert model_usage["requests"] == 1, "Expected exactly one request increment" + # Map the tokens to pydantic usage fields + # "inputTokens" -> prompt_tokens, "outputTokens" -> completion_tokens + assert output["usage"]["inputTokens"] == model_usage["prompt_tokens"] == 40 + assert output["usage"]["outputTokens"] == model_usage["completion_tokens"] == 30 + assert output["usage"]["totalTokens"] == model_usage["total_tokens"] == 70 + + +@pytest.mark.skip_clickhouse_client +@mock_aws +def test_bedrock_converse_stream(client: weave.trace.weave_client.WeaveClient) -> None: + bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1") + patch_client(bedrock_client) + + with patch( + "botocore.client.BaseClient._make_api_call", new=mock_converse_make_api_call + ): + response = bedrock_client.converse_stream( + modelId=model_id, + system=[{"text": system_message}], + messages=messages, + inferenceConfig={"maxTokens": 30}, + ) + + # Existing checks + stream = response.get("stream") + assert stream is not None, "Stream not found in response" + + # Accumulate the streamed response + final_response = "" + for event in stream: + if "contentBlockDelta" in event: + final_response += event["contentBlockDelta"]["delta"]["text"] + + assert final_response is not None + + # Now verify that a trace was captured. + calls = list(client.calls()) + assert len(calls) == 1, "Expected exactly one trace call for the stream test" + call = calls[0] + + assert call.exception is None + assert call.ended_at is not None + + output = call.output + # For a streaming response, you might confirm final partial text is present + # in the final output or usage data is recorded + + assert "To list all text files" in output["content"] + + # Check usage in a style similar to mistral tests + summary = call.summary + assert summary is not None, "Summary should not be None" + model_usage = summary["usage"][model_id] + assert model_usage["requests"] == 1 + assert output["usage"]["inputTokens"] == model_usage["prompt_tokens"] == 55 + assert output["usage"]["outputTokens"] == model_usage["completion_tokens"] == 30 + assert output["usage"]["totalTokens"] == model_usage["total_tokens"] == 85 + + +@pytest.mark.skip_clickhouse_client +@mock_aws +def test_bedrock_invoke(client: weave.trace.weave_client.WeaveClient) -> None: + bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1") + patch_client(bedrock_client) + + with patch( + "botocore.client.BaseClient._make_api_call", new=mock_invoke_make_api_call + ): + # Call the custom op that wraps the bedrock_client.invoke_model call + body = json.dumps( + { + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 30, + "temperature": 0.7, + "messages": [{"role": "user", "content": invoke_prompt}], + } + ) + + response = bedrock_client.invoke_model( + modelId=model_id, + body=body, + contentType="application/json", + accept="application/json", + ) + response = json.loads(response.get("body").read()) + # Basic assertions on the response + assert response is not None + assert "type" in response + assert response["type"] == "message" + assert "content" in response + assert response["content"][0]["text"].startswith( + "To list all text files in the current directory" + ) + + # Check that a trace was captured + calls = list(client.calls()) + assert len(calls) == 1, "Expected exactly one trace call for invoke command" + call = calls[0] + assert call.exception is None + assert call.ended_at is not None + + output = call.output + assert "body" in output + # Confirm usage in the summary + summary = call.summary + print(summary) + assert summary is not None, "Summary should not be None" + model_usage = summary["usage"][model_id] + assert model_usage["requests"] == 1 diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/FilterRow.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/FilterRow.tsx index e0444c4d6398..333690383255 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/FilterRow.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/FilterRow.tsx @@ -6,7 +6,12 @@ import {GridFilterItem} from '@mui/x-data-grid-pro'; import React, {useMemo} from 'react'; import {Button} from '../../../../Button'; -import {FilterId, getFieldType, getOperatorOptions, isWeaveRef} from './common'; +import { + FilterId, + getFieldType, + getGroupedOperatorOptions, + isWeaveRef, +} from './common'; import {SelectField, SelectFieldOption} from './SelectField'; import {SelectOperator} from './SelectOperator'; import {SelectValue} from './SelectValue'; @@ -36,7 +41,7 @@ export const FilterRow = ({ }; const operatorOptions = useMemo( - () => getOperatorOptions(item.field), + () => getGroupedOperatorOptions(item.field), [item.field] ); diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/SelectOperator.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/SelectOperator.tsx index 01966b1fa7c1..45ae20c796bf 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/SelectOperator.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/SelectOperator.tsx @@ -3,12 +3,13 @@ */ import {Select} from '@wandb/weave/components/Form/Select'; import React from 'react'; +import {components, GroupHeadingProps} from 'react-select'; import {Tooltip} from '../../../../Tooltip'; -import {SelectOperatorOption} from './common'; +import {OperatorGroupedOption, SelectOperatorOption} from './common'; type SelectOperatorProps = { - options: SelectOperatorOption[]; + options: OperatorGroupedOption[]; value: string; onSelectOperator: (value: string) => void; isDisabled?: boolean; @@ -24,13 +25,22 @@ const OptionLabel = (props: SelectOperatorOption) => { ); }; +const GroupHeading = ( + props: GroupHeadingProps +) => { + return ; +}; + export const SelectOperator = ({ options, value, onSelectOperator, isDisabled, }: SelectOperatorProps) => { - const selectedOption = options.find(o => o.value === value) ?? options[0]; + // Find the operator from the grouped selection: + const flattenedOptions = options.flatMap(group => group.options); + const selectedOption = + flattenedOptions.find(o => o.value === value) ?? flattenedOptions[0]; const onReactSelectChange = (option: SelectOperatorOption | null) => { if (option) { @@ -39,13 +49,14 @@ export const SelectOperator = ({ }; return ( - + options={options} value={selectedOption} placeholder="Select operator" onChange={onReactSelectChange} formatOptionLabel={OptionLabel} isDisabled={isDisabled} + components={{GroupHeading}} /> ); }; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/common.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/common.ts index a0abb3c6801c..f8603c86b44a 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/common.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/filters/common.ts @@ -75,64 +75,108 @@ export const getFieldType = (field: string): string => { return FIELD_TYPE[field] ?? 'text'; }; -const allOperators = [ +export type OperatorGroup = 'string' | 'number' | 'boolean' | 'date' | 'any'; +export type SelectOperatorOption = { + label: string; + value: string; + group: OperatorGroup; +}; + +const allOperators: SelectOperatorOption[] = [ { value: '(string): contains', label: 'contains', + group: 'string', }, { value: '(string): equals', label: 'equals', + group: 'string', }, { value: '(string): in', label: 'in', + group: 'string', }, { value: '(number): =', label: '=', + group: 'number', }, { value: '(number): !=', label: '≠', + group: 'number', }, { value: '(number): <', label: '<', + group: 'number', }, { value: '(number): <=', label: '≤', + group: 'number', }, { value: '(number): >', label: '>', + group: 'number', }, { value: '(number): >=', label: '≥', + group: 'number', }, { value: '(bool): is', label: 'is', + group: 'boolean', }, { value: '(date): after', label: 'after', + group: 'date', }, { value: '(date): before', label: 'before', + group: 'date', }, { value: '(any): isEmpty', label: 'is empty', + group: 'any', }, { value: '(any): isNotEmpty', label: 'is not empty', + group: 'any', }, ]; + +// Display labels +const GROUP_LABELS: Record = { + string: 'Text', + number: 'Number', + boolean: 'Boolean', + date: 'Date', + any: 'Other', +}; + +export function getGroupedOperatorOptions( + field: string +): OperatorGroupedOption[] { + // Get operators / operator groups + const availableOperators = getOperatorOptions(field); + const groups = [...new Set(availableOperators.map(op => op.group))]; + // Create grouped options + return groups.map(group => ({ + label: GROUP_LABELS[group], + options: availableOperators.filter(op => op.group === group), + })); +} + const operatorLabels: Record = allOperators.reduce( (acc, operator) => { acc[operator.value] = operator.label; @@ -151,11 +195,6 @@ export const isNumericOperator = (operator: string) => { return operator.startsWith('(number):'); }; -export type SelectOperatorOption = { - value: string; - label: string; -}; - export const getOperatorLabel = (operatorValue: string): string => { const label = operatorLabels[operatorValue]; if (label) { @@ -184,10 +223,12 @@ export const getOperatorOptions = (field: string): SelectOperatorOption[] => { { value: '(string): equals', label: 'equals', + group: 'string', }, { value: '(string): in', label: 'in', + group: 'string', }, ]; } @@ -196,10 +237,12 @@ export const getOperatorOptions = (field: string): SelectOperatorOption[] => { { value: '(date): after', label: 'after', + group: 'date', }, { value: '(date): before', label: 'before', + group: 'date', }, ]; } @@ -208,10 +251,12 @@ export const getOperatorOptions = (field: string): SelectOperatorOption[] => { { value: 'is', label: 'is', + group: 'boolean', }, { value: 'is not', label: 'is not', + group: 'boolean', }, ]; } @@ -220,6 +265,7 @@ export const getOperatorOptions = (field: string): SelectOperatorOption[] => { { value: '(string): equals', label: 'equals', + group: 'string', }, ]; } @@ -312,3 +358,8 @@ export const upsertFilter = ( items, }; }; + +export type OperatorGroupedOption = { + label: string; + options: SelectOperatorOption[]; +}; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts b/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts index ed9adf9d22a2..45eea688a37e 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/grid/pagination.ts @@ -1,7 +1,7 @@ import {GridPaginationModel} from '@mui/x-data-grid-pro'; const MAX_PAGE_SIZE = 100; -export const DEFAULT_PAGE_SIZE = 100; +export const DEFAULT_PAGE_SIZE = 50; export const getValidPaginationModel = ( queryPage: string | undefined, diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallDetails.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallDetails.tsx index 03519960bf37..3e2cb8886771 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallDetails.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/CallDetails.tsx @@ -189,6 +189,7 @@ export const CallDetails: FC<{ entity={call.entity} project={call.project} allowedColumnPatterns={ALLOWED_COLUMN_PATTERNS} + paginationModel={isPeeking ? {page: 0, pageSize: 10} : undefined} /> ); if (isPeeking) { diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ValueViewString.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ValueViewString.tsx index 0e017b929feb..2de53bffead0 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ValueViewString.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallPage/ValueViewString.tsx @@ -39,13 +39,13 @@ const Spacer = styled.div` `; Spacer.displayName = 'S.Spacer'; -const Collapsed = styled.div<{hasScrolling: boolean}>` +const Collapsed = styled.div` min-height: 38px; line-height: 38px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - cursor: ${props => (props.hasScrolling ? 'pointer' : 'default')}; + cursor: pointer; `; Collapsed.displayName = 'S.Collapsed'; @@ -100,12 +100,10 @@ export const ValueViewString = ({value, isExpanded}: ValueViewStringProps) => { setMode(hasScrolling ? (isExpanded ? 1 : 0) : 0); }, [hasScrolling, isExpanded]); - const onClick = hasScrolling - ? () => { - const numModes = hasFull ? 3 : 2; - setMode((mode + 1) % numModes); - } - : undefined; + const onClick = () => { + const numModes = hasFull ? 3 : 2; + setMode((mode + 1) % numModes); + }; const copy = useCallback(() => { copyToClipboard(value); toast('Copied to clipboard'); @@ -209,9 +207,5 @@ export const ValueViewString = ({value, isExpanded}: ValueViewStringProps) => { ); } - return ( - - {content} - - ); + return {content}; }; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTable.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTable.tsx index 3632664aaf09..fd8ac8c888e6 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTable.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTable.tsx @@ -941,7 +941,6 @@ export const CallsTable: FC<{ paginationMode="server" paginationModel={paginationModel} onPaginationModelChange={onPaginationModelChange} - pageSizeOptions={[DEFAULT_PAGE_SIZE]} // PAGINATION SECTION END rowHeight={38} columns={muiColumns} @@ -949,6 +948,7 @@ export const CallsTable: FC<{ rowSelectionModel={rowSelectionModel} // columnGroupingModel={groupingModel} columnGroupingModel={columns.colGroupingModel} + hideFooter={!callsLoading && callsTotal === 0} hideFooterSelectedRowCount onColumnWidthChange={newCol => { setUserDefinedColumnWidths(curr => { @@ -1022,7 +1022,7 @@ export const CallsTable: FC<{ ); }, columnMenu: CallsCustomColumnMenu, - pagination: PaginationButtons, + pagination: () => , columnMenuSortDescendingIcon: IconSortDescending, columnMenuSortAscendingIcon: IconSortAscending, columnMenuHideIcon: IconNotVisible, diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx index e6956fb3d4a8..52e31390c85c 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/CallsPage/CallsTableButtons.tsx @@ -26,6 +26,7 @@ import classNames from 'classnames'; import React, {Dispatch, FC, SetStateAction, useRef, useState} from 'react'; import * as userEvents from '../../../../../../integrations/analytics/userEvents'; +import {Select} from '../../../../../Form/Select'; import {useWFHooks} from '../wfReactInterface/context'; import {Query} from '../wfReactInterface/traceServerClientInterface/query'; import { @@ -529,28 +530,24 @@ function makeCodeText( sortBy: Array<{field: string; direction: 'asc' | 'desc'}>, includeFeedback: boolean ) { - let codeStr = `import weave\nassert weave.__version__ >= "0.50.14", "Please upgrade weave!" \n\nclient = weave.init("${entity}/${project}")`; - codeStr += `\ncalls = client.server.calls_query_stream({\n`; - codeStr += ` "project_id": "${entity}/${project}",\n`; - + let codeStr = `import weave\nassert weave.__version__ >= "0.51.29", "Please upgrade weave!"\n\nclient = weave.init("${project}")`; + codeStr += `\ncalls = client.get_calls(\n`; const filteredCallIds = callIds ?? filter.callIds; if (filteredCallIds && filteredCallIds.length > 0) { - codeStr += ` "filter": {"call_ids": ["${filteredCallIds.join( - '", "' - )}"]},\n`; + codeStr += ` filter={"call_ids": ["${filteredCallIds.join('", "')}"]},\n`; if (expandColumns.length > 0) { const expandColumnsStr = JSON.stringify(expandColumns, null, 0); - codeStr += ` "expand_columns": ${expandColumnsStr},\n`; + codeStr += ` expand_columns=${expandColumnsStr},\n`; } if (includeFeedback) { - codeStr += ` "include_feedback": true,\n`; + codeStr += ` include_feedback=True,\n`; } // specifying call_ids ignores other filters, return early codeStr += `})`; return codeStr; } if (Object.values(filter).some(value => value !== undefined)) { - codeStr += ` "filter": {`; + codeStr += ` filter={`; if (filter.opVersionRefs) { codeStr += `"op_names": ["${filter.opVersionRefs.join('", "')}"],`; } @@ -573,21 +570,21 @@ function makeCodeText( codeStr += `},\n`; } if (query) { - codeStr += ` "query": ${JSON.stringify(query, null, 0)},\n`; + codeStr += ` query=${JSON.stringify(query, null, 0)},\n`; } if (expandColumns.length > 0) { const expandColumnsStr = JSON.stringify(expandColumns, null, 0); - codeStr += ` "expand_columns": ${expandColumnsStr},\n`; + codeStr += ` expand_columns=${expandColumnsStr},\n`; } if (sortBy.length > 0) { - codeStr += ` "sort_by": ${JSON.stringify(sortBy, null, 0)},\n`; + codeStr += ` sort_by=${JSON.stringify(sortBy, null, 0)},\n`; } if (includeFeedback) { - codeStr += ` "include_feedback": True,\n`; + codeStr += ` include_feedback=True,\n`; } - codeStr += `})`; + codeStr += `)`; return codeStr; } @@ -646,7 +643,16 @@ curl '${baseUrl}/calls/stream_query' \\ return baseCurl; } -export const PaginationButtons = () => { +type PageSizeOption = { + readonly value: number; + readonly label: string; +}; + +type PaginationButtonsProps = { + hideControls?: boolean; +}; + +export const PaginationButtons = ({hideControls}: PaginationButtonsProps) => { const apiRef = useGridApiContext(); const page = useGridSelector(apiRef, gridPageSelector); const pageCount = useGridSelector(apiRef, gridPageCountSelector); @@ -665,35 +671,81 @@ export const PaginationButtons = () => { const start = rowCount > 0 ? page * pageSize + 1 : 0; const end = Math.min(rowCount, (page + 1) * pageSize); + const pageSizes = [10, 25, 50, 100]; + if (!pageSizes.includes(pageSize)) { + pageSizes.push(pageSize); + pageSizes.sort((a, b) => a - b); + } + const pageSizeOptions = pageSizes.map(sz => ({ + value: sz, + label: sz.toString(), + })); + const pageSizeValue = pageSizeOptions.find(o => o.value === pageSize); + const onPageSizeChange = (option: PageSizeOption | null) => { + if (option) { + apiRef.current.setPageSize(option.value); + } + }; + return ( - - +
+ + + Response selected + +
) : ( )} @@ -95,12 +128,11 @@ export const ChoicesDrawer = ({ choice={c} isStructuredOutput={isStructuredOutput} choiceIndex={index} - isNested /> ))} - + ); }; diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChoicesViewCarousel.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChoicesViewCarousel.tsx index 3addbdded819..0612d7d8096f 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChoicesViewCarousel.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/ChoicesViewCarousel.tsx @@ -69,7 +69,7 @@ export const ChoicesViewCarousel = ({ icon="visible" onClick={() => setIsDrawerOpen(true)} tooltip="View all choices"> - Review + View trials } diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx index 8cec95707fa7..560585fd10da 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/ChatView/MessagePanel.tsx @@ -122,8 +122,10 @@ export const MessagePanel = ({
{messageHeader} {isPlayground && editorHeight ? ( @@ -167,7 +169,7 @@ export const MessagePanel = ({ )}
- {isOverflowing && !editorHeight && ( + {isOverflowing && !hasToolCalls && !editorHeight && ( { return (
+ className={classNames('flex w-full items-center justify-center', { + 'pt-[4px]': isShowingMore, + [`absolute z-[1] mt-[-32px] rounded-b-xl bg-gradient-to-t from-70% pb-[4px] pt-[16px] ${ + isUser ? 'from-[#f4fbe8]' : 'from-white' + } to-transparent`]: !isShowingMore, + })}>