openai_toolflow 0.0.1
openai_toolflow: ^0.0.1 copied to clipboard
A Dart package for orchestrating OpenAI tool calls with audits. Enables chaining tool calls where each step can pass structured outputs or issues to later steps.
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.