tool_schema_generator 0.3.0 copy "tool_schema_generator: ^0.3.0" to clipboard
tool_schema_generator: ^0.3.0 copied to clipboard

Generate JSON Schema Draft 2020-12 tool definitions and dispatchers for LLM function calling from annotated Dart functions.

tool_schema_generator #

A code generator for Dart that automatically produces JSON Schema (Draft 2020-12) tool definitions for Large Language Models (LLMs) from your annotated Dart functions.

pub package

If you are building AI agents with Gemini, OpenAI, Claude, or other LLMs, you often need to provide a JSON schema describing the tools (functions) the model can call. Instead of writing and maintaining massive JSON maps by hand, tool_schema_generator lets you write standard Dart functions and automatically generates the precise schemas your LLM needs.


🌟 Features #

  • Zero Boilerplate: Automatically infers types, names, and nullability directly from Dart syntax.
  • Full Analyzer Support: Supports String, int, double, bool, List<T>, Map<String, dynamic>, enums, and custom nested classes.
  • Seamless Integration: Uses the canonical source_gen combining builder. It outputs to a standard .g.dart file and plays nicely alongside other generators like json_serializable.
  • Customizable: Override tool names and descriptions, or let it automatically extract descriptions from your Dart doc comments.
  • Runtime Injection: Hide app-controlled parameters from the LLM schema with @Inject() while still passing them during dispatch.

📦 Installation #

Add the package to your pubspec.yaml:

dependencies:
  tool_schema_generator: ^0.3.0

dev_dependencies:
  build_runner: ^2.4.0

🚀 Quick Start #

1. Annotate your functions #

Create a .dart file and use the @Tool() annotation on your top-level functions. You can use the @Describe() annotation to add rich descriptions to individual parameters.

// lib/tools.dart
import 'package:tool_schema_generator/tool_schema_generator.dart';

// IMPORTANT: Declare the part file
part 'tools.g.dart';

/// Sends an email to a specific user.
@Tool()
void sendEmail(
  @Describe('The email address of the recipient') String to,
  @Describe('The subject line of the email') String subject, {
  @Describe('The main body content') required String body,
  bool isHtml = false,
}) {
  // Your logic here
}

2. Run the generator #

Run the build runner command in your terminal:

dart run build_runner build -d

3. Use the generated schemas and dispatcher #

The generator creates a tools.g.dart file containing a toolRegistry instance. This registry contains all your schemas and automatically routes LLM tool calls back to your Dart functions safely.

You can pass the schemas directly to your LLM framework using toolRegistry.allSchemas, or select individual ones via strongly-typed getters like toolRegistry.sendEmail.

  • toolRegistry.allSchemas gives you List<Map<String, dynamic>> (all schemas in the file).
  • toolRegistry.sendEmail gives you a single Map<String, dynamic> just for that tool. (You pass these directly into your LLM's tools parameter).

then disispatching (When the LLM replies)

final result = await toolRegistry.call(
  toolCall.name,
  toolCall.arguments
);

The registry takes the raw string name and raw JSON Map<String, dynamic> from the LLM, finds the right Dart closure, safely parses and casts all arguments, calls your function, and awaits the result.

This makes us reach to our final life cycle which what the tool returns after its been called and processed. It guarantees a return of the sealed class ToolResult. You never have to try/catch argument parsing errors yourself.

  • ToolSuccess: Contains the raw return .value of your Dart function.
  • ToolError: Contains structured data (.code, .message, .field) if the LLM hallucinated an argument, forgot a required field, or if your function threw an internal exception. You can feed this error message directly back to the LLM so it can fix its mistake!

This gives you a completely type-safe, boilerplate-free bridge between Dart code and LLM agent loops.

import 'tools.dart';

void main() async {
  // 1. Pass the schemas to your LLM
  final response = await llm.generate(
    prompt: "Send an email to hello@example.com saying Hi!",
    // toolRegistry.allSchemas -> is a List<Map<String, dynamic>>
    // toolRegistry.sendEmail -> is a Map<String, dynamic>
    tools: toolRegistry.allSchemas, // or [toolRegistry.sendEmail]
  );

  // 2. When the LLM decides to call a tool,
  // you just simply call the dispatch method on the
  for (final toolCall in response.toolCalls) {
    // The registry safely casts arguments,
    // which handles missing required fields,
    // and catches internal exceptions.
    final result = await toolRegistry.call(
      toolCall.name,
      toolCall.arguments,
    );

    switch (result) {
      case ToolSuccess(:final value):
        print("Tool returned: $value");
      case ToolError(:final code, :final message):
        print("Tool failed (\$code): \$message");
    }
  }
}

🧠 Advanced Usage #

Enums #

Enums are automatically converted to JSON Schema string enums:

enum Priority { low, normal, high }

@Tool()
void setTaskPriority(Priority priority) {}
// Generates: {"type": "string", "enum": ["low", "normal", "high"]}

Nested Objects #

Custom classes are introspected. The generator looks at the class's constructor parameters to build a nested JSON Schema object:

class Location {
  final double lat;
  final double lng;
  Location({required this.lat, required this.lng});
}

@Tool()
void updateLocation(Location location) {}
// Generates nested object with properties `lat` and `lng` (both required).

Overriding Names and Descriptions #

If you don't want to use the Dart function name or doc comment, you can override them directly in the annotation:

@Tool(
  name: 'custom_search_tool',
  description: 'A highly specific search tool description.'
)
void search(String query) {}

Injected Runtime Parameters #

Use @Inject() for app-controlled named parameters that should not appear in the schema sent to the LLM. Injected parameters are still read from the same arguments map passed to toolRegistry.call, so you can merge values like user IDs, tenant IDs, or request locale at invocation time.

Injected parameters must be optional: nullable, have a Dart default value, or both.

@Tool()
Future<void> createTask(
  @Describe('Task title') String title, {
  @Inject() String? userId,
  @Inject() String locale = 'en',
}) async {
  // userId and locale are available here, but only title is in the schema.
}

final result = await toolRegistry.call(toolCall.name, {
  ...toolCall.arguments,
  'userId': currentUser.id,
  'locale': request.locale,
});

🤝 Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License #

This project is licensed under the MIT License.

0
likes
160
points
291
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Generate JSON Schema Draft 2020-12 tool definitions and dispatchers for LLM function calling from annotated Dart functions.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

analyzer, build, source_gen

More

Packages that depend on tool_schema_generator