{"slug": "building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims", "title": "Building Local AI Agents in Java with Tools4AI and Ollama: An Insurance Claims Use Case", "summary": "Tools4AI, a 100% Java agentic AI framework, can be combined with Ollama's local LLM server to build fully offline AI agents that never send data to third-party APIs. In a tutorial, a developer demonstrates an insurance claims triage agent that reads free-text incident reports, routes actions, extracts structured data, gates high-value payouts behind human approval, and records an audit trail.", "body_md": "[Tools4AI](https://github.com/vishalmysore/Tools4AI) is a 100% Java agentic AI framework that turns any annotated Java method into an AI-callable action. [Ollama](https://ollama.com) runs open models like Llama 3.1 and Phi-4 locally and exposes an OpenAI-compatible API. Point Tools4AI at `http://localhost:11434/v1`\n\nand you get a **fully offline, on-premise AI agent** — no data ever leaves your network. In this tutorial we build an **insurance claims triage agent** that reads a claimant's free-text incident report, routes it to the right business action, extracts structured data, gates high-value payouts behind a human approval, and records a compliance audit trail.\n\nWho is this for?Java developers, solution architects, and engineering leaders in regulated industries (insurance, banking, healthcare) who want agentic AIwithout sending sensitive data to a third-party API.\n\nInsurance runs on **personally identifiable information (PII)**: names, addresses, policy numbers, medical details, vehicle data, and loss descriptions. Sending that data to a hosted LLM API creates regulatory, contractual, and reputational risk. At the same time, claims teams are drowning in unstructured text — **First Notice of Loss (FNOL)** reports, adjuster notes, emails, and call transcripts.\n\nA **local AI agent** solves both problems at once:\n\nThat combination — private inference plus governed execution — is exactly what Tools4AI + Ollama gives you.\n\n[Tools4AI](https://github.com/vishalmysore/Tools4AI) (`io.github.vishalmysore:tools4ai`\n\non Maven Central) is a lightweight, pure-Java **agentic AI framework** and ADK. Its core idea is simple and powerful:\n\nAnnotate a Java class with\n\n`@Agent`\n\nand its methods with`@Action`\n\n. Tools4AI scans the classpath, and at runtime it maps anatural-language promptto the correct method, extracts the parameters, and invokes it — no manual function schemas required.\n\nKey capabilities used in this article:\n\n| Feature | What it does |\n|---|---|\nAction routing |\nMaps a prompt to the right `@Action` method automatically |\nAutomatic parameter mapping |\nFills method arguments (including POJOs, lists, maps, dates) from the prompt |\nPOJO transformation |\nConverts free text into a populated Java object |\nRisk gating |\nBlocks `HIGH` -risk actions unless a human approves |\nAudit trail |\nRecords every action for compliance |\nAgent memory |\nKeeps conversation context across turns |\n\nBecause it is provider-agnostic, the same code runs on Gemini, OpenAI, Anthropic — or, as we will do here, a **local Ollama model** through its OpenAI-compatible endpoint.\n\nOllama serves open-weight models (Llama 3.1, Phi-4, Mistral, Gemma, and more) behind an **OpenAI-compatible REST API** at `http://localhost:11434/v1`\n\n. Because Tools4AI's `OpenAiActionProcessor`\n\nalready speaks that protocol, wiring the two together is pure configuration — **no adapter, no new code**.\n\nPull a model that is good at **function calling** and start it:\n\n```\nollama pull llama3.1\nollama run llama3.1\n```\n\nOllama now serves the OpenAI-compatible API locally. Verify it:\n\n```\ncurl http://localhost:11434/v1/chat/completions \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\":\"llama3.1\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK\"}],\"stream\":false}'\n```\n\nYou should get a JSON response with `\"content\": \"OK\"`\n\n.\n\n```\n<dependency>\n    <groupId>io.github.vishalmysore</groupId>\n    <artifactId>tools4ai</artifactId>\n    <version>1.2.1</version>\n</dependency>\n```\n\nCheck\n\n[Maven Central]for the latest version. If you build from source, remember to enable the`-parameters`\n\ncompiler flag so Tools4AI can read method parameter names.\n\nCreate `tools4ai.properties`\n\non your classpath (e.g. `src/main/resources/tools4ai.properties`\n\n):\n\n```\n## Any non-empty value works — Ollama ignores the key,\n## but Tools4AI only builds the model when a key is present.\nopenAiKey=ollama\n\n## Ollama's OpenAI-compatible base URL\nopenAiBaseURL=http://localhost:11434/v1\n\n## Use the exact Ollama model tag\nopenAiModelName=llama3.1\n```\n\nTwo gotchas worth knowing:\n\n`LocalAIActionProcessor`\n\nis a stub`null`\n\n. Do `-D`\n\nVM options.`tools4ai.properties`\n\nfirst and only falls back to `-DopenAiModelName=...`\n\nif the file value is empty. If you rely on VM options, leave the corresponding property blank.\n\n``` python\nimport com.t4a.processor.OpenAiActionProcessor;\n\npublic class Hello {\n    public static void main(String[] args) throws Exception {\n        OpenAiActionProcessor processor = new OpenAiActionProcessor();\n        String reply = processor.query(\"Reply with exactly one word: PONG\");\n        System.out.println(reply); // -> PONG\n    }\n}\n```\n\nThat single `query()`\n\ncall already round-trips through Tools4AI's config loader, langchain4j, and your local Ollama model. If it prints `PONG`\n\n, you are fully offline and ready to build an agent.\n\nLet's build something real: an agent that handles the **First Notice of Loss (FNOL)** — the moment a policyholder reports an incident. Our agent will:\n\nThis is the whole point of Tools4AI: your business logic is just an ordinary Java class. Annotate the class with `@Agent`\n\n, annotate each callable method with `@Action`\n\n, and you're done — **no interface to implement, no boilerplate**. Tools4AI scans the classpath, registers *every* `@Action`\n\nmethod (many per class is fine), instantiates the class for you, and reads `riskLevel`\n\nstraight from the annotation.\n\n``` python\nimport com.t4a.annotations.Action;\nimport com.t4a.annotations.Agent;\nimport com.t4a.api.ActionRisk;\n\n@Agent(groupName = \"claims\", groupDescription = \"insurance policy and claims actions\")\npublic class ClaimsAgent {\n\n    @Action(description = \"check whether an insurance policy is active and in good standing \"\n            + \"given its policy number\")\n    public String checkPolicyStatus(String policyNumber) {\n        return \"Policy \" + policyNumber + \" is ACTIVE | coverage: AUTO | deductible: $500\";\n    }\n\n    @Action(description = \"open a new insurance claim for a policyholder describing an incident, \"\n            + \"given the policy number, the incident type (collision, theft, fire, water damage) \"\n            + \"and a short description\")\n    public String fileClaim(String policyNumber, String incidentType, String description) {\n        String claimId = \"CLM-\" + Math.abs((policyNumber + incidentType).hashCode() % 100000);\n        return \"Opened claim \" + claimId + \" (\" + incidentType + \") for policy \" + policyNumber;\n    }\n\n    @Action(description = \"estimate the payout amount in US dollars for a described incident, \"\n            + \"given the incident type and the estimated damage amount in dollars\")\n    public String estimatePayout(String incidentType, double estimatedDamage) {\n        double payout = Math.max(0, estimatedDamage - 500);   // minus deductible\n        return \"Estimated payout for \" + incidentType + \": $\" + payout;\n    }\n\n    // Settling a claim moves money. Declaring riskLevel = HIGH right on the annotation is enough:\n    // Tools4AI refuses to trigger it via auto-prediction, so it can only be invoked explicitly.\n    @Action(description = \"approve and settle a claim payout to the policyholder, \"\n            + \"given the claim id, the amount in dollars, and who approved it\",\n            riskLevel = ActionRisk.HIGH)\n    public String settleClaim(String claimId, double amount, String approvedBy) {\n        return \"SETTLED \" + claimId + \" for $\" + amount + \" approved by \" + approvedBy;\n    }\n}\n```\n\nTwo things to know.(1)`riskLevel`\n\nbelongs on`@Action`\n\n, not`@Agent`\n\n(`@Agent`\n\nonly carries`groupName`\n\n,`groupDescription`\n\n, and an optional`prompt`\n\n). (2) The class needs a public no-arg constructor — Tools4AI instantiates it for you.\n\n(There is an older style where an action class`implements JavaMethodAction`\n\n. Avoid it unless you need it: that path registers only the first`@Action`\n\nmethod per class and ignores`@Action(riskLevel=…)`\n\n— you'd have to split every action into its own class and override`getActionRisk()`\n\n. Plain`@Agent`\n\nclasses, as above, have neither limitation.)\n\n``` js\n### 2. Natural-language action routing\n\nNow let the agent decide which method to call from plain English. No `if/else`, no intent parser — Tools4AI does the mapping.\n```\n\njava\n\nimport com.t4a.processor.OpenAiActionProcessor;\n\nOpenAiActionProcessor agent = new OpenAiActionProcessor();\n\n// The AI picks checkPolicyStatus and extracts policyNumber = \"AUTO-88213\"\n\nObject status = agent.processSingleAction(\n\n\"Can you tell me if my policy AUTO-88213 is still active?\");\n\nSystem.out.println(status);\n\n// -> Policy AUTO-88213 is ACTIVE, auto coverage, $500 deductible\n\n// The AI picks fileClaim and extracts all three arguments\n\nObject claim = agent.processSingleAction(\n\n\"I need to file a claim on policy AUTO-88213. Someone rear-ended my car \" +\n\n\"in a parking lot and the bumper is cracked.\");\n\nSystem.out.println(claim);\n\n// -> Opened claim CLM-12345 (collision) for policy AUTO-88213\n\n```\nYou can also pass the action explicitly (`agent.processSingleAction(prompt, new FileClaimAction())`) when you already know which capability to use — handy for deterministic, single-purpose endpoints.\n\n### 3. Turn free text into a structured claim (POJO extraction)\n\nClaimants describe incidents in messy prose. Use `OpenAIPromptTransformer` to convert that into a clean Java object you can validate and persist. The `@Prompt` annotation adds per-field instructions such as date formatting.\n```\n\njava\n\nimport com.t4a.annotations.Prompt;\n\nimport java.util.Date;\n\npublic class ClaimReport {\n\npublic String claimantName;\n\npublic String policyNumber;\n\npublic String incidentType; // e.g. collision, theft, fire, water damage\n\n```\n@Prompt(describe = \"estimated cost of damage in US dollars, number only\")\npublic double  estimatedDamage;\n\n@Prompt(dateFormat = \"yyyy-MM-dd\", describe = \"date the incident occurred\")\npublic Date    incidentDate;\n\npublic String  location;\n\npublic String toString() {\n    return \"ClaimReport{name=\" + claimantName + \", policy=\" + policyNumber +\n           \", type=\" + incidentType + \", damage=$\" + estimatedDamage +\n           \", date=\" + incidentDate + \", location=\" + location + \"}\";\n}\n```\n\n}\n\njava\n\nimport com.t4a.transform.OpenAIPromptTransformer;\n\nOpenAIPromptTransformer transformer = new OpenAIPromptTransformer();\n\nString fnol =\n\n\"Hi, this is Priya Sharma, policy HOME-55021. On July 12th 2026 a burst pipe \" +\n\n\"flooded my kitchen in Austin. A plumber estimated about $3,200 in damage.\";\n\nClaimReport report = (ClaimReport) transformer.transformIntoPojo(fnol, ClaimReport.class.getName());\n\nSystem.out.println(report);\n\n// -> ClaimReport{name=Priya Sharma, policy=HOME-55021, type=water damage,\n\n// damage=$3200.0, date=2026-07-12, location=Austin}\n\n```\nOne call turns an unstructured report into a validated, typed record ready for your claims pipeline.\n\n### 4. Gate high-value payouts with human-in-the-loop\n\nSettling a claim moves money — it must **never** be triggered automatically by the model. With the **core** Tools4AI API you get two guarantees, no extra libraries required:\n\n1. **Auto-prediction refuses `HIGH`-risk actions.** If you call `processSingleAction(prompt)` with no explicit action and the best match is `HIGH` risk, it is not executed.\n2. **Explicit calls run only if a human approves.** Pass a `HumanInLoop` approver to `processSingleAction(prompt, action, approver, explain)` — the action runs only when the approver returns valid.\n\nProvide an approver by implementing `HumanInLoop`. In production each call would open a ticket, page an adjuster, or invoke your workflow engine and block until a decision returns:\n```\n\njava\n\nimport com.t4a.detect.FeedbackLoop;\n\nimport com.t4a.detect.HumanInLoop;\n\nimport java.util.Map;\n\npublic class AdjusterApproval implements HumanInLoop {\n\nprivate final boolean approve; // wire to a real approval channel\n\npublic AdjusterApproval(boolean approve) { this.approve = approve; }\n\n```\n@Override\npublic FeedbackLoop allow(String prompt, String methodName, Map<String, Object> params) {\n    return () -> approve;\n}\n@Override\npublic FeedbackLoop allow(String prompt, String methodName, String params) {\n    return () -> approve;   // core calls this String overload with the action JSON\n}\n```\n\n}\n\n```\nBecause `ClaimsAgent` is a plain `@Agent` POJO (not an `AIAction`), grab the registered action from Tools4AI's registry by name to invoke it explicitly:\n```\n\njava\n\nimport com.t4a.api.AIAction;\n\nimport com.t4a.predict.PredictionLoader;\n\nimport com.t4a.processor.LogginggExplainDecision;\n\nimport com.t4a.processor.OpenAiActionProcessor;\n\nOpenAiActionProcessor agent = new OpenAiActionProcessor();\n\nAIAction settle = PredictionLoader.getInstance().getAiAction(\"settleClaim\");\n\n// Human declines -> settleClaim never runs; you get \"Human verification failed\"\n\nagent.processSingleAction(\"Settle claim CLM-12345 for $8000, approved by Vishal\",\n\nsettle, new AdjusterApproval(false), new LogginggExplainDecision());\n\n// Human approves -> the settlement executes\n\nObject result = agent.processSingleAction(\"Settle claim CLM-12345 for $8000, approved by Vishal\",\n\nsettle, new AdjusterApproval(true), new LogginggExplainDecision());\n\n// -> SETTLED CLM-12345 for $8000.0 approved by Vishal\n\n```\n> Because `riskLevel = HIGH` is declared on the `@Action`, the auto-prediction refusal in step 1 works with no extra code.\n\n### 5. Going further: the agent toolkit\n\nBeyond the core, Tools4AI ships an **agent toolkit** (`com.t4a.agent.*`) of composable decorators over the `AIProcessor` interface — a two-signoff `RiskGatedActionProcessor`, a JSON `AuditedActionProcessor` for compliance, `InMemoryActionMetrics`, retry/rate-limit resilience, `AgentMemory` for multi-turn claims, and multi-agent orchestration. For example, a compliance audit trail is one wrapper:\n```\n\njava\n\n// Requires a Tools4AI build that includes the agent toolkit (com.t4a.agent.*)\n\nAuditTrail trail = new JsonFileAuditTrail(\"/var/claims/audit.jsonl\");\n\nAIProcessor audited = new AuditedActionProcessor(new OpenAiActionProcessor(), trail);\n\naudited.processSingleAction(\"File a claim on policy AUTO-88213 for a cracked bumper\");\n\n```\n> **Availability note:** the agent toolkit is part of newer Tools4AI builds and may not be in every published Maven Central release. Check that `com.t4a.agent.*` is present in your resolved jar before importing it. The InsureJAI demo above uses **core only**, so it builds against the published artifact as-is. For multi-turn claims, `AgentMemory` / `PersistentFileAgentMemory` from the same toolkit keep conversation context across turns and restarts.\n\n---\n\n## Choosing the right local model\n\n**Model capability directly determines how well function calling works.** In our testing:\n\n- **Capable instruction models** (`llama3.1`, `phi4`) reliably route prompts to the correct `@Action` **and** fill in the arguments — including numbers, dates, and nested POJOs.\n- **Very small models** (e.g. `gemma3:270m`) often select the right action but leave parameters empty. They are fine for a plain `query()`, but unreliable for full tool calling.\n\n**Recommendation:** start with `llama3.1` (8B) or `phi4` (14B) for claims-style extraction. Use a larger model if your prompts are long or multi-entity. Match the model to your hardware — bigger models need more RAM/VRAM and are slower on CPU.\n\n## Production hardening: composing the agent stack\n\nEvery agent-toolkit capability is a decorator over the `AIProcessor` interface, so they nest in any order — and the order encodes your policy. A production-grade claims agent might look like this (requires the `com.t4a.agent.*` toolkit noted above):\n```\n\njava\n\nAIProcessor claimsAgent =\n\nnew AuditedActionProcessor( // records the final outcome\n\nnew MeteredActionProcessor( // latency + error rates\n\nnew RiskGatedActionProcessor( // human approval for HIGH-risk settlements\n\nnew RetryActionProcessor( // survive transient model hiccups\n\nnew OpenAiActionProcessor(), // -> your local Ollama model\n\n3, 500, null),\n\nnew AdjusterApproval()),\n\nmetrics),\n\nnew JsonFileAuditTrail(\"/var/claims/audit.jsonl\"));\n\n```\nReading outside-in: audit the *final* decision, measure *user-visible* latency, require approval *once* (not per retry), and retry transient failures closest to the model. Rearranging the layers changes the semantics — choose deliberately.\n\n## Frequently asked questions\n\n**Does any claim data leave my network?**\nNo. With Ollama the model runs locally and Tools4AI talks to `http://localhost:11434`. Nothing is sent to a hosted API.\n\n**Do I need an OpenAI API key?**\nNo. Set `openAiKey=ollama` (any non-empty placeholder). Ollama ignores it; Tools4AI just needs a non-empty value to initialize the client.\n\n**Why does the model pick the right action but leave the fields blank?**\nThe model is too small for reliable function calling. Switch to `llama3.1` or `phi4`.\n\n**Can I use the same code with a cloud provider later?**\nYes. Tools4AI is provider-agnostic. Swap the processor (or the base URL/model) and your `@Agent` / `@Action` code is unchanged.\n\n**Is this production-ready?**\nThe building blocks — risk gating, audit, retry, metrics, memory — are designed for production. As always, validate model outputs, keep humans in the loop for money-moving actions, and test against your own data.\n\n## Conclusion\n\nWith **Tools4AI + Ollama** you get the best of both worlds: **private, on-premise LLM inference** and **governed, testable Java business logic**. In a few dozen lines we built an insurance FNOL agent that understands natural language, extracts structured claims, protects high-value payouts behind human approval, and logs everything for compliance — all without a single byte of PII leaving the building.\n\n- 🏥 Runnable project for this article: [github.com/vishalmysore/insureJAI](https://github.com/vishalmysore/insureJAI)\n- ⭐ Star the framework: [github.com/vishalmysore/Tools4AI](https://github.com/vishalmysore/Tools4AI)\n- 📦 Maven Central: [io.github.vishalmysore:tools4ai](https://central.sonatype.com/artifact/io.github.vishalmysore/tools4ai)\n- 📚 Architecture deep-dive: [Tools4AI ARCHITECTURE.md](https://github.com/vishalmysore/Tools4AI/blob/main/ARCHITECTURE.md)\n\n*Build private AI agents in Java. Keep your data where it belongs.*\n```\n\n", "url": "https://wpnews.pro/news/building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims", "canonical_source": "https://dev.to/vishalmysore/building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims-use-case-2m0m", "published_at": "2026-07-28 21:20:10+00:00", "updated_at": "2026-07-28 21:35:38.624500+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "large-language-models", "ai-infrastructure"], "entities": ["Tools4AI", "Ollama", "Llama 3.1", "Phi-4", "Maven Central"], "alternates": {"html": "https://wpnews.pro/news/building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims", "markdown": "https://wpnews.pro/news/building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims.md", "text": "https://wpnews.pro/news/building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims.txt", "jsonld": "https://wpnews.pro/news/building-local-ai-agents-in-java-with-tools4ai-and-ollama-an-insurance-claims.jsonld"}}