Skip to content

Commit cca8863

Browse files
aaronvghellovai
andauthored
add test for streaming failure for lists (#2092)
Co-authored-by: hellovai <vbv@boundaryml.com>
1 parent 4a917ef commit cca8863

27 files changed

Lines changed: 2643 additions & 2562 deletions

File tree

.claude/settings.local.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(grep:*)",
5+
"Bash(uv run maturin develop:*)",
6+
"Bash(BAML_LOG_INTERNAL=debug BAML_INTERNAL_LOG=debug infisical run --env=test -- uv run pytest tests/test_functions.py::test_semantic_streaming -s)",
7+
"Bash(python test:*)",
8+
"Bash(uv run:*)",
9+
"Bash(find:*)",
10+
"Bash(rg:*)",
11+
"Bash(cargo check:*)"
12+
],
13+
"deny": []
14+
}
15+
}

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Issues labeled `good first issue` are a good place to start.
7777

7878
### Steps to Build and Test Locally
7979

80+
1. Install protobuf generator for Go `brew install protoc-gen-go`
81+
8082
1. Install Rust
8183

8284
2. Run `cargo build` in `engine/` and make sure everything builds on your machine.

engine/baml-lib/jsonish/src/deserializer/coercer/field_type.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,18 @@ impl TypeCoercer for TypeIR {
124124
}
125125
}
126126
if let Some(CompletionState::Incomplete) = value.map(|v| v.completion_state()) {
127-
result.iter_mut().for_each(|v| v.add_flag(Flag::Incomplete));
127+
match result {
128+
Ok(mut v) => {
129+
if self.meta().streaming_behavior.done
130+
&& ctx.do_not_use_mode == baml_types::StreamingMode::Streaming
131+
{
132+
return Err(ctx.error_internal("Streaming field is not done"));
133+
}
134+
v.add_flag(Flag::Incomplete);
135+
return Ok(v);
136+
}
137+
Err(e) => return Err(e),
138+
}
128139
}
129140
result
130141
}

engine/baml-lib/jsonish/src/deserializer/coercer/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::jsonish;
2121
pub struct ParsingContext<'a> {
2222
pub scope: Vec<String>,
2323
visited: HashSet<(String, jsonish::Value)>,
24+
/// THIS IS A TEMPORARY HACK (ask vaibhav)
25+
pub do_not_use_mode: baml_types::StreamingMode,
2426
pub of: &'a OutputFormatContent,
2527
}
2628

@@ -32,10 +34,14 @@ impl ParsingContext<'_> {
3234
self.scope.join(".")
3335
}
3436

35-
pub(crate) fn new(of: &OutputFormatContent) -> ParsingContext<'_> {
37+
pub(crate) fn new(
38+
of: &OutputFormatContent,
39+
mode: baml_types::StreamingMode,
40+
) -> ParsingContext<'_> {
3641
ParsingContext {
3742
scope: Vec::new(),
3843
visited: HashSet::new(),
44+
do_not_use_mode: mode,
3945
of,
4046
}
4147
}
@@ -47,6 +53,7 @@ impl ParsingContext<'_> {
4753
scope: new_scope,
4854
visited: self.visited.clone(),
4955
of: self.of,
56+
do_not_use_mode: self.do_not_use_mode.clone(),
5057
}
5158
}
5259

@@ -63,6 +70,7 @@ impl ParsingContext<'_> {
6370
scope: self.scope.clone(),
6471
visited: new_visited,
6572
of: self.of,
73+
do_not_use_mode: self.do_not_use_mode.clone(),
6674
}
6775
}
6876

