Skip to content

Commit 4a9194d

Browse files
Feature/API 004 news risk factors (#68)
* AIP-004 add maritime news sources, RSS parsing, and news configuration #50 Made-with: Cursor * AIP-004 add risk factors, composite schema, and price suggestion news integration #50 Made-with: Cursor * AIP-004 add tests and update phase-2 docs for news risk analysis #50 Made-with: Cursor
1 parent 846779b commit 4a9194d

33 files changed

Lines changed: 2137 additions & 82 deletions

docs/ISSUES-PHASE2.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,14 +1500,14 @@ suggestion. The LLM synthesizes news headlines into risk factors that may affect
15001500

15011501
```java
15021502
public interface NewsProvider {
1503-
List<NewsItem> getRecentHeadlines(String route, int maxResults);
1503+
List<MaritimeNewsArticle> getRecentHeadlines(String route, int maxResults);
15041504
}
15051505
```
15061506

1507-
**`NewsItem` DTO:**
1507+
**`MaritimeNewsArticle` DTO:**
15081508

15091509
```java
1510-
public class NewsItem {
1510+
public class MaritimeNewsArticle {
15111511
private String headline;
15121512
private String source;
15131513
private LocalDate publishedDate;

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@
101101
<artifactId>json-schema-validator</artifactId>
102102
<version>1.0.87</version>
103103
</dependency>
104+
<!-- RSS feed parsing -->
105+
<dependency>
106+
<groupId>com.rometools</groupId>
107+
<artifactId>rome</artifactId>
108+
<version>2.1.0</version>
109+
</dependency>
104110
</dependencies>
105111

106112
<build>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.shipping.freightops.dto;
2+
3+
import java.time.LocalDate;
4+
5+
/**
6+
* Represents a maritime news article used for shipping risk analysis and freight pricing decisions.
7+
* Contains headline, source, publication date, and summary information relevant to maritime
8+
* operations.
9+
*/
10+
public class MaritimeNewsArticle {
11+
private String headline;
12+
private String source;
13+
private LocalDate publishedDate;
14+
private String summary;
15+
16+
public MaritimeNewsArticle() {}
17+
18+
public MaritimeNewsArticle(
19+
String headline, String source, LocalDate publishedDate, String summary) {
20+
this.headline = headline;
21+
this.source = source;
22+
this.publishedDate = publishedDate;
23+
this.summary = summary;
24+
}
25+
26+
public String getHeadline() {
27+
return headline;
28+
}
29+
30+
public void setHeadline(String headline) {
31+
this.headline = headline;
32+
}
33+
34+
public String getSource() {
35+
return source;
36+
}
37+
38+
public void setSource(String source) {
39+
this.source = source;
40+
}
41+
42+
public LocalDate getPublishedDate() {
43+
return publishedDate;
44+
}
45+
46+
public void setPublishedDate(LocalDate publishedDate) {
47+
this.publishedDate = publishedDate;
48+
}
49+
50+
public String getSummary() {
51+
return summary;
52+
}
53+
54+
public void setSummary(String summary) {
55+
this.summary = summary;
56+
}
57+
}

src/main/java/com/shipping/freightops/dto/PriceSuggestionResponse.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.shipping.freightops.enums.ContainerSize;
55
import com.shipping.freightops.enums.PriceSuggestionConfidence;
66
import java.math.BigDecimal;
7+
import java.util.ArrayList;
8+
import java.util.List;
79

810
public class PriceSuggestionResponse {
911
private String voyageNumber;
@@ -17,6 +19,7 @@ public class PriceSuggestionResponse {
1719
private BigDecimal historicalAvgUsd;
1820
private BigDecimal historicalMinUsd;
1921
private BigDecimal historicalMaxUsd;
22+
private List<RiskFactor> riskFactors;
2023

2124
public String getVoyageNumber() {
2225
return voyageNumber;
@@ -111,6 +114,14 @@ public void setHistoricalMaxUsd(BigDecimal historicalMaxUsd) {
111114
this.historicalMaxUsd = historicalMaxUsd;
112115
}
113116

117+
public List<RiskFactor> getRiskFactors() {
118+
return riskFactors;
119+
}
120+
121+
public void setRiskFactors(List<RiskFactor> riskFactors) {
122+
this.riskFactors = riskFactors;
123+
}
124+
114125
/** Creates a fallback response for no-data or parse-failure scenarios. */
115126
public static PriceSuggestionResponse fallback(
116127
String voyageNumber, String route, ContainerSize containerSize, String reasoning) {
@@ -126,6 +137,7 @@ public static PriceSuggestionResponse fallback(
126137
response.setHistoricalAvgUsd(null);
127138
response.setHistoricalMinUsd(null);
128139
response.setHistoricalMaxUsd(null);
140+
response.setRiskFactors(new ArrayList<>());
129141
return response;
130142
}
131143
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.shipping.freightops.dto;
2+
3+
import com.shipping.freightops.enums.RiskImpact;
4+
5+
public class RiskFactor {
6+
private String factor;
7+
private RiskImpact impact;
8+
private String description;
9+
10+
public RiskFactor() {}
11+
12+
public RiskFactor(String factor, RiskImpact impact, String description) {
13+
this.factor = factor;
14+
this.impact = impact;
15+
this.description = description;
16+
}
17+
18+
public String getFactor() {
19+
return factor;
20+
}
21+
22+
public void setFactor(String factor) {
23+
this.factor = factor;
24+
}
25+
26+
public RiskImpact getImpact() {
27+
return impact;
28+
}
29+
30+
public void setImpact(RiskImpact impact) {
31+
this.impact = impact;
32+
}
33+
34+
public String getDescription() {
35+
return description;
36+
}
37+
38+
public void setDescription(String description) {
39+
this.description = description;
40+
}
41+
}

src/main/java/com/shipping/freightops/dto/VoyageResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public LocalDateTime getArrivalDate() {
8686
return arrivalTime;
8787
}
8888

89-
public void setArrivalDate(LocalDateTime departureTime) {
89+
public void setArrivalDate(LocalDateTime arrivalTime) {
9090
this.arrivalTime = arrivalTime;
9191
}
9292

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.shipping.freightops.enums;
2+
3+
/** Represents the potential impact level of a risk factor on shipping pricing. */
4+
public enum RiskImpact {
5+
HIGH,
6+
MEDIUM,
7+
LOW
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.shipping.freightops.news;
2+
3+
import com.shipping.freightops.dto.MaritimeNewsArticle;
4+
import java.util.List;
5+
6+
public interface MaritimeNewsSource {
7+
8+
List<MaritimeNewsArticle> getRecentHeadlines(String route, int maxResults);
9+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.shipping.freightops.news;
2+
3+
import com.shipping.freightops.dto.MaritimeNewsArticle;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.Set;
7+
import java.util.stream.Collectors;
8+
import org.springframework.stereotype.Service;
9+
10+
@Service
11+
public class ShippingNewsAnalyzer {
12+
13+
private static final String ROUTE_SPLIT_REGEX = "[→\\-\\s]+";
14+
15+
private static final Set<String> SHIPPING_KEYWORDS =
16+
Set.of(
17+
"port",
18+
"shipping",
19+
"container",
20+
"vessel",
21+
"freight",
22+
"cargo",
23+
"maritime",
24+
"ocean",
25+
"terminal",
26+
"logistics",
27+
"trade",
28+
"export",
29+
"import",
30+
"suez",
31+
"panama",
32+
"canal",
33+
"strait",
34+
"route",
35+
"disruption");
36+
37+
private final MaritimeNewsSource maritimeNewsSource;
38+
39+
public ShippingNewsAnalyzer(MaritimeNewsSource maritimeNewsSource) {
40+
this.maritimeNewsSource = maritimeNewsSource;
41+
}
42+
43+
public List<MaritimeNewsArticle> getRelevantHeadlines(String route, int maxResults) {
44+
List<MaritimeNewsArticle> allHeadlines =
45+
maritimeNewsSource.getRecentHeadlines(route, maxResults * 2);
46+
47+
if (allHeadlines.isEmpty()) {
48+
return List.of();
49+
}
50+
51+
Set<String> routeKeywords = extractRouteKeywords(route);
52+
53+
return allHeadlines.stream()
54+
.filter(item -> isRelevantToRoute(item, routeKeywords))
55+
.limit(maxResults)
56+
.collect(Collectors.toList());
57+
}
58+
59+
private Set<String> extractRouteKeywords(String route) {
60+
return Arrays.stream(route.split(ROUTE_SPLIT_REGEX))
61+
.map(String::trim)
62+
.map(String::toLowerCase)
63+
.filter(keyword -> !keyword.isEmpty())
64+
.collect(Collectors.toSet());
65+
}
66+
67+
private boolean isRelevantToRoute(MaritimeNewsArticle item, Set<String> routeKeywords) {
68+
String content = (item.getHeadline() + " " + item.getSummary()).toLowerCase();
69+
70+
boolean matchesRouteKeywords =
71+
routeKeywords.stream().anyMatch(keyword -> content.contains(keyword));
72+
boolean matchesShippingKeywords = containsShippingKeywords(content);
73+
74+
return matchesRouteKeywords || matchesShippingKeywords;
75+
}
76+
77+
private boolean containsShippingKeywords(String content) {
78+
return SHIPPING_KEYWORDS.stream().anyMatch(content::contains);
79+
}
80+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.shipping.freightops.news.config;
2+
3+
import java.net.http.HttpClient;
4+
import java.time.Duration;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.http.client.JdkClientHttpRequestFactory;
8+
import org.springframework.web.client.RestClient;
9+
10+
@Configuration
11+
public class NewsConfig {
12+
13+
@Bean
14+
public RestClient.Builder newsRestClientBuilder(NewsProperties properties) {
15+
var requestFactory =
16+
new JdkClientHttpRequestFactory(
17+
HttpClient.newBuilder()
18+
.connectTimeout(Duration.ofSeconds(properties.getConnectTimeout()))
19+
.build());
20+
21+
return RestClient.builder().requestFactory(requestFactory);
22+
}
23+
}

0 commit comments

Comments
 (0)