DevUI: Serialize workflow input as string to maintain conformance with OpenAI Responses format (#2021)

Co-authored-by: Victor Dibia <chuvidi2003@gmail.com>
This commit is contained in:
Reuben Bond
2025-11-07 19:25:08 -08:00
committed by GitHub
Unverified
parent 1aaf37dab8
commit 4201b9a122
5 changed files with 78 additions and 39 deletions
@@ -182,7 +182,9 @@ internal sealed class ResponseInputJsonConverter : JsonConverter<ResponseInput>
return messages is not null ? ResponseInput.FromMessages(messages) : null;
}
throw new JsonException($"Unexpected token type for ResponseInput: {reader.TokenType}");
throw new JsonException(
"ResponseInput must be either a string or an array of messages. " +
$"Objects are not supported. Received token type: {reader.TokenType}");
}
/// <inheritdoc/>
@@ -344,6 +344,20 @@ public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase
Assert.NotNull(request.Input);
}
[Fact]
public void Deserialize_InvalidInputObject_ThrowsHelpfulException()
{
// Arrange
const string Json = "{\"model\":\"gpt-4o-mini\",\"input\":{\"input\":\"testing!\"},\"stream\":true}";
// Act & Assert
var exception = Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize(Json, OpenAIHostingJsonContext.Default.CreateResponse));
Assert.Contains("ResponseInput must be either a string or an array of messages", exception.Message);
Assert.Contains("Objects are not supported", exception.Message);
}
[Fact]
public void Deserialize_AllRequests_CanBeDeserialized()
{
File diff suppressed because one or more lines are too long
@@ -747,7 +747,7 @@ class ApiClient {
// Convert to OpenAI format - use metadata.entity_id for routing
const openAIRequest: AgentFrameworkRequest = {
metadata: { entity_id: workflowId }, // Entity ID in metadata for routing
input: request.input_data || "", // Send dict directly, no stringification needed
input: JSON.stringify(request.input_data || {}), // Serialize workflow input as JSON string
stream: true,
conversation: request.conversation_id, // Include conversation if present
extra_body: request.checkpoint_id
@@ -238,6 +238,29 @@ def test_executor_parse_raw_falls_back_to_string():
assert parsed == "hi there"
def test_executor_parse_stringified_json_workflow_input():
"""Stringified JSON workflow input (from frontend JSON.stringify) is correctly parsed."""
from pydantic import BaseModel
class WorkflowInput(BaseModel):
input: str
metadata: dict | None = None
executor = AgentFrameworkExecutor(EntityDiscovery(None), MessageMapper())
start_executor = _DummyStartExecutor(handlers={WorkflowInput: lambda *_: None})
workflow = _DummyWorkflow(start_executor)
# Simulate frontend sending JSON.stringify({"input": "testing!", "metadata": {"key": "value"}})
stringified_json = '{"input": "testing!", "metadata": {"key": "value"}}'
parsed = executor._parse_raw_workflow_input(workflow, stringified_json)
# Should parse into WorkflowInput object
assert isinstance(parsed, WorkflowInput)
assert parsed.input == "testing!"
assert parsed.metadata == {"key": "value"}
async def test_executor_handles_non_streaming_agent():
"""Test executor can handle agents with only run() method (no run_stream)."""
from agent_framework import AgentRunResponse, AgentThread, ChatMessage, Role, TextContent