Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
3 changes: 3 additions & 0 deletions instructor/core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ def from_openai(
instructor.Mode.TOOLS,
instructor.Mode.OPENROUTER_STRUCTURED_OUTPUTS,
instructor.Mode.JSON,
instructor.Mode.TOON,
}

if provider in {Provider.ANYSCALE, Provider.TOGETHER}:
Expand All @@ -817,6 +818,7 @@ def from_openai(
instructor.Mode.JSON,
instructor.Mode.JSON_SCHEMA,
instructor.Mode.MD_JSON,
instructor.Mode.TOON,
}

if provider in {Provider.OPENAI, Provider.DATABRICKS}:
Expand All @@ -830,6 +832,7 @@ def from_openai(
instructor.Mode.JSON_O1,
instructor.Mode.RESPONSES_TOOLS,
instructor.Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
instructor.Mode.TOON,
}

if isinstance(client, openai.OpenAI):
Expand Down
113 changes: 112 additions & 1 deletion instructor/dsl/partial.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
from pydantic.fields import FieldInfo

from instructor.mode import Mode
from instructor.utils import extract_json_from_stream, extract_json_from_stream_async
from instructor.utils import (
extract_json_from_stream,
extract_json_from_stream_async,
extract_code_block_from_stream,
extract_code_block_from_stream_async,
)

T_Model = TypeVar("T_Model", bound=BaseModel)