engine/baml-lib/jsonish/src/helpers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ fn relevant_data_models<'a>(
237237
.map(|field| find_existing_class_field(name, &field, &walker, env_values))
238238
.map(|field| {
239239
let (name, t, prop1, prop2) = field?;
240-
let t = if partialize {
240+
let t = if partialize && !metadata.streaming_behavior.done {
241241
t.to_streaming_type(ir).to_ir_type()
242242
} else {
243243
t

engine/baml-lib/jsonish/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,14 @@ pub fn from_str(
245245

246246
// Pick the schema that is the most specific.
247247
log::debug!("Parsed JSONish (step 1 of parsing) {raw_string_is_done}: {value:#?}");
248-
let ctx = ParsingContext::new(of);
248+
let ctx = ParsingContext::new(
249+
of,
250+
if raw_string_is_done {
251+
baml_types::StreamingMode::NonStreaming
252+
} else {
253+
baml_types::StreamingMode::Streaming
254+
},
255+
);
249256

250257
// Determine the best way to get the desired schema from the parsed schema.
251258

engine/baml-lib/jsonish/src/tests/macros.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,19 +201,19 @@ macro_rules! test_partial_deserializer_streaming_failure {
201201

202202
let parsed = from_str(&target, &target_type, $raw_string, false);
203203

204-
assert!(parsed.is_ok(), "Failed to parse: {:?}", parsed);
205-
206-
let result = crate::helpers::parsed_value_to_response(
207-
&ir,
208-
parsed.unwrap(),
209-
baml_types::StreamingMode::Streaming,
210-
);
211-
212-
assert!(
213-
result.is_err(),
214-
"Failed not to parse: {}",
215-
json!(result.unwrap().serialize_partial())
216-
);
204+
assert!(parsed.is_err(), "Got a result: {:?}", parsed);
205+
206+
// let result = crate::helpers::parsed_value_to_response(
207+
// &ir,
208+
// parsed.unwrap(),
209+
// baml_types::StreamingMode::Streaming,
210+
// );
211+
212+
// assert!(
213+
// result.is_err(),
214+
// "Failed not to parse: {}",
215+
// json!(result.unwrap().serialize_partial())
216+
// );
217217
}
218218
};
219219
}

engine/baml-lib/jsonish/src/tests/test_class_2.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,232 @@ Actions:
527527
}
528528
]
529529
);
530+
531+
const TEST_FILE_STREAMING_NOT_NULL_LIST: &str = r###"
532+
class SemanticContainer2 {
533+
three_small_things SmallThing[] @description("Should have three items.")
534+
}
535+
536+
class SemanticContainer {
537+
sixteen_digit_number int
538+
string_with_twenty_words string @stream.done
539+
class_1 ClassWithoutDone
540+
class_2 ClassWithBlockDone
541+
class_done_needed ClassWithBlockDone @stream.not_null
542+
class_needed ClassWithoutDone @stream.not_null
543+
three_small_things SmallThing[] @description("Should have three items.")
544+
final_string string
545+
}
546+
547+
class ClassWithoutDone {
548+
i_16_digits int
549+
s_20_words string @description("A string with 20 words in it") @stream.with_state
550+
}
551+
552+
class ClassWithBlockDone {
553+
i_16_digits int
554+
s_20_words string
555+
@@stream.done
556+
}
557+
558+
class SmallThing {
559+
i_16_digits int @stream.not_null
560+
i_8_digits int
561+
}
562+
563+
function MakeSemanticContainer() -> SemanticContainer {
564+
client "openai/gpt-4o"
565+
prompt #"
566+
{{ ctx.output_format }}
567+
"#
568+
}
569+
"###;
570+
571+
test_deserializer!(
572+
test_streaming_not_null_list,
573+
TEST_FILE_STREAMING_NOT_NULL_LIST,
574+
r#"
575+
{
576+
"sixteen_digit_number": 1234567890123456,
577+
"string_with_twenty_words": "This is an example string that contains exactly twenty words for the requested output in JSON format. It is quite informative.",
578+
"class_1": {
579+
"i_16_digits": 9876543210123456,
580+
"s_20_words": "Another example string used for class one with twenty distinct words to complete the requirements of this JSON structure."
581+
},
582+
"class_2": {
583+
"i_16_digits": 4567890123456789,
584+
"s_20_words": "Class two also has a longer description that encompasses twenty unique words to fulfill the output specifications."
585+
},
586+
"class_done_needed": {
587+
"i_16_digits": 7890123456789012,
588+
"s_20_words": "This class indicates what has been completed and also includes twenty words in its description for clarity."
589+
},
590+
"class_needed": {
591+
"i_16_digits": 3210987654321098,
592+
"s_20_words": "Another class that explains what is currently needed for completion includes another twenty-word explanation for better understanding."
593+
},
594+
"three_small_things": [
595+
{
596+
"i_16_digits": 1357924680135792,
597+
"i_8_digits": 24681357
598+
},
599+
{
600+
"i_16_digits": 2468135790246813,
601+
"i_8_digits": 86421357
602+
},
603+
{
604+
"i_16_digits": 3571598642035791,
605+
"i_8_digits": 97586421
606+
}
607+
],
608+
"final_string": "This final string provides a brief conclusion or summary of the entire JSON formatted data provided above."
609+
}
610+
"#,
611+
TypeIR::class("SemanticContainer"),
612+
{
613+
"sixteen_digit_number": 1234567890123456i64,
614+
"string_with_twenty_words": "This is an example string that contains exactly twenty words for the requested output in JSON format. It is quite informative.",
615+
"class_1": {
616+
"i_16_digits": 9876543210123456i64,
617+
"s_20_words": "Another example string used for class one with twenty distinct words to complete the requirements of this JSON structure."
618+
},
619+
"class_2": {
620+
"i_16_digits": 4567890123456789i64,
621+
"s_20_words": "Class two also has a longer description that encompasses twenty unique words to fulfill the output specifications."
622+
},
623+
"class_done_needed": {
624+
"i_16_digits": 7890123456789012i64,
625+
"s_20_words": "This class indicates what has been completed and also includes twenty words in its description for clarity."
626+
},
627+
"class_needed": {
628+
"i_16_digits": 3210987654321098i64,
629+
"s_20_words": "Another class that explains what is currently needed for completion includes another twenty-word explanation for better understanding."
630+
},
631+
"three_small_things": [
632+
{
633+
"i_16_digits": 1357924680135792i64,
634+
"i_8_digits": 24681357
635+
},
636+
{
637+
"i_16_digits": 2468135790246813i64,
638+
"i_8_digits": 86421357
639+
},
640+
{
641+
"i_16_digits": 3571598642035791i64,
642+
"i_8_digits": 97586421
643+
}
644+
],
645+
"final_string": "This final string provides a brief conclusion or summary of the entire JSON formatted data provided above."
646+
}
647+
);
648+
649+
test_partial_deserializer!(
650+
test_streaming_not_null_list_partial,
651+
TEST_FILE_STREAMING_NOT_NULL_LIST,
652+
r#"
653+
{
654+
"sixteen_digit_number": 1234567890123456,
655+
"string_with_twenty_words": "This is an example string that contains exactly twenty words for the requested output in JSON format. It is quite informative.",
656+
"class_1": {
657+
"i_16_digits": 9876543210123456,
658+
"s_20_words": "Another example string used for class one with twenty distinct words to complete the requirements of this JSON structure."
659+
},
660+
"class_2": {
661+
"i_16_digits": 4567890123456789,
662+
"s_20_words": "Class two also has a longer description that encompasses twenty unique words to fulfill the output specifications."
663+
},
664+
"class_done_needed": {
665+
"i_16_digits": 7890123456789012,
666+
"s_20_words": "This class indicates what has been completed and also includes twenty words in its description for clarity."
667+
},
668+
"class_needed": {
669+
"i_16_digits": 3210987654321098,
670+
"s_20_words": "Another class that explains what is currently needed for completion includes another twenty-word explanation for better understanding."
671+
},
672+
"three_small_things": [{
673+
"i_16_digits": 123
674+
675+
"#,
676+
TypeIR::class("SemanticContainer"),
677+
{
678+
"sixteen_digit_number": 1234567890123456i64,
679+
"string_with_twenty_words": "This is an example string that contains exactly twenty words for the requested output in JSON format. It is quite informative.",
680+
"class_1": {
681+
"i_16_digits": 9876543210123456i64,
682+
"s_20_words": "Another example string used for class one with twenty distinct words to complete the requirements of this JSON structure."
683+
},
684+
"class_2": {
685+
"i_16_digits": 4567890123456789i64,
686+
"s_20_words": "Class two also has a longer description that encompasses twenty unique words to fulfill the output specifications."
687+
},
688+
"class_done_needed": {
689+
"i_16_digits": 7890123456789012i64,
690+
"s_20_words": "This class indicates what has been completed and also includes twenty words in its description for clarity."
691+
},
692+
"class_needed": {
693+
"i_16_digits": 3210987654321098i64,
694+
"s_20_words": "Another class that explains what is currently needed for completion includes another twenty-word explanation for better understanding."
695+
},
696+
"three_small_things": [
697+
],
698+
"final_string": null
699+
}
700+
);
701+
702+
test_deserializer!(
703+
test_streaming_not_null_list_partial_2,
704+
TEST_FILE_STREAMING_NOT_NULL_LIST,
705+
r#"
706+
{
707+
"i_16_digits": 123456789012345,
708+
"i_8_digits": 12345
709+
}
710+
"#,
711+
TypeIR::class("SmallThing"),
712+
{
713+
"i_16_digits": 123456789012345i64,
714+
"i_8_digits": 12345
715+
}
716+
);
717+
718+
test_partial_deserializer!(
719+
test_streaming_not_null_list_partial_3_0,
720+
TEST_FILE_STREAMING_NOT_NULL_LIST,
721+
r#"
722+
{
723+
"three_small_things": [
724+
{
725+
"i_16_digits": 123456789012345
726+
"#,
727+
TypeIR::class("SemanticContainer2"),
728+
// we should return an empty list here, not null
729+
// SmallThing.i_16_digits is not_null, so the entry is not valid
730+
// until the number is complete
731+
{
732+
"three_small_things": [
733+
]
734+
}
735+
);
736+
737+
test_partial_deserializer!(
738+
test_streaming_not_null_list_partial_3_1,
739+
TEST_FILE_STREAMING_NOT_NULL_LIST,
740+
r#"
741+
{
742+
"three_small_things": [
743+
{
744+
"i_16_digits": 123456789012345,
745+
"#,
746+
TypeIR::class("SemanticContainer2"),
747+
// we should return an empty list here, not null
748+
// SmallThing.i_16_digits is not_null, so the entry is not valid
749+
// until the number is complete
750+
{
751+
"three_small_things": [
752+
{
753+
"i_16_digits": 123456789012345i64,
754+
"i_8_digits": null
755+
}
756+
]
757+
}
758+
);

0 commit comments

Comments
 (0)