Core classes and interfaces of LangChain4j providing foundational abstractions for LLM interaction, RAG, embeddings, agents, and observability
Package: dev.langchain4j.agent.tool
Thread-Safety: Tool instances should be stateless or synchronized
Validation: Always validate tool inputs - LLMs can provide invalid data
Annotation-based tool definition for LLM function calling with automatic specification generation. LangChain4j provides a declarative approach to defining tools that LLMs can invoke.
Target: Methods Retention: Runtime Thread-Safety: N/A (annotation)
Marks methods as tools for LLM function calling.
/**
* Marks a method as a tool that can be called by the LLM
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Tool {
/**
* Optional tool name (defaults to method name)
* @return Tool name (non-null, max 64 chars recommended)
*/
String name() default "";
/**
* Tool description (helps LLM understand when to use it)
* Multi-line descriptions supported via array
* @return Tool description (non-null, max 1024 chars recommended)
*/
String[] value() default {};
/**
* Controls whether result is returned to LLM or directly to user
* TO_LLM (default): Result goes back to LLM for processing
* IMMEDIATE: Result goes directly to user, bypassing LLM
* @return Return behavior (non-null)
*/
ReturnBehavior returnBehavior() default ReturnBehavior.TO_LLM;
/**
* JSON string for LLM-provider-specific metadata
* Can be used to pass additional provider-specific configuration
* @return JSON metadata string (default empty object)
* @since 1.10.0
* @Experimental Currently only supported by langchain4j-anthropic
*/
String metadata() default "{}";
}Usage Example:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.exception.ToolExecutionException;
public class WeatherTools {
/**
* IMPORTANT: Keep tool methods stateless or thread-safe
* Tool instances may be called concurrently by multiple threads
*/
`@Tool`("Get current weather for a location")
public String getCurrentWeather(String city, String country) {
// Input validation (CRITICAL - LLMs can hallucinate invalid values)
if (city == null || city.trim().isEmpty()) {
throw new ToolExecutionException("City name is required");
}
if (country == null || !country.matches("[A-Z]{2}")) {
throw new ToolExecutionException("Country must be 2-letter ISO code (e.g., US, FR)");
}
try {
// Implementation
WeatherData data = weatherService.getWeather(city, country);
return String.format("Temperature: %d°C, Condition: %s",
data.temperature(), data.condition());
} catch (ServiceException e) {
// Always wrap in ToolExecutionException for proper error propagation
throw new ToolExecutionException("Weather service error: " + e.getMessage(), e);
}
}
`@Tool`(
name = "get_forecast",
value = {"Get 5-day weather forecast", "Returns daily forecast for specified location"}
)
public String getForecast(String city) {
// Validate and implement
if (city == null || city.isEmpty()) {
throw new ToolExecutionException("City is required");
}
return "Mon: 20°C, Tue: 22°C, Wed: 19°C, Thu: 21°C, Fri: 23°C";
}
}Best Practices:
@P annotationsCommon Pitfalls:
Target: Method parameters Retention: Runtime Purpose: Provides parameter descriptions for LLM
Annotates tool method parameters with descriptions to help the LLM understand usage.
/**
* Provides description for tool method parameters
* Helps LLM understand expected parameter format and constraints
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface P {
/**
* Parameter description shown to LLM
* Should include format, constraints, examples
* @return Description text (non-null, max 512 chars recommended)
*/
String value();
}Usage Example:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
public class CalculatorTools {
`@Tool`("Add two numbers together")
public double add(
`@P`("The first number (any decimal value)") double a,
`@P`("The second number (any decimal value)") double b
) {
return a + b;
}
`@Tool`("Calculate compound interest for an investment")
public double compoundInterest(
`@P`("Principal amount in dollars (positive value)") double principal,
`@P`("Annual interest rate as decimal, e.g., 0.05 for 5% (0.0-1.0)") double rate,
`@P`("Number of years (positive integer)") int years,
`@P`("Compounding frequency per year, e.g., 12 for monthly (positive integer)") int compoundsPerYear
) {
// Validation
if (principal <= 0) {
throw new ToolExecutionException("Principal must be positive");
}
if (rate < 0 || rate > 1) {
throw new ToolExecutionException("Rate must be between 0.0 and 1.0");
}
if (years <= 0 || compoundsPerYear <= 0) {
throw new ToolExecutionException("Years and compounds must be positive");
}
return principal * Math.pow(1 + rate / compoundsPerYear, compoundsPerYear * years);
}
}Best Practices for @P Descriptions:
Common Pitfalls:
@P annotations - LLM won't understand parametersTarget: Method parameters Retention: Runtime Purpose: Injects memory/user ID for multi-user scenarios
Marks a parameter as the memory ID for multi-user/multi-session scenarios. The framework automatically injects the current user/session ID.
/**
* Marks a parameter as the memory/user ID
* Framework automatically injects current user/session context
* Parameter type: typically String or Object
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface ToolMemoryId {
}Usage Example:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import dev.langchain4j.agent.tool.P;
public class UserPreferencesTools {
private final PreferenceStore preferenceStore;
public UserPreferencesTools(PreferenceStore store) {
this.preferenceStore = store;
}
/**
* Save user preference
* @param userId Automatically injected by framework (not provided by LLM)
* @param key Preference key
* @param value Preference value
* @return Confirmation message
*/
`@Tool`("Save user preference for current user")
public String savePreference(
`@Tool`MemoryId String userId, // Injected automatically
`@P`("Preference key (e.g., 'theme', 'language')") String key,
`@P`("Preference value") String value
) {
// userId is never null - framework guarantees injection
// Validate other parameters
if (key == null || key.trim().isEmpty()) {
throw new ToolExecutionException("Preference key cannot be empty");
}
if (value == null) {
throw new ToolExecutionException("Preference value cannot be null");
}
preferenceStore.save(userId, key, value);
return String.format("Preference '%s' saved for user %s", key, userId);
}
`@Tool`("Get user preference for current user")
public String getPreference(
`@Tool`MemoryId String userId, // Injected automatically
`@P`("Preference key to retrieve") String key
) {
if (key == null || key.trim().isEmpty()) {
throw new ToolExecutionException("Preference key cannot be empty");
}
String value = preferenceStore.get(userId, key);
if (value == null) {
return String.format("No preference '%s' found for current user", key);
}
return value;
}
}Important Notes:
MemoryId parameters are NEVER provided by the LLM@P description - LLM should not know about itCommon Pitfalls:
@P annotation on @ToolMemoryId parameterThread-Safety: Immutable enum Values: TO_LLM (default), IMMEDIATE
Controls how tool results are handled.
/**
* Controls whether tool result goes to LLM or directly to user
*/
enum ReturnBehavior {
/**
* Return result to LLM for processing (default)
* LLM can format, summarize, or contextualize the result
* Use for: Data retrieval, calculations, queries
*/
TO_LLM,
/**
* Return result immediately to user, skip LLM
* Result goes directly to user without LLM processing
* Use for: Actions, commands, system operations
*/
IMMEDIATE
}Usage Example:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ReturnBehavior;
public class SystemTools {
/**
* IMMEDIATE behavior: Result bypasses LLM and goes directly to user
* Use for: Actions that should happen immediately
*/
`@Tool`(
value = {"Shut down the system"},
returnBehavior = ReturnBehavior.IMMEDIATE
)
public String shutdownSystem() {
performShutdown();
return "System is shutting down...";
// This message goes directly to user, LLM never sees it
}
/**
* TO_LLM behavior (default): Result goes to LLM for formatting
* Use for: Data that needs interpretation or formatting
*/
`@Tool`("Get system status")
public String getStatus() {
return "CPU: 45%, Memory: 60%, Disk: 75%";
// LLM receives this and can format nicely for user:
// "Your system is running normally with moderate resource usage..."
}
/**
* IMMEDIATE: Direct response for time-sensitive operations
*/
`@Tool`(
value = {"Execute emergency protocol"},
returnBehavior = ReturnBehavior.IMMEDIATE
)
public String emergencyProtocol() {
executeEmergencyActions();
return "Emergency protocol activated!";
// User sees this immediately without LLM delay
}
}When to Use Each:
| ReturnBehavior | Use Case | Example |
|---|---|---|
| TO_LLM (default) | Data retrieval | Weather, calculations, database queries |
| TO_LLM | Results needing interpretation | System metrics, logs, complex data |
| TO_LLM | Results LLM should summarize | Long text, multiple items |
| IMMEDIATE | Actions/commands | Shutdown, restart, execute |
| IMMEDIATE | Time-sensitive operations | Emergency alerts, real-time updates |
| IMMEDIATE | Simple confirmations | "File deleted", "Order placed" |
Performance Notes:
Thread-Safety: Immutable (after build)
Builder: Available via builder()
Complete tool specification for LLM including name, description, and parameter schemas.
/**
* Complete specification of a tool
* Immutable after construction
*/
class ToolSpecification {
/**
* @return Builder for constructing specifications
*/
static Builder builder();
/**
* @return Tool name (never null, non-empty)
*/
String name();
/**
* @return Tool description (never null, may be empty)
*/
String description();
/**
* @return Parameter specifications (never null, may be empty)
*/
ToolParameters parameters();
}Builder Methods:
/**
* Builder for ToolSpecification
* Not thread-safe - use from single thread
*/
class Builder {
/**
* Sets tool name
* @param name The tool name (required, non-null, non-empty)
* @return This builder
* @throws IllegalArgumentException if name is null or empty
*/
Builder name(String name);
/**
* Sets tool description
* @param description The tool description (required, non-null)
* @return This builder
*/
Builder description(String description);
/**
* Sets parameter specifications
* @param parameters The parameters (nullable)
* @return This builder
*/
Builder parameters(ToolParameters parameters);
/**
* Adds a single parameter
* @param name Parameter name (required, non-null, non-empty)
* @param properties Parameter properties (type, description, etc.)
* @return This builder
*/
Builder addParameter(String name, Map<String, Object> properties);
/**
* Builds the specification
* @return ToolSpecification instance (immutable)
* @throws IllegalStateException if required fields missing
*/
ToolSpecification build();
}Usage Example:
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolParameters;
import java.util.Map;
import java.util.List;
// Manual specification (alternative to `@Tool` annotation)
ToolSpecification spec = ToolSpecification.builder()
.name("get_weather")
.description("Get current weather for a location")
.parameters(ToolParameters.builder()
.addProperty("city", Map.of(
"type", "string",
"description", "City name, e.g., 'Paris', 'New York'"
))
.addProperty("country", Map.of(
"type", "string",
"description", "ISO 3166-1 alpha-2 country code, e.g., 'FR', 'US'"
))
.addProperty("unit", Map.of(
"type", "string",
"enum", List.of("celsius", "fahrenheit"),
"description", "Temperature unit"
))
.required(List.of("city", "country")) // unit is optional
.build())
.build();
// Use in chat request
ChatRequest request = ChatRequest.builder()
.messages(messages)
.toolSpecifications(List.of(spec))
.build();Common Pitfalls:
build() - returns Builder, not ToolSpecificationrequired() listThread-Safety: Immutable (after build)
Builder: Available via builder()
Tool parameter specifications with JSON Schema format.
/**
* Parameters specification for a tool
* Follows JSON Schema format
* Immutable after construction
*/
class ToolParameters {
/**
* @return Builder for constructing parameters
*/
static Builder builder();
/**
* @return Always "object" (JSON Schema requirement)
*/
String type();
/**
* @return Map of parameter name to parameter schema (never null)
*/
Map<String, Map<String, Object>> properties();
/**
* @return List of required parameter names (never null, may be empty)
*/
List<String> required();
}Usage Example:
import dev.langchain4j.agent.tool.ToolParameters;
import java.util.Map;
import java.util.List;
ToolParameters params = ToolParameters.builder()
// String parameter with constraints
.addProperty("email", Map.of(
"type", "string",
"description", "User email address",
"pattern", "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
))
// Number parameter with range
.addProperty("age", Map.of(
"type", "number",
"description", "User age in years",
"minimum", 0,
"maximum", 150
))
// Enum parameter
.addProperty("role", Map.of(
"type", "string",
"description", "User role",
"enum", List.of("admin", "user", "guest")
))
// Array parameter
.addProperty("tags", Map.of(
"type", "array",
"description", "List of tags",
"items", Map.of("type", "string")
))
// Mark required parameters
.required(List.of("email", "role")) // age and tags are optional
.build();JSON Schema Property Types:
string: Text valuesnumber: Numeric values (float/double)integer: Integer valuesboolean: true/falsearray: List of valuesobject: Nested objectenum: Fixed set of valuesCommon Constraints:
pattern: Regex pattern (strings)minimum/maximum: Numeric boundsminLength/maxLength: String length boundsminItems/maxItems: Array size boundsenum: List of allowed valuesThread-Safety: Utility class with static methods (thread-safe) Purpose: Extract specifications from annotated classes
Helper for generating specifications from annotated classes.
/**
* Utility for extracting tool specifications from annotated objects
* All methods are static and thread-safe
*/
class ToolSpecifications {
/**
* Extracts tool specifications from an object's `@Tool` methods
* Scans all public methods annotated with `@Tool`
* @param objectWithTools Object containing `@Tool` annotated methods (non-null)
* @return List of tool specifications (never null, may be empty)
* @throws IllegalArgumentException if objectWithTools is null
*/
static List<ToolSpecification> toolSpecificationsFrom(Object objectWithTools);
/**
* Extracts tool specifications from a class's `@Tool` methods
* Scans all public methods annotated with `@Tool`
* @param classWithTools Class containing `@Tool` annotated methods (non-null)
* @return List of tool specifications (never null, may be empty)
* @throws IllegalArgumentException if classWithTools is null
*/
static List<ToolSpecification> toolSpecificationsFrom(Class<?> classWithTools);
}Usage Example:
import dev.langchain4j.agent.tool.ToolSpecifications;
import dev.langchain4j.agent.tool.ToolSpecification;
import java.util.List;
public class MyTools {
`@Tool`("Calculate sum of two numbers")
public int sum(`@P`("First number") int a, `@P`("Second number") int b) {
return a + b;
}
`@Tool`("Get current timestamp in ISO 8601 format")
public String getCurrentTime() {
return Instant.now().toString();
}
// Private methods are NOT included
private void helperMethod() {
// Not extracted
}
}
// Extract specifications from instance
MyTools tools = new MyTools();
List<ToolSpecification> specs = ToolSpecifications.toolSpecificationsFrom(tools);
System.out.println("Found " + specs.size() + " tools"); // Prints: Found 2 tools
for (ToolSpecification spec : specs) {
System.out.println("Tool: " + spec.name());
System.out.println("Description: " + spec.description());
System.out.println("Parameters: " + spec.parameters().properties().keySet());
}
// Extract from class (alternative)
List<ToolSpecification> specsFromClass =
ToolSpecifications.toolSpecificationsFrom(MyTools.class);Important Notes:
@Tool@P annotationsCommon Pitfalls:
@Tool (won't be found)Thread-Safety: Immutable Source: Created by LLM during function calling
Request to execute a tool (created by LLM).
/**
* Request from LLM to execute a tool
* Immutable value object
*/
class ToolExecutionRequest {
/**
* @return Builder for constructing requests
*/
static Builder builder();
/**
* @return Unique ID for this tool call (never null, non-empty)
* Generated by LLM, used to correlate request with result
*/
String id();
/**
* @return Name of the tool to execute (never null, non-empty)
*/
String name();
/**
* @return Tool arguments as JSON string (never null, may be empty "{}")
* Format: JSON object with parameter names as keys
*/
String arguments();
}Usage Example:
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.Map;
// Typically received from LLM in AiMessage
AiMessage aiMessage = chatResponse.aiMessage();
if (aiMessage.hasToolExecutionRequests()) {
List<ToolExecutionRequest> requests = aiMessage.toolExecutionRequests();
for (ToolExecutionRequest request : requests) {
System.out.println("Tool: " + request.name());
System.out.println("ID: " + request.id());
System.out.println("Args: " + request.arguments());
// Parse arguments (JSON string to Map)
ObjectMapper mapper = new ObjectMapper();
try {
Map<String, Object> args = mapper.readValue(
request.arguments(),
new TypeReference<Map<String, Object>>() {}
);
// Extract typed values
String city = (String) args.get("city");
String country = (String) args.get("country");
// Execute tool (your implementation)
String result = executeWeatherTool(city, country);
// Create result message
ToolExecutionResultMessage resultMsg =
ToolExecutionResultMessage.from(request, result);
} catch (JsonProcessingException e) {
// Handle malformed JSON from LLM
System.err.println("Invalid JSON arguments: " + e.getMessage());
}
}
}Argument Parsing Pattern:
/**
* Safe argument parsing with type checking and validation
*/
public class ToolArgumentParser {
public static Map<String, Object> parseArguments(String json)
throws ToolArgumentsException {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(json, new TypeReference<Map<String, Object>>() {});
} catch (JsonProcessingException e) {
throw new ToolArgumentsException("Failed to parse arguments: " + e.getMessage(), e);
}
}
public static String getRequiredString(Map<String, Object> args, String key)
throws ToolArgumentsException {
Object value = args.get(key);
if (value == null) {
throw new ToolArgumentsException("Missing required parameter: " + key);
}
if (!(value instanceof String)) {
throw new ToolArgumentsException("Parameter " + key + " must be string");
}
return (String) value;
}
public static Integer getRequiredInteger(Map<String, Object> args, String key)
throws ToolArgumentsException {
Object value = args.get(key);
if (value == null) {
throw new ToolArgumentsException("Missing required parameter: " + key);
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
throw new ToolArgumentsException("Parameter " + key + " must be integer");
}
}Common Pitfalls:
Thread-Safety: Immutable (exception object) Usage: Throw when tool execution fails
Exception thrown during tool execution.
/**
* Thrown when tool execution fails
* Signals to LLM that tool failed (LLM can inform user)
* Extends LangChain4jException (unchecked)
*/
class ToolExecutionException extends LangChain4jException {
/**
* Creates exception with message
* @param message Error message (should be user-friendly)
*/
public ToolExecutionException(String message);
/**
* Creates exception with message and cause
* @param message Error message
* @param cause The underlying cause
*/
public ToolExecutionException(String message, Throwable cause);
/**
* Creates exception with message and error code
* @param message Error message
* @param errorCode Optional error code for specific failure types
*/
public ToolExecutionException(String message, Integer errorCode);
/**
* Creates exception with cause and error code
* @param cause The underlying cause
* @param errorCode Optional error code
*/
public ToolExecutionException(Throwable cause, Integer errorCode);
/**
* @return Optional error code for specific failure types (may be null)
*/
public Integer errorCode();
}Usage Example:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.exception.ToolExecutionException;
public class DatabaseTools {
`@Tool`("Query database")
public String query(String sql) {
// Validation
if (sql == null || sql.trim().isEmpty()) {
throw new ToolExecutionException("SQL query cannot be empty");
}
// Security check
if (sql.toUpperCase().contains("DROP") || sql.toUpperCase().contains("DELETE")) {
throw new ToolExecutionException("Destructive SQL operations not allowed", 403);
}
try {
// Execute query
ResultSet results = database.executeQuery(sql);
return formatResults(results);
} catch (SQLException e) {
// Database error - wrap in ToolExecutionException
throw new ToolExecutionException(
"Database query failed: " + e.getMessage(),
e
);
} catch (TimeoutException e) {
// Timeout - include error code
throw new ToolExecutionException("Query timed out", 408);
}
}
}Error Code Conventions:
400-499: Client errors (invalid input, unauthorized)500-599: Server errors (service failure, timeout)Best Practices:
Thread-Safety: Immutable (exception object) Usage: Throw when tool arguments are invalid
Exception thrown when tool arguments are invalid or cannot be parsed.
/**
* Thrown when tool arguments are invalid or cannot be parsed
* Signals to LLM that arguments are malformed (LLM may retry with corrections)
* Extends LangChain4jException (unchecked)
*/
class ToolArgumentsException extends LangChain4jException {
/**
* Creates exception with message
* @param message Error message describing argument issue
*/
public ToolArgumentsException(String message);
/**
* Creates exception with message and cause
* @param message Error message
* @param cause The underlying cause (e.g., JsonProcessingException)
*/
public ToolArgumentsException(String message, Throwable cause);
/**
* Creates exception with message and error code
* @param message Error message
* @param errorCode Optional error code (e.g., 400, 422)
*/
public ToolArgumentsException(String message, Integer errorCode);
/**
* Creates exception with cause and error code
* @param cause The underlying cause
* @param errorCode Optional error code
*/
public ToolArgumentsException(Throwable cause, Integer errorCode);
/**
* @return Optional error code for specific argument errors (may be null)
*/
public Integer errorCode();
}Usage Example:
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.exception.ToolArgumentsException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DataTools {
private final ObjectMapper mapper = new ObjectMapper();
`@Tool`("Process JSON data")
public String processData(String jsonData) {
// Validate not empty
if (jsonData == null || jsonData.trim().isEmpty()) {
throw new ToolArgumentsException("JSON data cannot be empty", 400);
}
try {
// Parse JSON
Map<String, Object> data = mapper.readValue(
jsonData,
new TypeReference<Map<String, Object>>() {}
);
// Validate required fields
if (!data.containsKey("id")) {
throw new ToolArgumentsException("Missing required field: 'id'", 422);
}
if (!data.containsKey("type")) {
throw new ToolArgumentsException("Missing required field: 'type'", 422);
}
// Validate field types
if (!(data.get("id") instanceof Number)) {
throw new ToolArgumentsException("Field 'id' must be numeric", 422);
}
// Process data
return processValidData(data);
} catch (JsonProcessingException e) {
// JSON parsing failed
throw new ToolArgumentsException(
"Invalid JSON format: " + e.getMessage(),
e,
400
);
}
}
}When to Use ToolArgumentsException vs ToolExecutionException:
| Exception | Use When | Example |
|---|---|---|
| ToolArgumentsException | Argument format/type wrong | Invalid JSON, wrong type, missing field |
| ToolArgumentsException | Argument validation fails | Out of range, invalid format, constraint violation |
| ToolExecutionException | Tool execution fails | Service error, timeout, permission denied |
| ToolExecutionException | Business logic error | Invalid operation, conflict, not found |
Best Practices:
import dev.langchain4j.agent.tool.*;
import dev.langchain4j.model.chat.*;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.data.message.*;
import dev.langchain4j.exception.ToolExecutionException;
import dev.langchain4j.exception.ToolArgumentsException;
import java.util.List;
import java.util.Map;
/**
* Complete tool usage example with error handling
*/
public class CompleteToolExample {
// Define tools with proper validation
public static class CalculatorTools {
`@Tool`("Add two numbers")
public double add(
`@P`("First number") double a,
`@P`("Second number") double b
) {
return a + b;
}
`@Tool`("Multiply two numbers")
public double multiply(
`@P`("First number") double a,
`@P`("Second number") double b
) {
return a * b;
}
`@Tool`("Calculate square root")
public double sqrt(`@P`("The number (must be non-negative)") double x) {
if (x < 0) {
throw new ToolExecutionException(
"Cannot calculate square root of negative number");
}
return Math.sqrt(x);
}
`@Tool`("Divide two numbers")
public double divide(
`@P`("Numerator") double a,
`@P`("Denominator (must be non-zero)") double b
) {
if (b == 0) {
throw new ToolExecutionException("Division by zero not allowed");
}
return a / b;
}
}
public static void main(String[] args) {
// Initialize chat model
ChatModel chatModel = /* OpenAiChatModel, etc. */;
// Initialize tools
CalculatorTools tools = new CalculatorTools();
// Extract tool specifications
List<ToolSpecification> toolSpecs =
ToolSpecifications.toolSpecificationsFrom(tools);
System.out.println("Loaded " + toolSpecs.size() + " tools");
// Create chat request with tools
ChatRequest request = ChatRequest.builder()
.messages(
SystemMessage.from("You are a helpful calculator assistant."),
UserMessage.from("What is 15 multiplied by 7, then add 23, then take the square root?")
)
.toolSpecifications(toolSpecs)
.build();
// Send to LLM
ChatResponse response = chatModel.chat(request);
AiMessage aiMessage = response.aiMessage();
// Process tool calls (may be multiple)
List<ChatMessage> conversationHistory = new ArrayList<>();
conversationHistory.add(SystemMessage.from("You are a helpful calculator assistant."));
conversationHistory.add(UserMessage.from("What is 15 multiplied by 7, then add 23, then take the square root?"));
while (aiMessage.hasToolExecutionRequests()) {
conversationHistory.add(aiMessage);
List<ToolExecutionRequest> requests = aiMessage.toolExecutionRequests();
System.out.println("\nLLM wants to execute " + requests.size() + " tool(s)");
for (ToolExecutionRequest toolRequest : requests) {
System.out.println("- Tool: " + toolRequest.name());
System.out.println(" Arguments: " + toolRequest.arguments());
try {
// Execute tool
String result = executeTool(tools, toolRequest);
System.out.println(" Result: " + result);
// Add result to conversation
conversationHistory.add(
ToolExecutionResultMessage.from(toolRequest, result));
} catch (ToolExecutionException e) {
System.err.println(" Execution error: " + e.getMessage());
conversationHistory.add(
ToolExecutionResultMessage.from(toolRequest,
"Error: " + e.getMessage()));
} catch (ToolArgumentsException e) {
System.err.println(" Argument error: " + e.getMessage());
conversationHistory.add(
ToolExecutionResultMessage.from(toolRequest,
"Invalid arguments: " + e.getMessage()));
}
}
// Send tool results back to LLM
ChatRequest followUpRequest = ChatRequest.builder()
.messages(conversationHistory)
.toolSpecifications(toolSpecs)
.build();
response = chatModel.chat(followUpRequest);
aiMessage = response.aiMessage();
}
// Final answer from LLM
System.out.println("\nFinal answer: " + aiMessage.text());
}
/**
* Execute tool with argument parsing and validation
*/
private static String executeTool(CalculatorTools tools, ToolExecutionRequest request)
throws ToolExecutionException, ToolArgumentsException {
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> args = mapper.readValue(
request.arguments(),
new TypeReference<Map<String, Object>>() {}
);
switch (request.name()) {
case "add":
double a = getNumber(args, "a");
double b = getNumber(args, "b");
return String.valueOf(tools.add(a, b));
case "multiply":
a = getNumber(args, "a");
b = getNumber(args, "b");
return String.valueOf(tools.multiply(a, b));
case "sqrt":
double x = getNumber(args, "x");
return String.valueOf(tools.sqrt(x));
case "divide":
a = getNumber(args, "a");
b = getNumber(args, "b");
return String.valueOf(tools.divide(a, b));
default:
throw new ToolExecutionException("Unknown tool: " + request.name());
}
} catch (JsonProcessingException e) {
throw new ToolArgumentsException("Failed to parse arguments", e);
}
}
/**
* Extract and validate numeric argument
*/
private static double getNumber(Map<String, Object> args, String key)
throws ToolArgumentsException {
Object value = args.get(key);
if (value == null) {
throw new ToolArgumentsException("Missing required parameter: " + key);
}
if (!(value instanceof Number)) {
throw new ToolArgumentsException("Parameter " + key + " must be numeric");
}
return ((Number) value).doubleValue();
}
}Output Example:
Loaded 4 tools
LLM wants to execute 1 tool(s)
- Tool: multiply
Arguments: {"a":15,"b":7}
Result: 105.0
LLM wants to execute 1 tool(s)
- Tool: add
Arguments: {"a":105.0,"b":23}
Result: 128.0
LLM wants to execute 1 tool(s)
- Tool: sqrt
Arguments: {"x":128.0}
Result: 11.313708498984761
Final answer: The result is approximately 11.31@P descriptions@ToolMemoryId for user context@Tool, @P, @ToolMemoryId)Install with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-core