Skip to main content

RunnableBinding: Configuring runnables at runtime

Sometimes we want to invoke a Runnable within a Runnable sequence with constant options that are not part of the output of the preceding Runnable in the sequence, and which are not part of the user input. We can use Runnable.bind() to pass these options in.

Suppose we have a simple prompt + model sequence:

final model = ChatOpenAI(apiKey: openaiApiKey);
const outputParser = StringOutputParser<ChatResult>();

final promptTemplate = ChatPromptTemplate.fromTemplates([
(ChatMessageType.system,
'Write out the following equation using algebraic symbols then solve it. '
'Use the format\n\nEQUATION:...\nSOLUTION:...\n\n'),
(ChatMessageType.human, '{equation_statement}'),
]);

final chain = Runnable.getMapFromInput<String>('equation_statement')
.pipe(promptTemplate)
.pipe(model)
.pipe(outputParser);

final res = await chain.invoke('x raised to the third plus seven equals 12');
print(res);
// EQUATION: \(x^3 + 7 = 12\)
//
// SOLUTION:
// Subtract 7 from both sides:
// \(x^3 = 5\)
//
// Take the cube root of both sides:
// \(x = \sqrt[3]{5}\)

and want to call the model with certain stop words:

final chain2 = Runnable.getMapFromInput<String>('equation_statement')
.pipe(promptTemplate)
.pipe(model.bind(ChatOpenAIOptions(stop: ['SOLUTION'])))
.pipe(outputParser);
final res2 = await chain2.invoke('x raised to the third plus seven equals 12');
print(res2);
// EQUATION: \( x^3 + 7 = 12 \)

You can use this pattern to configure different options for the same runnable without having to create a new instance. For example, you can use different models for different prompts:

final chatModel = ChatOpenAI(apiKey: openaiApiKey);
const outputParser = StringOutputParser<ChatResult>();
final prompt1 = PromptTemplate.fromTemplate('How are you {name}?');
final prompt2 = PromptTemplate.fromTemplate('How old are you {name}?');

final chain = Runnable.fromMap({
'q1': prompt1 |
chatModel.bind(ChatOpenAIOptions(model: 'gpt-4-turbo')) |
outputParser,
'q2': prompt2 |
chatModel.bind(ChatOpenAIOptions(model: 'gpt-4o-mini')) |
outputParser,
});

final res = await chain.invoke({'name': 'David'});
print(res);
// {q1: Hello! I'm just a computer program, so I don't have feelings,
// q2: I am an AI digital assistant, so I do not have an age like humans do.}

Another similar use case is to use different temperature settings for different parts of the chain. You can easily do this by using model.bind(ChatOpenAIOptions(temperature: 1)) as shown above.

Attaching tools

One particularly useful application of Runnable.bind() is to attach the tools that the model can call.

final model = ChatOpenAI(apiKey: openaiApiKey);
final outputParser = ToolsOutputParser();

final promptTemplate = ChatPromptTemplate.fromTemplates([
(ChatMessageType.system, 'Write out the following equation using algebraic symbols then solve it.'),
(ChatMessageType.human, '{equation_statement}'),
]);

const tool = ToolSpec(
name: 'solver',
description: 'Formulates and solves an equation',
inputJsonSchema: {
'type': 'object',
'properties': {
'equation': {
'type': 'string',
'description': 'The algebraic expression of the equation',
},
'solution': {
'type': 'string',
'description': 'The solution to the equation',
},
},
'required': ['equation', 'solution'],
},
);

final chain = Runnable.getMapFromInput<String>('equation_statement')
.pipe(promptTemplate)
.pipe(model.bind(ChatOpenAIOptions(tools: [tool])))
.pipe(outputParser);

final res = await chain.invoke('x raised to the third plus seven equals 12');
print(res);
// [ParsedToolCall{
// id: call_T2Y3g7rU5s0CzEG4nL35FJYK,
// name: solver,
// arguments: {
// equation: x^3 + 7 = 12,
// solution: x = 1
// },
// }]