Expand Down Expand Up @@ -166,6 +171,10 @@ def from_streaming_response(

if mode == Mode.WRITER_TOOLS:
yield from cls.writer_model_from_chunks(json_chunks, **kwargs)
elif mode == Mode.TOON:
yield from cls.toon_model_from_chunks(
extract_code_block_from_stream(json_chunks), **kwargs
)
else:
yield from cls.model_from_chunks(json_chunks, **kwargs)

Expand All @@ -181,6 +190,10 @@ async def from_streaming_response_async(
if mode == Mode.WRITER_TOOLS:
async for item in cls.writer_model_from_chunks_async(json_chunks, **kwargs):
yield item
elif mode == Mode.TOON:
toon_chunks = extract_code_block_from_stream_async(json_chunks)
async for item in cls.toon_model_from_chunks_async(toon_chunks, **kwargs):
yield item
else:
async for item in cls.model_from_chunks_async(json_chunks, **kwargs):
yield item
Expand Down Expand Up @@ -277,6 +290,102 @@ async def model_from_chunks_async(
obj = partial_model.model_validate(obj, strict=None, **kwargs)
yield obj

@classmethod
def toon_model_from_chunks(
cls, toon_chunks: Iterable[Any], **kwargs: Any
) -> Generator[T_Model, None, None]:
"""Parse TOON content from streaming chunks.

TOON's line-based format allows partial parsing - we decode after each
complete line and yield partial models as content streams in.
"""
try:
from toon_format import decode
except ImportError as e:
raise ImportError(
"The 'toon-format' package is required for TOON mode streaming. "
"Install it with: pip install 'instructor[toon]' or pip install toon-format"
) from e

partial_model = cls.get_partial_model()
full_content = ""
last_successful_data: dict[str, Any] | None = None

for chunk in toon_chunks:
full_content += chunk

if "\n" not in full_content:
continue

last_newline = full_content.rfind("\n")
complete_lines = full_content[: last_newline + 1]

try:
data = decode(complete_lines.strip())
if data and data != last_successful_data:
last_successful_data = data
yield partial_model.model_validate(data, strict=None, **kwargs)
except Exception:
pass

if full_content.strip():
try:
data = decode(full_content.strip())
yield cls.model_validate(data, strict=None, **kwargs)
except Exception:
if last_successful_data:
yield partial_model.model_validate(
last_successful_data, strict=None, **kwargs
)

@classmethod
async def toon_model_from_chunks_async(
cls, toon_chunks: AsyncGenerator[str, None], **kwargs: Any
) -> AsyncGenerator[T_Model, None]:
"""Parse TOON content from async streaming chunks.

TOON's line-based format allows partial parsing - we decode after each
complete line and yield partial models as content streams in.
"""
try:
from toon_format import decode
except ImportError as e:
raise ImportError(
"The 'toon-format' package is required for TOON mode streaming. "
"Install it with: pip install 'instructor[toon]' or pip install toon-format"
) from e

partial_model = cls.get_partial_model()
full_content = ""
last_successful_data: dict[str, Any] | None = None

async for chunk in toon_chunks:
full_content += chunk

if "\n" not in full_content:
continue

last_newline = full_content.rfind("\n")
complete_lines = full_content[: last_newline + 1]

try:
data = decode(complete_lines.strip())
if data and data != last_successful_data:
last_successful_data = data
yield partial_model.model_validate(data, strict=None, **kwargs)
except Exception:
pass

if full_content.strip():
try:
data = decode(full_content.strip())
yield cls.model_validate(data, strict=None, **kwargs)
except Exception:
if last_successful_data:
yield partial_model.model_validate(
last_successful_data, strict=None, **kwargs
)

@staticmethod
def extract_json(
completion: Iterable[Any], mode: Mode
Expand Down Expand Up @@ -353,6 +462,7 @@ def extract_json(
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
Mode.WRITER_JSON,
Mode.TOON,
}:
if json_chunk := chunk.choices[0].delta.content:
yield json_chunk
Expand Down Expand Up @@ -443,6 +553,7 @@ async def extract_json_async(
Mode.FIREWORKS_JSON,
Mode.PERPLEXITY_JSON,
Mode.WRITER_JSON,
Mode.TOON,
}:
if json_chunk := chunk.choices[0].delta.content:
yield json_chunk
Expand Down
2 changes: 2 additions & 0 deletions instructor/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Mode(enum.Enum):
JSON_O1 = "json_o1"
MD_JSON = "markdown_json_mode"
JSON_SCHEMA = "json_schema_mode"
TOON = "toon"

# Add new modes to support responses api
RESPONSES_TOOLS = "responses_tools"
Expand Down Expand Up @@ -117,6 +118,7 @@ def json_modes(cls) -> set["Mode"]:
cls.OPENROUTER_STRUCTURED_OUTPUTS,
cls.MISTRAL_STRUCTURED_OUTPUTS,
cls.XAI_JSON,
cls.TOON,
}

@classmethod
Expand Down
83 changes: 83 additions & 0 deletions instructor/processing/function_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ def from_response(
if mode == Mode.WRITER_JSON:
return cls.parse_writer_json(completion, validation_context, strict)

if mode == Mode.TOON:
return cls.parse_toon(completion, validation_context, strict)

if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}:
return cls.parse_responses_tools(
completion,
Expand Down Expand Up @@ -781,6 +784,86 @@ def parse_json(
# Validate the model from the JSON
return _validate_model_from_json(cls, json_content, validation_context, strict)

@classmethod
def parse_toon(
cls: type[BaseModel],
completion: ChatCompletion,
validation_context: Optional[dict[str, Any]] = None,
strict: Optional[bool] = None,
) -> BaseModel:
"""Parse TOON mode responses.

TOON (Token-Oriented Object Notation) is a compact format that achieves
~60% token reduction compared to JSON. This method extracts TOON content
from the response and decodes it to a Pydantic model.
"""
_handle_incomplete_output(completion)

message = _extract_text_content(completion)
if not message:
if hasattr(completion, "choices") and completion.choices:
message = completion.choices[0].message.content or ""

if not message:
raise ResponseParsingError(
"No text content found in TOON response",
mode="TOON",
raw_response=completion,
)

toon_content = _extract_toon_from_response(message)

if not toon_content:
raise ResponseParsingError(
"Could not extract TOON content from response",
mode="TOON",
raw_response=completion,
)

try:
from toon_format import decode

try:
data = decode(toon_content)
return cls.model_validate(
data,
context=validation_context,
strict=strict if strict is not None else False,
)
except Exception as e:
raise ResponseParsingError(
f"Failed to parse TOON content: {e}",
mode="TOON",
raw_response=completion,
) from e

except ImportError as e:
raise ImportError(
"The 'toon-format' package is required for TOON mode. "
"Install it with: pip install 'instructor[toon]' or pip install toon-format"
) from e


def _extract_toon_from_response(text: str) -> str:
"""Extract TOON content from an LLM response.

Handles ```toon code blocks, generic code blocks, and raw TOON content.
"""
if not text:
return ""

text = text.strip()

toon_match = re.search(r"```toon\s*(.*?)\s*```", text, re.DOTALL | re.IGNORECASE)
if toon_match:
return toon_match.group(1).strip()

generic_match = re.search(r"```\s*(.*?)\s*```", text, re.DOTALL)
if generic_match:
return generic_match.group(1).strip()

return text


def openai_schema(cls: type[BaseModel]) -> OpenAISchema:
"""
Expand Down
5 changes: 5 additions & 0 deletions instructor/processing/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,12 @@ class User(BaseModel):
handle_responses_tools_with_inbuilt_tools,
handle_tools,
handle_tools_strict,
handle_toon,
reask_default,
reask_md_json,
reask_responses_tools,
reask_tools,
reask_toon,
)

# Perplexity utils
Expand Down Expand Up @@ -464,6 +466,7 @@ def handle_response_model(
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS: handle_responses_tools_with_inbuilt_tools,
Mode.XAI_JSON: handle_xai_json,
Mode.XAI_TOOLS: handle_xai_tools,
Mode.TOON: handle_toon,
}

if mode in mode_handlers:
Expand Down Expand Up @@ -662,6 +665,8 @@ def handle_reask_kwargs(
# XAI modes
Mode.XAI_JSON: reask_xai_json,
Mode.XAI_TOOLS: reask_xai_tools,
# TOON mode
Mode.TOON: reask_toon,
}

if mode in REASK_HANDLERS:
Expand Down
Loading