Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 74 additions & 0 deletions bindings/typescript/tests/two-step-conversion.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
37 changes: 28 additions & 9 deletions crates/lingua/src/processing/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,6 +117,19 @@ fn try_converting_to_messages(data: &Value) -> Vec<Message> {
}
}

// Try Responses API output format
if let Ok(provider_messages) =
serde_json::from_value::<Vec<openai::OutputItem>>(data_to_parse.clone())
{
if let Ok(messages) =
<Vec<Message> as TryFromLLM<Vec<openai::OutputItem>>>::try_from(provider_messages)
{
if !messages.is_empty() {
return messages;
}
}
}

// Try Anthropic format
if let Ok(provider_messages) =
serde_json::from_value::<Vec<anthropic::InputMessage>>(data_to_parse.clone())
Expand Down