Skip to content

Commit 649718b

Browse files
author
Andrei Bratu
committed
jsonify output of decorators if necessary
1 parent 4d854ed commit 649718b

File tree

5 files changed

+47
-5
lines changed

5 files changed

+47
-5
lines changed

src/humanloop/client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ def call_llm(messages):
236236
}
237237
```
238238
239+
The decorated function should return a string or the output should be JSON serializable. If
240+
the output cannot be serialized, TypeError will be raised.
241+
242+
If the function raises an exception, the log created by the function will have the output
243+
field set to None and the error field set to the string representation of the exception.
244+
239245
:param path: The path where the Prompt is created. If not
240246
provided, the function name is used as the path and the File
241247
is created in the root of your Humanloop organization workspace.
@@ -308,6 +314,13 @@ def calculator(a: int, b: Optional[int]) -> int:
308314
},
309315
"output": 3
310316
}
317+
```
318+
319+
The decorated function should return a string or the output should be JSON serializable. If
320+
the output cannot be serialized, TypeError will be raised.
321+
322+
If the function raises an exception, the log created by the function will have the output
323+
field set to None and the error field set to the string representation of the exception.
311324
312325
:param path: The path to the Tool. If not provided, the function name
313326
will be used as the path and the File will be created in the root
@@ -365,6 +378,12 @@ def entrypoint():
365378
will be nested, allowing you to track the whole conversation session
366379
between the user and the assistant.
367380
381+
The decorated function should return a string or the output should be JSON serializable. If
382+
the output cannot be serialized, TypeError will be raised.
383+
384+
If the function raises an exception, the log created by the function will have the output
385+
field set to None and the error field set to the string representation of the exception.
386+
368387
:param path: The path to the Flow. If not provided, the function name
369388
will be used as the path and the File will be created in the root
370389
of your organization workspace.

src/humanloop/decorators/flow.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from functools import wraps
34
from typing import Any, Callable, Mapping, Optional, Sequence
@@ -10,7 +11,7 @@
1011
from humanloop.eval_utils.types import File
1112
from humanloop.otel import TRACE_FLOW_CONTEXT, FlowContext
1213
from humanloop.otel.constants import HUMANLOOP_FILE_KEY, HUMANLOOP_FILE_TYPE_KEY, HUMANLOOP_LOG_KEY, HUMANLOOP_PATH_KEY
13-
from humanloop.otel.helpers import generate_span_id, write_to_opentelemetry_span
14+
from humanloop.otel.helpers import generate_span_id, jsonify_if_not_string, write_to_opentelemetry_span
1415
from humanloop.requests import FlowKernelRequestParams as FlowDict
1516
from humanloop.requests.flow_kernel_request import FlowKernelRequestParams
1617

@@ -63,6 +64,10 @@ def wrapper(*args: Sequence[Any], **kwargs: Mapping[str, Any]) -> Any:
6364
# Call the decorated function
6465
try:
6566
output = func(*args, **kwargs)
67+
output = jsonify_if_not_string(
68+
func=func,
69+
output=output,
70+
)
6671
error = None
6772
except Exception as e:
6873
logger.error(f"Error calling {func.__name__}: {e}")

src/humanloop/decorators/prompt.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from humanloop.eval_utils import File
1212
from humanloop.otel import TRACE_FLOW_CONTEXT, FlowContext
1313
from humanloop.otel.constants import HUMANLOOP_FILE_KEY, HUMANLOOP_FILE_TYPE_KEY, HUMANLOOP_LOG_KEY, HUMANLOOP_PATH_KEY
14-
from humanloop.otel.helpers import generate_span_id, write_to_opentelemetry_span
14+
from humanloop.otel.helpers import generate_span_id, jsonify_if_not_string, write_to_opentelemetry_span
1515

1616
logger = logging.getLogger("humanloop.sdk")
1717

@@ -54,6 +54,10 @@ def wrapper(*args: Sequence[Any], **kwargs: Mapping[str, Any]) -> Any:
5454
# Call the decorated function
5555
try:
5656
output = func(*args, **kwargs)
57+
output = jsonify_if_not_string(
58+
func=func,
59+
output=output,
60+
)
5761
error = None
5862
except Exception as e:
5963
logger.error(f"Error calling {func.__name__}: {e}")

src/humanloop/decorators/tool.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
HUMANLOOP_LOG_KEY,
2121
HUMANLOOP_PATH_KEY,
2222
)
23-
from humanloop.otel.helpers import generate_span_id, write_to_opentelemetry_span
23+
from humanloop.otel.helpers import generate_span_id, jsonify_if_not_string, write_to_opentelemetry_span
2424
from humanloop.requests.tool_function import ToolFunctionParams
2525
from humanloop.requests.tool_kernel_request import ToolKernelRequestParams
2626

@@ -72,6 +72,10 @@ def wrapper(*args, **kwargs):
7272
# Call the decorated function
7373
try:
7474
output = func(*args, **kwargs)
75+
output = jsonify_if_not_string(
76+
func=func,
77+
output=output,
78+
)
7579
error = None
7680
except Exception as e:
7781
logger.error(f"Error calling {func.__name__}: {e}")
@@ -178,7 +182,7 @@ def _build_function_parameters_property(func) -> _JSONSchemaFunctionParameters:
178182
try:
179183
parameter_signature = _parse_annotation(parameter.annotation)
180184
except ValueError as e:
181-
raise ValueError(f"{func.__name__}: {e.args[0]}") from e
185+
raise ValueError(f"Error parsing signature of @tool annotated function {func.__name__}: {e}") from e
182186
param_json_schema = _annotation_parse_to_json_schema(parameter_signature)
183187
properties[parameter.name] = param_json_schema
184188
if not _parameter_is_optional(parameter):

src/humanloop/otel/helpers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import json
12
import uuid
2-
from typing import Union
3+
from typing import Any, Callable, Union
34

45
from opentelemetry.sdk.trace import ReadableSpan
56
from opentelemetry.trace import SpanKind
@@ -289,3 +290,12 @@ def module_is_installed(module_name: str) -> bool:
289290

290291
def generate_span_id() -> str:
291292
return str(uuid.uuid4())
293+
294+
295+
def jsonify_if_not_string(func: Callable, output: Any) -> str:
296+
if not isinstance(output, str):
297+
try:
298+
output = json.dumps(output)
299+
except TypeError as e:
300+
raise TypeError(f"Output of {func.__name__} must be a string or JSON serializable") from e
301+
return output

0 commit comments

Comments
 (0)