|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import inspect
|
| 4 | +import json |
4 | 5 | import warnings
|
5 |
| -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Protocol |
| 6 | +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Literal, Optional, Protocol |
6 | 7 |
|
7 | 8 | from pydantic import BaseModel, Field, create_model
|
8 | 9 |
|
@@ -62,41 +63,69 @@ class ToolResult:
|
62 | 63 | """
|
63 | 64 | A result from a tool invocation
|
64 | 65 |
|
65 |
| - Return this value from a tool if you want to separate what gets sent |
66 |
| - to the model vs what value gets yielded to the user. |
| 66 | + Return an instance of this class from a tool function in order to: |
| 67 | +
|
| 68 | + 1. Yield content for the user (i.e., the downstream consumer of a `.stream()` or `.chat()`) |
| 69 | + to display. |
| 70 | + 2. Control how the tool result gets formatted for the model (i.e., the assistant). |
67 | 71 |
|
68 | 72 | Parameters
|
69 | 73 | ----------
|
70 |
| - value |
71 |
| - The tool's return value. If `serialized_value` is not provided, the |
72 |
| - string representation of this value is sent to the model. |
73 |
| - response_output |
74 |
| - A value to yield when the tool is called during response generation. If |
75 |
| - `None`, no value is yielded. This is primarily useful for producing |
76 |
| - custom UI in the response output to indicate to the user that a tool |
77 |
| - call has completed (for example, return shiny UI here when |
78 |
| - `.stream()`-ing inside a shiny app). |
79 |
| - serialized_value |
80 |
| - The serialized value to send to the model. If `None`, the value is serialized |
81 |
| - using `str()`. This is useful when the value is not JSON |
| 74 | + assistant |
| 75 | + The tool result to send to the llm (i.e., assistant). If the result is |
| 76 | + not a string, `format_as` determines how to the value is formatted |
| 77 | + before sending it to the model. |
| 78 | + user |
| 79 | + A value to yield to the user (i.e., the consumer of a `.stream()`) when |
| 80 | + the tool is called. If `None`, no value is yielded. This is primarily |
| 81 | + useful for producing custom UI in the response output to indicate to the |
| 82 | + user that a tool call has completed (for example, return shiny UI here |
| 83 | + when `.stream()`-ing inside a shiny app). |
| 84 | + format_as |
| 85 | + How to format the `assistant` value for the model. The default, |
| 86 | + `"auto"`, first attempts to format the value as a JSON string. If that |
| 87 | + fails, it gets converted to a string via `str()`. To force |
| 88 | + `json.dumps()` or `str()`, set to `"json"` or `"str"`. Finally, |
| 89 | + `"as_is"` is useful for doing your own formatting and/or passing a |
| 90 | + non-string value (e.g., a list or dict) straight to the model. |
| 91 | + Non-string values are useful for tools that return images or other |
| 92 | + 'known' non-text content types. |
82 | 93 | """
|
83 | 94 |
|
84 | 95 | def __init__(
|
85 | 96 | self,
|
86 |
| - value: Stringable, |
87 |
| - response_output: Optional[Stringable] = None, |
88 |
| - serialized_value: Optional[str] = None, |
| 97 | + assistant: Stringable, |
| 98 | + *, |
| 99 | + user: Optional[Stringable] = None, |
| 100 | + format_as: Literal["auto", "json", "str", "as_is"] = "auto", |
89 | 101 | ):
|
90 |
| - self.value = value |
91 |
| - self.response_output = response_output |
92 |
| - if serialized_value is None: |
93 |
| - serialized_value = str(value) |
94 |
| - self.serialized_value = serialized_value |
| 102 | + # TODO: if called when an active user session, perhaps we could |
| 103 | + # provide a smart default here |
| 104 | + self.user = user |
| 105 | + self.assistant = self._format_value(assistant, format_as) |
95 | 106 | # TODO: we could consider adding an "emit value" -- that is, the thing to
|
96 | 107 | # display when `echo="all"` is used. I imagine that might be useful for
|
97 | 108 | # advanced users, but let's not worry about it until someone asks for it.
|
98 | 109 | # self.emit = emit
|
99 | 110 |
|
| 111 | + def _format_value(self, value: Stringable, mode: str) -> Stringable: |
| 112 | + if isinstance(value, str): |
| 113 | + return value |
| 114 | + |
| 115 | + if mode == "auto": |
| 116 | + try: |
| 117 | + return json.dumps(value) |
| 118 | + except Exception: |
| 119 | + return str(value) |
| 120 | + elif mode == "json": |
| 121 | + return json.dumps(value) |
| 122 | + elif mode == "str": |
| 123 | + return str(value) |
| 124 | + elif mode == "as_is": |
| 125 | + return value |
| 126 | + else: |
| 127 | + raise ValueError(f"Unknown format mode: {mode}") |
| 128 | + |
100 | 129 |
|
101 | 130 | def func_to_schema(
|
102 | 131 | func: Callable[..., Any] | Callable[..., Awaitable[Any]],
|
|
0 commit comments