16 KiB
16 KiB
LangChain4j Tool & Function Calling - Practical Examples
Production-ready examples for tool calling and function execution patterns with LangChain4j.
1. Basic Tool Calling
Scenario: Simple tools that LLM can invoke automatically.
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.model.openai.OpenAiChatModel;
class Calculator {
@Tool("Add two numbers together")
int add(@P("first number") int a, @P("second number") int b) {
return a + b;
}
@Tool("Multiply two numbers")
int multiply(@P("first number") int a, @P("second number") int b) {
return a * b;
}
@Tool("Divide two numbers")
double divide(@P("dividend") double a, @P("divisor") double b) {
if (b == 0) throw new IllegalArgumentException("Cannot divide by zero");
return a / b;
}
}
interface CalculatorAssistant {
String chat(String query);
}
public class BasicToolExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.temperature(0.0) // Deterministic for tools
.build();
var assistant = AiServices.builder(CalculatorAssistant.class)
.chatModel(chatModel)
.tools(new Calculator())
.build();
System.out.println(assistant.chat("What is 25 + 37?"));
System.out.println(assistant.chat("Calculate 12 * 8"));
System.out.println(assistant.chat("Divide 100 by 4"));
}
}
2. Multiple Tool Objects
Scenario: LLM selecting from multiple tool domains.
class WeatherService {
@Tool("Get current weather for a city")
String getWeather(@P("city name") String city) {
// Simulate API call
return "Weather in " + city + ": 22°C, Partly cloudy";
}
@Tool("Get weather forecast for next 5 days")
String getForecast(@P("city name") String city) {
return "5-day forecast for " + city + ": Sunny, Cloudy, Rainy, Sunny, Cloudy";
}
}
class DateTimeService {
@Tool("Get current date and time")
String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
@Tool("Get day of week for a date")
String getDayOfWeek(@P("date in YYYY-MM-DD format") String date) {
LocalDate localDate = LocalDate.parse(date);
return localDate.getDayOfWeek().toString();
}
}
interface MultiToolAssistant {
String help(String query);
}
public class MultipleToolsExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(MultiToolAssistant.class)
.chatModel(chatModel)
.tools(new WeatherService(), new DateTimeService())
.build();
System.out.println(assistant.help("What's the weather in Paris?"));
System.out.println(assistant.help("What time is it?"));
System.out.println(assistant.help("What day is 2024-12-25?"));
}
}
3. Tool with Complex Return Types
Scenario: Tools returning structured objects.
class UserRecord {
public String id;
public String name;
public String email;
public LocalDate createdDate;
public UserRecord(String id, String name, String email, LocalDate createdDate) {
this.id = id;
this.name = name;
this.email = email;
this.createdDate = createdDate;
}
}
class UserService {
@Tool("Look up user information by ID")
UserRecord getUserById(@P("user ID") String userId) {
// Simulate database lookup
return new UserRecord(userId, "John Doe", "john@example.com", LocalDate.now());
}
@Tool("List all users (returns top 10)")
List<UserRecord> listUsers() {
return Arrays.asList(
new UserRecord("1", "Alice", "alice@example.com", LocalDate.now()),
new UserRecord("2", "Bob", "bob@example.com", LocalDate.now())
);
}
@Tool("Search users by name pattern")
List<UserRecord> searchByName(@P("name pattern") String pattern) {
return Arrays.asList(
new UserRecord("1", "John Smith", "john.smith@example.com", LocalDate.now())
);
}
}
interface UserAssistant {
String answer(String query);
}
public class ComplexReturnTypeExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(UserAssistant.class)
.chatModel(chatModel)
.tools(new UserService())
.build();
System.out.println(assistant.answer("Who is user 123?"));
System.out.println(assistant.answer("List all users"));
System.out.println(assistant.answer("Find users named John"));
}
}
4. Error Handling in Tools
Scenario: Graceful handling of tool errors.
class DatabaseService {
@Tool("Execute read query on database")
String queryDatabase(@P("SQL query") String query) {
// Validate query is SELECT only
if (!query.trim().toUpperCase().startsWith("SELECT")) {
throw new IllegalArgumentException("Only SELECT queries allowed");
}
return "Query result: 42 rows returned";
}
@Tool("Get user count by status")
int getUserCount(@P("status") String status) {
if (!Arrays.asList("active", "inactive", "pending").contains(status)) {
throw new IllegalArgumentException("Invalid status: " + status);
}
return 150;
}
}
interface ResilientAssistant {
String execute(String command);
}
public class ErrorHandlingExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(ResilientAssistant.class)
.chatModel(chatModel)
.tools(new DatabaseService())
// Handle tool execution errors
.toolExecutionErrorHandler((toolCall, exception) -> {
System.err.println("Tool error in " + toolCall.name() + ": " + exception.getMessage());
return "Error: " + exception.getMessage();
})
// Handle malformed tool arguments
.toolArgumentsErrorHandler((toolCall, exception) -> {
System.err.println("Invalid arguments for " + toolCall.name());
return "Invalid arguments";
})
.build();
System.out.println(assistant.execute("Execute SELECT * FROM users"));
System.out.println(assistant.execute("How many active users?"));
}
}
5. Streaming Tool Execution
Scenario: Tools called during streaming responses.
import dev.langchain4j.service.TokenStream;
interface StreamingToolAssistant {
TokenStream execute(String command);
}
public class StreamingToolsExample {
public static void main(String[] args) {
var streamingModel = OpenAiStreamingChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(StreamingToolAssistant.class)
.streamingChatModel(streamingModel)
.tools(new Calculator())
.build();
assistant.execute("Calculate (5 + 3) * 4 and explain")
.onNext(token -> System.out.print(token))
.onToolExecuted(execution ->
System.out.println("\n[Tool: " + execution.request().name() + "]"))
.onCompleteResponse(response ->
System.out.println("\n--- Complete ---"))
.onError(error -> System.err.println("Error: " + error))
.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
6. Dynamic Tool Provider
Scenario: Select tools dynamically based on query context.
interface DynamicToolAssistant {
String help(String query);
}
class MathTools {
@Tool("Add two numbers")
int add(@P("a") int a, @P("b") int b) { return a + b; }
}
class TextTools {
@Tool("Convert text to uppercase")
String toUpper(@P("text") String text) { return text.toUpperCase(); }
@Tool("Convert text to lowercase")
String toLower(@P("text") String text) { return text.toLowerCase(); }
}
public class DynamicToolProviderExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(DynamicToolAssistant.class)
.chatModel(chatModel)
// Provide tools dynamically
.toolProvider(context -> {
if (context.userMessage().contains("math") || context.userMessage().contains("calculate")) {
return Collections.singletonList(new MathTools());
} else if (context.userMessage().contains("text") || context.userMessage().contains("convert")) {
return Collections.singletonList(new TextTools());
}
return Collections.emptyList();
})
.build();
System.out.println(assistant.help("Calculate 25 + 37"));
System.out.println(assistant.help("Convert HELLO to lowercase"));
}
}
7. Tool with Memory Context
Scenario: Tools accessing conversation memory.
class ContextAwareDataService {
private Map<String, String> userPreferences = new HashMap<>();
@Tool("Save user preference")
void savePreference(@P("key") String key, @P("value") String value) {
userPreferences.put(key, value);
System.out.println("Saved: " + key + " = " + value);
}
@Tool("Get user preference")
String getPreference(@P("key") String key) {
return userPreferences.getOrDefault(key, "Not found");
}
@Tool("List all preferences")
Map<String, String> listPreferences() {
return new HashMap<>(userPreferences);
}
}
interface ContextAssistant {
String chat(String message);
}
public class ToolMemoryExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var dataService = new ContextAwareDataService();
var assistant = AiServices.builder(ContextAssistant.class)
.chatModel(chatModel)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.tools(dataService)
.build();
System.out.println(assistant.chat("Remember that I like Java"));
System.out.println(assistant.chat("What do I like?"));
System.out.println(assistant.chat("Also remember I use Spring Boot"));
System.out.println(assistant.chat("What are all my preferences?"));
}
}
8. Stateful Tool Execution
Scenario: Tools that maintain state across calls.
class StatefulCounter {
private int count = 0;
@Tool("Increment counter by 1")
int increment() {
return ++count;
}
@Tool("Decrement counter by 1")
int decrement() {
return --count;
}
@Tool("Get current counter value")
int getCount() {
return count;
}
@Tool("Reset counter to zero")
void reset() {
count = 0;
}
}
interface CounterAssistant {
String interact(String command);
}
public class StatefulToolExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var counter = new StatefulCounter();
var assistant = AiServices.builder(CounterAssistant.class)
.chatModel(chatModel)
.tools(counter)
.build();
System.out.println(assistant.interact("Increment the counter"));
System.out.println(assistant.interact("Increment again"));
System.out.println(assistant.interact("What's the current count?"));
System.out.println(assistant.interact("Reset the counter"));
System.out.println(assistant.interact("Decrement"));
}
}
9. Tool Validation and Authorization
Scenario: Validate and authorize tool execution.
class SecureDataService {
@Tool("Get sensitive data")
String getSensitiveData(@P("data_id") String dataId) {
// This should normally check authorization
if (!dataId.matches("^[A-Z][0-9]{3}$")) {
throw new IllegalArgumentException("Invalid data ID format");
}
return "Sensitive data for " + dataId;
}
@Tool("Delete data (requires authorization)")
void deleteData(@P("data_id") String dataId) {
if (!dataId.matches("^[A-Z][0-9]{3}$")) {
throw new IllegalArgumentException("Invalid data ID");
}
System.out.println("Data " + dataId + " deleted");
}
}
interface SecureAssistant {
String execute(String command);
}
public class AuthorizationExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(SecureAssistant.class)
.chatModel(chatModel)
.tools(new SecureDataService())
.toolExecutionErrorHandler((request, exception) -> {
System.err.println("Authorization/validation failed: " + exception.getMessage());
return "Operation denied: " + exception.getMessage();
})
.build();
System.out.println(assistant.execute("Get data A001"));
System.out.println(assistant.execute("Get data invalid"));
}
}
10. Advanced: Tool Result Processing
Scenario: Process and transform tool results before returning to LLM.
class DataService {
@Tool("Fetch user data from API")
String fetchUserData(@P("user_id") String userId) {
return "User{id=" + userId + ", name=John, role=Admin}";
}
}
interface ProcessingAssistant {
String answer(String query);
}
public class ToolResultProcessingExample {
public static void main(String[] args) {
var chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
var assistant = AiServices.builder(ProcessingAssistant.class)
.chatModel(chatModel)
.tools(new DataService())
// Can add interceptors for tool results if needed
// This would be in a future LangChain4j version
.build();
System.out.println(assistant.answer("What is the role of user 123?"));
}
}
Best Practices
- Clear Descriptions: Write detailed @Tool descriptions for LLM context
- Strong Typing: Use specific types (int, String) instead of generic Object
- Parameter Descriptions: Use @P with clear descriptions of expected formats
- Error Handling: Always implement error handlers for graceful failures
- Temperature: Set temperature=0 for deterministic tool selection
- Validation: Validate all parameters before execution
- Logging: Log tool calls and results for debugging
- State Management: Keep tools stateless or manage state explicitly
- Timeout: Set timeouts on long-running tools
- Authorization: Validate authorization before executing sensitive operations