From knowing to doing — Phase 4 of the Jarvis AI Platform
The Problem with Knowledge-Only AI
After Phase 3, Jarvis could remember you across sessions and search your documents.
But it still had a fundamental limitation.
You: "What is the weather in Kathmandu right now?"
Jarvis: "I don't have access to real-time weather data."
You: "What is 2847 × 391?"
Jarvis: "The answer is approximately 1.1 million." ← WRONG
An AI that only knows things from training data is useful.
An AI that can do things is transformative.
That is what Phase 4 built.
What Is a Tool Engine?
A tool engine gives the AI model the ability to call real functions during a conversation.
The flow looks like this:
User: "What is the weather in Kathmandu?"
↓
AI Model
↓
"I should call WeatherTool"
↓
WeatherTool.getWeather("Kathmandu")
↓
"22°C, Clear sky, Humidity: 45%"
↓
AI Model
↓
"The weather in Kathmandu is 22°C and clear."
The key insight: the AI decides when to call a tool and with what input.
We don't hardcode "if user asks about weather, call WeatherTool."
The model figures that out from the tool descriptions we provide.
The Architecture Decision
The most important architectural decision in Phase 4 was the package structure.
ai.jarvis.tools/
├── JarvisTool.java ← marker interface (root)
├── ToolRegistry.java ← manages all tools (root)
├── builtin/ ← built-in tools
│ ├── DateTimeTool.java
│ ├── CalculatorTool.java
│ ├── WeatherTool.java
│ └── WebSearchTool.java
└── mcp/ ← MCP protocol
└── McpServerConfig.java
Why not put tools inside ai/?
The ai/ package handles HOW Jarvis talks to AI models.
Tools define WHAT Jarvis can do.
These are fundamentally different responsibilities.
Mixing them would mean every new tool requires changes to AI infrastructure code.
Keeping them, separate means adding a new tool requires exactly one file.
The JarvisTool Pattern
Every tool in Jarvis implements one interface.
/**
* Marker interface for all Jarvis tools.
* Spring auto-discovers all @Component implementations.
* ToolRegistry collects them all automatically.
* Adding a new tool = just add @Component.
*/
public interface JarvisTool {
// Marker interface — no methods required
}
This is the Strategy Pattern in its simplest form.
The @Tool annotation on methods tells Spring AI what each function does and when the AI should call it.
@Component
public class WeatherTool implements JarvisTool {
@Tool(description =
"Get current weather conditions for any city. "
+ "Use when user asks about weather, "
+ "temperature, or climate.")
public String getWeather(
@ToolParam(description = "City name in English")
String city) {
// Real OpenWeatherMap API call
return "Kathmandu: 22°C, Clear sky";
}
}
Three things make this work well:
The description matters enormously.
The AI reads the description to decide whether to call this tool. A vague description like "Weather tool" produces unreliable results. A specific description that explains exactly when to use it produces consistently good results.
Never throw exceptions to the AI.
If the API is down, return a friendly error string. The AI can then tell the user something went wrong instead of crashing the entire session.
Return plain strings.
Every tool returns String. The AI handles formatting. Keep tools focused on data retrieval, not presentation.
The ToolRegistry
When Spring starts, it automatically discovers every class annotated with @Component that implements JarvisTool.
@Component
public class ToolRegistry {
private final List<JarvisTool> tools;
// Spring injects ALL JarvisTool beans automatically
public ToolRegistry(List<JarvisTool> tools) {
this.tools = Collections
.unmodifiableList(tools);
log.info("ToolRegistry: {} tools registered",
tools.size());
}
public Object[] asArray() {
return tools.toArray();
}
}
The OllamaProvider then passes these to the AI model:
@Override
public Flux<String> streamChat(Prompt prompt) {
if (toolRegistry.hasTools()) {
return chatClient
.prompt(prompt)
.tools(toolRegistry.asArray()) // ← inject tools
.stream()
.content();
}
return chatClient.prompt(prompt)
.stream().content();
}
The entire tool ecosystem is now self-registering.
When we added WeatherTool, zero changes were required to OllamaProvider, AiOrchestrator, or PromptAssembler.
We added one file. That was it.
The Four Built-in Tools
DateTimeTool
@Tool(description =
"Get the current date and time. "
+ "Use when user asks what time or date it is.")
public String getCurrentDateTime() {
return ZonedDateTime.now()
.format(FULL_FORMATTER);
}
@Tool(description =
"Get time in a specific timezone. "
+ "Use when user asks about time in a city.")
public String getCurrentTimeInZone(
@ToolParam(description =
"IANA timezone ID like America/New_York")
String timezone) {
ZoneId zoneId = ZoneId.of(timezone);
return ZonedDateTime.now(zoneId)
.format(FULL_FORMATTER);
}
Before this tool, Jarvis knew the time because WorkingMemoryBuilderinjected it into every prompt.
But WorkingMemoryBuilder only injects the local server time once per request.
DateTimeTool lets the AI ask for ANY timezone ON DEMAND during the conversation.
CalculatorTool
AI models are notoriously bad at arithmetic.
GPT-4: "What is 2847 × 391?"
Response: "1,113,177" ← correct by luck
Llama 3.1 8B: "2847 × 391 ≈ 1,112,397" ← WRONG
The CalculatorTool solves this completely.
@Tool(description =
"Evaluate a mathematical expression. "
+ "Always use this instead of calculating yourself.")
public String calculate(String expression) {
try {
double result = new ExpressionBuilder(expression)
.build()
.evaluate();
return formatResult(expression, result);
} catch (Exception e) {
return "Could not evaluate: " + expression;
}
}
We switched from the JavaScript ScriptEngine(Nashorn, removed in JDK 15+) to exp4j— a pure Java expression evaluator.
This was actually discovered by a contributor during Phase 4 testing. The original implementation used Nashorn which does not exist in Java 21.
The lesson: community testing finds real issues.
WeatherTool
@Tool(description =
"Get current weather for any city. "
+ "Returns temperature, description, "
+ "humidity, and wind speed.")
public String getWeather(String city) {
if (!isConfigured()) {
return "Set OPENWEATHER_API_KEY in .env "
+ "to enable weather.";
}
// Real API call to OpenWeatherMap
}
Two design decisions worth noting:
Graceful degradation. If the API key is not set, the tool returns a helpful setup message rather than failing. The AI can still respond intelligently: "Weather is not configured on this system, but I can tell you that Kathmandu generally has a temperate climate..."
@JsonProperty on API response records. OpenWeatherMap returns feels_like in snake_case. Without @JsonProperty("feels_like"), Jackson maps it incorrectly and feelsLike is always 0.0. A CodeRabbit review caught this before it reached production.
WebSearchTool
@Tool(description =
"Search the web for current information. "
+ "Use when you need information beyond "
+ "your training data.")
public String search(String query) {
// DuckDuckGo Instant Answer API
// FREE — no API key needed
// Privacy-respecting
}
DuckDuckGo was the right choice here for three reasons:
- It is free. No API key, no rate limits for reasonable usage.
- It is private. Fits the Jarvis local-first philosophy. Your searches don't feed into advertising profiles.
-
It returns structured data. The Instant Answer API provides
AbstractText,Answer, andRelatedTopicsin JSON — easy to parse and summarize. - One CodeRabbit catch: DuckDuckGo returns PascalCase field names (
AbstractText,AbstractURL,RelatedTopics) but Jackson defaults to camelCase. Every field was deserializing as null until we added@JsonPropertyannotations to all response record fields.
MCP Server — Jarvis as a Tool Provider
The Model Context Protocol (MCP) is an open standard that allows AI systems to share tools with each other.
After building our four built-in tools, we exposed them as an MCP server:
@Configuration
@RequiredArgsConstructor
public class McpServerConfig {
private final ToolRegistry toolRegistry;
@Bean
public ToolCallbackProvider jarvisToolCallbacks() {
return MethodToolCallbackProvider
.builder()
.toolObjects(toolRegistry.asArray())
.build();
}
}
What this means:
External AI clients — Claude Desktop, VS Code AI extensions, any MCP-compatible client — can now connect to Jarvis and use its tools.
Your locally-running WeatherTool, CalculatorTool, and WebSearchTool become available to Claude, GPT, or any other AI that speaks MCP.
This was one line of configuration.
The payoff of building JarvisTool as a clean abstraction from the start.
Parallel Context Loading
One concern with adding more context sources (Phase 2: memories, Phase 3: RAG, Phase 4: tools) was performance.
Loading everything sequentially would add seconds to every request.
The solution was Mono.zip:
.then(
Mono.zip(
// All three load SIMULTANEOUSLY
sessionMemoryService.loadHistory(sessionId),
memoryService.formatForPrompt(userId, message),
ragSearchService.formatForPrompt(userId, message)
)
)
Sequential: 1ms + 20ms + 20ms = 41ms
Parallel: max(1ms, 20ms, 20ms) = 20ms
50% latency reduction for context loading at zero cost.
What We Learned
Tool descriptions are prompts.
The description you write for a @Tool method is essentially a micro-prompt. Spend as much time on descriptions as on the implementation. A well-written description means the AI uses the tool correctly every time. A vague one means unpredictable behavior.
Never let tools throw exceptions.
Every @Tool method should be wrapped in try-catch. If a tool fails, return a useful error string. The AI should always receive a String response it can reason about.
The Open/Closed Principle pays off.
Adding WeatherTool required zero changes to existing code. The @Componentannotation, JarvisTool interface, and ToolRegistry auto-discovery made this possible. This is exactly why separating tool concerns from AI infrastructure concerns was the right call.
*Community review catches real bugs.
*
The @JsonProperty issues in WeatherTool and WebSearchTool were caught by CodeRabbit. The Nashorn/exp4j issue was caught by a contributor's integration tests. Both would have caused silent failures in production. Code review — automated and human — is essential.
Results
After Phase 4, a conversation with Jarvis looks like this:
You: What time is it in Tokyo, and what's
the weather there?
Jarvis: [calls getCurrentTimeInZone("Asia/Tokyo")]
It is currently 3:45 PM JST in Tokyo.
[calls getWeather("Tokyo")]
The weather in Tokyo is 28°C with
partly cloudy skies and humidity at 65%.
It is a warm afternoon in Tokyo right now.
Two tool calls, two real-time data sources, one coherent answer.
No Python. No LangChain. Pure Java and Spring AI.
What's Next
Phase 5 added voice — Whisper transcription via Groq API and cross-platform text-to-speech.
Phase 6 built the Agent System — a full ReACT loop where Jarvis can plan and execute multi-step tasks using these exact tools.
Phase 7 is coming: a web UI where you can interact with all of this from a browser.
The foundation is solid. The tools are ready. The agents are running.
Contributing
Jarvis is open source under Apache 2.0.
If you want to add a tool, it is genuinely this simple:
@Component
public class MyTool implements JarvisTool {
@Tool(description = "What this does and when to use it")
public String doSomething(String input) {
// your implementation
return result;
}
}
That is the entire contribution. One file. One interface. Automatic registration.
GitHub: github.com/sujankim/jarvis-ai-platform
Good first issues are labeled and waiting.
Part 5: Adding Voice to a Java AI Assistant — Whisper, TTS, and the voice conversation loop.
Your AI. Your Data. Your Machine.
Top comments (0)