Building an AI-Powered Flight Tracker with Spring Boot and Local LLMs
Inspired by the excellent Quarkus flight tracker tutorial - here's how to build the same concept using Spring Boot and LangChain4j.
Ever wondered if you could build your own flight tracking assistant that runs entirely on your machine? No external AI services, no subscription fees, just pure Spring Boot magic combined with local AI models.
Today, we're building exactly that—an intelligent aviation assistant that can answer questions like "What commercial flights are currently over London?" or "Show me any emergency aircraft" using nothing but Java, Spring Boot, and a locally running language model.
What You'll Learn
✈️ How to integrate real-time flight data APIs
🤖 Building AI assistants with LangChain4j
🔧 Creating AI "tools" that extend language model capabilities
🏗️ Architecting Spring Boot applications with local AI models
🛡️ Adding rate limiting and error handling
Estimated Time: 30-45 minutes
Prerequisites
Before we start, make sure you have:
Java 21+ installed
Ollama running locally (download here)
llama3.1:8b model downloaded (
ollama run llama3.1:8b
)Basic understanding of Spring Boot
An internet connection (for aircraft data)
💡 New to Ollama? It's a tool that lets you run large language models locally. Think of it as your personal ChatGPT, running on your computer.
Architecture Overview
Here's how our flight tracker works:
Key Components:
Spring Boot: Our web framework and application foundation
LangChain4j: Connects the AI model with our custom tools
Ollama: Runs the language model locally (no cloud needed!)
Flight Data Tools: Custom functions that fetch real aircraft data
ADSB.fi API: Provides real-time aircraft tracking data
Getting Started
Want to see it working immediately? Clone and run:
# 1. Clone the complete example
git clone https://github.com/rokon12/flight-tracker-ai.git
cd flight-tracker-ai
# 2. Make sure Ollama is running
ollama run llama3.1:8b
# 3. Start the application
./gradlew bootRun
# 4. Test it out
curl -X POST http://localhost:8080/api/aviation/ask \
-H "Content-Type: text/plain" \
-d "Are there any military aircraft visible right now?"
Prefer to build from scratch? Continue reading below! 👇
The Tech Stack
We're using a modern, locally-hosted stack:
Java 21+ - Latest features and performance improvements
Spring Boot 3.5+ - Mature, reliable application framework
LangChain4j - Simplifies AI tool calling and memory management
Ollama - Local AI hosting (no cloud dependencies!)
ADSB.fi API - Free, real-time aircraft data
Gradle - Dependency management and builds
🌟 Why Local AI? No API keys to manage, no monthly bills, no data leaving your machine, and it works offline once set up.
To build from scratch, start by creating a new Spring Boot project:
curl https://start.spring.io/starter.zip \
-d dependencies=web,actuator \
-d groupId=dev.example \
-d artifactId=flight-tracker-ai \
-d name=flight-tracker-ai \
-d description="AI-powered flight tracker with Spring Boot" \
-d packageName=dev.example.flighttracker \
-o flight-tracker-ai.zip
unzip flight-tracker-ai.zip && cd flight-tracker-ai
Add LangChain4j to your build.gradle
:
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// AI and LLM integration
implementation 'dev.langchain4j:langchain4j:1.1.0'
implementation 'dev.langchain4j:langchain4j-ollama:1.1.0-rc1'
// Rate limiting
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Configuration That Makes Sense
Configure your application in application.yml
:
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: llama3.1:8b
options:
temperature: 0.2
management:
endpoints:
web:
exposure:
include: health,info
flight-tracker:
adsb-api:
base-url: https://opendata.adsb.fi/api/v2
timeout: 30s
rate-limit:
requests-per-minute: 60
logging:
level:
dev.example.flighttracker: DEBUG
org.springframework.ai: DEBUG
⚠️ Important: The temperature setting (0.2) makes responses more consistent and factual. Higher values (0.7+) make responses more creative but less reliable for data queries.
Modelling Aircraft Data
First, let's define our data structures. Aircraft data from ADSB.fi comes in a specific format:
package dev.example.flighttracker.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
@JsonIgnoreProperties(ignoreUnknown = true)
public record Aircraft(
@JsonProperty("hex") String hexCode,
@JsonProperty("flight") String callsign,
@JsonProperty("r") String registration,
@JsonProperty("t") String aircraftType,
@JsonProperty("desc") String description, // New field
@JsonProperty("ownOp") String ownerOperator, // New field
@JsonProperty("year") String year, // New field
@JsonProperty("category") String category,
@JsonProperty("mil") Boolean military,
@JsonProperty("lat") Double latitude,
@JsonProperty("lon") Double longitude,
@JsonProperty("alt_baro") JsonNode altBaroRaw,
@JsonProperty("alt_geom") Integer altitudeGeom, // Renamed for clarity
@JsonProperty("gs") Double groundSpeed,
@JsonProperty("track") Double heading,
@JsonProperty("nav_heading") Double navHeading,
@JsonProperty("geom_rate") Integer geomRate,
@JsonProperty("type") String type,
@JsonProperty("squawk") String squawk,
@JsonProperty("emergency") String emergency,
@JsonProperty("dbFlags") Integer dbFlags,
@JsonProperty("nav_qnh") Double navQnh,
@JsonProperty("nav_altitude_mcp") Integer navAltitudeMcp,
@JsonProperty("version") Integer version,
@JsonProperty("messages") Long messages,
@JsonProperty("seen") Double seen,
@JsonProperty("seen_pos") Double seenPos,
@JsonProperty("rssi") Double rssi,
@JsonProperty("nic") Integer nic,
@JsonProperty("rc") Integer rc,
@JsonProperty("nic_baro") Integer nicBaro,
@JsonProperty("nac_p") Integer nacP,
@JsonProperty("nac_v") Integer nacV,
@JsonProperty("sil") Integer sil,
@JsonProperty("sil_type") String silType,
@JsonProperty("gva") Integer gva,
@JsonProperty("sda") Integer sda,
@JsonProperty("alert") Integer alert,
@JsonProperty("spi") Integer spi,
@JsonProperty("mlat") JsonNode mlat,
@JsonProperty("tisb") JsonNode tisb
) {
public boolean hasLocation() {
return latitude != null && longitude != null;
}
public boolean isEmergency() {
return emergency != null && !"none".equalsIgnoreCase(emergency);
}
public String getDisplayName() {
return (callsign != null && !callsign.isBlank()) ? callsign.trim()
: registration != null ? registration : hexCode;
}
public boolean hasAltitude() {
return altitudeFt() != null;
}
public String formattedAlt() {
return hasAltitude() ? altitudeFt() + " ft" : "GROUND";
}
/**
* Returns the best available altitude. It prefers the geometric altitude (`alt_geom`)
* but will fall back to the barometric altitude (`alt_baro`) if it's a valid integer.
*
* @return The altitude in feet, or null if no valid altitude is available.
*/
public Integer altitudeFt() {
if (altitudeGeom != null) {
return altitudeGeom;
}
// This correctly handles cases where alt_baro is "ground" (returns null)
// or an integer value.
return (altBaroRaw != null && altBaroRaw.isInt()) ? altBaroRaw.intValue() : null;
}
}
And the API response wrapper:
package dev.example.flighttracker.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public record FlightDataResponse(
@JsonProperty("aircraft")
@JsonAlias("ac")
List<Aircraft> aircraft,
@JsonProperty("resultCount")
@JsonAlias("total")
Integer total,
@JsonProperty("now")
Long timestamp,
@JsonProperty("ptime")
Long parseTime,
@JsonProperty("msg")
String message,
@JsonProperty("ctime")
Long creationTime
) {
/**
* Returns the list of aircraft, or an empty list if the source is null.
* This prevents NullPointerExceptions downstream.
*/
public List<Aircraft> getAircraft() {
return aircraft != null ? aircraft : List.of();
}
}
Building the Flight Data Service
Now for the heart of our application - the service that fetches real aircraft data:
package dev.example.flighttracker.service;
import dev.example.flighttracker.model.Aircraft;
import dev.example.flighttracker.model.FlightDataResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.List;
@Service
public class FlightDataService {
private static final Logger logger = LoggerFactory.getLogger(FlightDataService.class);
private final WebClient webClient;
public FlightDataService(@Value("${flight-tracker.adsb-api.base-url}") String baseUrl,
@Value("${flight-tracker.adsb-api.timeout}") Duration timeout) {
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.build();
}
public List<Aircraft> findMilitaryAircraft() {
logger.debug("Fetching military aircraft data");
return makeRequest("/mil");
}
public List<Aircraft> findAircraftNear(double latitude, double longitude, int radiusNm) {
logger.debug("Fetching aircraft near {}, {} within {}nm", latitude, longitude, radiusNm);
String path = String.format("/lat/%.4f/lon/%.4f/dist/%d", latitude, longitude, radiusNm);
return makeRequest(path);
}
public List<Aircraft> findByCallsign(String callsign) {
logger.debug("Fetching aircraft with callsign: {}", callsign);
return makeRequest("/callsign/" + callsign.toUpperCase());
}
public List<Aircraft> findEmergencyAircraft() {
logger.debug("Fetching emergency aircraft (squawk 7700)");
return makeRequest("/sqk/7700");
}
private List<Aircraft> makeRequest(String path) {
try {
FlightDataResponse response = webClient.get()
.uri(path)
.retrieve()
.bodyToMono(FlightDataResponse.class)
.timeout(Duration.ofSeconds(30))
.block();
return response != null ? response.getAircraft() : List.of();
} catch (Exception e) {
logger.error("Failed to fetch flight data from path: {}", path, e);
return List.of();
}
}
}
Creating AI Tools for Flight Data
Here's where LangChain4j shines. We define functions that our AI can call.
Think of AI Tools as the specialized skills or hands that the AI Assistant can use to interact with the outside world and gather specific information. Our AI Assistant is a flight tracker, so its tools are designed to get flight data.
Each tool is like a mini-program or a function that performs a single, specific task related to finding flight information. For example:
One tool might be able to find aircraft based on a location.
Another tool might specialize in finding military planes.
Yet another tool could search for a specific flight using its callsign.
The AI Assistant's brain (the AI model) analyzes your question and decides which tool (or tools) it needs to call to get the data required to answer you. It's like you asking a knowledgeable friend a question, and that friend knowing exactly which expert or database to consult to get the answer.
🧠 How AI Tools Work: When you ask "What military aircraft are flying?", the AI model reads your question, realizes it needs to call the
findMilitaryAircraft
tool, gets the data, and then formats a natural response.
package dev.example.flighttracker.service;
package ca.bazlur.flighttracker.sevice;
import ca.bazlur.flighttracker.model.Aircraft;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class FlightDataFunctions {
private final FlightDataService flightDataService;
public FlightDataFunctions(FlightDataService flightDataService) {
this.flightDataService = flightDataService;
}
@Tool("Find aircraft near a specific location")
public String findAircraftNearLocation(double latitude, double longitude, int radiusNm) {
List<Aircraft> aircraft = flightDataService.findAircraftNear(latitude, longitude, radiusNm);
return formatAircraftList(aircraft, "Aircraft near location");
}
@Tool("Find military aircraft currently in flight")
public String findMilitaryAircraft() {
List<Aircraft> aircraft = flightDataService.findMilitaryAircraft();
return formatAircraftList(aircraft, "Military aircraft");
}
@Tool("Find aircraft by callsign")
public String findAircraftByCallsign(String callsign) {
List<Aircraft> aircraft = flightDataService.findByCallsign(callsign);
return formatAircraftList(aircraft, "Aircraft with callsign " + callsign);
}
@Tool("Find aircraft in emergency situation")
public String findEmergencyAircraft() {
List<Aircraft> aircraft = flightDataService.findEmergencyAircraft();
return formatAircraftList(aircraft, "Emergency aircraft");
}
private String formatAircraftList(List<Aircraft> aircraft, String title) {
if (aircraft.isEmpty()) {
return String.format("No %s found at this time.", title.toLowerCase());
}
StringBuilder result = new StringBuilder();
result.append(String.format("%s (%d found):\n", title, aircraft.size()));
aircraft.forEach(ac -> {
// Main header for the aircraft
result.append("\n✈️ ").append(ac.getDisplayName());
if (ac.registration() != null && !ac.getDisplayName().equals(ac.registration())) {
result.append(" (").append(ac.registration()).append(")");
}
result.append("\n");
// Use a list to build details for cleaner formatting
List<String> details = new java.util.ArrayList<>();
// Type, Description, and Year
StringBuilder typeInfo = new StringBuilder();
if (ac.description() != null && !ac.description().isBlank()) {
typeInfo.append(ac.description());
if (ac.aircraftType() != null) {
typeInfo.append(" (").append(ac.aircraftType()).append(")");
}
} else if (ac.aircraftType() != null) {
typeInfo.append(ac.aircraftType());
}
if (ac.year() != null && !ac.year().isBlank()) {
typeInfo.append(", built ").append(ac.year());
}
if (!typeInfo.isEmpty()) {
details.add("Type: " + typeInfo);
}
// Operator
if (ac.ownerOperator() != null && !ac.ownerOperator().isBlank()) {
details.add("Operator: " + ac.ownerOperator());
}
// Position and Heading
if (ac.hasLocation()) {
String position = String.format("Position: %.4f, %.4f", ac.latitude(), ac.longitude());
if (ac.heading() != null) {
position += String.format(" | Heading: %.0f°", ac.heading());
}
details.add(position);
}
// Altitude and Vertical Speed
if (ac.hasAltitude()) {
String altitude = "Altitude: " + ac.formattedAlt();
if (ac.geomRate() != null && ac.geomRate() != 0) {
String direction = ac.geomRate() > 0 ? "Climbing" : "Descending";
altitude += String.format(" (%s at %d fpm)", direction, Math.abs(ac.geomRate()));
}
details.add(altitude);
}
// Speed
if (ac.groundSpeed() != null) {
details.add(String.format("Speed: %.0f kts", ac.groundSpeed()));
}
// Squawk code
if (ac.squawk() != null) {
details.add("Squawk: " + ac.squawk());
}
// Status (Emergency/Military)
List<String> statuses = new java.util.ArrayList<>();
if (ac.isEmergency()) {
statuses.add("🚨 EMERGENCY: " + ac.emergency());
}
if (Boolean.TRUE.equals(ac.military())) {
statuses.add("🪖 MILITARY");
}
if (!statuses.isEmpty()) {
details.add("Status: " + String.join(" | ", statuses));
}
// Append all details with indentation
for (String detail : details) {
result.append(" - ").append(detail).append("\n");
}
});
return result.toString();
}
}
Notice that each tool method:
Has a clear
@Tool
description.Calls the
flightDataService
to get specific data.Uses the
formatAircraftList
helper to make the result readable.
The formatAircraftList
method is a simple helper. It takes the raw list of Aircraft
objects returned by the flightDataService
and builds a human-readable string, often using Markdown (\n
, *
, -
) to make it look good in the chat interface. This step is helpful because it gives the AI model a pre-formatted block of text it can directly use or slightly adjust, rather than making the AI model parse the raw, complex Aircraft
objects itself
💡 Pro Tip: The
@Tool
annotation description is crucial - it tells the AI when to use each function. Make these descriptions clear and specific!
The sequence diagram would appear as follows.
The AI Assistant Service
The core logic for the AI Assistant is primarily handled by the AviationAssistantService.java
and AviationAiService.java
files, using a library called LangChain4j. LangChain4j is a framework that makes it much easier to build applications powered by language models. It helps connect the model, memory, tools, and prompts.
Let's look at AviationAssistantService.java
:
package dev.example.flighttracker.sevice;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.service.AiServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class AviationAssistantService {
private static final Logger logger = LoggerFactory.getLogger(AviationAssistantService.class);
private final AviationAiService aviationAiService;
public AviationAssistantService(@Value("${langchain4j.ollama.chat-model.base-url:http://localhost:11434}") String baseUrl,
@Value("${langchain4j.ollama.chat-model.model-name:llama3.1:8b}") String modelName,
@Value("${langchain4j.ollama.chat-model.temperature:0.2}") Double temperature,
FlightDataFunctions flightDataFunctions) {
var chatLanguageModel = OllamaChatModel.builder()
.baseUrl(baseUrl)
.modelName(modelName)
.temperature(temperature)
.build();
aviationAiService = AiServices.builder(AviationAiService.class)
.chatModel(chatLanguageModel)
.chatMemory(MessageWindowChatMemory.withMaxMessages(20))
.tools(flightDataFunctions)
.build();
}
public String processQuery(String userQuery) {
try {
return aviationAiService.processQuery(userQuery);
} catch (Exception e) {
logger.error("error processing query", e);
return "I'm currently experiencing technical difficulties. Please try again later.";
}
}
}
it uses
AiServices.builder(AviationAiService.class)
from LangChain4j. This is the key part! LangChain4j takes ourAviationAiService
interface (we'll look at it next) and automatically creates a class that implements it, wiring up thechatModel
(Ollama),chatMemory
, andtools
.MessageWindowChatMemory.withMaxMessages(20)
creates a simple chat memory that remembers the last 20 messages.tools(flightDataFunctions)
connects the collection of functions that can fetch real flight data.The
processQuery(String userQuery)
method is a public method. Its job is simply to pass the user's query to theaviationAiService
instance createdby LangChain4j.
Now, let's look at AviationAiService.java
:
1
package dev.example.flighttracker.service;
package ca.bazlur.flighttracker.sevice;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
public interface AviationAiService {
@SystemMessage("""
You are an intelligent aviation assistant with access to real-time flight data.
Your primary function is to use the available tools to answer user questions about aviation.
- Always use tools when users ask about specific flights or locations.
- Provide context and explanations, not just raw data. Be helpful and educational.
- Alert users if you find emergency or unusual situations.
- Use nautical miles for distances.
- When presenting flight data, format it as a clean, easy-to-read summary. Use Markdown for clarity.
- For single aircraft, use a card format:
- **Callsign:** [Callsign]
- **Altitude:** [Altitude] ft
- **Speed:** [Speed] kts
- **Coordinates:** [Lat], [Lon]
- For multiple aircraft, create a list of these cards.
Major airports coordinates for reference:
- Frankfurt (FRA): 50.0379, 8.5622
- Munich (MUC): 48.3537, 11.7863
- Berlin (BER): 52.3667, 13.5033
- London Heathrow (LHR): 51.4700, -0.4543
- Paris CDG (CDG): 49.0097, 2.5479
""")
String processQuery(@UserMessage String userMessage);
}
🎯 System Message Magic: This is like giving the AI a job description. It instructs the AI on how to behave, what tools it has, and how to format its responses.
REST Controller and Rate Limiting
Let's add the REST endpoint with basic rate limiting:
package dev.example.flighttracker.controller;
package ca.bazlur.flighttracker.controller;
import ca.bazlur.flighttracker.sevice.AviationAssistantService;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Refill;
import jakarta.validation.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/api/aviation")
public class AviationController {
private static final Logger logger = LoggerFactory.getLogger(AviationController.class);
private final AviationAssistantService assistantService;
private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
public AviationController(AviationAssistantService assistantService) {
this.assistantService = assistantService;
}
@PostMapping("/ask")
public ResponseEntity<String> askQuestion(@RequestBody @NotBlank String question,
@RequestHeader(value = "X-Forwarded-For", defaultValue = "unknown") String clientIp) {
// Rate limiting
Bucket bucket = buckets.computeIfAbsent(clientIp, this::createBucket);
if (!bucket.tryConsume(1)) {
return ResponseEntity.status(429).body("Rate limit exceeded. Please try again later.");
}
try {
logger.info("Processing aviation query from {}: {}", clientIp, question);
String response = assistantService.processQuery(question);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Error processing query: {}", question, e);
return ResponseEntity.status(500)
.body("Sorry, I encountered an error processing your question. Please try again.");
}
}
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Aviation AI Assistant is ready for takeoff! ✈️");
}
private Bucket createBucket(String key) {
Bandwidth limit = Bandwidth.classic(60, Refill.intervally(60, Duration.ofMinutes(1)));
return Bucket4j.builder()
.addLimit(limit)
.build();
}
}
Add the bucket4j dependency to your build.gradle
:
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
Running the Complete Example
The easiest way to get started is to use the complete implementation:
Ensure you have Ollama running locally.
Follow the instructions here if you haven't installed it: https://ollama.com/download
# Make sure Ollama is running with your chosen model
ollama run llama3.1:8b
# Start the application
./gradlew bootRun
Now you can interact with your aviation assistant:
# Check if everything's working
curl http://localhost:8080/api/aviation/health
# Ask about military aircraft
curl -X POST http://localhost:8080/api/aviation/ask \
-H "Content-Type: text/plain" \
-d "Are there any military aircraft visible right now?"
# Check for flights near a major airport
curl -X POST http://localhost:8080/api/aviation/ask \
-H "Content-Type: text/plain" \
-d "What commercial flights are currently within 20 nautical miles of London Heathrow?"
# Look for specific flights
curl -X POST http://localhost:8080/api/aviation/ask \
-H "Content-Type: text/plain" \
-d "Can you find flight BA123?"
What We've Accomplished
This isn't just another Spring Boot tutorial. We've built something genuinely useful - a conversational AI that understands aviation and can answer real questions about what's happening in the skies right now.
The system combines
Real-time data from the ADSB.fi network
Local AI processing with Ollama (no cloud required)
Spring Boot's reliability and ecosystem
Function calling to bridge natural language and APIs
Rate limiting to be respectful of external services
The best part? Everything runs on your machine. No external AI services, no monthly bills, just pure Java doing what it does best.
Troubleshooting Common Issues
Ollama Connection Problems
Error: Connection refused to localhost:11434
Solution:
# Check if Ollama is running
curl http://localhost:11434/api/tags
# If not running, start it
ollama serve
# Make sure you have the model
ollama run llama3.1:8b
No Aircraft Found
Error: All queries return "No aircraft found"
Possible Causes:
ADSB.fi API might be experiencing high traffic
Your internet connection might be blocking the API
The specific search area might genuinely have no aircraft
Solutions:
# Test the API directly
curl "https://opendata.adsb.fi/api/v2/lat/51.4700/lon/-0.4543/dist/50"
# Try a broader search area
curl -X POST http://localhost:8080/api/aviation/ask \
-H "Content-Type: text/plain" \
-d "What's flying within 100 nautical miles of London?"
Rate Limiting Issues
Error: Rate limit exceeded
Solution:
Wait one minute between requests
Adjust the rate limit in
application.yml
if testing locallyUse different IP addresses for testing
Model Performance Issues
Problem: Slow responses or timeouts
Solutions:
# Use a smaller, faster model
ollama run llama3.1:8b # instead of larger models
# Increase timeouts in application.yml
flight-tracker:
adsb-api:
timeout: 60s # increase from 30s
The key fixes:
Added proper
###
headers for each subsectionUsed consistent bullet points (
-
instead of*
)Removed broken code block syntax
Made the formatting consistent throughout
Simple Web Interface
Once your application is running, simply open your browser and navigate to:
http://localhost:8080/
Next Steps
Want to extend this further? Here are some ideas:
Add weather data integration for runway conditions
Include flight delay predictions
Build an improved web UI for easier interaction
Add persistent conversation history
Integrate with flight planning APIs
The foundation is solid - now it's time to make it your own.
💡 Pro Tip: Star the GitHub repository and consider contributing improvements back to the community!