promptlayer 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,6 +68,22 @@ def message_text(content):
68
68
  return ""
69
69
 
70
70
 
71
+ def message_thinking(content):
72
+ if isinstance(content, list):
73
+ parts = []
74
+ for block in content:
75
+ if isinstance(block, dict) and block.get("type") == "thinking":
76
+ thinking = block.get("thinking")
77
+ if isinstance(thinking, str) and thinking:
78
+ parts.append(thinking)
79
+ return "\n".join(parts).strip()
80
+ if isinstance(content, dict) and content.get("type") == "thinking":
81
+ thinking = content.get("thinking")
82
+ if isinstance(thinking, str):
83
+ return thinking
84
+ return ""
85
+
86
+
71
87
  def flatten_indexed(prefix, items, out):
72
88
  for i, item in enumerate(items):
73
89
  for key, value in item.items():
@@ -101,6 +117,69 @@ def is_tool_result_user(rec):
101
117
  )
102
118
 
103
119
 
120
+ def assistant_message_id(rec):
121
+ if rec.get("type") != "assistant":
122
+ return ""
123
+ msg = rec.get("message", {})
124
+ if not isinstance(msg, dict):
125
+ return ""
126
+ return stringify(msg.get("id"))
127
+
128
+
129
+ def merge_content_blocks(existing, incoming):
130
+ if isinstance(existing, list) and isinstance(incoming, list):
131
+ return existing + incoming
132
+ if existing is None:
133
+ return incoming
134
+ if incoming is None:
135
+ return existing
136
+ if isinstance(existing, list):
137
+ return existing + [incoming]
138
+ if isinstance(incoming, list):
139
+ return [existing] + incoming
140
+ return [existing, incoming]
141
+
142
+
143
+ def coalesce_assistant_message_fragments(records):
144
+ coalesced = []
145
+ for rec in records:
146
+ if not coalesced:
147
+ coalesced.append(rec)
148
+ continue
149
+
150
+ prev = coalesced[-1]
151
+ rec_msg_id = assistant_message_id(rec)
152
+ prev_msg_id = assistant_message_id(prev)
153
+ same_assistant_message = (
154
+ rec_msg_id
155
+ and prev_msg_id == rec_msg_id
156
+ and rec.get("sessionId") == prev.get("sessionId")
157
+ )
158
+ if not same_assistant_message:
159
+ coalesced.append(rec)
160
+ continue
161
+
162
+ prev_msg = prev.get("message", {})
163
+ rec_msg = rec.get("message", {})
164
+ if not isinstance(prev_msg, dict) or not isinstance(rec_msg, dict):
165
+ coalesced.append(rec)
166
+ continue
167
+
168
+ prev_msg["content"] = merge_content_blocks(
169
+ prev_msg.get("content"),
170
+ rec_msg.get("content"),
171
+ )
172
+ for key in ("model", "stop_reason", "stop_sequence", "stop_details", "context_management"):
173
+ if rec_msg.get(key) is not None:
174
+ prev_msg[key] = rec_msg.get(key)
175
+ if rec_msg.get("usage"):
176
+ prev_msg["usage"] = rec_msg.get("usage")
177
+ if rec.get("timestamp"):
178
+ prev["timestamp"] = rec.get("timestamp")
179
+
180
+ return coalesced
181
+
182
+
104
183
  def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, expected_session_id=None):
105
184
  records = []
106
185
  with open(transcript_path, encoding="utf-8") as f:
@@ -118,6 +197,8 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
118
197
  except Exception:
119
198
  continue
120
199
 
200
+ records = coalesce_assistant_message_fragments(records)
201
+
121
202
  if not records:
122
203
  now_ns = int(datetime.now(timezone.utc).timestamp() * 1_000_000_000)
123
204
  return {"turn": {"start_ns": now_ns, "end_ns": now_ns}, "tools": [], "llms": []}
@@ -137,7 +218,6 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
137
218
  llms = []
138
219
  pending_tool_uses = []
139
220
  pending_payload_idx = 0
140
- saw_human_input = False
141
221
 
142
222
  turn_start_ns = turn_start_fallback
143
223
  turn_end_ns = turn_start_fallback
@@ -160,7 +240,6 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
160
240
  if content:
161
241
  append_history_item(history, {"role": "user", "content": content})
162
242
  last_input_ns = timestamp_ns or last_input_ns
163
- saw_human_input = True
164
243
  continue
165
244
 
166
245
  if rec_type == "user":
@@ -231,7 +310,6 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
231
310
  user_text = content_to_text(content)
232
311
  append_history_item(history, {"role": "user", "content": user_text})
233
312
  last_input_ns = timestamp_ns or last_input_ns
234
- saw_human_input = True
235
313
  continue
236
314
 
237
315
  if rec_type != "assistant":
@@ -248,6 +326,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
248
326
  prompt_tokens = safe_int(usage.get("input_tokens"), 0)
249
327
  completion_tokens = safe_int(usage.get("output_tokens"), 0)
250
328
  output_text = message_text(msg.get("content"))
329
+ output_thinking = message_thinking(msg.get("content"))
251
330
 
252
331
  tool_calls = []
253
332
  content_blocks = msg.get("content")
@@ -277,7 +356,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
277
356
  }
278
357
  )
279
358
 
280
- if not output_text and not tool_calls:
359
+ if not output_text and not output_thinking and not tool_calls:
281
360
  continue
282
361
 
283
362
  llm_start_ns = last_input_ns or timestamp_ns or turn_start_ns
@@ -309,11 +388,13 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
309
388
  flatten_indexed("gen_ai.prompt", history, attrs)
310
389
 
311
390
  completion_item = {"role": "assistant", "content": output_text}
391
+ if output_thinking:
392
+ completion_item["thinking"] = output_thinking
312
393
  if tool_calls:
313
394
  completion_item["tool_calls"] = tool_calls
314
395
  flatten_indexed("gen_ai.completion", [completion_item], attrs)
315
396
 
316
- span_name = "LLM Call (User)" if saw_human_input else "LLM call"
397
+ span_name = "LLM call"
317
398
 
318
399
  if emit_for_turn:
319
400
  llms.append(
@@ -326,10 +407,11 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
326
407
  )
327
408
 
328
409
  assistant_history = {"role": "assistant", "content": output_text}
410
+ if output_thinking:
411
+ assistant_history["thinking"] = output_thinking
329
412
  if tool_calls:
330
413
  assistant_history["tool_calls"] = tool_calls
331
414
  history.append(assistant_history)
332
- saw_human_input = False
333
415
 
334
416
  if turn_start_ns is None:
335
417
  turn_start_ns = int(datetime.now(timezone.utc).timestamp() * 1_000_000_000)
@@ -369,14 +451,14 @@ def build_stop_hook_span_specs(
369
451
  trace_id=trace_id,
370
452
  span_id=session_span_id,
371
453
  parent_span_id=session_parent_span_id,
372
- name="Claude Code session",
454
+ name="LLM session",
373
455
  kind="1",
374
456
  start_ns=str(session_start_ns),
375
457
  end_ns=turn_end_ns,
376
458
  attrs={
377
459
  "source": "claude-code",
378
460
  "hook": session_hook_attr,
379
- "node_type": "WORKFLOW",
461
+ "node_type": "LLM_SESSION",
380
462
  "session.lifecycle": session_lifecycle_attr,
381
463
  },
382
464
  )