Skip to content
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
073cf62
Drafting a quick test script
riedgar-ms Nov 7, 2025
244f2cf
Corrected JSON support
riedgar-ms Nov 7, 2025
9fccc5b
Expand testing
riedgar-ms Nov 7, 2025
dd36ea2
Don't need this
riedgar-ms Nov 7, 2025
170b16b
Some small refinements
riedgar-ms Nov 7, 2025
dd56600
Draft unit test updates
riedgar-ms Nov 7, 2025
8df25b9
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 9, 2025
5405dec
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 13, 2025
fa4ca37
Proposal for schema smuggling
riedgar-ms Nov 13, 2025
7390271
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 13, 2025
320d58c
Linting issues
riedgar-ms Nov 13, 2025
842cd03
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 13, 2025
80e8cd4
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 14, 2025
2003466
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 14, 2025
45cb825
Add the JSONResponseConfig class
riedgar-ms Nov 15, 2025
ec4efaa
Better name
riedgar-ms Nov 15, 2025
1eb4395
Start on other changes
riedgar-ms Nov 15, 2025
29fdb2f
Next changes
riedgar-ms Nov 15, 2025
2c8e919
Try dealing with some linting
riedgar-ms Nov 15, 2025
9009edf
More changes....
riedgar-ms Nov 16, 2025
d899af4
Correct responses setup
riedgar-ms Nov 16, 2025
c78f819
blacken
riedgar-ms Nov 16, 2025
45f73a6
Fix a test....
riedgar-ms Nov 16, 2025
becb214
Fix reponses tests
riedgar-ms Nov 16, 2025
f37d070
Fix chat target tests
riedgar-ms Nov 16, 2025
6072ae3
blacken
riedgar-ms Nov 16, 2025
4262cd7
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 20, 2025
970c4f2
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 21, 2025
55502ff
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Nov 21, 2025
388f138
Resync with origin/main
riedgar-ms Dec 2, 2025
b6182e9
Merge from main
riedgar-ms Dec 3, 2025
630842a
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
Dec 8, 2025
4da3b9d
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 8, 2025
fd03bba
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 10, 2025
21fe8a0
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 11, 2025
48c61cc
Switching auth
riedgar-ms Dec 11, 2025
cf71d2d
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 12, 2025
d231d79
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 13, 2025
c8b4d0d
Working on next bit
riedgar-ms Dec 13, 2025
03c59a6
Get one test working again...
riedgar-ms Dec 13, 2025
5275b14
Should be the final test working (plus linting)
riedgar-ms Dec 13, 2025
9dfdcf6
Think this is the other place?
riedgar-ms Dec 13, 2025
5cef55d
Missing import?
riedgar-ms Dec 13, 2025
bea538c
And another
riedgar-ms Dec 13, 2025
e5bbea0
Sort imports
riedgar-ms Dec 13, 2025
f325976
A bad merge
riedgar-ms Dec 13, 2025
271ea14
More missed merges
riedgar-ms Dec 13, 2025
e90e409
ruff fix
riedgar-ms Dec 13, 2025
b1a7fdb
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 17, 2025
a272af9
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 17, 2025
eee44be
Need a doc string
riedgar-ms Dec 17, 2025
41f576c
Forgot doc hook
riedgar-ms Dec 17, 2025
955ce14
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 22, 2025
5c53356
Address ruff issues
riedgar-ms Dec 22, 2025
8b746d5
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 28, 2025
3cd53b2
Resolve transpondian confusion
riedgar-ms Dec 28, 2025
489b897
Fixing merge
riedgar-ms Dec 29, 2025
b9faa93
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 30, 2025
7aee618
Don't need to quote self in type annotation
riedgar-ms Dec 30, 2025
5ebe871
Fix indent in docstring
riedgar-ms Dec 30, 2025
fb9ebc0
Put links in docstring
riedgar-ms Dec 30, 2025
97b64e3
Better naming
riedgar-ms Dec 30, 2025
b5cc797
Update tests
riedgar-ms Dec 30, 2025
3ad5b7d
blacken
riedgar-ms Dec 30, 2025
91a88b8
Another blacken
riedgar-ms Dec 30, 2025
ee160f9
Python 3.10 grrrr....
riedgar-ms Dec 30, 2025
573d13b
Run isort
riedgar-ms Dec 30, 2025
ee0eb39
Drafting a notebook update
riedgar-ms Dec 30, 2025
6b2dd82
Update notebook
riedgar-ms Dec 30, 2025
560d4e9
Moving JsonResponseConfig to private
riedgar-ms Dec 30, 2025
406c7b7
Run isort on notebook
riedgar-ms Dec 30, 2025
5edfd65
Add section on JSON to the chat notebook
riedgar-ms Dec 30, 2025
ceb512d
Get the notebook fixed and regenerated
riedgar-ms Dec 30, 2025
693ced5
Fix OpenAI docs issue
riedgar-ms Dec 30, 2025
75f7e31
Linting notebooks
riedgar-ms Dec 30, 2025
2481ca6
Try running pre-commit
riedgar-ms Dec 30, 2025
e75d561
exec notebooks
romanlutz Dec 30, 2025
f2eae6c
Merge branch 'riedgar-ms/selfask-scorer-fix-01' of https://github.com…
romanlutz Dec 30, 2025
c2cce69
rerun notebooks and pre-commit
romanlutz Dec 30, 2025
d6e0c7b
Switch auth back for tests
riedgar-ms Dec 30, 2025
4f7c536
Merge remote-tracking branch 'origin/main' into riedgar-ms/selfask-sc…
riedgar-ms Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ API Reference
AttackOutcome
AttackResult
DecomposedSeedGroup
JsonResponseConfig
Message
MessagePiece
PromptDataType
Expand Down
2 changes: 2 additions & 0 deletions pyrit/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
)
from pyrit.models.embeddings import EmbeddingData, EmbeddingResponse, EmbeddingSupport, EmbeddingUsageInformation
from pyrit.models.identifiers import Identifier
from pyrit.models.json_response_config import JsonResponseConfig
from pyrit.models.literals import ChatMessageRole, PromptDataType, PromptResponseError
from pyrit.models.message import (
Message,
Expand Down Expand Up @@ -69,6 +70,7 @@
"group_message_pieces_into_conversations",
"Identifier",
"ImagePathDataTypeSerializer",
"JsonResponseConfig",
"Message",
"MessagePiece",
"PromptDataType",
Expand Down
48 changes: 48 additions & 0 deletions pyrit/models/json_response_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Dict, Optional


@dataclass
class JsonResponseConfig:
"""
Configuration for JSON responses (with OpenAI).
"""

enabled: bool = False
schema: Optional[Dict[str, Any]] = None
schema_name: str = "CustomSchema"
strict: bool = True

@classmethod
def from_metadata(cls, *, metadata: Optional[Dict[str, Any]]) -> "JsonResponseConfig":
if not metadata:
return cls(enabled=False)

response_format = metadata.get("response_format")
if response_format != "json":
return cls(enabled=False)

schema_val = metadata.get("json_schema")
if schema_val:
if isinstance(schema_val, str):
try:
schema = json.loads(schema_val) if schema_val else None
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON schema provided: {schema_val}")
else:
schema = schema_val

return cls(
enabled=True,
schema=schema,
schema_name=metadata.get("schema_name", "CustomSchema"),
strict=metadata.get("strict", True),
)

return cls(enabled=True)
23 changes: 13 additions & 10 deletions pyrit/prompt_target/common/prompt_chat_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import abc
from typing import Optional

from pyrit.models import MessagePiece
from pyrit.models import JsonResponseConfig, MessagePiece
from pyrit.prompt_target import PromptTarget


Expand Down Expand Up @@ -75,16 +75,19 @@ def is_response_format_json(self, message_piece: MessagePiece) -> bool:
include a "response_format" key.

Returns:
bool: True if the response format is JSON and supported, False otherwise.
bool: True if the response format is JSON, False otherwise.

Raises:
ValueError: If "json" response format is requested but unsupported.
"""
if message_piece.prompt_metadata:
response_format = message_piece.prompt_metadata.get("response_format")
if response_format == "json":
if not self.is_json_response_supported():
target_name = self.get_identifier()["__type__"]
raise ValueError(f"This target {target_name} does not support JSON response format.")
return True
return False
config = self.get_json_response_config(message_piece=message_piece)
return config.enabled

def get_json_response_config(self, *, message_piece: MessagePiece) -> JsonResponseConfig:
config = JsonResponseConfig.from_metadata(metadata=message_piece.prompt_metadata)

if config.enabled and not self.is_json_response_supported():
target_name = self.get_identifier()["__type__"]
raise ValueError(f"This target {target_name} does not support JSON response format.")

return config
31 changes: 25 additions & 6 deletions pyrit/prompt_target/openai/openai_chat_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Licensed under the MIT license.

import logging
from typing import Any, MutableSequence, Optional
from typing import Any, Dict, MutableSequence, Optional

from pyrit.common import convert_local_image_to_data_url
from pyrit.exceptions import (
Expand All @@ -13,6 +13,7 @@
from pyrit.models import (
ChatMessage,
ChatMessageListDictContent,
JsonResponseConfig,
Message,
MessagePiece,
construct_response_from_request,
Expand Down Expand Up @@ -182,16 +183,15 @@ async def send_prompt_async(self, *, message: Message) -> list[Message]:
self._validate_request(message=message)

message_piece: MessagePiece = message.message_pieces[0]

is_json_response = self.is_response_format_json(message_piece)
json_config = self.get_json_response_config(message_piece=message_piece)

# Get conversation from memory and append the current message
conversation = self._memory.get_conversation(conversation_id=message_piece.conversation_id)
conversation.append(message)

logger.info(f"Sending the following prompt to the prompt target: {message}")

body = await self._construct_request_body(conversation=conversation, is_json_response=is_json_response)
body = await self._construct_request_body(conversation=conversation, json_config=json_config)

# Use unified error handling - automatically detects ChatCompletion and validates
response = await self._handle_openai_request(
Expand Down Expand Up @@ -376,8 +376,11 @@ async def _build_chat_messages_for_multi_modal_async(self, conversation: Mutable
chat_messages.append(chat_message.model_dump(exclude_none=True))
return chat_messages

async def _construct_request_body(self, conversation: MutableSequence[Message], is_json_response: bool) -> dict:
async def _construct_request_body(
self, *, conversation: MutableSequence[Message], json_config: JsonResponseConfig
) -> dict:
messages = await self._build_chat_messages_async(conversation)
response_format = self._build_response_format(json_config)

body_parameters = {
"model": self._model_name,
Expand All @@ -391,7 +394,7 @@ async def _construct_request_body(self, conversation: MutableSequence[Message],
"seed": self._seed,
"n": self._n,
"messages": messages,
"response_format": {"type": "json_object"} if is_json_response else None,
"response_format": response_format,
}

if self._extra_body_parameters:
Expand Down Expand Up @@ -419,3 +422,19 @@ def _validate_request(self, *, message: Message) -> None:
for prompt_data_type in converted_prompt_data_types:
if prompt_data_type not in ["text", "image_path"]:
raise ValueError(f"This target only supports text and image_path. Received: {prompt_data_type}.")

def _build_response_format(self, json_config: JsonResponseConfig) -> Optional[Dict[str, Any]]:
if not json_config.enabled:
return None

if json_config.schema:
return {
"type": "json_schema",
"json_schema": {
"name": json_config.schema_name,
"schema": json_config.schema,
"strict": json_config.strict,
},
}

return {"type": "json_object"}
33 changes: 29 additions & 4 deletions pyrit/prompt_target/openai/openai_response_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
pyrit_target_retry,
)
from pyrit.models import (
JsonResponseConfig,
Message,
MessagePiece,
PromptDataType,
Expand Down Expand Up @@ -313,7 +314,9 @@ async def _build_input_for_multi_modal_async(self, conversation: MutableSequence

return input_items

async def _construct_request_body(self, conversation: MutableSequence[Message], is_json_response: bool) -> dict:
async def _construct_request_body(
self, *, conversation: MutableSequence[Message], json_config: JsonResponseConfig
) -> dict:
"""
Construct the request body to send to the Responses API.

Expand All @@ -322,6 +325,8 @@ async def _construct_request_body(self, conversation: MutableSequence[Message],
"""
input_items = await self._build_input_for_multi_modal_async(conversation)

text_format = self._build_text_format(json_config=json_config)

body_parameters = {
"model": self._model_name,
"max_output_tokens": self._max_output_tokens,
Expand All @@ -330,7 +335,7 @@ async def _construct_request_body(self, conversation: MutableSequence[Message],
"stream": False,
"input": input_items,
# Correct JSON response format per Responses API
"response_format": {"type": "json_object"} if is_json_response else None,
"text": text_format,
}

if self._extra_body_parameters:
Expand All @@ -339,6 +344,23 @@ async def _construct_request_body(self, conversation: MutableSequence[Message],
# Filter out None values
return {k: v for k, v in body_parameters.items() if v is not None}

def _build_text_format(self, json_config: JsonResponseConfig) -> Optional[Dict[str, Any]]:
if not json_config.enabled:
return None

if json_config.schema:
return {
"format": {
"type": "json_schema",
"name": json_config.schema_name,
"schema": json_config.schema,
"strict": json_config.strict,
}
}

logger.info("Using json_object format without schema - consider providing a schema for better results")
return {"format": {"type": "json_object"}}

def _check_content_filter(self, response: Any) -> bool:
"""
Check if a Response API response has a content filter error.
Expand Down Expand Up @@ -436,7 +458,10 @@ async def send_prompt_async(self, *, message: Message) -> list[Message]:
self._validate_request(message=message)

message_piece: MessagePiece = message.message_pieces[0]
is_json_response = self.is_response_format_json(message_piece)
json_config = JsonResponseConfig(enabled=False)
if message.message_pieces:
last_piece = message.message_pieces[-1]
json_config = self.get_json_response_config(message_piece=last_piece)

# Get full conversation history from memory and append the current message
conversation: MutableSequence[Message] = self._memory.get_conversation(
Expand All @@ -453,7 +478,7 @@ async def send_prompt_async(self, *, message: Message) -> list[Message]:
while True:
logger.info(f"Sending conversation with {len(conversation)} messages to the prompt target")

body = await self._construct_request_body(conversation=conversation, is_json_response=is_json_response)
body = await self._construct_request_body(conversation=conversation, json_config=json_config)

# Use unified error handling - automatically detects Response and validates
result = await self._handle_openai_request(
Expand Down
Loading