1. Background
In LLM application development, we often need to combine multiple basic operations (atomic operations) into more complex workflows. These workflows can handle complex tasks such as data retrieval, code generation, multi-turn dialogues, and more. By combining these atomic operations into Standard Operating Procedures (SOPs), we can:
- Improve code reusability
- Simplify implementation of complex tasks
- Standardize common workflows
- Reduce development and maintenance costs
This document introduces how to combine atomic operations (Ops) to form new composite operation tools using the FlowLLM framework.
2. Technical Solution
2.1 Atomic Operation Definition
Each operation (Op) needs to define the following core attributes:
class BaseAsyncToolOp:
description: str # Description of the operation
input_schema: Dict[str, ParamAttr] # Input parameter schema definition
output_schema: Dict[str, ParamAttr] # Output parameter schema definition
Where ParamAttr
defines parameter type, whether it's required, and other attributes:
class ParamAttr:
type: Type # Parameter type, such as str, int, Dict, etc.
required: bool = True # Whether it must be provided
default: Any = None # Default value
description: str = "" # Parameter description
2.2 SOP Composition Process
Step 1: Create Atomic Operation Instances
First, instantiate the required atomic operations:
from flowllm.op.gallery.mock_op import MockOp
from flowllm.op.search.tavily_search_op import TavilySearchOp
from flowllm.op.agent.react_v2_op import ReactV2Op
# Create atomic operation instances
search_op = TavilySearchOp()
react_op = ReactV2Op()
summary_op = MockOp(
description="Summarize search results",
input_schema={"search_results": ParamAttr(type=str, description="Search results to summarize")},
output_schema={"summary": ParamAttr(type=str, description="Summarized content")}
)
Step 2: Define Data Flow Between Operations
Set up input-output relationships between operations, defining how data flows between them:
# Set input parameter sources
react_op.set_input("context",
"search_summary") # react_op's context parameter is retrieved from search_summary in memory
# Set output parameter destinations
search_op.set_output("results", "search_results") # search_op's results output to search_results in memory
summary_op.set_output("summary", "search_summary") # summary_op's summary output to search_summary in memory
Step 3: Build Operation Flow Graph
Use operators to build the operation flow graph, defining execution order and parallel relationships:
# Build operation flow graph
flow = search_op >> summary_op >> react_op
# Or more complex flows
# Parallel operations use the | operator, sequential operations use the >> operator
complex_flow = (search_op >> summary_op) | (another_search_op >> another_summary_op) >> react_op
Operator explanation:
>>
: Sequential execution, execute the next operation after the previous one completes|
: Parallel execution, execute multiple operations simultaneously
Step 4: Create Composite Operation Class
Encapsulate the built operation flow into a new composite operation class:
class SearchAndReactOp(BaseToolOp):
description = "Search for information and generate a response based on search results"
input_schema = ...
output_schema = ...
def build_flow(self):
search_op = TavilySearchOp()
summary_op = MockOp()
react_op = ReactV2Op()
# Set data flow
search_op.set_output("results", "search_results")
summary_op.set_input("search_results", "search_results")
summary_op.set_output("summary", "search_summary")
react_op.set_input("context", "search_summary")
react_op.set_output("response", "response")
# Build operation flow graph
return search_op >> summary_op >> react_op
async def execute(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
# Execute operation flow
return await self.flow.execute(inputs)