From fe5d9341ab113d67e069d85a0b25de83c67a20cf Mon Sep 17 00:00:00 2001 From: Louis Mandel Date: Mon, 6 Oct 2025 16:12:20 -0400 Subject: [PATCH] feat: `structuredDecoding` field on the `model` block Signed-off-by: Louis Mandel --- .vscode/settings.json | 7 ++- examples/tutorial/parser-regex.pdl | 1 - examples/tutorial/structured_decoding.pdl | 34 ++++++++++--- examples/tutorial/type_checking.pdl | 3 +- examples/tutorial/type_error.pdl | 29 +++++++++++ pdl-live-react/src/pdl_ast.d.ts | 23 +++++---- src/pdl/pdl-schema.json | 51 ++++++++----------- src/pdl/pdl_ast.py | 10 ++-- src/pdl/pdl_dumper.py | 2 + src/pdl/pdl_interpreter.py | 2 +- src/pdl/pdl_llms.py | 9 ++-- .../tutorial/structured_decoding.0.result | 1 + .../examples/tutorial/type_checking.0.result | 1 + tests/test_examples_run.yaml | 3 +- 14 files changed, 115 insertions(+), 61 deletions(-) create mode 100644 examples/tutorial/type_error.pdl create mode 100644 tests/results/examples/tutorial/structured_decoding.0.result create mode 100644 tests/results/examples/tutorial/type_checking.0.result diff --git a/.vscode/settings.json b/.vscode/settings.json index d925c8ad0..1e4c8ec31 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,10 @@ }, "files.associations": { "*.pdl": "yaml", - } + }, + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } diff --git a/examples/tutorial/parser-regex.pdl b/examples/tutorial/parser-regex.pdl index c6d38a10d..d890442ca 100644 --- a/examples/tutorial/parser-regex.pdl +++ b/examples/tutorial/parser-regex.pdl @@ -5,7 +5,6 @@ text: parameters: # Tell the LLM to stop after generating an exclamation point. stop: ['!'] - spec: {"name": string} parser: spec: name: string diff --git a/examples/tutorial/structured_decoding.pdl b/examples/tutorial/structured_decoding.pdl index aaedd1406..978c5d628 100644 --- a/examples/tutorial/structured_decoding.pdl +++ b/examples/tutorial/structured_decoding.pdl @@ -1,8 +1,28 @@ +# Expected not to type check +description: Creating JSON Data +defs: + data: + read: type_checking_data.yaml + parser: yaml + spec: { questions: [string], answers: [object] } text: -- role: system - text: You are an AI language model developed by IBM Research. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. - contribute: [context] -- "\nWhat is the color of the sky? Write it as JSON\n" -- model: watsonx/ibm/granite-34b-code-instruct - parser: json - spec: { color: string } \ No newline at end of file + - model: ollama_chat/granite3.2:2b + def: model_output + input: + array: + - role: user + content: + text: + - for: + question: ${ data.questions } + answer: ${ data.answers } + repeat: | + ${ question } + ${ answer } + - > + Question: Generate only a JSON object with fields 'name' and 'age' and set them appropriately. Write the age all in letters. Only generate a single JSON object and nothing else. + spec: {name: string, age: integer} + parser: json + parameters: + stop: ["Question"] + temperature: 0 diff --git a/examples/tutorial/type_checking.pdl b/examples/tutorial/type_checking.pdl index 676db9ff5..a5e7bd437 100644 --- a/examples/tutorial/type_checking.pdl +++ b/examples/tutorial/type_checking.pdl @@ -8,7 +8,6 @@ defs: text: - model: ollama_chat/granite3.2:2b def: model_output - spec: {name: string, age: integer} input: array: - role: user @@ -22,7 +21,7 @@ text: ${ answer } - > Question: Generate only a JSON object with fields 'name' and 'age' and set them appropriately. Write the age all in letters. Only generate a single JSON object and nothing else. - parser: yaml + parser: json parameters: stop: ["Question"] temperature: 0 diff --git a/examples/tutorial/type_error.pdl b/examples/tutorial/type_error.pdl new file mode 100644 index 000000000..b6712fdca --- /dev/null +++ b/examples/tutorial/type_error.pdl @@ -0,0 +1,29 @@ +# Expected not to type check +description: Creating JSON Data +defs: + data: + read: type_checking_data.yaml + parser: yaml + spec: { questions: [string], answers: [object] } +text: + - model: ollama_chat/granite3.2:2b + def: model_output + input: + array: + - role: user + content: + text: + - for: + question: ${ data.questions } + answer: ${ data.answers } + repeat: | + ${ question } + ${ answer } + - > + Question: Generate only a JSON object with fields 'name' and 'age' and set them appropriately. Write the age all in letters. Only generate a single JSON object and nothing else. + spec: {name: string, age: integer} + parser: json + structuredDecoding: false + parameters: + stop: ["Question"] + temperature: 0 diff --git a/pdl-live-react/src/pdl_ast.d.ts b/pdl-live-react/src/pdl_ast.d.ts index 41c70d8fd..993b86bc1 100644 --- a/pdl-live-react/src/pdl_ast.d.ts +++ b/pdl-live-react/src/pdl_ast.d.ts @@ -264,9 +264,6 @@ export type Anyof = PatternType[] export type Array = PatternType[] export type Any = null export type ExpressionBool = LocalizedExpression | boolean | string -export type PdlCaseResult = boolean | null -export type PdlIfResult = boolean | null -export type PdlMatched = boolean | null /** * List of cases to match. * @@ -463,7 +460,7 @@ export type Aggregator = "context" | FileAggregatorConfig * Documentation associated to the aggregator config. * */ -export type Description1 = string | null +export type Description = string | null /** * Name of the file to which contribute. */ @@ -1015,9 +1012,9 @@ export interface RegexParser { * Single requirement definition. */ export interface RequirementType { - description: unknown - evaluate: Evaluate - transformContext: Transformcontext + expect: unknown + evaluate?: Evaluate + transformContext?: Transformcontext } export interface LocalizedExpression { pdl__expr: PdlExpr @@ -1191,6 +1188,10 @@ export interface LitellmModelBlock { */ model: LocalizedExpression | string parameters?: Parameters1 + /** + * Perform structured decoding if possible (i.e., `parser` and `spec` are provided and the inference platform supports it). + */ + structuredDecoding?: boolean | null } /** * Set of definitions executed before the execution of the block. @@ -2068,9 +2069,9 @@ export interface MatchCase { case?: PatternType | null if?: ExpressionBool | null then: BlockType - pdl__case_result?: PdlCaseResult - pdl__if_result?: PdlIfResult - pdl__matched?: PdlMatched + pdl__case_result?: boolean | null + pdl__if_result?: boolean | null + pdl__matched?: boolean | null } /** * Match any of the patterns. @@ -3384,7 +3385,7 @@ export interface Defs20 { [k: string]: BlockType } export interface FileAggregatorConfig { - description?: Description1 + description?: Description file: File1 mode?: Mode1 encoding?: Encoding diff --git a/src/pdl/pdl-schema.json b/src/pdl/pdl-schema.json index aa12df222..e6fa7cb22 100644 --- a/src/pdl/pdl-schema.json +++ b/src/pdl/pdl-schema.json @@ -2645,6 +2645,11 @@ "default": null, "description": "Parameters to send to the model.\n ", "title": "Parameters" + }, + "structuredDecoding": { + "$ref": "#/$defs/OptionalBool", + "default": true, + "description": "Perform structured decoding if possible (i.e., `parser` and `spec` are provided and the inference platform supports it)." } }, "required": [ @@ -3430,40 +3435,16 @@ "$ref": "#/$defs/BlockType" }, "pdl__case_result": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Pdl Case Result" + "$ref": "#/$defs/OptionalBool", + "default": null }, "pdl__if_result": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Pdl If Result" + "$ref": "#/$defs/OptionalBool", + "default": null }, "pdl__matched": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Pdl Matched" + "$ref": "#/$defs/OptionalBool", + "default": null } }, "required": [ @@ -3798,6 +3779,16 @@ } ] }, + "OptionalBool": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, "OptionalBoolOrStr": { "anyOf": [ { diff --git a/src/pdl/pdl_ast.py b/src/pdl/pdl_ast.py index 9f13293eb..faffcd14e 100644 --- a/src/pdl/pdl_ast.py +++ b/src/pdl/pdl_ast.py @@ -41,6 +41,8 @@ def _ensure_lower(value): """Optional string.""" OptionalInt = TypeAliasType("OptionalInt", Optional[int]) """Optional integer.""" +OptionalBool = TypeAliasType("OptionalBool", Optional[bool]) +"""Optional Boolean.""" OptionalBoolOrStr = TypeAliasType("OptionalBoolOrStr", Optional[Union[bool, str]]) """Optional boolean or string.""" OptionalAny = TypeAliasType("OptionalAny", Optional[Any]) @@ -629,6 +631,8 @@ class LitellmModelBlock(ModelBlock): parameters: Optional[LitellmParameters | ExpressionType[dict]] = None """Parameters to send to the model. """ + structuredDecoding: OptionalBool = True + """Perform structured decoding if possible (i.e., `parser` and `spec` are provided and the inference platform supports it).""" class GraniteioProcessor(BaseModel): @@ -835,9 +839,9 @@ class MatchCase(BaseModel): """Branch to execute if the value is matched and the condition is satisfied. """ # Field for internal use - pdl__case_result: Optional[bool] = None - pdl__if_result: Optional[bool] = None - pdl__matched: Optional[bool] = None + pdl__case_result: OptionalBool = None + pdl__if_result: OptionalBool = None + pdl__matched: OptionalBool = None class MatchBlock(StructuredBlock): diff --git a/src/pdl/pdl_dumper.py b/src/pdl/pdl_dumper.py index 0eb8b108a..f3a804703 100644 --- a/src/pdl/pdl_dumper.py +++ b/src/pdl/pdl_dumper.py @@ -154,6 +154,8 @@ def block_to_dict( # noqa: C901 d["parameters"] = expr_to_dict(block.parameters, json_compatible) if block.modelResponse is not None: d["modelResponse"] = block.modelResponse + if not block.structuredDecoding: + d["structuredDecoding"] = block.structuredDecoding if block.pdl__usage is not None: d["pdl__usage"] = usage_to_dict(block.pdl__usage) if block.pdl__model_input is not None: diff --git a/src/pdl/pdl_interpreter.py b/src/pdl/pdl_interpreter.py index 79702894f..99f4d1cba 100644 --- a/src/pdl/pdl_interpreter.py +++ b/src/pdl/pdl_interpreter.py @@ -1923,9 +1923,9 @@ def generate_client_response_streaming( scope.get("pdl_model_default_parameters", []), ) msg_stream = LitellmModel.generate_text_stream( + block, model_id=value_of_expr(block.model), messages=model_input, - spec=block.spec, parameters=litellm_parameters_to_dict(parameters), ) case GraniteioModelBlock(): diff --git a/src/pdl/pdl_llms.py b/src/pdl/pdl_llms.py index ea0a03d74..bf8cf214e 100644 --- a/src/pdl/pdl_llms.py +++ b/src/pdl/pdl_llms.py @@ -33,7 +33,8 @@ async def async_generate_text( ) -> tuple[dict[str, Any], Any]: try: spec = block.spec - parameters = set_structured_decoding_parameters(spec, parameters) + if block.structuredDecoding: + parameters = set_structured_decoding_parameters(spec, parameters) if parameters.get("mock_response") is not None: import litellm @@ -140,12 +141,14 @@ def update_end_nanos(future): @staticmethod def generate_text_stream( + block: LitellmModelBlock, model_id: str, messages: ModelInput, - spec: Any, parameters: dict[str, Any], ) -> Generator[dict[str, Any], Any, Any]: - parameters = set_structured_decoding_parameters(spec, parameters) + spec = block.spec + if block.structuredDecoding: + parameters = set_structured_decoding_parameters(spec, parameters) from litellm import completion response = completion( diff --git a/tests/results/examples/tutorial/structured_decoding.0.result b/tests/results/examples/tutorial/structured_decoding.0.result new file mode 100644 index 000000000..0ca23cb73 --- /dev/null +++ b/tests/results/examples/tutorial/structured_decoding.0.result @@ -0,0 +1 @@ +{"name": "John", "age": 30} \ No newline at end of file diff --git a/tests/results/examples/tutorial/type_checking.0.result b/tests/results/examples/tutorial/type_checking.0.result new file mode 100644 index 000000000..e73049ca5 --- /dev/null +++ b/tests/results/examples/tutorial/type_checking.0.result @@ -0,0 +1 @@ +{"name": "John", "age": "twentyfive"} \ No newline at end of file diff --git a/tests/test_examples_run.yaml b/tests/test_examples_run.yaml index c1bdbe372..f478739e6 100644 --- a/tests/test_examples_run.yaml +++ b/tests/test_examples_run.yaml @@ -15,7 +15,6 @@ skip: - examples/rag/pdf_index.pdl - examples/rag/pdf_query.pdl - examples/rag/rag_library1.pdl - - examples/tutorial/structured_decoding.pdl - pdl-live-react/src-tauri/tests/cli/code-python.pdl - pdl-live-react/demos/error.pdl - pdl-live-react/demos/demo1.pdl @@ -100,7 +99,7 @@ expected_runtime_error: - examples/callback/repair_prompt.pdl - examples/demos/repair_prompt.pdl - examples/tutorial/type_list.pdl - - examples/tutorial/type_checking.pdl + - examples/tutorial/type_error.pdl - tests/data/line/hello3.pdl - tests/data/line/hello9.pdl - tests/data/line/hello12.pdl