Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
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
103 changes: 72 additions & 31 deletions dspy/adapters/chat_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(
native_response_types: list[type[type]] | None = None,
use_json_adapter_fallback: bool = True,
parallel_tool_calls: bool | None = None,
field_markers : bool = True,
):
"""
Args:
Expand All @@ -58,6 +59,10 @@ def __init__(
Configuration errors, engine failures and programming bugs propagate. Defaults to True.
parallel_tool_calls: Whether to request provider-side parallel tool-call generation when native function
calling is active. If None, the adapter does not set the provider option.
field_markers: Whether to wrap each field in `[[ ## field_name ## ]]` markers in prompts and
completions. Defaults to True. Set to False to reduce token overhead — only supported for
signatures with a single output field, since markers are what let the parser separate
multiple output fields in the raw completion text
"""
super().__init__(
callbacks=callbacks,
Expand All @@ -66,15 +71,24 @@ def __init__(
native_response_types=native_response_types,
)
self.use_json_adapter_fallback = use_json_adapter_fallback
self.field_markers = field_markers

def _validate_field_markers(self, signature: type[Signature]) -> None:
if not self.field_markers and len(signature.output_fields) > 1:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
raise ValueError(
"field_markers=False is only supported for signatures with a single output field, "
f"but got {len(signature.output_fields)} output fields: "
f"{list(signature.output_fields.keys())}. Field markers are required to separate "
"multiple output fields in the raw completion text. Use JSONAdapter instead if you "
"need both low token overhead and multiple output fields."
)

def _make_json_adapter_fallback(self):
from dspy.adapters.json_adapter import JSONAdapter

return JSONAdapter(
use_native_function_calling=self.use_native_function_calling,
parallel_tool_calls=self.parallel_tool_calls,
)

def __call__(
self,
lm: BaseLM,
Expand All @@ -83,34 +97,32 @@ def __call__(
demos: list[dict[str, Any]],
inputs: dict[str, Any],
) -> list[dict[str, Any]]:
with adapter_fallback_scope() as progress:
try:
return super().__call__(lm, lm_kwargs, signature, demos, inputs)
except AdapterParseError:
from dspy.adapters.json_adapter import JSONAdapter

if progress.emitted or isinstance(self, JSONAdapter) or not self.use_json_adapter_fallback:
raise
return self._make_json_adapter_fallback()(lm, lm_kwargs, signature, demos, inputs)

async def acall(
self,
processed_signature = self._call_preprocess(lm, lm_kwargs, signature, inputs)
self._validate_field_markers(processed_signature)
with adapter_fallback_scope() as progress:
try:
return super().__call__(lm, lm_kwargs, signature, demos, inputs)
except AdapterParseError:
from dspy.adapters.json_adapter import JSONAdapter
if progress.emitted or isinstance(self, JSONAdapter) or not self.use_json_adapter_fallback:
raise
return self._make_json_adapter_fallback()(lm, lm_kwargs, signature, demos, inputs)
lm: BaseLM,
lm_kwargs: dict[str, Any],
signature: type[Signature],
demos: list[dict[str, Any]],
inputs: dict[str, Any],
) -> list[dict[str, Any]]:
Comment on lines 110 to 115

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Restore the async declaration

The fallback changes leave acall’s parameters without an async def acall( declaration. Python raises a syntax error at line 115, so import dspy fails before any adapter call or test can run. Restore the declaration and correct the method indentation.

Artifacts

Executed adapter parser and import check source

  • The authored Python script parses either revision and attempts a real dspy import at head; it is the source used for both captured runs.

Previous review revision parser output

  • Ran the check against SHA 9393276 from /home/user/repo with exit code 0; the adapter parsed successfully.

PR head parser and import failure output

  • Ran the check against head from /home/user/repo with exit code 1; parsing and importing dspy both fail at chat_adapter.py:115.

View artifacts

T-Rex Ran code and verified through T-Rex

with adapter_fallback_scope() as progress:
try:
return await super().acall(lm, lm_kwargs, signature, demos, inputs)
except AdapterParseError:
from dspy.adapters.json_adapter import JSONAdapter

if progress.emitted or isinstance(self, JSONAdapter) or not self.use_json_adapter_fallback:
raise
return await self._make_json_adapter_fallback().acall(lm, lm_kwargs, signature, demos, inputs)

processed_signature = self._call_preprocess(lm, lm_kwargs, signature, inputs)
self._validate_field_markers(processed_signature)
with adapter_fallback_scope() as progress:
try:
return await super().acall(lm, lm_kwargs, signature, demos, inputs)
except AdapterParseError:
from dspy.adapters.json_adapter import JSONAdapter
if progress.emitted or isinstance(self, JSONAdapter) or not self.use_json_adapter_fallback:
raise
return await self._make_json_adapter_fallback().acall(lm, lm_kwargs, signature, demos, inputs)
def format_field_description(self, signature: type[Signature]) -> str:
return (
f"Your input fields are:\n{get_field_description_string(signature.input_fields)}\n"
Expand All @@ -123,8 +135,11 @@ def format_field_structure(self, signature: type[Signature]) -> str:
`[[ ## field_name ## ]]`. An arbitrary field `completed` ([[ ## completed ## ]]) is added to the end of the
output fields section to indicate the end of the output fields.
"""
parts = []
parts.append("All interactions will be structured in the following way, with the appropriate values filled in.")
if not self.field_markers:
parts = []
parts.append(format_signature_fields_for_instructions(signature.input_fields))
parts.append(format_signature_fields_for_instructions(signature.output_fields))
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
return "\n\n".join(parts).strip()

def format_signature_fields_for_instructions(fields: dict[str, FieldInfo]):
return self.format_field_with_value(
Expand All @@ -134,6 +149,7 @@ def format_signature_fields_for_instructions(fields: dict[str, FieldInfo]):
},
)

parts = []
parts.append(format_signature_fields_for_instructions(signature.input_fields))
parts.append(format_signature_fields_for_instructions(signature.output_fields))
parts.append("[[ ## completed ## ]]\n")
Expand All @@ -157,8 +173,10 @@ def format_user_message_content(
if k in inputs:
value = inputs.get(k)
formatted_field_value = format_field_value(field_info=v, value=value)
messages.append(f"[[ ## {k} ## ]]\n{formatted_field_value}")

if self.field_markers:
messages.append(f"[[ ## {k} ## ]]\n{formatted_field_value}")
else:
messages.append(formatted_field_value)
if main_request:
output_requirements = self.user_message_output_requirements(signature)
if output_requirements is not None:
Expand Down Expand Up @@ -193,6 +211,12 @@ def type_info(v):
else:
return ""

if not self.field_markers:
if not signature.output_fields:
return None
(f, v), = signature.output_fields.items()
return f"Respond with the value for `{f}`{type_info(v)}. Do not include any other text."
Comment on lines +213 to +217

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Indentation prevents package loading

The changed if not self.field_markers: line has six leading spaces, which does not match an enclosing indentation level in user_message_output_requirements(). Compiling this method independently raises IndentationError at line 214, so fixing the earlier broken acall declaration alone will still leave the package unable to load. Align this branch with the method body and indent its contents consistently.

Artifacts

Evidence from the check

  • Extracts the unchanged method text from both Git revisions and compiles each inside a minimal class wrapper, isolating the new indentation error.

Command output from the check

  • Captures the exact command, source, and successful compilation at 901b2e5, showing that the previous method has no indentation parse failure.

Command output from the check

  • Captures the exact command, source, and IndentationError at head line 214, confirming the new syntax blocker independently.

View artifacts

TREX Ran code and verified through TREX


message = "Respond with the corresponding output fields, starting with the field "
message += ", then ".join(f"`[[ ## {f} ## ]]`{type_info(v)}" for f, v in signature.output_fields.items())
message += ", and then ending with the marker for `[[ ## completed ## ]]`."
Expand All @@ -210,10 +234,25 @@ def format_assistant_message_content(
for k, v in signature.output_fields.items()
},
)
assistant_message_content += "\n\n[[ ## completed ## ]]\n"
if self.field_markers:
assistant_message_content += "\n\n[[ ## completed ## ]]\n"
return assistant_message_content

def parse(self, signature: type[Signature], completion: str) -> dict[str, Any]:
if not self.field_markers:
if not signature.output_fields:
return {}
(field_name, field_info), = signature.output_fields.items()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
try:
return {field_name: parse_value(completion.strip(), field_info.annotation)}
except Exception as e:
raise AdapterParseError(
adapter_name="ChatAdapter",
signature=signature,
lm_response=completion,
message=f"Failed to parse field {field_name} with value {completion!r} from the LM response. Error message: {e}",
)

sections = [(None, [])]

for line in completion.splitlines():
Expand Down Expand Up @@ -267,8 +306,10 @@ def format_field_with_value(self, fields_with_values: dict[FieldInfoWithName, An
output = []
for field, field_value in fields_with_values.items():
formatted_field_value = format_field_value(field_info=field.info, value=field_value)
output.append(f"[[ ## {field.name} ## ]]\n{formatted_field_value}")

if self.field_markers:
output.append(f"[[ ## {field.name} ## ]]\n{formatted_field_value}")
else:
output.append(formatted_field_value)
return "\n\n".join(output).strip()

def format_finetune_data(
Expand Down
27 changes: 27 additions & 0 deletions tests/adapters/test_chat_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3050,7 +3050,34 @@ class OptionalSyntaxSignature(dspy.Signature):
with dspy.context(lm=DummyLM(responses), adapter=dspy.ChatAdapter()):
with pytest.raises(AdapterParseError):
dspy.Predict(OptionalSyntaxSignature)(question="anything")

def test_field_markers_false_single_output_field():
"""field_markers=False should produce marker-free prompts and parse correctly for single-output signatures."""
adapter = dspy.ChatAdapter(field_markers=False)

class SimpleSignature(dspy.Signature):
question: str = dspy.InputField()
answer: str = dspy.OutputField()

messages = adapter.format(SimpleSignature, [], {"question": "What is 2+2?"})
full_text = " ".join(m["content"] for m in messages)
assert "[[ ##" not in full_text # no markers anywhere in the prompt

parsed = adapter.parse(SimpleSignature, "4")
assert parsed == {"answer": "4"}


def test_field_markers_false_multi_output_field_raises():
"""field_markers=False should raise a clear error for signatures with more than one output field."""
adapter = dspy.ChatAdapter(field_markers=False)

class MultiOutputSignature(dspy.Signature):
question: str = dspy.InputField()
answer: str = dspy.OutputField()
confidence: float = dspy.OutputField()

with pytest.raises(ValueError, match="single output field"):
adapter._validate_field_markers(MultiOutputSignature)
Comment thread
ramyasreenagarajan marked this conversation as resolved.

def test_chat_adapter_formats_pep604_union_like_typing_union():
class QA(dspy.Signature):
Expand Down