feat: Add dual output support to Agent component with structured JSON parsing (#8836)
* feat: Add dual output support to Agent component with structured JSON parsing ## Summary - Add "Structured Response" output alongside existing "Response" output - Filter out conflicting json_mode field from OpenAI inputs - Implement robust JSON parsing with fallback handling ## Changes Made ### Agent Component (agent.py) - Add second output: "Structured Response" (Data type) with tool_mode=False - Filter json_mode from OpenAI inputs to prevent UI conflicts - Add json_response() method with multi-stage JSON parsing: - Direct JSON parsing for valid responses - Regex extraction for embedded JSON in text - Graceful error handling with diagnostic info - Share execution between outputs (no duplicate agent runs) - Fix model building to handle missing json_mode attribute ### Tests (test_agent_component.py) - Add 9 comprehensive test cases covering: - Dual output structure validation - Input filtering verification - JSON parsing (valid, embedded, error cases) - Model building without json_mode - Shared execution efficiency - Frontend node structure - Component initialization ## Benefits - Users get both Message and Data output types to choose from - Clean UI without confusing duplicate JSON toggles - Robust JSON parsing handles various response formats - Efficient single-execution approach - Maintains backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * [autofix.ci] apply automated fixes * update to templates with model list update * [autofix.ci] apply automated fixes * Update test_agent_component.py * update to the test and update to templates * [autofix.ci] apply automated fixes --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
parent
400ff562c4
commit
3bb22b29cc
18 changed files with 400 additions and 251 deletions
|
|
@ -8,7 +8,9 @@ OPENAI_MODELS_DETAILED = [
|
|||
create_model_metadata(provider="OpenAI", name="gpt-4.1", icon="OpenAI", tool_calling=True),
|
||||
create_model_metadata(provider="OpenAI", name="gpt-4.1-mini", icon="OpenAI", tool_calling=True),
|
||||
create_model_metadata(provider="OpenAI", name="gpt-4.1-nano", icon="OpenAI", tool_calling=True),
|
||||
create_model_metadata(provider="OpenAI", name="gpt-4.5-preview", icon="OpenAI", tool_calling=True, preview=True),
|
||||
create_model_metadata(
|
||||
provider="OpenAI", name="gpt-4.5-preview", icon="OpenAI", tool_calling=True, preview=True, not_supported=True
|
||||
),
|
||||
create_model_metadata(provider="OpenAI", name="gpt-4-turbo", icon="OpenAI", tool_calling=True),
|
||||
create_model_metadata(
|
||||
provider="OpenAI", name="gpt-4-turbo-preview", icon="OpenAI", tool_calling=True, preview=True
|
||||
|
|
@ -17,8 +19,8 @@ OPENAI_MODELS_DETAILED = [
|
|||
create_model_metadata(provider="OpenAI", name="gpt-3.5-turbo", icon="OpenAI", tool_calling=True),
|
||||
# Reasoning Models
|
||||
create_model_metadata(provider="OpenAI", name="o1", icon="OpenAI", reasoning=True),
|
||||
create_model_metadata(provider="OpenAI", name="o1-mini", icon="OpenAI", reasoning=True),
|
||||
create_model_metadata(provider="OpenAI", name="o1-pro", icon="OpenAI", reasoning=True),
|
||||
create_model_metadata(provider="OpenAI", name="o1-mini", icon="OpenAI", reasoning=True, not_supported=True),
|
||||
create_model_metadata(provider="OpenAI", name="o1-pro", icon="OpenAI", reasoning=True, not_supported=True),
|
||||
create_model_metadata(provider="OpenAI", name="o3-mini", icon="OpenAI", reasoning=True),
|
||||
create_model_metadata(provider="OpenAI", name="o3", icon="OpenAI", reasoning=True),
|
||||
create_model_metadata(provider="OpenAI", name="o3-pro", icon="OpenAI", reasoning=True),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import json
|
||||
import re
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
|
||||
from langflow.base.agents.agent import LCToolsAgentComponent
|
||||
|
|
@ -18,6 +21,7 @@ from langflow.custom.utils import update_component_build_config
|
|||
from langflow.field_typing import Tool
|
||||
from langflow.io import BoolInput, DropdownInput, IntInput, MultilineInput, Output
|
||||
from langflow.logging import logger
|
||||
from langflow.schema.data import Data
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.schema.message import Message
|
||||
|
||||
|
|
@ -40,6 +44,13 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
|
||||
memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]
|
||||
|
||||
# Filter out json_mode from OpenAI inputs since we handle structured output differently
|
||||
openai_inputs_filtered = [
|
||||
input_field
|
||||
for input_field in MODEL_PROVIDERS_DICT["OpenAI"]["inputs"]
|
||||
if not (hasattr(input_field, "name") and input_field.name == "json_mode")
|
||||
]
|
||||
|
||||
inputs = [
|
||||
DropdownInput(
|
||||
name="agent_llm",
|
||||
|
|
@ -51,7 +62,7 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
input_types=[],
|
||||
options_metadata=[MODELS_METADATA[key] for key in MODEL_PROVIDERS_LIST] + [{"icon": "brain"}],
|
||||
),
|
||||
*MODEL_PROVIDERS_DICT["OpenAI"]["inputs"],
|
||||
*openai_inputs_filtered,
|
||||
MultilineInput(
|
||||
name="system_prompt",
|
||||
display_name="Agent Instructions",
|
||||
|
|
@ -78,7 +89,10 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
value=True,
|
||||
),
|
||||
]
|
||||
outputs = [Output(name="response", display_name="Response", method="message_response")]
|
||||
outputs = [
|
||||
Output(name="response", display_name="Response", method="message_response"),
|
||||
Output(name="structured_response", display_name="Structured Response", method="json_response", tool_mode=False),
|
||||
]
|
||||
|
||||
async def message_response(self) -> Message:
|
||||
try:
|
||||
|
|
@ -114,7 +128,11 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
system_prompt=self.system_prompt,
|
||||
)
|
||||
agent = self.create_agent_runnable()
|
||||
return await self.run_agent(agent)
|
||||
result = await self.run_agent(agent)
|
||||
|
||||
# Store result for potential JSON output
|
||||
self._agent_result = result
|
||||
# return result
|
||||
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
logger.error(f"{type(e).__name__}: {e!s}")
|
||||
|
|
@ -125,6 +143,41 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e!s}")
|
||||
raise
|
||||
else:
|
||||
return result
|
||||
|
||||
async def json_response(self) -> Data:
|
||||
"""Convert agent response to structured JSON Data output."""
|
||||
# Run the regular message response first to get the result
|
||||
if not hasattr(self, "_agent_result"):
|
||||
await self.message_response()
|
||||
|
||||
result = self._agent_result
|
||||
|
||||
# Extract content from result
|
||||
if hasattr(result, "content"):
|
||||
content = result.content
|
||||
elif hasattr(result, "text"):
|
||||
content = result.text
|
||||
else:
|
||||
content = str(result)
|
||||
|
||||
# Try to parse as JSON
|
||||
try:
|
||||
json_data = json.loads(content)
|
||||
return Data(data=json_data)
|
||||
except json.JSONDecodeError:
|
||||
# If it's not valid JSON, try to extract JSON from the content
|
||||
json_match = re.search(r"\{.*\}", content, re.DOTALL)
|
||||
if json_match:
|
||||
try:
|
||||
json_data = json.loads(json_match.group())
|
||||
return Data(data=json_data)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# If we can't extract JSON, return the raw content as data
|
||||
return Data(data={"content": content, "error": "Could not parse as JSON"})
|
||||
|
||||
async def get_memory_data(self):
|
||||
# TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.
|
||||
|
|
@ -171,7 +224,11 @@ class AgentComponent(ToolCallingAgentComponent):
|
|||
if provider_info:
|
||||
inputs = provider_info.get("inputs")
|
||||
prefix = provider_info.get("prefix")
|
||||
model_kwargs = {input_.name: getattr(self, f"{prefix}{input_.name}") for input_ in inputs}
|
||||
# Filter out json_mode and only use attributes that exist on this component
|
||||
model_kwargs = {}
|
||||
for input_ in inputs:
|
||||
if hasattr(self, f"{prefix}{input_.name}"):
|
||||
model_kwargs[input_.name] = getattr(self, f"{prefix}{input_.name}")
|
||||
|
||||
return component.set(**model_kwargs)
|
||||
return component
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue