Repository: stanfordnlp/dspy

Issue:
[Bug] History constructs assistant messages without [[ ## completed ## ]] tags.
### What happened?

The DSPy History example produces an incorrectly formatted assistant message.
https://dspy.ai/api/primitives/History/

The System message says the [[ ## answer ## ]] should be followed by [[ ## completed ## ]]. This is not a response from the LM! It is a answer constructed inside Adapter.format_assistant_message_content().


### Steps to reproduce

```
import dspy

dspy.settings.configure(lm=dspy.LM("openai/gpt-4o-mini"))

class MySignature(dspy.Signature):
    question: str = dspy.InputField()
    history: dspy.History = dspy.InputField()
    answer: str = dspy.OutputField()

history = dspy.History(
    messages=[
        {"question": "What is the capital of France?", "answer": "Paris"},
        {"question": "What is the capital of Germany?", "answer": "Berlin"},
    ]
)

predict = dspy.Predict(MySignature)
outputs = predict(question="What is the capital of France?", history=history)

print('System message:')
print(dspy.settings.lm.history[-1]['messages'][0]['content'])
print('Constructed assistant message:')
print(dspy.settings.lm.history[-1]['messages'][2]['content'])
```


```
System message:
Your input fields are:
1. `question` (str)
2. `history` (History)
Your output fields are:
1. `answer` (str)
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## question ## ]]
{question}

[[ ## history ## ]]
{history}

[[ ## answer ## ]]
{answer}

[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        Given the fields `question`, `history`, produce the fields `answer`.
Constructed assistant message:
[[ ## answer ## ]]
Paris
```

The [[ ## answer ## ]] tag constructed by Adapter.format_assistant_message_content() should have a [[ ## completed ## ]] tag after it.

Thanks for your excellent work on DSPy!


### DSPy version

2.6.23


Discussion:

Patch:
diff --git a/dspy/adapters/chat_adapter.py b/dspy/adapters/chat_adapter.py
index 5d5f2400f..97bb173c2 100644
--- a/dspy/adapters/chat_adapter.py
+++ b/dspy/adapters/chat_adapter.py
@@ -141,12 +141,14 @@ def format_assistant_message_content(
         outputs: dict[str, Any],
         missing_field_message=None,
     ) -> str:
-        return self.format_field_with_value(
+        assistant_message_content = self.format_field_with_value(
             {
                 FieldInfoWithName(name=k, info=v): outputs.get(k, missing_field_message)
                 for k, v in signature.output_fields.items()
             },
         )
+        assistant_message_content += "\n\n[[ ## completed ## ]]\n"
+        return assistant_message_content
 
     def parse(self, signature: Type[Signature], completion: str) -> dict[str, Any]:
         sections = [(None, [])]


Test patch:
diff --git a/tests/adapters/test_chat_adapter.py b/tests/adapters/test_chat_adapter.py
index 3d8a67b7b..969a8f177 100644
--- a/tests/adapters/test_chat_adapter.py
+++ b/tests/adapters/test_chat_adapter.py
@@ -244,6 +244,9 @@ class MySignature(dspy.Signature):
     # 1 system message, 2 few shot examples (1 user and assistant message for each example), 1 user message
     assert len(messages) == 6
 
+    assert "[[ ## completed ## ]]\n" in messages[2]["content"]
+    assert "[[ ## completed ## ]]\n" in messages[4]["content"]
+
     assert {"type": "image_url", "image_url": {"url": "https://example.com/image1.jpg"}} in messages[1]["content"]
     assert {"type": "image_url", "image_url": {"url": "https://example.com/image2.jpg"}} in messages[3]["content"]
     assert {"type": "image_url", "image_url": {"url": "https://example.com/image3.jpg"}} in messages[5]["content"]
@@ -350,3 +353,26 @@ def get_population(country: str, year: int) -> str:
     # Tool arguments format should be included in the user message
     assert "{'city': {'type': 'string'}}" in messages[1]["content"]
     assert "{'country': {'type': 'string'}, 'year': {'type': 'integer'}}" in messages[1]["content"]
+
+
+def test_chat_adapter_formats_conversation_history():
+    class MySignature(dspy.Signature):
+        question: str = dspy.InputField()
+        history: dspy.History = dspy.InputField()
+        answer: str = dspy.OutputField()
+
+    history = dspy.History(
+        messages=[
+            {"question": "What is the capital of France?", "answer": "Paris"},
+            {"question": "What is the capital of Germany?", "answer": "Berlin"},
+        ]
+    )
+
+    adapter = dspy.ChatAdapter()
+    messages = adapter.format(MySignature, [], {"question": "What is the capital of France?", "history": history})
+
+    assert len(messages) == 6
+    assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?"
+    assert messages[2]["content"] == "[[ ## answer ## ]]\nParis\n\n[[ ## completed ## ]]\n"
+    assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?"
+    assert messages[4]["content"] == "[[ ## answer ## ]]\nBerlin\n\n[[ ## completed ## ]]\n"
diff --git a/tests/adapters/test_json_adapter.py b/tests/adapters/test_json_adapter.py
index cd561fe04..7db4a3202 100644
--- a/tests/adapters/test_json_adapter.py
+++ b/tests/adapters/test_json_adapter.py
@@ -419,3 +419,26 @@ def get_population(country: str, year: int) -> str:
             },
         },
     }
+
+
+def test_json_adapter_formats_conversation_history():
+    class MySignature(dspy.Signature):
+        question: str = dspy.InputField()
+        history: dspy.History = dspy.InputField()
+        answer: str = dspy.OutputField()
+
+    history = dspy.History(
+        messages=[
+            {"question": "What is the capital of France?", "answer": "Paris"},
+            {"question": "What is the capital of Germany?", "answer": "Berlin"},
+        ]
+    )
+
+    adapter = dspy.JSONAdapter()
+    messages = adapter.format(MySignature, [], {"question": "What is the capital of France?", "history": history})
+
+    assert len(messages) == 6
+    assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?"
+    assert messages[2]["content"] == '{\n  "answer": "Paris"\n}'
+    assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?"
+    assert messages[4]["content"] == '{\n  "answer": "Berlin"\n}'