Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
61 changes: 53 additions & 8 deletions dspy/adapters/chat_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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 @@ -57,6 +58,10 @@ def __init__(
JSONAdapter. 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 @@ -65,15 +70,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 @@ -82,6 +96,8 @@ def __call__(
demos: list[dict[str, Any]],
inputs: dict[str, Any],
) -> list[dict[str, Any]]:
processed_signature = self._call_preprocess(lm, lm_kwargs, signature, inputs)
self._validate_field_markers(signature)
try:
return super().__call__(lm, lm_kwargs, signature, demos, inputs)
except Exception as e:
Expand All @@ -102,6 +118,8 @@ async def acall(
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

processed_signature = self._call_preprocess(lm, lm_kwargs, signature, inputs)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
self._validate_field_markers(signature)
try:
return await super().acall(lm, lm_kwargs, signature, demos, inputs)
except Exception as e:
Expand All @@ -126,6 +144,8 @@ 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.
"""
if not self.field_markers:
return ""
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
parts = []
parts.append("All interactions will be structured in the following way, with the appropriate values filled in.")

Expand Down Expand Up @@ -160,8 +180,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 @@ -196,6 +218,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." # <-- also inside type_info(), and dedented one level short

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 Streamed answers disappear

With field_markers=False, the prompt requests an unmarked answer, but StreamListener still waits for [[ ## field_name ## ]] before emitting field chunks. Passing raw answer chunks through the listener produces no field events, including at finalization, while the same answer with markers streams correctly. The completed raw answer still parses successfully. Teach the listener to stream the sole output without markers, or reject this combination clearly.

Artifacts

Executed isolated streaming validation script

  • Extracts and executes the exact relevant HEAD source with real LiteLLM chunks and explicit dependency shells, making the narrow reproduction inspectable and repeatable.

Executed commands and output capture script

  • Runs the package import, both comparison cases, and tracked-file integrity check while recording each command, working directory, and exit code.

Package import failure with unchanged source

  • Attempts a real DSPy import and captures the syntax error at chat_adapter.py:115, showing why overall integration could not run.

Marker-enabled control streams the answer

  • Runs the real listener class with split field markers and answer chunks, showing six field events and successful parsing.

Marker-free answer produces no field events

  • Runs the same answer chunks without markers through the real listener class, showing zero field events despite successful completed-answer parsing.

Tracked files remain unchanged

  • Runs git diff against HEAD after validation and captures exit code zero, confirming no tracked source was repaired or modified.

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 @@ -213,10 +241,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 @@ -270,8 +313,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 All @@ -296,4 +341,4 @@ def format_finetune_data(
)
assistant_message = {"role": "assistant", "content": assistant_message_content}
messages = system_user_messages + [assistant_message]
return {"messages": messages}
return {"messages": messages}
28 changes: 28 additions & 0 deletions tests/adapters/test_chat_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2946,3 +2946,31 @@ 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)