This commit is contained in:
root
2025-10-13 18:21:26 +05:00
parent 89ab124664
commit e55eecd069
10 changed files with 390 additions and 19 deletions
+112
View File
@@ -0,0 +1,112 @@
# Интеграция OpenAI API
## Обзор
Создан новый сервис `OpenAIAnalyticsService` для замены `OllamaAnalyticsService`. Новый сервис использует OpenAI API для анализа текста и генерации контента.
## Основные изменения
### 1. Новый сервис OpenAIAnalyticsService
- **Файл**: `src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java`
- **Функциональность**: Аналогична `OllamaAnalyticsService`, но использует OpenAI API
- **Методы**:
- `analyzeText(String text)` - анализ текста с извлечением саммари, тегов, тональности и сущностей
- `analyzeText(String text, String language)` - анализ с указанием языка
- `generateWithInstruction(String text, String instruction)` - генерация текста по инструкции
- `generateWithInstruction(String text, String instruction, String language)` - генерация с указанием языка
### 2. Обновленные сервисы
Все сервисы, которые использовали `OllamaAnalyticsService`, теперь используют `OpenAIAnalyticsService`:
- `ReportSynthesisService`
- `ReportGenerationService`
- `KursivParserService`
- `KapitalParserService`
- `LsmParserService`
- `RbcParserService`
- `VedomostiParserService`
### 3. Конфигурация
В `application.properties` уже настроены параметры для OpenAI:
```properties
# OpenAI Configuration
openai.api.key=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
openai.api.url=https://api.openai.com/v1/chat/completions
openai.model.name=gpt-4o-mini
openai.timeoutMs=90000
```
### 4. Тестовый контроллер
Создан `OpenAITestController` для тестирования функциональности:
- `POST /api/openai/analyze` - анализ текста
- `POST /api/openai/generate` - генерация по инструкции
- `GET /api/openai/test` - тест подключения
## Использование
### Анализ текста
```java
@Autowired
private OpenAIAnalyticsService openAIAnalyticsService;
// Анализ текста на русском языке
MarketItem.Analytics analytics = openAIAnalyticsService.analyzeText(text, "ru");
// Получение саммари
String summary = analytics.getSummary();
// Получение тегов
String[] tags = analytics.getTags();
// Получение тональности
String sentiment = analytics.getSentiment();
// Получение сущностей
Map<String, Object> entities = analytics.getEntities();
```
### Генерация текста
```java
// Генерация отчета
String instruction = "Напиши краткий отчет на основе следующих данных:";
String generatedText = openAIAnalyticsService.generateWithInstruction(data, instruction, "ru");
```
## Преимущества OpenAI API
1. **Высокое качество**: GPT-4o-mini обеспечивает более качественный анализ и генерацию текста
2. **Надежность**: Стабильная работа API без необходимости локального сервера
3. **Масштабируемость**: Легко масштабируется под нагрузку
4. **Многоязычность**: Отличная поддержка русского и английского языков
5. **Консистентность**: Более предсказуемые результаты
## Миграция
Все существующие вызовы `OllamaAnalyticsService` автоматически заменены на `OpenAIAnalyticsService`. API остается совместимым, поэтому дополнительных изменений в коде не требуется.
## Тестирование
Для тестирования нового сервиса:
1. Запустите приложение
2. Откройте `GET /api/openai/test` для проверки подключения
3. Используйте `POST /api/openai/analyze` для анализа текста
4. Используйте `POST /api/openai/generate` для генерации контента
## Настройка
Убедитесь, что в `application.properties` указан корректный API ключ OpenAI:
```properties
openai.api.key=your-openai-api-key-here
```
Модель по умолчанию: `gpt-4o-mini` (можно изменить в настройках).
@@ -0,0 +1,42 @@
package kz.konturai.parser.controller;
import kz.konturai.parser.model.MarketItem;
import kz.konturai.parser.service.OpenAIAnalyticsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/openai")
public class OpenAITestController {
@Autowired
private OpenAIAnalyticsService openAIAnalyticsService;
@PostMapping("/analyze")
public MarketItem.Analytics analyzeText(@RequestParam String text,
@RequestParam(defaultValue = "ru") String language) {
return openAIAnalyticsService.analyzeText(text, language);
}
@PostMapping("/generate")
public String generateWithInstruction(@RequestParam String text,
@RequestParam String instruction,
@RequestParam(defaultValue = "ru") String language) {
return openAIAnalyticsService.generateWithInstruction(text, instruction, language);
}
@GetMapping("/test")
public String testConnection() {
String testText = "Это тестовая новость о развитии технологий в Казахстане. Компания Kaspi Bank объявила о новых инвестициях в цифровые сервисы.";
MarketItem.Analytics analytics = openAIAnalyticsService.analyzeText(testText);
StringBuilder result = new StringBuilder();
result.append("OpenAI Analytics Service Test:\n");
result.append("Summary: ").append(analytics.getSummary()).append("\n");
result.append("Sentiment: ").append(analytics.getSentiment()).append("\n");
result.append("Tags: ").append(String.join(", ", analytics.getTags())).append("\n");
result.append("Entities: ").append(analytics.getEntities()).append("\n");
return result.toString();
}
}
@@ -33,7 +33,7 @@ public class KapitalParserService implements ParserService {
private MarketItemRepository marketItemRepository;
@Autowired
private OllamaAnalyticsService analyticsService;
private OpenAIAnalyticsService analyticsService;
@Value("${rss.kapital.url}")
private String rssFeedUrl;
@@ -33,7 +33,7 @@ public class KursivParserService implements ParserService {
private MarketItemRepository marketItemRepository;
@Autowired
private OllamaAnalyticsService analyticsService;
private OpenAIAnalyticsService analyticsService;
@Value("${rss.feed.url}")
private String rssFeedUrl;
@@ -33,7 +33,7 @@ public class LsmParserService implements ParserService {
private MarketItemRepository marketItemRepository;
@Autowired
private OllamaAnalyticsService analyticsService;
private OpenAIAnalyticsService analyticsService;
@Value("${rss.lsm.url}")
private String rssFeedUrl;
@@ -0,0 +1,217 @@
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.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
@Service
public class OpenAIAnalyticsService {
private static final Logger logger = LoggerFactory.getLogger(OpenAIAnalyticsService.class);
private final WebClient webClient;
@Value("${openai.model.name:gpt-4o-mini}")
private String modelName;
@Value("${openai.timeoutMs:90000}")
private long timeoutMs;
@Value("${openai.api.key}")
private String apiKey;
public OpenAIAnalyticsService(@Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String openaiUrl) {
HttpClient httpClient = HttpClient.create()
.compress(true)
.responseTimeout(Duration.ofMillis(timeoutMs));
this.webClient = WebClient.builder()
.baseUrl(openaiUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
public MarketItem.Analytics analyzeText(String text) {
return analyzeText(text, "ru");
}
public MarketItem.Analytics analyzeText(String text, String language) {
MarketItem.Analytics analytics = new MarketItem.Analytics();
boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language);
try {
String summaryPrompt = isRussian
? "Напиши краткое саммари следующей новостной статьи на русском языке. Ответ должен содержать только саммари из 3-4 предложений, без лишних вступлений. Статья: "
: "Write a brief summary of the following news article in English. The response should contain only a summary of 3-4 sentences, without any unnecessary introductions. Article: ";
String summary = generate(text, summaryPrompt);
analytics.setSummary(summary);
} catch (Exception e) {
logger.warn("Failed to get summary from OpenAI: {}", e.getMessage());
}
try {
String tagsPrompt = isRussian
? "Извлеки 5-7 ключевых слов или тегов из текста новостной статьи. В ответе дай только список тегов через запятую, без нумерации и заголовков. Статья: "
: "Extract 5-7 key words or tags from the news article text. In your response, provide only a list of tags separated by commas, without numbering or headers. Article: ";
String tagsCsv = generate(text, tagsPrompt);
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 OpenAI: {}", e.getMessage());
}
try {
String sentimentPrompt = isRussian
? "Определи тональность текста новостной статьи. В ответе дай только одно слово латиницей: positive, negative или neutral. Статья: "
: "Determine the sentiment of the news article text. In your response, provide only one word in English: positive, negative, or neutral. Article: ";
String sentiment = generate(text, sentimentPrompt);
if (sentiment != null) {
sentiment = sentiment.trim().toLowerCase();
}
analytics.setSentiment(sentiment);
} catch (Exception e) {
logger.warn("Failed to get sentiment from OpenAI: {}", e.getMessage());
}
try {
String entitiesPrompt = isRussian
? "Извлеки из текста имена людей, названия компаний и географические локации. В ответе дай только JSON объект следующей структуры: {\"persons\": [], \"companies\": [], \"locations\": []}. Статья: "
: "Extract from the text names of people, company names, and geographic locations. In your response, provide only a JSON object with the following structure: {\"persons\": [], \"companies\": [], \"locations\": []}. Article: ";
String entitiesJson = generate(text, entitiesPrompt);
Map<String, Object> entities = new HashMap<>();
if (entitiesJson != null && entitiesJson.trim().startsWith("{")) {
try {
ObjectMapper mapper = new ObjectMapper();
entities = mapper.readValue(entitiesJson, new TypeReference<Map<String, Object>>() {
});
} catch (Exception parseEx) {
entities.put("raw", entitiesJson);
}
}
analytics.setEntities(entities);
} catch (Exception e) {
logger.warn("Failed to get entities from OpenAI: {}", e.getMessage());
}
return analytics;
}
private String generate(String text, String instructionPrefix) {
String prompt = instructionPrefix + text;
Map<String, Object> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", modelName);
requestBody.put("messages", List.of(message));
requestBody.put("max_tokens", 1000);
requestBody.put("temperature", 0.3);
try {
Map<String, Object> response = this.webClient.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
})
.onErrorResume(err -> {
logger.warn("OpenAI request failed: {}", err.getMessage());
return Mono.empty();
})
.block(Duration.ofMillis(timeoutMs));
if (response == null) {
return null;
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> firstChoice = choices.get(0);
Map<String, Object> messageObj = (Map<String, Object>) firstChoice.get("message");
if (messageObj != null) {
Object content = messageObj.get("content");
return content == null ? null : String.valueOf(content).trim();
}
}
return null;
} catch (Exception e) {
logger.warn("Error calling OpenAI generate: {}", e.getMessage());
return null;
}
}
public String generateWithInstruction(String text, String instruction) {
return generateWithInstruction(text, instruction, "ru");
}
public String generateWithInstruction(String text, String instruction, String language) {
String prompt = instruction + "\n\n" + text;
Map<String, Object> message = new HashMap<>();
message.put("role", "user");
message.put("content", prompt);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", modelName);
requestBody.put("messages", List.of(message));
requestBody.put("max_tokens", 2000);
requestBody.put("temperature", 0.3);
try {
Map<String, Object> response = this.webClient.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
})
.onErrorResume(err -> {
logger.warn("OpenAI request failed: {}", err.getMessage());
return Mono.empty();
})
.block(Duration.ofMillis(timeoutMs));
if (response == null) {
return null;
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
if (choices != null && !choices.isEmpty()) {
Map<String, Object> firstChoice = choices.get(0);
Map<String, Object> messageObj = (Map<String, Object>) firstChoice.get("message");
if (messageObj != null) {
Object content = messageObj.get("content");
String result = content == null ? null : String.valueOf(content).trim();
System.out.println("OpenAI response: " + result);
return result;
}
}
return null;
} catch (Exception e) {
logger.warn("Error calling OpenAI generate (generic): {}", e.getMessage());
return null;
}
}
}
@@ -33,7 +33,7 @@ public class RbcParserService implements ParserService {
private MarketItemRepository marketItemRepository;
@Autowired
private OllamaAnalyticsService analyticsService;
private OpenAIAnalyticsService analyticsService;
@Value("${rss.rbc.url}")
private String rssFeedUrl;
@@ -82,18 +82,18 @@ public class ReportGenerationService {
}
private final MarketItemRepository marketItemRepository;
private final OllamaAnalyticsService ollamaAnalyticsService;
private final OpenAIAnalyticsService openAIAnalyticsService;
private final ReportHistoryRepository reportHistoryRepository;
private final MinIOService minIOService;
private final EmailService emailService;
public ReportGenerationService(MarketItemRepository marketItemRepository,
OllamaAnalyticsService ollamaAnalyticsService,
OpenAIAnalyticsService openAIAnalyticsService,
ReportHistoryRepository reportHistoryRepository,
MinIOService minIOService,
EmailService emailService) {
this.marketItemRepository = marketItemRepository;
this.ollamaAnalyticsService = ollamaAnalyticsService;
this.openAIAnalyticsService = openAIAnalyticsService;
this.reportHistoryRepository = reportHistoryRepository;
this.minIOService = minIOService;
this.emailService = emailService;
@@ -131,7 +131,7 @@ public class ReportGenerationService {
+ "Не упоминай, что ты ИИ. Используй русский язык.\n\nКонтекст отчёта:\n" + contextBlock
+ "\nСводки новостей:\n";
String generated = ollamaAnalyticsService.generateWithInstruction(combined, instruction);
String generated = openAIAnalyticsService.generateWithInstruction(combined, instruction);
if (generated == null) {
generated = "(Не удалось сгенерировать текст отчёта по предоставленным данным.)";
}
@@ -248,7 +248,7 @@ public class ReportGenerationService {
return "";
}
String instruction = "На основе этих кратких сводок новостей напиши общую аннотацию на 2-3 абзаца, выделяя ключевые тренды и события.";
String res = ollamaAnalyticsService.generateWithInstruction(combined, instruction);
String res = openAIAnalyticsService.generateWithInstruction(combined, instruction);
return res == null ? "" : res;
}
@@ -257,16 +257,16 @@ public class ReportGenerationService {
if (combined.isBlank()) {
return new Sections("", "", "", "");
}
String annotation = ollamaAnalyticsService.generateWithInstruction(combined,
String annotation = openAIAnalyticsService.generateWithInstruction(combined,
"На основе кратких сводок новостей напиши краткое содержание (2-3 абзаца), выделив ключевые тренды и события. Стиль: деловой, нейтральный.");
String intro = ollamaAnalyticsService.generateWithInstruction(combined,
String intro = openAIAnalyticsService.generateWithInstruction(combined,
"Напиши краткое введение к аналитическому отчёту за период. Укажи цель отчёта, источники данных и подход к анализу. Объём: 1 абзац.");
String mainPart = ollamaAnalyticsService.generateWithInstruction(combined,
String mainPart = openAIAnalyticsService.generateWithInstruction(combined,
"Сформируй основную часть отчёта: перечисли и кратко объясни 3-5 ключевых тенденций и событий периода, добавь замеченные риски и возможности. Используй маркированные пункты и короткие абзацы.");
String recs = ollamaAnalyticsService.generateWithInstruction(combined,
String recs = openAIAnalyticsService.generateWithInstruction(combined,
"На основе сводок сформулируй практические рекомендации (4-6 пунктов) для бизнеса/маркетинга. Формат: маркированный список, краткие и конкретные формулировки.");
return new Sections(annotation, intro, mainPart, recs);
@@ -18,13 +18,13 @@ import java.util.stream.Collectors;
@Service
public class ReportSynthesisService {
private final OllamaAnalyticsService ollamaAnalyticsService;
private final OpenAIAnalyticsService openAIAnalyticsService;
private final OpenAiChartService openAiChartService;
private final ObjectMapper objectMapper = new ObjectMapper();
public ReportSynthesisService(OllamaAnalyticsService ollamaAnalyticsService,
public ReportSynthesisService(OpenAIAnalyticsService openAIAnalyticsService,
OpenAiChartService openAiChartService) {
this.ollamaAnalyticsService = ollamaAnalyticsService;
this.openAIAnalyticsService = openAIAnalyticsService;
this.openAiChartService = openAiChartService;
}
@@ -32,7 +32,7 @@ public class ReportSynthesisService {
String aggregatedLearnings = formatLearnings(learnings);
String prompt = buildTextPrompt(originalQuery, aggregatedLearnings, lang);
return Mono
.fromCallable(() -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings, prompt, lang));
.fromCallable(() -> openAIAnalyticsService.generateWithInstruction(aggregatedLearnings, prompt, lang));
}
public Mono<FinalReportPayload> synthesizeReportAndCharts(String originalQuery, List<String> learnings,
@@ -40,7 +40,7 @@ public class ReportSynthesisService {
String aggregatedLearnings = formatLearnings(learnings);
Mono<String> textMono = Mono.fromCallable(
() -> ollamaAnalyticsService.generateWithInstruction(aggregatedLearnings,
() -> openAIAnalyticsService.generateWithInstruction(aggregatedLearnings,
buildTextPrompt(originalQuery, aggregatedLearnings, lang), lang))
.subscribeOn(Schedulers.boundedElastic());
@@ -33,7 +33,7 @@ public class VedomostiParserService implements ParserService {
private MarketItemRepository marketItemRepository;
@Autowired
private OllamaAnalyticsService analyticsService;
private OpenAIAnalyticsService analyticsService;
@Value("${rss.vedomosti.url}")
private String rssFeedUrl;