Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/dartantic_ai/lib/dartantic_ai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'package:dartantic_interface/dartantic_interface.dart';
export 'src/agent/agent.dart';
export 'src/agent/model_string_parser.dart';
export 'src/agent/orchestrators/orchestrators.dart';
export 'src/agent/tool_middleware.dart';
export 'src/chat_models/chat_models.dart';
export 'src/embeddings_models/embeddings_models.dart';
export 'src/logging_options.dart';
Expand Down
14 changes: 12 additions & 2 deletions packages/dartantic_ai/lib/src/agent/agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'media_response_accumulator.dart';
import 'model_string_parser.dart';
import 'orchestrators/default_streaming_orchestrator.dart';
import 'streaming_state.dart';
import 'tool_middleware.dart';

/// An agent that manages chat models and provides tool execution and message
/// collection capabilities.
Expand All @@ -42,6 +43,7 @@ class Agent {
/// - [tools]: List of tools the agent can use
/// - [temperature]: Model temperature (0.0 to 1.0)
/// - [enableThinking]: Enable extended thinking/reasoning (default: false)
/// - [middleware]: List of middleware to intercept tool calls
/// - [chatModelOptions]: Provider-specific chat model configuration
/// - [embeddingsModelOptions]: Provider-specific embeddings configuration
/// - [mediaModelOptions]: Provider-specific media generation configuration
Expand All @@ -51,6 +53,7 @@ class Agent {
double? temperature,
bool enableThinking = false,
String? displayName,
List<ToolMiddleware>? middleware,
this.chatModelOptions,
this.embeddingsModelOptions,
this.mediaModelOptions,
Expand Down Expand Up @@ -86,10 +89,12 @@ class Agent {
_tools = tools;
_temperature = temperature;
_enableThinking = enableThinking;
_middleware = middleware;

_logger.fine(
'Agent created successfully with ${tools?.length ?? 0} tools, '
'temperature: $temperature, enableThinking: $enableThinking',
'temperature: $temperature, enableThinking: $enableThinking, '
'middleware: ${middleware?.length ?? 0}',
);
}

Expand All @@ -103,6 +108,7 @@ class Agent {
double? temperature,
bool enableThinking = false,
String? displayName,
List<ToolMiddleware>? middleware,
this.chatModelOptions,
this.embeddingsModelOptions,
this.mediaModelOptions,
Expand All @@ -129,10 +135,12 @@ class Agent {
_tools = tools;
_temperature = temperature;
_enableThinking = enableThinking;
_middleware = middleware;

_logger.fine(
'Agent created from provider with ${tools?.length ?? 0} tools, '
'temperature: $temperature, enableThinking: $enableThinking',
'temperature: $temperature, enableThinking: $enableThinking, '
'middleware: ${middleware?.length ?? 0}',
);
}

Expand Down Expand Up @@ -189,6 +197,7 @@ class Agent {
late final double? _temperature;
late final bool _enableThinking;
late final String? _displayName;
late final List<ToolMiddleware>? _middleware;

static final Logger _logger = Logger('dartantic.chat_agent');

Expand Down Expand Up @@ -320,6 +329,7 @@ class Agent {
final state = StreamingState(
conversationHistory: conversationHistory,
toolMap: {for (final tool in toolsToUse ?? <Tool>[]) tool.name: tool},
middleware: _middleware,
);

orchestrator.initialize(state);
Expand Down
7 changes: 5 additions & 2 deletions packages/dartantic_ai/lib/src/agent/streaming_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import 'package:logging/logging.dart';
import '../chat_models/helpers/tool_id_helpers.dart';
import 'message_accumulator.dart';
import 'tool_executor.dart';
import 'tool_middleware.dart';

/// Encapsulates all mutable state required during streaming operations
class StreamingState {
/// Creates a new StreamingState instance
StreamingState({
required List<ChatMessage> conversationHistory,
required Map<String, Tool> toolMap,
List<ToolMiddleware>? middleware,
}) : _conversationHistory = conversationHistory,
_toolMap = toolMap;
_toolMap = toolMap,
executor = ToolExecutor(middleware: middleware);

/// Logger for state.streaming operations.
static final Logger _logger = Logger('dartantic.state.streaming');
Expand All @@ -34,7 +37,7 @@ class StreamingState {
final MessageAccumulator accumulator = const MessageAccumulator();

/// Tool executor for provider-specific tool execution
final ToolExecutor executor = const ToolExecutor();
final ToolExecutor executor;

/// Coordinator for managing tool IDs across the conversation
final ToolIdCoordinator toolIdCoordinator = ToolIdCoordinator();
Expand Down
51 changes: 46 additions & 5 deletions packages/dartantic_ai/lib/src/agent/tool_executor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'dart:convert';
import 'package:dartantic_interface/dartantic_interface.dart';
import 'package:logging/logging.dart';

import 'tool_middleware.dart';

/// Result of executing a single tool
class ToolExecutionResult {
/// Creates a new ToolExecutionResult
Expand Down Expand Up @@ -36,9 +38,15 @@ class ToolExecutionResult {
/// - Executes tools sequentially
/// - Formats results as JSON strings
/// - Includes error details in results for LLM consumption
/// - Supports middleware for intercepting tool calls
class ToolExecutor {
/// Creates a new ToolExecutor
const ToolExecutor();
///
/// [middleware] - Optional list of middleware to intercept tool calls
const ToolExecutor({this.middleware});

/// Optional middleware to intercept tool calls
final List<ToolMiddleware>? middleware;

static final _logger = Logger('dartantic.executor.tool');

Expand Down Expand Up @@ -74,13 +82,46 @@ class ToolExecutor {
ToolPart toolCall,
Map<String, Tool> toolMap,
) async {
// Look up the tool first
final tool = toolMap[toolCall.name];

// If middleware exists, chain through it
if (middleware != null && middleware!.isNotEmpty) {
return _executeWithMiddleware(toolCall, tool, toolMap);
}

// Otherwise, execute directly (existing behavior)
return _executeDirectly(toolCall, tool);
}

/// Executes a tool call through the middleware chain.
Future<ToolExecutionResult> _executeWithMiddleware(
ToolPart toolCall,
Tool? tool,
Map<String, Tool> toolMap,
) async {
int index = 0;

Future<ToolExecutionResult> next() {
if (index < middleware!.length) {
final current = middleware![index++];
return current.intercept(toolCall, tool, next);
} else {
// Last middleware - execute actual tool (if found)
return _executeDirectly(toolCall, tool);
}
}

return next();
}

/// Executes a tool call directly without middleware.
Future<ToolExecutionResult> _executeDirectly(
ToolPart toolCall,
Tool? tool,
) async {
if (tool == null) {
_logger.warning(
'Tool ${toolCall.name} not found in available tools: '
'${toolMap.keys.join(', ')}',
);
_logger.warning('Tool ${toolCall.name} not found in available tools');

final error = Exception('Tool ${toolCall.name} not found');
return ToolExecutionResult(
Expand Down
82 changes: 82 additions & 0 deletions packages/dartantic_ai/lib/src/agent/tool_middleware.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'dart:async';

import 'package:dartantic_interface/dartantic_interface.dart';

import 'tool_executor.dart';

/// Middleware that can intercept tool calls before and after execution.
///
/// Middleware allows you to:
/// - Log tool calls and results
/// - Modify tool arguments before execution
/// - Skip tool execution and return custom results
/// - Modify or replace tool results after execution
/// - Implement custom error handling
///
/// Example:
/// ```dart
/// class LoggingMiddleware implements ToolMiddleware {
/// @override
/// Future<ToolExecutionResult> intercept(
/// ToolPart toolCall,
/// Tool? tool,
/// Future<ToolExecutionResult> Function() next,
/// ) async {
/// print('Before: ${toolCall.name}');
/// final result = await next();
/// print('After: ${toolCall.name} -> ${result.isSuccess}');
/// return result;
/// }
/// }
/// ```
abstract class ToolMiddleware {
/// Creates a ToolMiddleware
const ToolMiddleware();

/// Intercepts a tool call before and/or after execution.
///
/// [toolCall] - The tool call to intercept
/// [tool] - The matched tool instance, or null if the tool was not found
/// [next] - Callback to continue to the next middleware or actual execution
///
/// Returns the ToolExecutionResult (can be modified or replaced)
Future<ToolExecutionResult> intercept(
ToolPart toolCall,
Tool? tool,
Future<ToolExecutionResult> Function() next,
);
}

/// Adapter that wraps a function to implement ToolMiddleware.
///
/// This allows you to use a simple function as middleware instead of
/// creating a class.
///
/// Example:
/// ```dart
/// final middleware = FunctionToolMiddleware(
/// (toolCall, tool, next) async {
/// print('Executing ${toolCall.name}');
/// return next();
/// },
/// );
/// ```
class FunctionToolMiddleware implements ToolMiddleware {
/// Creates a FunctionToolMiddleware that wraps the given handler function.
const FunctionToolMiddleware(this.handler);

/// The function that handles the middleware logic
final Future<ToolExecutionResult> Function(
ToolPart toolCall,
Tool? tool,
Future<ToolExecutionResult> Function() next,
)
handler;

@override
Future<ToolExecutionResult> intercept(
ToolPart toolCall,
Tool? tool,
Future<ToolExecutionResult> Function() next,
) => handler(toolCall, tool, next);
}
Loading