diff --git a/bindings/typescript/tests/two-step-conversion.test.ts b/bindings/typescript/tests/two-step-conversion.test.ts new file mode 100644 index 0000000..6c722c4 --- /dev/null +++ b/bindings/typescript/tests/two-step-conversion.test.ts @@ -0,0 +1,74 @@ +/** + * Two-step conversion test: Responses API format -> Lingua Messages -> Thread + * + * This test demonstrates the full conversion pipeline: + * 1. Import messages from spans (Responses API format -> Lingua Messages) + * 2. Thread preprocessor extracts and filters messages + */ + +import { describe, test, expect } from "vitest"; +import { importMessagesFromSpans } from "../src/index"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +describe("Two-step conversion: Responses API to Thread", () => { + test("should extract messages from Responses API output format", () => { + // Your actual trace data - focusing on the output field + const outputFromTrace = [ + { + id: "rs_0c7105cd8354f2660069824fe9039081938557c4fcb69a4d1a", + summary: [], + type: "reasoning", + }, + { + content: [ + { + annotations: [], + logprobs: [], + text: "I consulted the magic 8-ball for you (I will not reveal its exact words). Its guidance leans positively — so take this as a hopeful, mystical nudge toward yes.", + type: "output_text", + }, + ], + id: "msg_0c7105cd8354f2660069824fef675481938a1fde9d9e5917b9", + role: "assistant", + status: "completed", + type: "message", + }, + ]; + + // Step 1: Try importing from just the output field + console.log("\n=== Testing Output Field Conversion ==="); + const messagesFromOutput = importMessagesFromSpans([ + { output: outputFromTrace }, + ]); + console.log( + "Messages from output:", + JSON.stringify(messagesFromOutput, null, 2) + ); + + // Check if assistant message was extracted + const assistantMessages = messagesFromOutput.filter( + (m: any) => m.role === "assistant" + ); + console.log(`Found ${assistantMessages.length} assistant message(s)`); + + if (assistantMessages.length > 0) { + // Find the message with actual text content (not reasoning) + const messageWithText = assistantMessages.find((m: any) => { + const content = JSON.stringify(m.content); + return content.includes("magic 8-ball"); + }); + + if (messageWithText) { + console.log("✅ Found assistant message with magic 8-ball content"); + expect(messageWithText).toBeDefined(); + } else { + console.log("❌ No assistant message contains 'magic 8-ball'"); + expect(messageWithText).toBeDefined(); + } + } else { + console.log("❌ No assistant messages found from output field!"); + expect(assistantMessages.length).toBeGreaterThan(0); + } + }); +}); diff --git a/crates/lingua/src/processing/import.rs b/crates/lingua/src/processing/import.rs index 127a963..eb97f2b 100644 --- a/crates/lingua/src/processing/import.rs +++ b/crates/lingua/src/processing/import.rs @@ -26,19 +26,25 @@ pub struct Span { /// Returns early to avoid expensive deserialization attempts on non-message data fn has_message_structure(data: &Value) -> bool { match data { - // Check if it's an array where first element has "role" field or is a choice object + // Check if it's an array where ANY element has "role" field or is a choice object Value::Array(arr) => { if arr.is_empty() { return false; } - if let Some(Value::Object(first)) = arr.first() { - // Direct message format: has "role" field - if first.contains_key("role") { - return true; - } - // Chat completions response choices format: has "message" field with role inside - if let Some(Value::Object(msg)) = first.get("message") { - return msg.contains_key("role"); + // Check if ANY element in the array looks like a message (not just the first) + // This handles mixed-type arrays from Responses API + for item in arr { + if let Value::Object(obj) = item { + // Direct message format: has "role" field + if obj.contains_key("role") { + return true; + } + // Chat completions response choices format: has "message" field with role inside + if let Some(Value::Object(msg)) = obj.get("message") { + if msg.contains_key("role") { + return true; + } + } } } false @@ -111,6 +117,19 @@ fn try_converting_to_messages(data: &Value) -> Vec { } } + // Try Responses API output format + if let Ok(provider_messages) = + serde_json::from_value::>(data_to_parse.clone()) + { + if let Ok(messages) = + as TryFromLLM>>::try_from(provider_messages) + { + if !messages.is_empty() { + return messages; + } + } + } + // Try Anthropic format if let Ok(provider_messages) = serde_json::from_value::>(data_to_parse.clone())