Skip to content

Commit c27abea

Browse files
committed
(TP-105) feat: add and impl AiService
1 parent c9c1243 commit c27abea

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package kattsyn.dev.rentplace.services;
2+
3+
import jakarta.security.auth.message.AuthException;
4+
import kattsyn.dev.rentplace.dtos.ai.GenerateDescriptionRequest;
5+
6+
public interface AiService {
7+
8+
String generateDescription(GenerateDescriptionRequest request) throws AuthException;
9+
10+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package kattsyn.dev.rentplace.services.impl;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.github.bucket4j.Bandwidth;
5+
import io.github.bucket4j.Bucket;
6+
import io.github.bucket4j.Bucket4j;
7+
import io.github.bucket4j.Refill;
8+
import jakarta.security.auth.message.AuthException;
9+
import jakarta.transaction.Transactional;
10+
import kattsyn.dev.rentplace.configs.OpenRouterProperties;
11+
import kattsyn.dev.rentplace.dtos.ai.GenerateDescriptionRequest;
12+
import kattsyn.dev.rentplace.entities.User;
13+
import kattsyn.dev.rentplace.exceptions.TooManyRequestsException;
14+
import kattsyn.dev.rentplace.services.AiService;
15+
import kattsyn.dev.rentplace.services.SecurityService;
16+
import lombok.RequiredArgsConstructor;
17+
import org.springframework.stereotype.Service;
18+
import org.springframework.web.reactive.function.client.WebClient;
19+
20+
import java.time.Duration;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
import java.util.concurrent.ConcurrentMap;
26+
27+
@Service
28+
@RequiredArgsConstructor
29+
public class AiServiceImpl implements AiService {
30+
31+
private final OpenRouterProperties openRouterProperties;
32+
private final WebClient openRouterWebClient;
33+
private final SecurityService securityService;
34+
35+
private final ConcurrentMap<Long, Bucket> buckets = new ConcurrentHashMap<>();
36+
37+
@Override
38+
@Transactional
39+
public String generateDescription(GenerateDescriptionRequest request) throws AuthException {
40+
User user = securityService.getCurrentUser();
41+
42+
Bucket bucket = buckets.computeIfAbsent(user.getUserId(), this::newBucket);
43+
if (!bucket.tryConsume(1)) {
44+
throw new TooManyRequestsException("Превышен лимит AI-запросов (макс 10 в час)");
45+
}
46+
47+
List<Map<String, String>> messages = new ArrayList<>();
48+
messages.add(Map.of(
49+
"role", "system",
50+
"content", openRouterProperties.defaultSystemPrompt()
51+
));
52+
if (request.getSystemPrompt() != null && !request.getSystemPrompt().isBlank()) {
53+
messages.add(Map.of(
54+
"role", "system",
55+
"content", request.getSystemPrompt()
56+
));
57+
}
58+
messages.add(Map.of(
59+
"role", "user",
60+
"content", request.getUserPrompt()
61+
));
62+
63+
JsonNode response = openRouterWebClient.post()
64+
.uri("/chat/completions")
65+
.bodyValue(Map.of(
66+
"model", openRouterProperties.model(),
67+
"messages", messages,
68+
"stream", false,
69+
"response_format", Map.of("type", "text")
70+
))
71+
.retrieve()
72+
.bodyToMono(JsonNode.class)
73+
.block();
74+
75+
String raw = response
76+
.path("choices")
77+
.path(0)
78+
.path("message")
79+
.path("content")
80+
.asText("");
81+
82+
String cleaned = raw.replaceAll("(?s)<think>.*?</think>\\s*", "").trim();
83+
84+
return cleaned;
85+
}
86+
87+
private Bucket newBucket(Long userId) {
88+
Refill refill = Refill.intervally(10, Duration.ofHours(1));
89+
Bandwidth limit = Bandwidth.classic(10, refill);
90+
return Bucket4j.builder()
91+
.addLimit(limit)
92+
.build();
93+
}
94+
}

0 commit comments

Comments
 (0)