feat: Add AgentInvoke component for agent-to-agent invocation (#11550)

- Implement AgentInvoke component for internal agent-to-agent calls
- Enable modular AI portal architecture with specialized agents
- Add dynamic routing and parameter passing between agents
- Include example templates: portal agent, multi-step workflow, dynamic selection
- Support session management and timeout configuration
- Direct internal invocation for better performance vs HTTP

Resolves #11550
This commit is contained in:
SmartDever02 2025-12-03 21:01:16 -03:00
parent 648342b62f
commit a49d977f55
4 changed files with 943 additions and 0 deletions

View file

@ -0,0 +1,326 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
AgentInvoke Component
This component enables agent-to-agent invocation within RAGFlow, allowing a portal
agent to dynamically delegate tasks to specialized downstream agents/workflows.
Key Features:
- Dynamic routing: Route user requests to specialized agents based on intent
- Parameter passing: Pass inputs and context to downstream agents
- Result aggregation: Collect and return agent outputs
- Modular design: Agents can be developed and maintained independently
Use Case Example:
Portal Agent receives: "Analyze Q3 sales data"
-> Invokes Sales Analysis Agent with parameters: {"period": "Q3", ...}
-> Sales Analysis Agent processes and returns results
-> Portal Agent formats and returns to user
"""
import asyncio
import json
import logging
import os
import time
from abc import ABC
from agent.component.base import ComponentBase, ComponentParamBase
from common.connection_utils import timeout
class AgentInvokeParam(ComponentParamBase):
"""
Parameters for the AgentInvoke component.
Attributes:
agent_id (str): The ID of the agent/workflow to invoke
agent_name (str): Optional human-readable name for logging
inputs (dict): Input parameters to pass to the agent
query (str): Optional query/question to pass to the agent
timeout_seconds (int): Maximum time to wait for agent response
create_new_session (bool): Whether to create a new session for each invocation
"""
def __init__(self):
super().__init__()
self.agent_id = ""
self.agent_name = ""
self.inputs = {}
self.query = ""
self.timeout_seconds = 300 # 5 minutes default
self.create_new_session = True
self.session_id = None
def check(self):
"""Validate component parameters"""
self.check_empty(self.agent_id, "Agent ID")
self.check_positive_integer(self.timeout_seconds, "Timeout in seconds")
self.check_boolean(self.create_new_session, "Create new session")
class AgentInvoke(ComponentBase, ABC):
"""
Component for invoking downstream agents/workflows.
This component allows a portal agent to delegate tasks to specialized
agents, enabling a modular, maintainable AI portal architecture.
"""
component_name = "AgentInvoke"
@timeout(int(os.environ.get("COMPONENT_EXEC_TIMEOUT", 360)))
def _invoke(self, **kwargs):
"""
Invoke a downstream agent and collect its output.
Args:
**kwargs: Additional context from the parent workflow
Returns:
dict: Agent output including answer, references, and metadata
"""
if self.check_if_canceled("AgentInvoke processing"):
return
# Get tenant ID from canvas
tenant_id = self._canvas.get_tenant_id()
if not tenant_id:
error_msg = "Tenant ID not available in canvas context"
logging.error(error_msg)
self.set_output("_ERROR", error_msg)
return error_msg
# Resolve agent_id if it's a variable reference
agent_id = self._param.agent_id
if agent_id and agent_id.startswith("{") and agent_id.endswith("}"):
try:
agent_id = self._canvas.get_variable_value(agent_id)
except Exception as e:
logging.warning(f"Failed to resolve agent_id variable: {e}")
if not agent_id:
error_msg = "Agent ID is required but was not provided"
logging.error(error_msg)
self.set_output("_ERROR", error_msg)
return error_msg
# Resolve query from parameter or variable
query = self._param.query
if query and query.startswith("{") and query.endswith("}"):
try:
query = self._canvas.get_variable_value(query)
except Exception as e:
logging.warning(f"Failed to resolve query variable: {e}")
query = ""
# Build inputs dictionary by resolving all variable references
inputs = {}
if self._param.inputs:
for key, value in self._param.inputs.items():
if isinstance(value, str) and value.startswith("{") and value.endswith("}"):
try:
inputs[key] = self._canvas.get_variable_value(value)
except Exception as e:
logging.warning(f"Failed to resolve input '{key}' variable: {e}")
inputs[key] = value
else:
inputs[key] = value
# Determine session_id
session_id = None
if not self._param.create_new_session:
session_id = self._param.session_id
if session_id and session_id.startswith("{") and session_id.endswith("}"):
try:
session_id = self._canvas.get_variable_value(session_id)
except Exception:
session_id = None
agent_name = self._param.agent_name or agent_id
logging.info(f"AgentInvoke: Invoking agent '{agent_name}' (ID: {agent_id}) with query: {query[:100]}...")
try:
# Import here to avoid circular dependencies
from api.db.services.canvas_service import completion as agent_completion
# Invoke the agent and collect output
result = self._invoke_agent_sync(
agent_completion,
tenant_id=tenant_id,
agent_id=agent_id,
query=query,
inputs=inputs,
session_id=session_id,
timeout=self._param.timeout_seconds
)
if result.get("error"):
error_msg = result["error"]
logging.error(f"AgentInvoke: Agent '{agent_name}' failed: {error_msg}")
self.set_output("_ERROR", error_msg)
return error_msg
# Set outputs
self.set_output("answer", result.get("answer", ""))
self.set_output("reference", result.get("reference", {}))
self.set_output("session_id", result.get("session_id", ""))
self.set_output("metadata", result.get("metadata", {}))
self.set_output("result", result.get("answer", "")) # Alias for compatibility
logging.info(f"AgentInvoke: Agent '{agent_name}' completed successfully")
return result.get("answer", "")
except Exception as e:
error_msg = f"Failed to invoke agent '{agent_name}': {str(e)}"
logging.exception(error_msg)
self.set_output("_ERROR", error_msg)
return error_msg
def _invoke_agent_sync(self, completion_func, tenant_id, agent_id, query, inputs, session_id, timeout):
"""
Synchronously invoke an agent and wait for completion.
This method handles the async completion generator and collects the full output.
Args:
completion_func: The agent completion function to call
tenant_id (str): Tenant/user ID
agent_id (str): Agent ID to invoke
query (str): Query/question for the agent
inputs (dict): Input parameters for the agent
session_id (str): Optional session ID to continue conversation
timeout (int): Timeout in seconds
Returns:
dict: Collected agent output
"""
import asyncio
# Create result container
result = {
"answer": "",
"reference": {},
"session_id": "",
"metadata": {},
"error": None
}
async def _run_agent():
"""Async wrapper to run the agent completion"""
try:
answer_parts = []
final_session_id = None
final_reference = {}
metadata = {}
# Run the agent completion generator
async for event_data in completion_func(
tenant_id=tenant_id,
agent_id=agent_id,
query=query,
inputs=inputs,
session_id=session_id
):
# Check if task was canceled
if self.check_if_canceled("AgentInvoke processing"):
result["error"] = "Task was canceled"
return
# Parse SSE format: "data:{json}\n\n"
if isinstance(event_data, str) and event_data.startswith("data:"):
json_str = event_data[5:].strip()
try:
event = json.loads(json_str)
except json.JSONDecodeError:
continue
else:
event = event_data
# Extract session_id if present
if "session_id" in event:
final_session_id = event["session_id"]
# Handle different event types
event_type = event.get("event", "")
data = event.get("data", {})
if event_type == "message":
# Accumulate message content
content = data.get("content", "")
if content:
answer_parts.append(content)
elif event_type == "message_end":
# Final message with references
if data and data.get("reference"):
final_reference = data["reference"]
elif event_type == "workflow_finished":
# Agent completed successfully
outputs = data.get("outputs", {})
metadata = {
"elapsed_time": data.get("elapsed_time", 0),
"created_at": data.get("created_at", time.time())
}
# If outputs contain content, use it as the answer
if isinstance(outputs, dict) and outputs.get("content"):
answer_parts = [outputs["content"]]
break
elif event_type == "error":
# Error occurred during agent execution
result["error"] = data.get("message", "Unknown error")
return
# Combine all answer parts
result["answer"] = "".join(answer_parts)
result["reference"] = final_reference
result["session_id"] = final_session_id or session_id or ""
result["metadata"] = metadata
except Exception as e:
result["error"] = str(e)
logging.exception(f"Error during agent invocation: {e}")
# Run the async function with timeout
try:
# Get or create event loop
loop = self._canvas._loop if hasattr(self._canvas, '_loop') else None
if loop and loop.is_running():
# If we're already in an async context, schedule the coroutine
future = asyncio.run_coroutine_threadsafe(_run_agent(), loop)
future.result(timeout=timeout)
else:
# Run in a new event loop
asyncio.run(asyncio.wait_for(_run_agent(), timeout=timeout))
except asyncio.TimeoutError:
result["error"] = f"Agent invocation timed out after {timeout} seconds"
logging.error(result["error"])
except Exception as e:
result["error"] = f"Failed to execute agent: {str(e)}"
logging.exception(result["error"])
return result
def thoughts(self) -> str:
"""Return current component status for UI display"""
agent_name = self._param.agent_name or "agent"
return f"Invoking {agent_name}..."

View file

@ -0,0 +1,251 @@
{
"description": "Dynamic Agent Selection Based on Document Type",
"graph": {
"nodes": [
{
"id": "begin",
"data": {
"name": "Start",
"component_name": "Begin",
"x": 100,
"y": 300
}
},
{
"id": "switch_0",
"data": {
"name": "Detect Document Type",
"component_name": "Switch",
"x": 300,
"y": 300
}
},
{
"id": "agent_invoke_pdf",
"data": {
"name": "PDF Analyzer",
"component_name": "AgentInvoke",
"x": 500,
"y": 100
}
},
{
"id": "agent_invoke_excel",
"data": {
"name": "Spreadsheet Analyzer",
"component_name": "AgentInvoke",
"x": 500,
"y": 250
}
},
{
"id": "agent_invoke_image",
"data": {
"name": "Image Analyzer",
"component_name": "AgentInvoke",
"x": 500,
"y": 400
}
},
{
"id": "agent_invoke_text",
"data": {
"name": "Text Analyzer",
"component_name": "AgentInvoke",
"x": 500,
"y": 550
}
},
{
"id": "message_0",
"data": {
"name": "Analysis Result",
"component_name": "Message",
"x": 700,
"y": 300
}
}
],
"edges": [
{
"source": "begin",
"target": "switch_0"
},
{
"source": "switch_0",
"target": "agent_invoke_pdf",
"label": "pdf"
},
{
"source": "switch_0",
"target": "agent_invoke_excel",
"label": "excel"
},
{
"source": "switch_0",
"target": "agent_invoke_image",
"label": "image"
},
{
"source": "switch_0",
"target": "agent_invoke_text",
"label": "default"
},
{
"source": "agent_invoke_pdf",
"target": "message_0"
},
{
"source": "agent_invoke_excel",
"target": "message_0"
},
{
"source": "agent_invoke_image",
"target": "message_0"
},
{
"source": "agent_invoke_text",
"target": "message_0"
}
]
},
"components": {
"begin": {
"obj": {
"component_name": "Begin",
"params": {
"prologue": "Upload any document and I'll analyze it using the appropriate specialist.",
"mode": "conversational"
}
},
"downstream": ["switch_0"],
"upstream": []
},
"switch_0": {
"obj": {
"component_name": "Switch",
"params": {
"conditions": [
{
"case_name": "pdf",
"expression": "sys.files[0].mime_type.contains('pdf')",
"goto": ["agent_invoke_pdf"]
},
{
"case_name": "excel",
"expression": "sys.files[0].mime_type.contains('spreadsheet') or sys.files[0].name.endswith('.xlsx')",
"goto": ["agent_invoke_excel"]
},
{
"case_name": "image",
"expression": "sys.files[0].mime_type.contains('image')",
"goto": ["agent_invoke_image"]
}
],
"default_goto": ["agent_invoke_text"]
}
},
"downstream": ["agent_invoke_pdf", "agent_invoke_excel", "agent_invoke_image", "agent_invoke_text"],
"upstream": ["begin"]
},
"agent_invoke_pdf": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_PDF_ANALYZER_AGENT_ID",
"agent_name": "PDF Analysis Agent",
"query": "{sys.query}",
"inputs": {
"files": "{sys.files}",
"extract_tables": true,
"ocr_enabled": true
},
"timeout_seconds": 300,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["switch_0"]
},
"agent_invoke_excel": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_EXCEL_ANALYZER_AGENT_ID",
"agent_name": "Spreadsheet Analysis Agent",
"query": "{sys.query}",
"inputs": {
"files": "{sys.files}",
"analyze_formulas": true,
"generate_charts": true
},
"timeout_seconds": 240,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["switch_0"]
},
"agent_invoke_image": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_IMAGE_ANALYZER_AGENT_ID",
"agent_name": "Image Analysis Agent",
"query": "{sys.query}",
"inputs": {
"files": "{sys.files}",
"detect_objects": true,
"extract_text": true
},
"timeout_seconds": 180,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["switch_0"]
},
"agent_invoke_text": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_TEXT_ANALYZER_AGENT_ID",
"agent_name": "Text Analysis Agent",
"query": "{sys.query}",
"inputs": {
"files": "{sys.files}",
"summarize": true,
"extract_entities": true
},
"timeout_seconds": 120,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["switch_0"]
},
"message_0": {
"obj": {
"component_name": "Message",
"params": {
"content": "{agent_invoke_pdf@answer}{agent_invoke_excel@answer}{agent_invoke_image@answer}{agent_invoke_text@answer}"
}
},
"downstream": [],
"upstream": ["agent_invoke_pdf", "agent_invoke_excel", "agent_invoke_image", "agent_invoke_text"]
}
},
"history": [],
"path": ["begin"],
"retrieval": {
"chunks": [],
"doc_aggs": []
},
"globals": {
"sys.query": "",
"sys.user_id": "",
"sys.conversation_turns": 0,
"sys.files": []
}
}

View file

@ -0,0 +1,161 @@
{
"description": "Multi-Step Workflow - Sentiment Analysis → Issues → Action Items",
"graph": {
"nodes": [
{
"id": "begin",
"data": {
"name": "Start",
"component_name": "Begin",
"x": 100,
"y": 200
}
},
{
"id": "agent_invoke_sentiment",
"data": {
"name": "Sentiment Analysis",
"component_name": "AgentInvoke",
"x": 300,
"y": 200
}
},
{
"id": "agent_invoke_issues",
"data": {
"name": "Extract Key Issues",
"component_name": "AgentInvoke",
"x": 500,
"y": 200
}
},
{
"id": "agent_invoke_actions",
"data": {
"name": "Generate Action Items",
"component_name": "AgentInvoke",
"x": 700,
"y": 200
}
},
{
"id": "message_0",
"data": {
"name": "Final Report",
"component_name": "Message",
"x": 900,
"y": 200
}
}
],
"edges": [
{
"source": "begin",
"target": "agent_invoke_sentiment"
},
{
"source": "agent_invoke_sentiment",
"target": "agent_invoke_issues"
},
{
"source": "agent_invoke_issues",
"target": "agent_invoke_actions"
},
{
"source": "agent_invoke_actions",
"target": "message_0"
}
]
},
"components": {
"begin": {
"obj": {
"component_name": "Begin",
"params": {
"prologue": "I'll analyze customer feedback and generate actionable insights for you.",
"mode": "workflow"
}
},
"downstream": ["agent_invoke_sentiment"],
"upstream": []
},
"agent_invoke_sentiment": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_SENTIMENT_ANALYSIS_AGENT_ID",
"agent_name": "Sentiment Analysis Agent",
"query": "{sys.query}",
"inputs": {
"analysis_depth": "detailed",
"include_emotions": true
},
"timeout_seconds": 120,
"create_new_session": true
}
},
"downstream": ["agent_invoke_issues"],
"upstream": ["begin"]
},
"agent_invoke_issues": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_ISSUE_EXTRACTION_AGENT_ID",
"agent_name": "Key Issues Extraction Agent",
"query": "Based on the sentiment analysis, extract the top 5 key issues:\n\n{agent_invoke_sentiment@answer}",
"inputs": {
"max_issues": 5,
"prioritize": "negative_sentiment"
},
"timeout_seconds": 120,
"create_new_session": true
}
},
"downstream": ["agent_invoke_actions"],
"upstream": ["agent_invoke_sentiment"]
},
"agent_invoke_actions": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_ACTION_ITEMS_AGENT_ID",
"agent_name": "Action Items Generator Agent",
"query": "Generate actionable recommendations for these issues:\n\n{agent_invoke_issues@answer}",
"inputs": {
"format": "actionable",
"include_timeline": true,
"include_ownership": true
},
"timeout_seconds": 150,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["agent_invoke_issues"]
},
"message_0": {
"obj": {
"component_name": "Message",
"params": {
"content": "# Customer Feedback Analysis Report\n\n## Sentiment Analysis\n{agent_invoke_sentiment@answer}\n\n## Key Issues Identified\n{agent_invoke_issues@answer}\n\n## Recommended Actions\n{agent_invoke_actions@answer}"
}
},
"downstream": [],
"upstream": ["agent_invoke_actions"]
}
},
"history": [],
"path": ["begin"],
"retrieval": {
"chunks": [],
"doc_aggs": []
},
"globals": {
"sys.query": "",
"sys.user_id": "",
"sys.conversation_turns": 0,
"sys.files": []
}
}

View file

@ -0,0 +1,205 @@
{
"description": "Portal Agent Example - Agent-to-Agent Invocation",
"graph": {
"nodes": [
{
"id": "begin",
"data": {
"name": "Begin",
"component_name": "Begin",
"x": 100,
"y": 100
}
},
{
"id": "categorize_0",
"data": {
"name": "Intent Classifier",
"component_name": "Categorize",
"x": 300,
"y": 100
}
},
{
"id": "agent_invoke_sales",
"data": {
"name": "Sales Analysis Agent",
"component_name": "AgentInvoke",
"x": 500,
"y": 50
}
},
{
"id": "agent_invoke_customer",
"data": {
"name": "Customer Service Agent",
"component_name": "AgentInvoke",
"x": 500,
"y": 200
}
},
{
"id": "agent_invoke_docs",
"data": {
"name": "Document Processing Agent",
"component_name": "AgentInvoke",
"x": 500,
"y": 350
}
},
{
"id": "message_0",
"data": {
"name": "Response",
"component_name": "Message",
"x": 700,
"y": 200
}
}
],
"edges": [
{
"source": "begin",
"target": "categorize_0"
},
{
"source": "categorize_0",
"target": "agent_invoke_sales",
"label": "sales"
},
{
"source": "categorize_0",
"target": "agent_invoke_customer",
"label": "customer_service"
},
{
"source": "categorize_0",
"target": "agent_invoke_docs",
"label": "documents"
},
{
"source": "agent_invoke_sales",
"target": "message_0"
},
{
"source": "agent_invoke_customer",
"target": "message_0"
},
{
"source": "agent_invoke_docs",
"target": "message_0"
}
]
},
"components": {
"begin": {
"obj": {
"component_name": "Begin",
"params": {
"prologue": "Welcome to our AI Portal! I can help you with sales analysis, customer service, or document processing.",
"mode": "conversational"
}
},
"downstream": ["categorize_0"],
"upstream": []
},
"categorize_0": {
"obj": {
"component_name": "Categorize",
"params": {
"description": "Classify user intent to route to appropriate specialized agent",
"llm_id": "",
"items": [
{
"name": "sales",
"description": "Questions about sales data, revenue, reports, or business metrics"
},
{
"name": "customer_service",
"description": "Customer support requests, complaints, or general inquiries"
},
{
"name": "documents",
"description": "Document processing, analysis, or extraction tasks"
}
]
}
},
"downstream": ["agent_invoke_sales", "agent_invoke_customer", "agent_invoke_docs"],
"upstream": ["begin"]
},
"agent_invoke_sales": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_SALES_AGENT_ID_HERE",
"agent_name": "Sales Analysis Agent",
"query": "{sys.query}",
"inputs": {
"period": "current",
"metrics": ["revenue", "units", "growth"]
},
"timeout_seconds": 180,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["categorize_0"]
},
"agent_invoke_customer": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_CUSTOMER_SERVICE_AGENT_ID_HERE",
"agent_name": "Customer Service Agent",
"query": "{sys.query}",
"inputs": {},
"timeout_seconds": 120,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["categorize_0"]
},
"agent_invoke_docs": {
"obj": {
"component_name": "AgentInvoke",
"params": {
"agent_id": "YOUR_DOCUMENT_PROCESSING_AGENT_ID_HERE",
"agent_name": "Document Processing Agent",
"query": "{sys.query}",
"inputs": {
"files": "{sys.files}"
},
"timeout_seconds": 300,
"create_new_session": true
}
},
"downstream": ["message_0"],
"upstream": ["categorize_0"]
},
"message_0": {
"obj": {
"component_name": "Message",
"params": {
"content": "{agent_invoke_sales@answer}{agent_invoke_customer@answer}{agent_invoke_docs@answer}"
}
},
"downstream": [],
"upstream": ["agent_invoke_sales", "agent_invoke_customer", "agent_invoke_docs"]
}
},
"history": [],
"path": ["begin"],
"retrieval": {
"chunks": [],
"doc_aggs": []
},
"globals": {
"sys.query": "",
"sys.user_id": "",
"sys.conversation_turns": 0,
"sys.files": []
}
}