Basic blog generation
This commit is contained in:
@@ -3,9 +3,11 @@ package com.homelabpulse;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
|
@EnableAsync
|
||||||
public class Application {
|
public class Application {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Application.class, args);
|
SpringApplication.run(Application.class, args);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.springframework.ai.ollama.api.OllamaApi;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class OllamaConfig {
|
public class OllamaConfig {
|
||||||
@@ -20,4 +21,15 @@ public class OllamaConfig {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(name = "ollamaExecutor")
|
||||||
|
public ThreadPoolTaskExecutor ollamaExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(10); // Set your core pool size
|
||||||
|
executor.setMaxPoolSize(25); // Set your maximum pool size
|
||||||
|
executor.setQueueCapacity(250); // Set your queue capacity
|
||||||
|
executor.setThreadNamePrefix("OllamaAsync-"); // Prefix for thread names
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.homelabpulse.controller;
|
|||||||
|
|
||||||
import com.homelabpulse.dto.BlogDTO;
|
import com.homelabpulse.dto.BlogDTO;
|
||||||
import com.homelabpulse.service.BlogService;
|
import com.homelabpulse.service.BlogService;
|
||||||
|
import com.homelabpulse.service.BlogWriterService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -14,12 +15,18 @@ import java.util.List;
|
|||||||
@RequestMapping("/blogs")
|
@RequestMapping("/blogs")
|
||||||
public class BlogController {
|
public class BlogController {
|
||||||
private final BlogService blogService;
|
private final BlogService blogService;
|
||||||
|
private final BlogWriterService blogWriterService;
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<BlogDTO> save(@RequestBody BlogDTO blogDTO) {
|
public ResponseEntity<BlogDTO> save(@RequestBody BlogDTO blogDTO) {
|
||||||
return new ResponseEntity<>(blogService.save(blogDTO), HttpStatus.CREATED);
|
return new ResponseEntity<>(blogService.save(blogDTO), HttpStatus.CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/write")
|
||||||
|
public ResponseEntity<BlogDTO> write(@PathVariable("id") Long id) {
|
||||||
|
return ResponseEntity.ok(blogWriterService.writeProductBlog(id));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<List<BlogDTO>> getAll() {
|
public ResponseEntity<List<BlogDTO>> getAll() {
|
||||||
List<BlogDTO> blogs = blogService.getAll();
|
List<BlogDTO> blogs = blogService.getAll();
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ import lombok.Setter;
|
|||||||
public class BlogDTO extends AuditableDTO {
|
public class BlogDTO extends AuditableDTO {
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long productId;
|
private Long productId;
|
||||||
|
private String title;
|
||||||
private String content;
|
private String content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ public class Blog extends AuditableEntity {
|
|||||||
@Column(name = "product_id")
|
@Column(name = "product_id")
|
||||||
private Long productId;
|
private Long productId;
|
||||||
|
|
||||||
|
@Column(name = "title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
@Column(name = "content")
|
@Column(name = "content")
|
||||||
private String content;
|
private String content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,5 @@ public class BlogService {
|
|||||||
blogOpt.ifPresent(blogRepository::delete);
|
blogOpt.ifPresent(blogRepository::delete);
|
||||||
return blogDto;
|
return blogDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
112
src/main/java/com/homelabpulse/service/BlogWriterService.java
Normal file
112
src/main/java/com/homelabpulse/service/BlogWriterService.java
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package com.homelabpulse.service;
|
||||||
|
|
||||||
|
import com.homelabpulse.dto.BlogDTO;
|
||||||
|
import com.homelabpulse.entity.Blog;
|
||||||
|
import com.homelabpulse.entity.Product;
|
||||||
|
import com.homelabpulse.mapper.BlogMapper;
|
||||||
|
import com.homelabpulse.repository.BlogRepository;
|
||||||
|
import com.homelabpulse.repository.ProductRepository;
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.chat.messages.SystemMessage;
|
||||||
|
import org.springframework.ai.chat.messages.UserMessage;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class BlogWriterService {
|
||||||
|
private final OllamaService ollamaService;
|
||||||
|
private final BlogRepository blogRepository;
|
||||||
|
private final ProductRepository productRepository;
|
||||||
|
private final BlogMapper blogMapper;
|
||||||
|
|
||||||
|
private static final String SYSTEM_MESSAGE = """
|
||||||
|
You are a blog writer for Homelab Pulse (https://homelabpulse.com), specializing in homelab tech.
|
||||||
|
You review products in a subtle way, so your readers aren't turned off by blatant advertising.
|
||||||
|
When possible, provide an example use case of a project for which the product can be used.
|
||||||
|
The products you review are not made or supported by homelabpulse.com.
|
||||||
|
""";
|
||||||
|
private static final String PRODUCT_MESSAGE_SEGMENT = """
|
||||||
|
Here is the product info:
|
||||||
|
|
||||||
|
``` Product URL
|
||||||
|
%1$s
|
||||||
|
```
|
||||||
|
|
||||||
|
``` Product Title
|
||||||
|
%2$s
|
||||||
|
```
|
||||||
|
|
||||||
|
``` Product Description
|
||||||
|
%3$s
|
||||||
|
```
|
||||||
|
""";
|
||||||
|
|
||||||
|
public BlogDTO writeProductBlog(Long blogId) {
|
||||||
|
log.info("Writing blogId={}", blogId);
|
||||||
|
Blog blog = blogRepository.findById(blogId).orElseThrow(() -> new EntityNotFoundException("Blog id=" + blogId + " not found"));
|
||||||
|
Product product = productRepository.findById(blog.getProductId()).orElseThrow(() -> new EntityNotFoundException("Product id=" + blog.getProductId() + " not found"));
|
||||||
|
|
||||||
|
boolean newTitle = false;
|
||||||
|
if (blog.getTitle() == null) {
|
||||||
|
newTitle = true;
|
||||||
|
String title = generateNewTitle(blog, product);
|
||||||
|
blog.setTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blog.getContent() == null || newTitle) {
|
||||||
|
String content = generateNewContent(blog, product);
|
||||||
|
blog.setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
blogRepository.save(blog);
|
||||||
|
log.info("Finished writing blogId={}", blogId);
|
||||||
|
return blogMapper.toDTO(blog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateNewContent(Blog blog, Product product) {
|
||||||
|
UserMessage userMessage = new UserMessage(
|
||||||
|
"""
|
||||||
|
There exists an empty blog titled "%1$s".
|
||||||
|
Write the blog post, in markdown format.
|
||||||
|
|
||||||
|
%2$s
|
||||||
|
""".formatted(blog.getTitle(), getProductSegment(product))
|
||||||
|
);
|
||||||
|
String content = ollamaService.call(
|
||||||
|
new SystemMessage(SYSTEM_MESSAGE),
|
||||||
|
userMessage
|
||||||
|
);
|
||||||
|
log.debug("Content for blog id={}:\n\n{}", blog.getId(), content);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateNewTitle(Blog blog, Product product) {
|
||||||
|
|
||||||
|
UserMessage userMessage = new UserMessage(
|
||||||
|
"""
|
||||||
|
Write the title of a blog post about the following product.
|
||||||
|
Only write the title of the post; do not add anything else to the response.
|
||||||
|
Maximum characters is 200.
|
||||||
|
|
||||||
|
%1$s
|
||||||
|
""".formatted(getProductSegment(product))
|
||||||
|
);
|
||||||
|
String title = ollamaService.call(
|
||||||
|
new SystemMessage(SYSTEM_MESSAGE),
|
||||||
|
userMessage
|
||||||
|
);
|
||||||
|
if (title.startsWith("Title: ")) {
|
||||||
|
log.debug("Removing 'Title: '");
|
||||||
|
title = title.replace("Title: ", "").strip();
|
||||||
|
}
|
||||||
|
log.info("Title for blogId={}: {}", blog.getId(), title);
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getProductSegment(Product product) {
|
||||||
|
return PRODUCT_MESSAGE_SEGMENT.formatted(product.getUrl(), product.getTitle(), product.getDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/com/homelabpulse/service/OllamaService.java
Normal file
43
src/main/java/com/homelabpulse/service/OllamaService.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package com.homelabpulse.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.chat.messages.SystemMessage;
|
||||||
|
import org.springframework.ai.chat.messages.UserMessage;
|
||||||
|
import org.springframework.ai.chat.model.ChatModel;
|
||||||
|
import org.springframework.ai.chat.model.ChatResponse;
|
||||||
|
import org.springframework.ai.chat.prompt.Prompt;
|
||||||
|
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class OllamaService {
|
||||||
|
private final ChatModel chatModel;
|
||||||
|
private static final String DEFAULT_MODEL = "hermes3";
|
||||||
|
|
||||||
|
public String call(SystemMessage systemMessage, UserMessage userMessage) {
|
||||||
|
return call(systemMessage, userMessage, DEFAULT_MODEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String call(SystemMessage systemMessage, UserMessage userMessage, String model) {
|
||||||
|
log.debug("UserMessage:\n{}", userMessage.getText());
|
||||||
|
OllamaOptions ollamaOptions = OllamaOptions.builder()
|
||||||
|
.model(model)
|
||||||
|
.build();
|
||||||
|
Prompt prompt = new Prompt(Arrays.asList(systemMessage, userMessage), ollamaOptions);
|
||||||
|
ChatResponse chatResponse = chatModel.call(prompt);
|
||||||
|
String responseStr = chatResponse.getResult().getOutput().getText();
|
||||||
|
log.debug("Response: {}", responseStr);
|
||||||
|
responseStr = responseStr.strip();
|
||||||
|
if (responseStr.charAt(0) == '"' && responseStr.charAt(responseStr.length() - 1) == '"') {
|
||||||
|
log.debug("Stripping quotes: {}", responseStr);
|
||||||
|
responseStr = responseStr.substring(1, responseStr.length() - 1).strip();
|
||||||
|
}
|
||||||
|
log.debug("Trimmed response: {}", responseStr);
|
||||||
|
return responseStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/main/resources/db/migration/V3__Blog_Title.sql
Normal file
3
src/main/resources/db/migration/V3__Blog_Title.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- V3__Blog_Title.sql
|
||||||
|
|
||||||
|
ALTER TABLE blog ADD COLUMN title VARCHAR(500);
|
||||||
Reference in New Issue
Block a user