|
@@ -0,0 +1,129 @@
|
|
|
|
|
+package com.danielbohry.stocks.client;
|
|
|
|
|
+
|
|
|
|
|
+import com.danielbohry.stocks.domain.Quote;
|
|
|
|
|
+import com.danielbohry.stocks.domain.StockInfo;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import lombok.AllArgsConstructor;
|
|
|
|
|
+import lombok.Data;
|
|
|
|
|
+import lombok.NoArgsConstructor;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static java.time.Instant.now;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class InferenceClient {
|
|
|
|
|
+
|
|
|
|
|
+ private final RestTemplate rest;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${clients.inference.url}")
|
|
|
|
|
+ private String baseUrl;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${clients.inference.model}")
|
|
|
|
|
+ private String model;
|
|
|
|
|
+
|
|
|
|
|
+ private static final String PROMPT = """
|
|
|
|
|
+ You are an API. Respond only with a **valid JSON object** using the exact format and keys below.
|
|
|
|
|
+ Do not include any explanation, markdown, or extra text.
|
|
|
|
|
+
|
|
|
|
|
+ Respond with JSON in the following format:
|
|
|
|
|
+ {
|
|
|
|
|
+ "founded": "<4-digit year or 'unknown'>",
|
|
|
|
|
+ "ipo": "<4-digit year or 'unknown'>",
|
|
|
|
|
+ "exchange": "<stock exchange name or 'unknown'>",
|
|
|
|
|
+ "headquarters": "<city and state/country or 'unknown'>",
|
|
|
|
|
+ "industry": "<industry sector or 'unknown'>",
|
|
|
|
|
+ "description": "<description or 'unknown'>"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Make sure the output is a valid JSON string with no extra text or markdown.
|
|
|
|
|
+
|
|
|
|
|
+ Company stock code:
|
|
|
|
|
+ """;
|
|
|
|
|
+
|
|
|
|
|
+ private static final ObjectMapper mapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ public StockInfo infer(Quote quote) {
|
|
|
|
|
+ String inference = infer(PROMPT + quote.getCode());
|
|
|
|
|
+
|
|
|
|
|
+ return buildStockInfo(quote, inference);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String infer(String prompt) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Map<String, Object> requestBody = new HashMap<>();
|
|
|
|
|
+ requestBody.put("model", model);
|
|
|
|
|
+ requestBody.put("prompt", prompt);
|
|
|
|
|
+ requestBody.put("stream", false);
|
|
|
|
|
+
|
|
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
+
|
|
|
|
|
+ HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
|
|
|
|
+ ResponseEntity<InferenceResponse> response = rest.postForEntity(baseUrl + "/api/generate", request, InferenceResponse.class);
|
|
|
|
|
+
|
|
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
|
|
+ return response.getBody().getResponse();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("Error during inference", e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private StockInfo buildStockInfo(Quote stock, String inference) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String cleanInference = inference.replaceAll("(?i)^[^{]*\\{", "{").replaceAll("[^}]*$", "}").trim();
|
|
|
|
|
+ Inference parsed = mapper.readValue(cleanInference, Inference.class);
|
|
|
|
|
+
|
|
|
|
|
+ return StockInfo.builder()
|
|
|
|
|
+ .code(stock.getCode())
|
|
|
|
|
+ .name(stock.getName())
|
|
|
|
|
+ .description(parsed.getDescription())
|
|
|
|
|
+ .foundation(parsed.getFounded())
|
|
|
|
|
+ .ipo(parsed.getIpo())
|
|
|
|
|
+ .exchange(parsed.getExchange())
|
|
|
|
|
+ .headquarters(parsed.getHeadquarters())
|
|
|
|
|
+ .industry(parsed.getIndustry())
|
|
|
|
|
+ .updatedAt(now())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("Failed to parse inference response [{}]", inference, e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @NoArgsConstructor
|
|
|
|
|
+ @AllArgsConstructor
|
|
|
|
|
+ @Data
|
|
|
|
|
+ public static class Inference {
|
|
|
|
|
+ private String founded;
|
|
|
|
|
+ private String ipo;
|
|
|
|
|
+ private String exchange;
|
|
|
|
|
+ private String headquarters;
|
|
|
|
|
+ private String industry;
|
|
|
|
|
+ private String description;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @NoArgsConstructor
|
|
|
|
|
+ @AllArgsConstructor
|
|
|
|
|
+ @Data
|
|
|
|
|
+ public static class InferenceResponse {
|
|
|
|
|
+ private String response;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|