Skip to content

Commit 8f3f8f0

Browse files
authored
Always strip Markdown fences from structured output (#3475)
1 parent c096d99 commit 8f3f8f0

File tree

3 files changed

+9
-32
lines changed

3 files changed

+9
-32
lines changed

pydantic_ai_slim/pydantic_ai/_output.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ def __init__(
470470
allows_image: bool,
471471
):
472472
super().__init__(
473-
processor=PromptedOutputProcessor(processor),
473+
processor=processor,
474474
allows_deferred_tools=allows_deferred_tools,
475475
allows_image=allows_image,
476476
)
@@ -494,13 +494,6 @@ def build_instructions(cls, template: str, object_def: OutputObjectDefinition) -
494494

495495
return template.format(schema=json.dumps(schema))
496496

497-
def instructions(self, default_template: str) -> str: # pragma: no cover
498-
"""Get instructions to tell model to output JSON matching the schema."""
499-
template = self.template or default_template
500-
object_def = self.object_def
501-
assert object_def is not None
502-
return self.build_instructions(template, object_def)
503-
504497

505498
@dataclass(init=False)
506499
class ToolOutputSchema(OutputSchema[OutputDataT]):
@@ -542,28 +535,6 @@ class BaseObjectOutputProcessor(BaseOutputProcessor[OutputDataT]):
542535
object_def: OutputObjectDefinition
543536

544537

545-
@dataclass(init=False)
546-
class PromptedOutputProcessor(BaseObjectOutputProcessor[OutputDataT]):
547-
wrapped: BaseObjectOutputProcessor[OutputDataT]
548-
549-
def __init__(self, wrapped: BaseObjectOutputProcessor[OutputDataT]):
550-
self.wrapped = wrapped
551-
super().__init__(object_def=wrapped.object_def)
552-
553-
async def process(
554-
self,
555-
data: str,
556-
run_context: RunContext[AgentDepsT],
557-
allow_partial: bool = False,
558-
wrap_validation_errors: bool = True,
559-
) -> OutputDataT:
560-
text = _utils.strip_markdown_fences(data)
561-
562-
return await self.wrapped.process(
563-
text, run_context, allow_partial=allow_partial, wrap_validation_errors=wrap_validation_errors
564-
)
565-
566-
567538
@dataclass(init=False)
568539
class ObjectOutputProcessor(BaseObjectOutputProcessor[OutputDataT]):
569540
outer_typed_dict_key: str | None = None
@@ -653,6 +624,9 @@ async def process(
653624
Returns:
654625
Either the validated output data (left) or a retry message (right).
655626
"""
627+
if isinstance(data, str):
628+
data = _utils.strip_markdown_fences(data)
629+
656630
try:
657631
output = self.validate(data, allow_partial)
658632
except ValidationError as e:

pydantic_ai_slim/pydantic_ai/_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,12 +467,14 @@ def validate_empty_kwargs(_kwargs: dict[str, Any]) -> None:
467467
raise exceptions.UserError(f'Unknown keyword arguments: {unknown_kwargs}')
468468

469469

470+
_MARKDOWN_FENCES_PATTERN = re.compile(r'```(?:\w+)?\n(\{.*\})', flags=re.DOTALL)
471+
472+
470473
def strip_markdown_fences(text: str) -> str:
471474
if text.startswith('{'):
472475
return text
473476

474-
regex = r'```(?:\w+)?\n(\{.*\})\n```'
475-
match = re.search(regex, text, re.DOTALL)
477+
match = re.search(_MARKDOWN_FENCES_PATTERN, text)
476478
if match:
477479
return match.group(1)
478480

tests/test_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ def test_merge_json_schema_defs():
495495
def test_strip_markdown_fences():
496496
assert strip_markdown_fences('{"foo": "bar"}') == '{"foo": "bar"}'
497497
assert strip_markdown_fences('```json\n{"foo": "bar"}\n```') == '{"foo": "bar"}'
498+
assert strip_markdown_fences('```json\n{\n "foo": "bar"\n}') == '{\n "foo": "bar"\n}'
498499
assert (
499500
strip_markdown_fences('{"foo": "```json\\n{"foo": "bar"}\\n```"}')
500501
== '{"foo": "```json\\n{"foo": "bar"}\\n```"}'

0 commit comments

Comments
 (0)