openai_toolflow 0.0.1 copy "openai_toolflow: ^0.0.1" to clipboard
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 issues
  • IssueSeverity.medium: Warnings that don't block execution
  • IssueSeverity.high: Significant issues that may cause retries
  • IssueSeverity.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.

1
likes
150
points
3
downloads

Documentation

API reference

Publisher

verified publisherimpiam.com

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

collection, dotenv, http, meta

More

Packages that depend on openai_toolflow