@@ -112,3 +112,157 @@ def test_span_map_result_fields(self):
112112 assert result .tools_called == [{"name" : "tool1" , "input_parameters" : {"a" : 1 }, "output" : "result" }]
113113 assert result .expected_output is None
114114 assert result .system_prompt is None
115+
116+
117+ def _make_service_normalized_session_spans (num_turns = 3 ):
118+ """Build service-normalized SESSION format spans (span_events[*].body).
119+
120+ This is the format the AgentCore service sends to Lambda for SESSION-level
121+ evaluators: one span with multiple span_events, each representing a turn.
122+ """
123+ span_events = []
124+ for i in range (num_turns ):
125+ span_events .append ({
126+ "body" : {
127+ "input" : {
128+ "messages" : [
129+ {"role" : "user" , "content" : {"content" : [{"text" : f"User message { i + 1 } " }]}}
130+ ]
131+ },
132+ "output" : {
133+ "messages" : [
134+ {"role" : "assistant" , "content" : {"message" : [{"text" : f"Assistant response { i + 1 } " }]}}
135+ ]
136+ },
137+ }
138+ })
139+ return [
140+ {
141+ "traceId" : "trace-multi" ,
142+ "spanId" : "span-multi" ,
143+ "source" : "adot_cw" ,
144+ "attributes" : {"session.id" : "session-1" },
145+ "span_events" : span_events ,
146+ }
147+ ]
148+
149+
150+ class TestServiceNormalizedMultiTurn :
151+ """Tests for multi-turn extraction from service-normalized SESSION format."""
152+
153+ def test_extracts_all_turns_from_span_events (self ):
154+ spans = _make_service_normalized_session_spans (num_turns = 3 )
155+ result = map_spans (spans )
156+
157+ assert result .turns is not None
158+ assert len (result .turns ) == 6 # 3 user + 3 assistant
159+ assert result .turns [0 ] == {"role" : "user" , "content" : "User message 1" }
160+ assert result .turns [1 ] == {"role" : "assistant" , "content" : "Assistant response 1" }
161+ assert result .turns [4 ] == {"role" : "user" , "content" : "User message 3" }
162+ assert result .turns [5 ] == {"role" : "assistant" , "content" : "Assistant response 3" }
163+
164+ def test_input_and_output_are_last_turn (self ):
165+ spans = _make_service_normalized_session_spans (num_turns = 3 )
166+ result = map_spans (spans )
167+
168+ assert result .input == "User message 3"
169+ assert result .actual_output == "Assistant response 3"
170+
171+ def test_single_span_event_returns_none_turns (self ):
172+ """Single span_event should NOT populate turns (not multi-turn)."""
173+ spans = _make_service_normalized_session_spans (num_turns = 1 )
174+ result = map_spans (spans )
175+
176+ # With only 1 turn (2 entries: user+assistant), turns should be None
177+ assert result .turns is None
178+ # But input/output should still be extracted
179+ assert result .input == "User message 1"
180+ assert result .actual_output == "Assistant response 1"
181+
182+ def test_handles_string_content_variant (self ):
183+ """Test spans where content is a plain string instead of list of dicts."""
184+ spans = [
185+ {
186+ "traceId" : "t1" ,
187+ "spanId" : "s1" ,
188+ "attributes" : {"session.id" : "sess" },
189+ "span_events" : [
190+ {
191+ "body" : {
192+ "input" : {"messages" : [{"role" : "user" , "content" : "Hello plain" }]},
193+ "output" : {"messages" : [{"role" : "assistant" , "content" : "Hi plain" }]},
194+ }
195+ },
196+ {
197+ "body" : {
198+ "input" : {"messages" : [{"role" : "user" , "content" : "Follow up" }]},
199+ "output" : {"messages" : [{"role" : "assistant" , "content" : "Got it" }]},
200+ }
201+ },
202+ ],
203+ }
204+ ]
205+ result = map_spans (spans )
206+
207+ assert result .turns is not None
208+ assert len (result .turns ) == 4
209+ assert result .turns [0 ] == {"role" : "user" , "content" : "Hello plain" }
210+ assert result .turns [3 ] == {"role" : "assistant" , "content" : "Got it" }
211+
212+ def test_handles_nested_content_dict_variant (self ):
213+ """Test the {content: {content: [{text: ...}]}} nesting."""
214+ spans = [
215+ {
216+ "traceId" : "t1" ,
217+ "spanId" : "s1" ,
218+ "attributes" : {"session.id" : "sess" },
219+ "span_events" : [
220+ {
221+ "body" : {
222+ "input" : {
223+ "messages" : [
224+ {"role" : "user" , "content" : {"content" : [{"text" : "Turn 1 input" }]}}
225+ ]
226+ },
227+ "output" : {
228+ "messages" : [
229+ {"role" : "assistant" , "content" : {"message" : [{"text" : "Turn 1 output" }]}}
230+ ]
231+ },
232+ }
233+ },
234+ {
235+ "body" : {
236+ "input" : {
237+ "messages" : [
238+ {"role" : "user" , "content" : {"content" : [{"text" : "Turn 2 input" }]}}
239+ ]
240+ },
241+ "output" : {
242+ "messages" : [
243+ {"role" : "assistant" , "content" : {"message" : [{"text" : "Turn 2 output" }]}}
244+ ]
245+ },
246+ }
247+ },
248+ ],
249+ }
250+ ]
251+ result = map_spans (spans )
252+
253+ assert result .turns is not None
254+ assert len (result .turns ) == 4
255+ assert result .turns [0 ]["content" ] == "Turn 1 input"
256+ assert result .turns [1 ]["content" ] == "Turn 1 output"
257+ assert result .turns [2 ]["content" ] == "Turn 2 input"
258+ assert result .turns [3 ]["content" ] == "Turn 2 output"
259+
260+ def test_five_turns_for_session_evaluation (self ):
261+ """Realistic test: 5-turn conversation as sent by the service."""
262+ spans = _make_service_normalized_session_spans (num_turns = 5 )
263+ result = map_spans (spans )
264+
265+ assert result .turns is not None
266+ assert len (result .turns ) == 10
267+ assert result .input == "User message 5"
268+ assert result .actual_output == "Assistant response 5"
0 commit comments