A minimal but complete example of an agent: a service that doesn't just answer from the LLM's own knowledge, but can decide to call Java "tools" (functions), look at the result, and keep going until it has a real answer.
User message
│
▼
AgentService.chat() ──────────────► GeminiClient.generateContent()
▲ │
│ Gemini decides:
│ "I need to call a tool" OR "here's my answer"
│ │
│ ┌─────────────────────────┴─────────────────────────┐
│ ▼ ▼
│ functionCall part plain text part
│ │ │
│ ToolRegistry.execute() │
│ (runs real Java code, │
│ e.g. CalculatorTool) │
│ │ │
│ result appended to conversation │
└──────────────┘ │
loop back to Gemini return to caller
This request/response/tool-call loop is what makes it "agentic" rather than a plain chatbot: the model is given autonomy to choose if, which, and how many tools to call before answering, and can chain several calls in a row.
src/main/java/com/example/agent/
├── AgentApplication.java Spring Boot entry point
├── config/
│ ├── GeminiProperties.java binds gemini.* from application.yml
│ └── AppConfig.java RestClient bean
├── service/
│ ├── GeminiClient.java raw HTTP call to Gemini's generateContent endpoint
│ └── tools/
│ ├── AgentTool.java interface every tool implements
│ ├── ToolRegistry.java auto-discovers all AgentTool beans
│ └── (16 tools, see catalog below)
├── agent/
│ └── AgentService.java the agent loop itself
└── controller/
└── AgentController.java REST endpoint: POST /api/agent/chat
| Tool | What it does |
|---|---|
calculator |
Exact arithmetic (add/subtract/multiply/divide) |
get_weather |
Weather lookup (mock data — swap for a real API) |
http_get |
Fetches a URL over HTTP(S), returns the response body |
get_current_time |
Current date/time in any IANA timezone |
unit_convert |
Converts length, weight, or temperature between units |
uuid_generate |
Generates one or more random UUIDs |
hash_text |
MD5 / SHA-1 / SHA-256 / SHA-512 of a string |
base64_encode / base64_decode |
Base64 conversion |
json_format |
Validates JSON and pretty-prints it |
word_count |
Line/word/character counts for a block of text |
string_replace |
Literal or regex find-and-replace |
regex_extract |
Pulls matches (and capture groups) out of text |
random_number |
Random integer in a given range |
read_file / write_file |
Read/write local files (unrestricted — see security note below) |
You don't touch the agent loop at all. Just add a new @Component implementing
AgentTool:
@Component
public class MyTool implements AgentTool {
public String getName() { return "my_tool"; }
public String getDescription() { return "Explain what it does and WHEN to use it"; }
public Map<String,Object> getParametersSchema() { /* JSON schema of args */ }
public String execute(JsonNode args) { /* do the work, return a string */ }
}ToolRegistry picks it up automatically via Spring's dependency injection
(List<AgentTool> constructor injection), and it will show up in the next
tools payload sent to Gemini.
- Get a free Gemini API key: https://aistudio.google.com/app/apikey
- Set it as an environment variable:
export GEMINI_API_KEY=your_key_here - Build & run (requires Java 17 and Maven):
mvn spring-boot:run
- Call the agent:
Watch the DEBUG logs — you'll see Gemini call
curl -X POST http://localhost:8080/api/agent/chat \ -H "Content-Type: application/json" \ -d '{"message":"What is 234 * 18, and what is the weather in Delhi?"}'
calculator, thenget_weather, then produce a final combined answer, all from a single user message.
- Conversation state is a single in-memory
Listfor demo simplicity. In a real app, key it per user/session (e.g.Map<String, List<...>>or store in Redis). - Model name:
gemini-3.6-flashis set inapplication.yml; check https://ai.google.dev/gemini-api/docs/models for the current model list, since Google renames/deprecates models over time. - Tool-result role: when sending a
functionResponseback to the model, userole: "user". Newer Gemini models (thegemini-3.xfamily) rejectrole: "function"with a 400 error — that role was used by some older models/SDKs but is no longer accepted.AgentService.javais already set up correctly for this. - thoughtSignature:
gemini-3.xmodels can attach athoughtSignaturetofunctionCallparts, which the model uses to keep track of its own reasoning across a multi-step tool-calling turn. This project already forwards the entirefunctionCallpart verbatim back into the conversation (seeAgentService.chat()), so anythoughtSignaturepresent is preserved automatically — you don't need to do anything extra unless you start transforming the raw JSON instead of passing it through as-is. - Error handling / retries around the HTTP call to Gemini are intentionally
left minimal — add
@Retryableor a circuit breaker (resilience4j) for production. - WeatherTool returns mock data — swap the body for a real HTTP call via the
same
RestClientpattern used inGeminiClient. read_file/write_file/http_gettouch the filesystem and network with no restriction — fine for local experimentation, but before exposing this agent beyond your own machine: allow-list a base directory for file access, and block requests tolocalhost/private IP ranges forhttp_getto prevent SSRF.- Consider streaming (
generateContentwithalt=sse/streamGenerateContent) if you want token-by-token output instead of waiting for the full response.