diff --git a/pom.xml b/pom.xml
index c4dfe0b..0577d56 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,12 @@
1.17.2
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/kz/konturai/parser/service/KapitalParserService.java b/src/main/java/kz/konturai/parser/service/KapitalParserService.java
index 3efcc3e..1124396 100644
--- a/src/main/java/kz/konturai/parser/service/KapitalParserService.java
+++ b/src/main/java/kz/konturai/parser/service/KapitalParserService.java
@@ -32,6 +32,9 @@ public class KapitalParserService implements ParserService {
@Autowired
private MarketItemRepository marketItemRepository;
+ @Autowired
+ private OllamaAnalyticsService analyticsService;
+
@Value("${rss.kapital.url}")
private String rssFeedUrl;
@@ -79,6 +82,13 @@ public class KapitalParserService implements ParserService {
try {
MarketItem marketItem = processRssEntry(entry);
if (marketItem != null) {
+ try {
+ MarketItem.Analytics analytics = analyticsService.analyzeText(marketItem.getRawText());
+ marketItem.setAnalytics(analytics);
+ } catch (Exception e) {
+ logger.warn("Analytics step failed for Kapital '{}': {}", marketItem.getTitle(),
+ e.getMessage());
+ }
MarketItem savedItem = saveOrUpdateMarketItem(marketItem);
if (savedItem != null) {
savedItems.add(savedItem);
diff --git a/src/main/java/kz/konturai/parser/service/KursivParserService.java b/src/main/java/kz/konturai/parser/service/KursivParserService.java
index ec3fbfa..b6812f4 100644
--- a/src/main/java/kz/konturai/parser/service/KursivParserService.java
+++ b/src/main/java/kz/konturai/parser/service/KursivParserService.java
@@ -32,6 +32,9 @@ public class KursivParserService implements ParserService {
@Autowired
private MarketItemRepository marketItemRepository;
+ @Autowired
+ private OllamaAnalyticsService analyticsService;
+
@Value("${rss.feed.url}")
private String rssFeedUrl;
@@ -79,6 +82,13 @@ public class KursivParserService implements ParserService {
try {
MarketItem marketItem = processRssEntry(entry);
if (marketItem != null) {
+ try {
+ MarketItem.Analytics analytics = analyticsService.analyzeText(marketItem.getRawText());
+ marketItem.setAnalytics(analytics);
+ } catch (Exception e) {
+ logger.warn("Analytics step failed for entry '{}': {}", marketItem.getTitle(),
+ e.getMessage());
+ }
MarketItem savedItem = saveOrUpdateMarketItem(marketItem);
if (savedItem != null) {
savedItems.add(savedItem);
diff --git a/src/main/java/kz/konturai/parser/service/LsmParserService.java b/src/main/java/kz/konturai/parser/service/LsmParserService.java
index 6b73e6f..ecb000a 100644
--- a/src/main/java/kz/konturai/parser/service/LsmParserService.java
+++ b/src/main/java/kz/konturai/parser/service/LsmParserService.java
@@ -32,6 +32,9 @@ public class LsmParserService implements ParserService {
@Autowired
private MarketItemRepository marketItemRepository;
+ @Autowired
+ private OllamaAnalyticsService analyticsService;
+
@Value("${rss.lsm.url}")
private String rssFeedUrl;
@@ -79,6 +82,13 @@ public class LsmParserService implements ParserService {
try {
MarketItem marketItem = processRssEntry(entry);
if (marketItem != null) {
+ try {
+ MarketItem.Analytics analytics = analyticsService.analyzeText(marketItem.getRawText());
+ marketItem.setAnalytics(analytics);
+ } catch (Exception e) {
+ logger.warn("Analytics step failed for LSM '{}': {}", marketItem.getTitle(),
+ e.getMessage());
+ }
MarketItem savedItem = saveOrUpdateMarketItem(marketItem);
if (savedItem != null) {
savedItems.add(savedItem);
diff --git a/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java b/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java
new file mode 100644
index 0000000..d4c8c94
--- /dev/null
+++ b/src/main/java/kz/konturai/parser/service/OllamaAnalyticsService.java
@@ -0,0 +1,119 @@
+package kz.konturai.parser.service;
+
+import kz.konturai.parser.model.MarketItem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.springframework.core.ParameterizedTypeReference;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class OllamaAnalyticsService {
+
+ private static final Logger logger = LoggerFactory.getLogger(OllamaAnalyticsService.class);
+
+ private final WebClient webClient;
+
+ @Value("${ollama.model:gemma3:1b}")
+ private String modelName;
+
+ public OllamaAnalyticsService(@Value("${ollama.host:http://185.35.223.45:11434}") String ollamaHost) {
+ this.webClient = WebClient.builder()
+ .baseUrl(ollamaHost)
+ .build();
+ }
+
+ public MarketItem.Analytics analyzeText(String text) {
+ MarketItem.Analytics analytics = new MarketItem.Analytics();
+
+ try {
+ String summary = generate(text,
+ "Напиши краткое саммари следующей новостной статьи на русском языке. Ответ должен содержать только саммари из 3-4 предложений, без лишних вступлений. Статья: ");
+ analytics.setSummary(summary);
+ } catch (Exception e) {
+ logger.warn("Failed to get summary from Ollama: {}", e.getMessage());
+ }
+
+ try {
+ String tagsCsv = generate(text,
+ "Извлеки 5-7 ключевых слов или тегов из текста новостной статьи. В ответе дай только список тегов через запятую, без нумерации и заголовков. Статья: ");
+ String[] tags = tagsCsv == null ? new String[] {} : tagsCsv.replace("\n", " ").split("\\s*,\\s*");
+ analytics.setTags(tags);
+ } catch (Exception e) {
+ logger.warn("Failed to get tags from Ollama: {}", e.getMessage());
+ }
+
+ try {
+ String sentiment = generate(text,
+ "Определи тональность текста новостной статьи. В ответе дай только одно слово латиницей: positive, negative или neutral. Статья: ");
+ if (sentiment != null) {
+ sentiment = sentiment.trim().toLowerCase();
+ }
+ analytics.setSentiment(sentiment);
+ } catch (Exception e) {
+ logger.warn("Failed to get sentiment from Ollama: {}", e.getMessage());
+ }
+
+ try {
+ String entitiesJson = generate(text,
+ "Извлеки из текста имена людей, названия компаний и географические локации. В ответе дай только JSON объект следующей структуры: {\\\"persons\\\": [], \\\"companies\\\": [], \\\"locations\\\": []}. Статья: ");
+ Map entities = new HashMap<>();
+ if (entitiesJson != null && entitiesJson.trim().startsWith("{")) {
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ entities = mapper.readValue(entitiesJson, new TypeReference