diff --git a/evals/text-complexity/ela-reading/sentence-structure/config.json b/evals/text-complexity/ela-reading/sentence-structure/config.json index f8a4fa4f6..a89b472c0 100644 --- a/evals/text-complexity/ela-reading/sentence-structure/config.json +++ b/evals/text-complexity/ela-reading/sentence-structure/config.json @@ -38,7 +38,7 @@ "input": "grade_level", "output": "rubric", "source_path": "rubric-grade-3.txt", - "sha256": "3ddacd7b3c80f1f8e85bfcddd01db945813fcf814d893e719f5ae48babdaf11f", + "sha256": "d2522ad29cf7a1edc4288a13d5509001c64776ed38242b9a7b790d621fa978a8", "condition": { "input": "grade_level", "in": ["3"] }, "implementation": { "python": { @@ -59,7 +59,7 @@ "input": "grade_level", "output": "rubric", "source_path": "rubric-grade-4.txt", - "sha256": "851c6afa4cd8a0ff0f599a38e96c96516dc2cccf0a4a7e945c982efebc89b6b7", + "sha256": "563ee744e2fe295ed1b8e5bcde00b7dfc89aac55730ed01088ed02b2a8175fd2", "condition": { "input": "grade_level", "in": ["4"] }, "implementation": { "python": { diff --git a/evals/text-complexity/ela-reading/sentence-structure/example_notebook.ipynb b/evals/text-complexity/ela-reading/sentence-structure/example_notebook.ipynb index 5413ac10b..09b54e80a 100644 --- a/evals/text-complexity/ela-reading/sentence-structure/example_notebook.ipynb +++ b/evals/text-complexity/ela-reading/sentence-structure/example_notebook.ipynb @@ -367,7 +367,7 @@ " try:\n", " analysis = run_sentence_analysis(text)\n", " features = add_engineered_features(analysis[\"formatted_output\"])\n", - " features_json = features_to_json(features)\n", + " features_json = features_to_json(features, cast_to_int=False)\n", " classification = run_classify_complexity(features_json, grade_level, text)\n", " return {\n", " \"analysis\": analysis,\n", diff --git a/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-3.txt b/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-3.txt index a803ab0a6..18bddb8cc 100644 --- a/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-3.txt +++ b/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-3.txt @@ -5,21 +5,21 @@ * **Description:** The text consists of simple, straightforward language and sentence structures. * **Statistical Guidelines:** The text is likely "Slightly Complex" if it meets at least TWO of the following criteria: * **Sentence Type:** Primarily simple sentences. (`percent_simple_sentences` is typically > 60%). - * **Sentence Length:** Short sentences. (`avg_sentence_length` is typically < 12 words). + * **Sentence Length:** Short sentences. (`avg_words_per_sentence` is typically < 12 words). * **Subordination:** Very low use of clauses. (`percent_sentences_with_subordinate` is typically < 25%). **Moderately Complex:** * **Description:** The text shows a mix of simple and more complex sentences, introducing some variety in structure without being overly demanding. * **Statistical Guidelines:** If the text is not "Slightly Complex", consider "Moderately Complex" if it generally aligns with these ranges: * **Sentence Type:** A balanced mix of sentence types. (`percent_simple_sentences` is typically between 40% and 60%). - * **Sentence Length:** Medium length sentences. (`avg_sentence_length` is typically between 12 and 16 words). + * **Sentence Length:** Medium length sentences. (`avg_words_per_sentence` is typically between 12 and 16 words). * **Subordination:** A moderate use of clauses. (`percent_sentences_with_subordinate` is typically between 25% and 45%). **Very Complex:** * **Description:** The text features more elaborate sentences with multiple clauses and ideas, requiring more effort from the reader to parse. This is often the default category for grade-level text that isn't simple or exceptionally difficult. * **Statistical Guidelines:** If the text is more complex than "Moderately" but does not meet the "Exceedingly" criteria, it is likely "Very Complex". Key indicators include: * **Sentence Type:** Complex structures are common. (`percent_simple_sentences` is a minority, typically < 40%). - * **Sentence Length:** Longer sentences are frequent. (`avg_sentence_length` is typically between 16 and 19 words). + * **Sentence Length:** Longer sentences are frequent. (`avg_words_per_sentence` is typically between 16 and 19 words). * **Subordination:** Subordinate clauses are a key feature. (`percent_sentences_with_subordinate` is typically > 45%). **Exceedingly Complex:** @@ -30,6 +30,6 @@ * Multiple Subordinates: `percent_sentences_with_multiple_subordinates` is consistently present (typically > 12%). * High Syntactic Complexity: `percent_compound_complex_sentences` is significant (typically > 15%). * **Length Indicators:** - * Extreme Sentence Length: `avg_sentence_length` is very long (typically > 19 words). + * Extreme Sentence Length: `avg_words_per_sentence` is very long (typically > 19 words). * Low Simplicity: `percent_simple_sentences` is very low (typically < 30%). * Concentrated Length: `percent_very_long_sentences` is notable (typically > 10%). diff --git a/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-4.txt b/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-4.txt index 6a81c2b81..815a80a73 100644 --- a/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-4.txt +++ b/evals/text-complexity/ela-reading/sentence-structure/rubric-grade-4.txt @@ -5,21 +5,21 @@ * **Description:** The text uses clear, direct language with basic sentence structures appropriate for developing readers. * **Statistical Guidelines:** The text is likely "Slightly Complex" if it meets at least TWO of the following criteria: * **Sentence Type:** Dominated by simple sentences. (`percent_simple_sentences` is typically > 55%). - * **Sentence Length:** Short to medium sentences. (`avg_sentence_length` is typically < 13 words). + * **Sentence Length:** Short to medium sentences. (`avg_words_per_sentence` is typically < 13 words). * **Subordination:** Infrequent use of clauses. (`percent_sentences_with_subordinate` is typically < 30%). **Moderately Complex:** * **Description:** The text contains a variety of sentence structures, including compound and complex sentences, but remains accessible. * **Statistical Guidelines:** If the text is not "Slightly Complex", consider "Moderately Complex" if it generally aligns with these ranges: * **Sentence Type:** A healthy mix of sentence types. (`percent_simple_sentences` is typically between 40% and 55%). - * **Sentence Length:** Medium length sentences. (`avg_sentence_length` is typically between 13 and 17 words). + * **Sentence Length:** Medium length sentences. (`avg_words_per_sentence` is typically between 13 and 17 words). * **Subordination:** A moderate number of clauses. (`percent_sentences_with_subordinate` is typically between 30% and 50%). **Very Complex:** * **Description:** The text is characterized by longer sentences and the regular use of dependent clauses, requiring readers to track multiple ideas. This is the default for challenging, on-grade-level texts. * **Statistical Guidelines:** If the text is more complex than "Moderately" but does not meet the "Exceedingly" criteria, it is likely "Very Complex". Key indicators include: * **Sentence Type:** Simple sentences are a clear minority. (`percent_simple_sentences` is typically < 40%). - * **Sentence Length:** Sentences are consistently long. (`avg_sentence_length` is typically between 17 and 22 words). + * **Sentence Length:** Sentences are consistently long. (`avg_words_per_sentence` is typically between 17 and 22 words). * **Subordination:** Subordination is a major feature. (`percent_sentences_with_subordinate` is typically > 50%). * **Multiple Subordination:** Sentences with multiple clauses appear more often. (`percent_sentences_with_multiple_subordinates` is typically > 8%). @@ -31,6 +31,6 @@ * Multiple Subordinates: `percent_sentences_with_multiple_subordinates` is high and consistent (typically > 15%). * High Syntactic Complexity: `percent_compound_complex_sentences` is a notable feature (typically > 20%). * **Length Indicators:** - * Extreme Sentence Length: `avg_sentence_length` is exceptionally long (typically > 22 words). + * Extreme Sentence Length: `avg_words_per_sentence` is exceptionally long (typically > 22 words). * Low Simplicity: `percent_simple_sentences` is very low (typically < 25%). * Concentrated Length: `percent_very_long_sentences` is significant (typically > 15%). diff --git a/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/config.json b/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/config.json index f8a4fa4f6..a89b472c0 100644 --- a/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/config.json +++ b/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/config.json @@ -38,7 +38,7 @@ "input": "grade_level", "output": "rubric", "source_path": "rubric-grade-3.txt", - "sha256": "3ddacd7b3c80f1f8e85bfcddd01db945813fcf814d893e719f5ae48babdaf11f", + "sha256": "d2522ad29cf7a1edc4288a13d5509001c64776ed38242b9a7b790d621fa978a8", "condition": { "input": "grade_level", "in": ["3"] }, "implementation": { "python": { @@ -59,7 +59,7 @@ "input": "grade_level", "output": "rubric", "source_path": "rubric-grade-4.txt", - "sha256": "851c6afa4cd8a0ff0f599a38e96c96516dc2cccf0a4a7e945c982efebc89b6b7", + "sha256": "563ee744e2fe295ed1b8e5bcde00b7dfc89aac55730ed01088ed02b2a8175fd2", "condition": { "input": "grade_level", "in": ["4"] }, "implementation": { "python": { diff --git a/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-3.txt b/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-3.txt index a803ab0a6..18bddb8cc 100644 --- a/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-3.txt +++ b/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-3.txt @@ -5,21 +5,21 @@ * **Description:** The text consists of simple, straightforward language and sentence structures. * **Statistical Guidelines:** The text is likely "Slightly Complex" if it meets at least TWO of the following criteria: * **Sentence Type:** Primarily simple sentences. (`percent_simple_sentences` is typically > 60%). - * **Sentence Length:** Short sentences. (`avg_sentence_length` is typically < 12 words). + * **Sentence Length:** Short sentences. (`avg_words_per_sentence` is typically < 12 words). * **Subordination:** Very low use of clauses. (`percent_sentences_with_subordinate` is typically < 25%). **Moderately Complex:** * **Description:** The text shows a mix of simple and more complex sentences, introducing some variety in structure without being overly demanding. * **Statistical Guidelines:** If the text is not "Slightly Complex", consider "Moderately Complex" if it generally aligns with these ranges: * **Sentence Type:** A balanced mix of sentence types. (`percent_simple_sentences` is typically between 40% and 60%). - * **Sentence Length:** Medium length sentences. (`avg_sentence_length` is typically between 12 and 16 words). + * **Sentence Length:** Medium length sentences. (`avg_words_per_sentence` is typically between 12 and 16 words). * **Subordination:** A moderate use of clauses. (`percent_sentences_with_subordinate` is typically between 25% and 45%). **Very Complex:** * **Description:** The text features more elaborate sentences with multiple clauses and ideas, requiring more effort from the reader to parse. This is often the default category for grade-level text that isn't simple or exceptionally difficult. * **Statistical Guidelines:** If the text is more complex than "Moderately" but does not meet the "Exceedingly" criteria, it is likely "Very Complex". Key indicators include: * **Sentence Type:** Complex structures are common. (`percent_simple_sentences` is a minority, typically < 40%). - * **Sentence Length:** Longer sentences are frequent. (`avg_sentence_length` is typically between 16 and 19 words). + * **Sentence Length:** Longer sentences are frequent. (`avg_words_per_sentence` is typically between 16 and 19 words). * **Subordination:** Subordinate clauses are a key feature. (`percent_sentences_with_subordinate` is typically > 45%). **Exceedingly Complex:** @@ -30,6 +30,6 @@ * Multiple Subordinates: `percent_sentences_with_multiple_subordinates` is consistently present (typically > 12%). * High Syntactic Complexity: `percent_compound_complex_sentences` is significant (typically > 15%). * **Length Indicators:** - * Extreme Sentence Length: `avg_sentence_length` is very long (typically > 19 words). + * Extreme Sentence Length: `avg_words_per_sentence` is very long (typically > 19 words). * Low Simplicity: `percent_simple_sentences` is very low (typically < 30%). * Concentrated Length: `percent_very_long_sentences` is notable (typically > 10%). diff --git a/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-4.txt b/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-4.txt index 6a81c2b81..815a80a73 100644 --- a/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-4.txt +++ b/sdks/python/src/learning_commons_evaluators/contracts/_generated/text_complexity/ela_reading/sentence_structure/rubric-grade-4.txt @@ -5,21 +5,21 @@ * **Description:** The text uses clear, direct language with basic sentence structures appropriate for developing readers. * **Statistical Guidelines:** The text is likely "Slightly Complex" if it meets at least TWO of the following criteria: * **Sentence Type:** Dominated by simple sentences. (`percent_simple_sentences` is typically > 55%). - * **Sentence Length:** Short to medium sentences. (`avg_sentence_length` is typically < 13 words). + * **Sentence Length:** Short to medium sentences. (`avg_words_per_sentence` is typically < 13 words). * **Subordination:** Infrequent use of clauses. (`percent_sentences_with_subordinate` is typically < 30%). **Moderately Complex:** * **Description:** The text contains a variety of sentence structures, including compound and complex sentences, but remains accessible. * **Statistical Guidelines:** If the text is not "Slightly Complex", consider "Moderately Complex" if it generally aligns with these ranges: * **Sentence Type:** A healthy mix of sentence types. (`percent_simple_sentences` is typically between 40% and 55%). - * **Sentence Length:** Medium length sentences. (`avg_sentence_length` is typically between 13 and 17 words). + * **Sentence Length:** Medium length sentences. (`avg_words_per_sentence` is typically between 13 and 17 words). * **Subordination:** A moderate number of clauses. (`percent_sentences_with_subordinate` is typically between 30% and 50%). **Very Complex:** * **Description:** The text is characterized by longer sentences and the regular use of dependent clauses, requiring readers to track multiple ideas. This is the default for challenging, on-grade-level texts. * **Statistical Guidelines:** If the text is more complex than "Moderately" but does not meet the "Exceedingly" criteria, it is likely "Very Complex". Key indicators include: * **Sentence Type:** Simple sentences are a clear minority. (`percent_simple_sentences` is typically < 40%). - * **Sentence Length:** Sentences are consistently long. (`avg_sentence_length` is typically between 17 and 22 words). + * **Sentence Length:** Sentences are consistently long. (`avg_words_per_sentence` is typically between 17 and 22 words). * **Subordination:** Subordination is a major feature. (`percent_sentences_with_subordinate` is typically > 50%). * **Multiple Subordination:** Sentences with multiple clauses appear more often. (`percent_sentences_with_multiple_subordinates` is typically > 8%). @@ -31,6 +31,6 @@ * Multiple Subordinates: `percent_sentences_with_multiple_subordinates` is high and consistent (typically > 15%). * High Syntactic Complexity: `percent_compound_complex_sentences` is a notable feature (typically > 20%). * **Length Indicators:** - * Extreme Sentence Length: `avg_sentence_length` is exceptionally long (typically > 22 words). + * Extreme Sentence Length: `avg_words_per_sentence` is exceptionally long (typically > 22 words). * Low Simplicity: `percent_simple_sentences` is very low (typically < 25%). * Concentrated Length: `percent_very_long_sentences` is significant (typically > 15%). diff --git a/sdks/python/src/learning_commons_evaluators/evaluators/text_complexity/ela_reading/sentence_structure.py b/sdks/python/src/learning_commons_evaluators/evaluators/text_complexity/ela_reading/sentence_structure.py index 5db6682f5..0795728a0 100644 --- a/sdks/python/src/learning_commons_evaluators/evaluators/text_complexity/ela_reading/sentence_structure.py +++ b/sdks/python/src/learning_commons_evaluators/evaluators/text_complexity/ela_reading/sentence_structure.py @@ -139,7 +139,7 @@ def sentence_features(analysis: Any) -> str: Takes the first step's output, which the contract names as this entry's ``input``. """ counts = analysis.model_dump() if isinstance(analysis, BaseModel) else dict(analysis) - return features_to_json(add_engineered_features(counts)) + return features_to_json(add_engineered_features(counts), cast_to_int=False) class SentenceStructureEvaluator( diff --git a/sdks/python/src/learning_commons_evaluators/features/sentence_features.py b/sdks/python/src/learning_commons_evaluators/features/sentence_features.py index f1e8e2be3..7752b0e78 100644 --- a/sdks/python/src/learning_commons_evaluators/features/sentence_features.py +++ b/sdks/python/src/learning_commons_evaluators/features/sentence_features.py @@ -5,8 +5,8 @@ arithmetic, so the contract declares it as preprocessing (``sentence_features``) rather than asking a model for it, and this module is that computation. -Every derived value is rounded to one place here and cast to a whole number on the way into -the prompt, which is the shape the rubric's thresholds are written against. +Every derived value is rounded to one place here. The classifier preserves that fractional +place so per-sentence averages and phrase densities below one remain meaningful. """ from __future__ import annotations @@ -196,26 +196,20 @@ def add_engineered_features(analysis: Mapping[str, Any]) -> dict[str, Any]: _ratio(_count(analysis, "num_sophisticated_transitions"), transitions) * 100 ) - # Deliberately Python's ties-to-even ``round``, not the SDK's half-up ``round_half_up``. - # These ratios are rationals over small counts, so they land on exact ties often, and a - # tie at ``n.95`` crosses an integer boundary once ``features_to_json`` casts it: 19 - # transitions over 20 sentences is 0.95, which reaches the prompt as 0 under this rule - # and as 1 under the other. This is the arithmetic the notebook and the fixtures were - # produced by; unifying it is DSCR-2223, decided once for both SDKs alongside the - # truncate-versus-round question it belongs with. + # Preserve the notebook's ties-to-even rounding; cross-SDK rounding alignment + # is tracked separately in DSCR-2223. features.update({name: round(value, PRECISION) for name, value in derived.items()}) return features -def features_to_json(features: Mapping[str, Any]) -> str: +def features_to_json(features: Mapping[str, Any], *, cast_to_int: bool = True) -> str: """:data:`FEATURE_COLS` as the JSON object bound to ``{sentence_features}``. - Every value is a whole number: the rubric's thresholds are written in whole percentages - and averages, and the fractional place the model would otherwise read invites precision - it cannot support. A feature the analysis did not produce is sent as ``null`` rather - than zero, so a missing count reads as missing rather than as "none found". + Set ``cast_to_int=False`` to retain one decimal place for the classifier, including + averages and densities below one. The default preserves the legacy serializer API. + A feature the analysis did not produce is sent as ``null`` rather than zero. """ - payload: dict[str, int | None] = {} + payload: dict[str, int | float | None] = {} for name in FEATURE_COLS: value = features.get(name) if isinstance(value, bool) or not isinstance(value, (int, float)): @@ -223,9 +217,7 @@ def features_to_json(features: Mapping[str, Any]) -> str: elif isinstance(value, int): payload[name] = value else: - # Truncated, not rounded: this is the notebook's arithmetic, and the fixtures - # and thresholds this evaluator was validated against were produced by it. - payload[name] = int(value) + payload[name] = int(value) if cast_to_int else round(value, PRECISION) return json.dumps(payload, indent=2) diff --git a/sdks/python/tests/unit/evaluators/text_complexity/ela_reading/test_sentence_structure.py b/sdks/python/tests/unit/evaluators/text_complexity/ela_reading/test_sentence_structure.py index fe3881029..4acebc6f6 100644 --- a/sdks/python/tests/unit/evaluators/text_complexity/ela_reading/test_sentence_structure.py +++ b/sdks/python/tests/unit/evaluators/text_complexity/ela_reading/test_sentence_structure.py @@ -45,6 +45,9 @@ "num_sentences": 2, "num_words": 20, "sentence_word_counts": [6, 14], + "num_subordinate_clauses": 1, + "num_simple_transitions": 1, + "num_sophisticated_transitions": 0, } ) @@ -91,10 +94,12 @@ async def test_the_classification_step_reads_the_features_derived_from_the_analy ) -> None: await SentenceStructureEvaluator(openai_api_key="o").evaluate(text=TEXT, grade_level=5) user = providers.calls[1]["messages"][1]["content"] - expected = features_to_json(add_engineered_features(ANALYSIS.model_dump())) + expected = features_to_json(add_engineered_features(ANALYSIS.model_dump()), cast_to_int=False) assert expected in user - # Derived, not passed through: 20 words over 2 sentences, truncated for the prompt. + # Derived, not passed through: 20 words over 2 sentences. assert json.loads(expected)["avg_words_per_sentence"] == 10 + assert json.loads(expected)["avg_subordinates_per_sentence"] == 0.5 + assert json.loads(expected)["avg_transitions_per_sentence"] == 0.5 assert TEXT in user @@ -109,6 +114,9 @@ async def test_the_rubric_for_the_grade_is_the_one_the_contract_names( rubric = SentenceStructureEvaluator.contract.document(marker) user = providers.calls[1]["messages"][1]["content"] assert f"# GRADE {grade} RUBRIC\n{rubric}" in user + assert "avg_sentence_length" not in user + if grade in (3, 4): + assert rubric.count("avg_words_per_sentence") == 4 async def test_returns_the_classification_as_the_result(providers: ProviderFactory) -> None: diff --git a/sdks/python/tests/unit/features/test_sentence_features.py b/sdks/python/tests/unit/features/test_sentence_features.py index 449e3168e..b19a0f6dd 100644 --- a/sdks/python/tests/unit/features/test_sentence_features.py +++ b/sdks/python/tests/unit/features/test_sentence_features.py @@ -132,3 +132,19 @@ def test_a_feature_the_analysis_did_not_produce_is_null_rather_than_zero() -> No payload = json.loads(features_to_json({})) assert set(payload) == set(FEATURE_COLS) assert all(value is None for value in payload.values()) + + +@pytest.mark.parametrize( + ("name", "value", "expected"), + [ + ("avg_transitions_per_sentence", 0.20, 0.2), + ("avg_subordinates_per_sentence", 0.43, 0.4), + ("appositive_phrase_density", 0.52, 0.5), + ("participle_phrase_density", 0.29, 0.3), + ("percent_simple_sentences", 60.5, 60.5), + ], +) +def test_fractional_signals_survive_serialization(name: str, value: float, expected: float) -> None: + payload = json.loads(features_to_json({name: value}, cast_to_int=False)) + assert payload[name] == expected + assert payload["num_sentences"] is None diff --git a/sdks/typescript/src/evaluators/text-complexity/ela-reading/sentence-structure.ts b/sdks/typescript/src/evaluators/text-complexity/ela-reading/sentence-structure.ts index 08712147c..2c811b117 100644 --- a/sdks/typescript/src/evaluators/text-complexity/ela-reading/sentence-structure.ts +++ b/sdks/typescript/src/evaluators/text-complexity/ela-reading/sentence-structure.ts @@ -83,7 +83,7 @@ export class SentenceStructureEvaluator extends defineMultiStepEvaluator< calculateReadabilityMetrics: (text) => formatGroundTruthCounts(String(text)), // Cast to int and single-space, matching the Python implementation's JSON. addEngineeredFeatures: (analysis) => - featuresToJSON(addEngineeredFeatures(analysis as SentenceAnalysis), 1, true), + featuresToJSON(addEngineeredFeatures(analysis as SentenceAnalysis), 1, false), }, documents: { 'rubric-grade-3.txt': RUBRIC_GRADE_3, diff --git a/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts b/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts index 3ab155dd4..b0e2fbb53 100644 --- a/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts +++ b/sdks/typescript/tests/unit/evaluators/sentence-structure.test.ts @@ -147,6 +147,40 @@ describe('SentenceStructureEvaluator - Evaluation Flow', () => { }); }); + it.each(['3', '4'] as const)('preserves fractional signals and valid rubric keys for grade %s', async (grade) => { + vi.mocked(mockProvider.generateStructured) + .mockResolvedValueOnce({ + data: { + ...createMockSentenceAnalysis(), + num_words: 200, + num_subordinate_clauses: 1, + num_simple_transitions: 1, + num_appositive_phrases: 1, + num_participle_phrases: 1, + }, + model: 'gpt-4o', + usage: { inputTokens: 1, outputTokens: 1 }, + latencyMs: 0, + }) + .mockResolvedValueOnce({ + data: { complexity_score: 'slightly_complex', reasoning: 'Mock classification' }, + model: 'gpt-4o', + usage: { inputTokens: 1, outputTokens: 1 }, + latencyMs: 0, + }); + + await evaluator.evaluate({ text: 'Test passage.', grade_level: grade }); + const messages = vi.mocked(mockProvider.generateStructured).mock.calls[1][0].messages; + const prompt = messages.map((message) => message.content).join('\n'); + const features = JSON.parse(prompt.split('# TEXT STATISTICS\n')[1]); + expect(features.avg_subordinates_per_sentence).toBe(0.5); + expect(features.avg_transitions_per_sentence).toBe(0.5); + expect(features.appositive_phrase_density).toBe(0.5); + expect(features.participle_phrase_density).toBe(0.5); + expect(prompt).not.toContain('avg_sentence_length'); + expect(prompt.match(/`avg_words_per_sentence`/g)).toHaveLength(4); + }); + describe('Error Handling', () => { it('should handle sentence analysis API failure', async () => { const testText = 'Test text here for API failure';