From ce58fcc9996f31b0631e12594b8cbe554f1032e9 Mon Sep 17 00:00:00 2001 From: "jetbrains-junie[bot]" <201638009+jetbrains-junie[bot]@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:17:02 +0000 Subject: [PATCH 1/2] feat: implement AI weather summary service with AJAX An AI summary service was implemented to generate weather summaries and activity suggestions using LangChain4j with Google Gemini. The report page now fetches the summary asynchronously via AJAX. Configuration for the Google Gemini API key was added for live summarization. --- build.gradle | 2 + .../weatherapp/ai/AiSummaryService.java | 85 +++++++++++++++++++ .../weatherapp/web/AiSummaryController.java | 40 +++++++++ src/main/resources/application.properties | 5 ++ src/main/resources/templates/report.html | 55 +++++++++++- 5 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/weatherapp/ai/AiSummaryService.java create mode 100644 src/main/java/com/example/weatherapp/web/AiSummaryController.java diff --git a/build.gradle b/build.gradle index a2bfc98..d29afa8 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,8 @@ dependencies { // LangChain4j core - used for PromptTemplate (no external API key required) implementation 'dev.langchain4j:langchain4j:0.35.0' + // LangChain4j Google Gemini (direct Gemini API via Google AI Studio) + implementation 'dev.langchain4j:langchain4j-google-ai-gemini:0.35.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/example/weatherapp/ai/AiSummaryService.java b/src/main/java/com/example/weatherapp/ai/AiSummaryService.java new file mode 100644 index 0000000..02c1d46 --- /dev/null +++ b/src/main/java/com/example/weatherapp/ai/AiSummaryService.java @@ -0,0 +1,85 @@ +package com.example.weatherapp.ai; + +import com.example.weatherapp.weather.WeatherService.WeatherReport; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +public class AiSummaryService { + + private final String apiKey; + private final String modelName; + private final ObjectMapper mapper = new ObjectMapper(); + + private volatile ChatLanguageModel cachedModel; + + public AiSummaryService( + @Value("${ai.gemini.api-key:}") String apiKey, + @Value("${ai.gemini.model:gemini-1.5-flash}") String modelName + ) { + this.apiKey = apiKey == null ? "" : apiKey.trim(); + this.modelName = (modelName == null || modelName.isBlank()) ? "gemini-1.5-flash" : modelName; + } + + public boolean isConfigured() { + return apiKey != null && !apiKey.isBlank(); + } + + public String summarize(WeatherReport report, String timezone, String city) { + if (!isConfigured()) { + return "AI summary unavailable: missing Google Gemini API key."; + } + ChatLanguageModel model = getModel(); + String reportJson; + try { + reportJson = mapper.writeValueAsString(report); + } catch (JsonProcessingException e) { + reportJson = safeReport(report); + } + + String location = city != null && !city.isBlank() ? city : "the provided coordinates"; + String tz = timezone != null ? timezone : "auto"; + + String prompt = "You are an assistant that summarizes short-term weather forecasts for lay people.\n" + + "Given the JSON weather report from Open-Meteo (hourly arrays for next ~72 hours) and context, provide:\n" + + "1) A concise overview of the upcoming weather for the next 1-3 days in " + location + " (timezone: " + tz + ").\n" + + "2) Practical tips.\n" + + "3) 3-5 activity suggestions suited to the conditions (indoor/outdoor).\n" + + "Be specific about temperature ranges, precipitation likelihood, wind, and humidity.\n" + + "Keep it under 180 words, use short paragraphs and bullet points.\n\n" + + "Weather JSON:\n" + reportJson + "\n\n" + + "Respond in plain text (no JSON)."; + + try { + return model.generate(prompt); + } catch (RuntimeException ex) { + return "AI summary unavailable at the moment. Reason: " + ex.getMessage(); + } + } + + private String safeReport(WeatherReport r) { + try { return mapper.writeValueAsString(r); } catch (Exception e) { return "{}"; } + } + + private ChatLanguageModel getModel() { + ChatLanguageModel m = cachedModel; + if (m == null) { + synchronized (this) { + m = cachedModel; + if (m == null) { + cachedModel = m = GoogleAiGeminiChatModel.builder() + .apiKey(apiKey) + .modelName(modelName) + .build(); + } + } + } + return m; + } +} diff --git a/src/main/java/com/example/weatherapp/web/AiSummaryController.java b/src/main/java/com/example/weatherapp/web/AiSummaryController.java new file mode 100644 index 0000000..c4e3832 --- /dev/null +++ b/src/main/java/com/example/weatherapp/web/AiSummaryController.java @@ -0,0 +1,40 @@ +package com.example.weatherapp.web; + +import com.example.weatherapp.ai.AiSummaryService; +import com.example.weatherapp.weather.WeatherService.WeatherReport; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/ai-summary") +public class AiSummaryController { + + private final AiSummaryService aiSummaryService; + + public AiSummaryController(AiSummaryService aiSummaryService) { + this.aiSummaryService = aiSummaryService; + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> summarize(@RequestBody WeatherReport report, + @RequestParam(value = "timezone", required = false) String timezone, + @RequestParam(value = "city", required = false) String city) { + try { + String text = aiSummaryService.summarize(report, timezone, city); + return ResponseEntity.ok(Map.of( + "summary", text, + "model", "gemini", + "configured", aiSummaryService.isConfigured() + )); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of( + "summary", "AI summary failed.", + "error", e.getMessage() + )); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0c074a1..5585809 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,8 @@ spring.application.name=weather-app-java server.port=8080 spring.thymeleaf.cache=false + +# Google Gemini API configuration (Google AI Studio / Generative Language API) +# Provide via environment variable for security in production +ai.gemini.api-key=${GOOGLE_GEMINI_API_KEY:} +ai.gemini.model=${GOOGLE_GEMINI_MODEL:gemini-1.5-flash} diff --git a/src/main/resources/templates/report.html b/src/main/resources/templates/report.html index c218800..a9f42db 100644 --- a/src/main/resources/templates/report.html +++ b/src/main/resources/templates/report.html @@ -92,10 +92,10 @@ ← Back
Timezone: auto
+Timezone: auto