openai_toolflow
A structured way to sequentially call OpenAI tool functions, passing outputs from previous steps as inputs to subsequent steps with strong typing, retry logic, auditing, sanitation, and token tracking.
Features
- Sequential Tool Execution: Chain OpenAI tool calls where each step's output becomes the next step's input
- Strong Typing: All step inputs and outputs are strongly typed with schema validation
- Configurable Retries: Retry failed steps with configurable attempt limits per step
- Audit System: Validate step outputs with custom audit functions and issue severity levels
- Token Usage Tracking: Monitor and aggregate token usage across all steps and retries
- Flexible Input Building: Compose step inputs from any combination of previous step outputs
- Data Sanitization: Clean and transform data between steps with input/output sanitizers
Installation
Add this to your package's pubspec.yaml file:
dependencies:
openai_toolflow: ^0.0.1
Then run:
dart pub get
Quick Start
Here's a simple example that generates a feature pitch and then creates a marketing plan, using strongly-typed outputs and concrete step definitions:
import 'package:openai_toolflow/openai_toolflow.dart';
// Step 1: Generate feature pitch
class FeaturePitchOutput extends ToolOutput {
final String name;
final String tagline;
final String valueProp;
FeaturePitchOutput({
required this.name,
required this.tagline,
required this.valueProp,
required super.round,
}) : super.subclass();
@override
Map<String, dynamic> toMap() => {
'name': name,
'tagline': tagline,
'value_prop': valueProp,
'_round': round,
};
factory FeaturePitchOutput.fromMap(Map<String, dynamic> map, int round) {
return FeaturePitchOutput(
name: map['name'] ?? '',
tagline: map['tagline'] ?? '',
valueProp: map['value_prop'] ?? '',
round: round,
);
}
}
// Step 2: Generate marketing plan
class MarketingPlanOutput extends ToolOutput {
final String blogPostTitle;
final String emailCampaignBody;
final List<String> socialMediaPosts;
MarketingPlanOutput({
required this.blogPostTitle,
required this.emailCampaignBody,
required this.socialMediaPosts,
required int round,
}) : super.subclass(round: round);
@override
Map<String, dynamic> toMap() => {
'blog_post_title': blogPostTitle,
'email_campaign_body': emailCampaignBody,
'social_media_posts': socialMediaPosts,
'_round': round,
};
factory MarketingPlanOutput.fromMap(Map<String, dynamic> map, int round) {
return MarketingPlanOutput(
blogPostTitle: map['blog_post_title'] ?? '',
emailCampaignBody: map['email_campaign_body'] ?? '',
socialMediaPosts: List<String>.from(map['social_media_posts'] ?? []),
round: round,
);
}
}
class FeaturePitchStepDefinition extends StepDefinition<FeaturePitchOutput> {
@override
ToolOutput fromMap(Map<String, dynamic> data, int round) {
return FeaturePitchOutput.fromMap(data, round);
}
@override
OutputSchema get outputSchema => OutputSchema(
properties: [
PropertyEntry.string(name: 'name'),
PropertyEntry.string(name: 'tagline'),
PropertyEntry.string(name: 'value_prop'),
],
);
@override
String get stepName => 'generate_feature_pitch';
}
class MarketingPlanStepDefinition extends StepDefinition<MarketingPlanOutput> {
@override
ToolOutput fromMap(Map<String, dynamic> data, int round) {
return MarketingPlanOutput.fromMap(data, round);
}
@override
OutputSchema get outputSchema => OutputSchema(
properties: [
PropertyEntry.string(name: 'blog_post_title'),
PropertyEntry.string(name: 'email_campaign_body'),
PropertyEntry.array(
name: 'social_media_posts',
items: PropertyType.string,
),
],
);
@override
String get stepName => 'generate_marketing_plan';
}
void main() async {
final config = OpenAIConfig(
apiKey: 'your-openai-api-key',
defaultModel: 'gpt-4',
);
final steps = [
ToolCallStep.fromStepDefinition(FeaturePitchStepDefinition()),
ToolCallStep.fromStepDefinition(MarketingPlanStepDefinition()),
];
final toolFlow = ToolFlow(config: config, steps: steps);
final result = await toolFlow.run(
input: {'product_category': 'project management tool'},
);
final encoder = JsonEncoder.withIndent(' ');
print('Feature Pitch:');
// Call toMap on vanilla output directly
print(
'Feature Pitch: ${encoder.convert(result.finalResults[1].output.toMap())}\n',
);
final marketingPlan = result.finalResults[2]
.asTyped<MarketingPlanOutput>()
.output; // Option to strongly type output for parameter access
print('Marketing Plan: ${encoder.convert(marketingPlan.toMap())}');
}
Examples
This package includes a comprehensive working example to help you get started:
Professional Color Theme Generator
A complete professional color theme generator workflow is available in example/usage.dart. This example demonstrates:
- 3-step sequential workflow with expert guidance
- Strongly-typed interfaces for each step
- Retry logic and audit functions
- Token usage tracking
- Issues analysis and reporting
The example shows how to create a complete color design system for enterprise applications, with each step building upon the previous one's output.
Core Components
ToolFlow
The main orchestrator that executes your workflow steps sequentially:
final toolFlow = ToolFlow(
config: OpenAIConfig(apiKey: 'your-key'),
steps: [step1, step2, step3],
);
final result = await toolFlow.run(input: {'initial': 'data'});
ToolCallStep
Defines individual steps in your workflow:
final step = ToolCallStep.fromStepDefinition(
MyStepDefinition(),
model: 'gpt-4', // Optional: override default model
stepConfig: StepConfig( // Optional: step-specific configuration
maxRetries: 3,
audits: [myAuditFunction],
),
inputBuilder: (previousResults) => { // Optional: compose input from previous steps
'data': previousResults[0].output.toMap()['key'],
'context': previousResults[1].output.toMap()['context'],
},
);
StepConfig
Configures individual step behavior:
final stepConfig = StepConfig(
maxRetries: 3, // Retry attempts for this step
audits: [SocialMediaPostsAudit()], // Custom validation functions
// Data transformation functions
inputSanitizer: (input) => cleanInput(input),
outputSanitizer: (output) => cleanOutput(output),
// Custom validation
customPassCriteria: (issues) => issues.isEmpty,
issuesSeverityFilter: IssueSeverity.high,
);
Advanced Configuration
Input Building Strategies
Default behavior (no inputBuilder): Use the previous step's output directly.
Custom input building: Compose inputs from multiple previous steps:
inputBuilder: (previousResults) => {
...previousResults.first.toMap(),
...previousResults.last.toMap(),
},
Data Sanitization
Input Sanitizer: Transforms data after inputBuilder but before step execution:
inputSanitizer: (input) {
final cleaned = Map<String, dynamic>.from(input);
// Remove internal fields
cleaned.removeWhere((key, value) => key.startsWith('_'));
return cleaned;
}
Output Sanitizer: Cleans step outputs after execution. This is important to ensure your output matches the user's expectation and maintains data quality:
outputSanitizer: (output) {
final cleaned = Map<String, dynamic>.from(output);
// Ensure proper color format
if (cleaned['colors'] is List) {
cleaned['colors'] = (cleaned['colors'] as List)
.where((color) => RegExp(r'^#[0-9A-Fa-f]{6}$').hasMatch(color))
.toList();
}
return cleaned;
}
Audit Functions
Create custom validation logic with configurable severity levels:
class SocialMediaPostsAudit extends AuditFunction {
@override
String get auditName => 'social_media_posts_audit';
@override
Future<List<Issue>> performAudit(Map<String, dynamic> output, int round) async {
final issues = <Issue>[];
final posts = output['social_media_posts'] as List?;
if (posts == null || posts.length < 3) {
issues.add(Issue(
id: 'insufficient_posts',
severity: IssueSeverity.critical,
description: 'Not enough social media posts generated - need at least 3 posts',
suggestions: ['Adjust your input or parameters to generate more posts'],
round: round,
));
}
return issues;
}
@override
bool passedCriteria(List<Issue> issues) =>
!issues.any((issue) => issue.severity == IssueSeverity.critical);
}
Token Usage Tracking
Monitor API usage across your entire workflow:
final result = await toolFlow.run(input: data);
print('Token usage summary:');
print(' Total tokens: ${result.tokenUsage.totalTokens}');
print(' Prompt tokens: ${result.tokenUsage.promptTokens}');
print(' Completion tokens: ${result.tokenUsage.completionTokens}');
// Per-step token usage
for (int i = 0; i < result.finalResults.length; i++) {
final stepTokens = result.finalResults[i].tokenUsage;
print(' Step $i tokens: ${stepTokens.totalTokens}');
}
Issue Management
Issues are generated by audit functions and can be filtered by severity:
IssueSeverity.low: Informational issuesIssueSeverity.medium: Warnings that don't block executionIssueSeverity.high: Significant issues that may cause retriesIssueSeverity.critical: Blocking issues that fail the step
Configure issue filtering per step:
StepConfig(
issuesSeverityFilter: IssueSeverity.high, // Include high and critical issues only
customPassCriteria: (issues) =>
!issues.any((issue) => issue.severity == IssueSeverity.critical),
)
Error Handling and Retries
Steps automatically retry on failure with exponential backoff:
StepConfig(
maxRetries: 3, // Retry up to 3 times
stopOnFailure: false, // Continue to next step even if this fails
customFailureReason: (issues) =>
'Failed due to: ${issues.map((e) => e.description).join(', ')}',
)
API Reference
For complete API documentation, see the API reference.
Contributing
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Libraries
- openai_toolflow
- A Dart package for orchestrating OpenAI tool calls with audits.