Skip to content

Commit

Permalink
fix: Support parsing primitive values from single-key objects (#1224)
Browse files Browse the repository at this point in the history
# Description
Fixes #1141

This PR adds support for parsing primitive values from single-key
objects in unions. When an LLM responds with an object-wrapped primitive
(e.g., `{"status": 1}`), we now correctly extract and validate the inner
value.

## Changes
- Add support for extracting primitive values (numbers, booleans,
strings) from single-key objects
- Track the extracted key name using `ImpliedKey` flag
- Reject invalid cases:
  - Nested objects (e.g., `{"status": {"code": 1}}`)
  - Arrays (e.g., `{"values": [1]}`)
  - Multi-key objects

## Test Plan
Added test cases covering:
- Basic object extraction: `{"status": 1}` → `1`
- Multiple primitive types in union
- Invalid cases (nested objects, arrays)

All tests are passing.
<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Adds support for parsing primitive values from single-key objects in
unions, with tests for valid and invalid cases.
> 
>   - **Behavior**:
> - Supports parsing primitive values from single-key objects in unions
in `coerce_literal.rs`.
> - Extracts and validates inner values if they are numbers, booleans,
or strings.
>     - Rejects nested objects, arrays, and multi-key objects.
>   - **Flags**:
>     - Uses `ImpliedKey` flag to track extracted key names.
>   - **Tests**:
> - Added tests in `test_literals.rs` for basic object extraction,
multiple primitive types, and invalid cases (nested objects, arrays).
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup>
for b4bf23f. It will automatically
update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->

---------

Co-authored-by: aaronvg <[email protected]>
  • Loading branch information
revidious and aaronvg authored Dec 11, 2024
1 parent 7384ba8 commit 935a190
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 0 deletions.
17 changes: 17 additions & 0 deletions engine/baml-lib/jsonish/src/deserializer/coercer/coerce_literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use internal_baml_core::ir::FieldType;
use crate::{
deserializer::{
coercer::{coerce_primitive::coerce_bool, match_string::match_string, TypeCoercer},
deserialize_flags::{DeserializerConditions, Flag},
types::BamlValueWithFlags,
},
jsonish,
Expand Down Expand Up @@ -46,6 +47,22 @@ impl TypeCoercer for LiteralValue {
Some(v) => v,
};

// If we get an object with a single key-value pair, try to extract the value
if let jsonish::Value::Object(obj) = value {
if obj.len() == 1 {
let (key, inner_value) = obj.iter().next().unwrap();
// only extract value if it's a primitive (not an object or array, hoping to god its fixed)
match inner_value {
jsonish::Value::Number(_) | jsonish::Value::Boolean(_) | jsonish::Value::String(_) => {
let mut result = self.coerce(ctx, target, Some(inner_value))?;
result.add_flag(Flag::ObjectToPrimitive(jsonish::Value::Object(obj.clone())));
return Ok(result);
}
_ => {}
}
}
}

match literal {
LiteralValue::Int(literal_int) => {
let BamlValueWithFlags::Int(coerced_int) = coerce_int(ctx, target, Some(value))?
Expand Down
134 changes: 134 additions & 0 deletions engine/baml-lib/jsonish/src/tests/test_literals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,137 @@ test_deserializer!(
]),
"TWO"
);

test_deserializer!(
test_union_literal_with_multiple_types_from_object,
EMPTY_FILE,
r#"{
"status": 1
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
]),
1
);

// Test with integer value
test_deserializer!(
test_union_literal_with_multiple_types_from_object_int,
EMPTY_FILE,
r#"{
"status": 1
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
]),
1
);

// Test with boolean value
test_deserializer!(
test_union_literal_with_multiple_types_from_object_bool,
EMPTY_FILE,
r#"{
"result": true
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
]),
true
);

// Test with string value
test_deserializer!(
test_union_literal_with_multiple_types_from_object_string,
EMPTY_FILE,
r#"{
"value": "THREE"
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
]),
"THREE"
);

// Test with object that has multiple keys (should fail)
test_failing_deserializer!(
test_union_literal_with_multiple_types_from_multi_key_object,
EMPTY_FILE,
r#"{
"status": 1,
"message": "success"
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
])
);

// Test with nested object (should fail)
test_failing_deserializer!(
test_union_literal_with_multiple_types_from_nested_object,
EMPTY_FILE,
r#"{
"status": {
"code": 1
}
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
])
);

// Test with quoted string value
test_deserializer!(
test_union_literal_with_multiple_types_from_object_quoted_string,
EMPTY_FILE,
r#"{
"value": "\"THREE\""
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
]),
"THREE"
);

// Test with string value and extra text
test_deserializer!(
test_union_literal_with_multiple_types_from_object_string_extra,
EMPTY_FILE,
r#"{
"value": "The answer is THREE"
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
]),
"THREE"
);

// Test with array value (should fail)
test_failing_deserializer!(
test_union_literal_with_multiple_types_from_object_array,
EMPTY_FILE,
r#"{
"values": [1]
}"#,
FieldType::Union(vec![
FieldType::Literal(LiteralValue::Int(1)),
FieldType::Literal(LiteralValue::Bool(true)),
FieldType::Literal(LiteralValue::String("THREE".into())),
])
);

0 comments on commit 935a190

Please sign in to comment.