From 9884cef452abcc7e92d09cdc446ff5668ec3d56d Mon Sep 17 00:00:00 2001 From: Hai Nguyen Date: Sat, 25 Jan 2025 18:08:01 -0800 Subject: [PATCH] Integrate open ai to provide the ai summary and conversation health evaluation (#60) --- build.gradle | 1 + ...lowinquiry.spring-cache-conventions.gradle | 5 - commons/build.gradle | 6 + .../io/flowinquiry/config/CacheConfig.java | 26 +++ .../config/FlowInquiryProperties.java | 14 -- .../config/SecurityConfiguration.java | 11 +- .../ai/config/ChatModelConfiguration.java | 30 ++++ .../modules/ai/service/ChatModelService.java | 46 ++++++ .../ai/service/OllamaChatModelService.java | 16 ++ .../ai/service/OpenAiChatModelService.java | 25 +++ .../collab/service/CommentService.java | 17 +- .../teams/controller/AiChatController.java | 26 +++ .../modules/teams/domain/TeamRequest.java | 9 ++ .../domain/TeamRequestConversationHealth.java | 51 ++++++ ...amRequestConversationHealthRepository.java | 13 ++ .../repository/TeamRequestRepository.java | 21 ++- .../service/TeamRequestHealthEvalService.java | 153 ++++++++++++++++++ .../dto/TeamRequestConversationHealthDTO.java | 59 +++++++ .../teams/service/dto/TeamRequestDTO.java | 1 + .../teams/service/dto/TicketHealthLevel.java | 23 +++ .../event/TeamRequestNewCommentEvent.java | 15 ++ ...mRequestCreatedAiSummaryEventListener.java | 36 +++++ ...ewTeamRequestCreatedMailEventListener.java | 2 +- ...aluateConversationHealthEventListener.java | 41 +++++ .../TeamRequestConversationHealthMapper.java | 27 ++++ .../service/mapper/TeamRequestMapper.java | 27 ++-- .../service/DomainUserDetailsService.java | 2 + gradle/libs.versions.toml | 8 + server/build.gradle | 1 - .../main/resources/config/application-dev.yml | 9 ++ .../resources/config/application-prod.yml | 6 + .../src/main/resources/config/application.yml | 5 +- .../resources/config/application-test.yml | 5 + .../changelog/001_request_workflow_tables.xml | 45 ++++++ 34 files changed, 735 insertions(+), 47 deletions(-) delete mode 100644 buildSrc/src/main/groovy/flowinquiry.spring-cache-conventions.gradle create mode 100644 commons/src/main/java/io/flowinquiry/config/CacheConfig.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/ai/config/ChatModelConfiguration.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/ai/service/ChatModelService.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/ai/service/OllamaChatModelService.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/ai/service/OpenAiChatModelService.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/controller/AiChatController.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequestConversationHealth.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestConversationHealthRepository.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/TeamRequestHealthEvalService.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestConversationHealthDTO.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TicketHealthLevel.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/event/TeamRequestNewCommentEvent.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedAiSummaryEventListener.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/listener/TeamRequestNewCommentAiEvaluateConversationHealthEventListener.java create mode 100644 commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestConversationHealthMapper.java diff --git a/build.gradle b/build.gradle index bf9a7561..da8f7a6f 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ allprojects { version = project.findProperty('version') repositories { mavenCentral() + maven { url 'https://repo.spring.io/milestone' } } subprojects { diff --git a/buildSrc/src/main/groovy/flowinquiry.spring-cache-conventions.gradle b/buildSrc/src/main/groovy/flowinquiry.spring-cache-conventions.gradle deleted file mode 100644 index 44408316..00000000 --- a/buildSrc/src/main/groovy/flowinquiry.spring-cache-conventions.gradle +++ /dev/null @@ -1,5 +0,0 @@ -dependencies { - implementation "org.springframework.boot:spring-boot-starter-cache" - implementation "javax.cache:cache-api" - implementation "org.hibernate.orm:hibernate-jcache" -} diff --git a/commons/build.gradle b/commons/build.gradle index fce79378..7de3de5a 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -8,6 +8,7 @@ group = 'io.flowinquiry' repositories { mavenCentral() + } dependencyManagement { @@ -18,8 +19,10 @@ dependencyManagement { dependencies { compileOnly(libs.lombok) + api(libs.bundles.caffeine.cache) api(libs.bundles.json) api(libs.bundles.shedlock) + api(libs.bundles.spring.ai) api(libs.dot.env) api(libs.j2html) api(libs.jclouds) { @@ -37,17 +40,20 @@ dependencies { api("jakarta.annotation:jakarta.annotation-api") api("org.apache.commons:commons-lang3") api("org.hibernate.orm:hibernate-core") + api("org.hibernate.orm:hibernate-jcache") api("org.hibernate.validator:hibernate-validator") api("org.springframework.boot:spring-boot-starter-actuator") api("org.springframework.boot:spring-boot-starter-aop") api("org.springframework.boot:spring-boot-starter-data-jpa") api("org.springframework.boot:spring-boot-loader-tools") + api("org.springframework.boot:spring-boot-starter-cache") api("org.springframework.boot:spring-boot-starter-logging") api("org.springframework.boot:spring-boot-starter-mail") api("org.springframework.boot:spring-boot-starter-oauth2-resource-server") api("org.springframework.boot:spring-boot-starter-security") api("org.springframework.boot:spring-boot-starter-thymeleaf") api("org.springframework.boot:spring-boot-starter-undertow") + modules { module("org.springframework.boot:spring-boot-starter-tomcat") { replacedBy("org.springframework.boot:spring-boot-starter-undertow", "Use Undertow instead of Tomcat") diff --git a/commons/src/main/java/io/flowinquiry/config/CacheConfig.java b/commons/src/main/java/io/flowinquiry/config/CacheConfig.java new file mode 100644 index 00000000..5c706216 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/config/CacheConfig.java @@ -0,0 +1,26 @@ +package io.flowinquiry.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + Caffeine caffeineBuilder = + Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) // Set cache expiration + .maximumSize(1000); // Set maximum cache size + + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(caffeineBuilder); + return cacheManager; + } +} diff --git a/commons/src/main/java/io/flowinquiry/config/FlowInquiryProperties.java b/commons/src/main/java/io/flowinquiry/config/FlowInquiryProperties.java index 4e0162c5..3b331eba 100644 --- a/commons/src/main/java/io/flowinquiry/config/FlowInquiryProperties.java +++ b/commons/src/main/java/io/flowinquiry/config/FlowInquiryProperties.java @@ -13,8 +13,6 @@ public class FlowInquiryProperties { private final Http http = new Http(); - private final Cache cache = new Cache(); - private final Security security = new Security(); private final CorsConfiguration cors = new CorsConfiguration(); @@ -27,18 +25,6 @@ public static class Mail { private String baseUrl = ""; } - @Getter - public static class Cache { - private final Ehcache ehcache = new Ehcache(); - - @Getter - @Setter - public static class Ehcache { - private int timeToLiveSeconds = 3600; - private long maxEntries = 100L; - } - } - @Getter public static class Http { private final Cache cache = new Cache(); diff --git a/commons/src/main/java/io/flowinquiry/config/SecurityConfiguration.java b/commons/src/main/java/io/flowinquiry/config/SecurityConfiguration.java index d6389499..dc7bb491 100644 --- a/commons/src/main/java/io/flowinquiry/config/SecurityConfiguration.java +++ b/commons/src/main/java/io/flowinquiry/config/SecurityConfiguration.java @@ -3,7 +3,6 @@ import static org.springframework.security.config.Customizer.withDefaults; import io.flowinquiry.modules.usermanagement.AuthoritiesConstants; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -28,10 +27,6 @@ public PasswordEncoder passwordEncoder() { } @Bean - @ConditionalOnProperty( - name = "flowinquiry.edition", - havingValue = "community", - matchIfMissing = true) public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { http.cors(withDefaults()) @@ -44,10 +39,10 @@ public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Buil mvc.pattern(HttpMethod.POST, "/api/authenticate"), mvc.pattern(HttpMethod.GET, "/api/authenticate"), mvc.pattern("/api/register"), + mvc.pattern("/api/files/**"), mvc.pattern("/api/activate"), mvc.pattern("/api/account/reset-password/init"), - mvc.pattern("/api/account/reset-password/finish"), - mvc.pattern("/api/test/**")) + mvc.pattern("/api/account/reset-password/finish")) .permitAll() .requestMatchers(mvc.pattern("/api/admin/**")) .hasAuthority(AuthoritiesConstants.ADMIN) @@ -56,7 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Buil .requestMatchers(mvc.pattern("/management/**")) .hasAuthority( AuthoritiesConstants.ADMIN)) // Enforces ROLE_ADMIN - .httpBasic(withDefaults()) // Enable Basic Authentication + // .httpBasic(withDefaults()) // Enable Basic Authentication .oauth2ResourceServer( oauth2 -> oauth2.jwt(withDefaults())) // Enable OAuth2 Resource Server .sessionManagement( diff --git a/commons/src/main/java/io/flowinquiry/modules/ai/config/ChatModelConfiguration.java b/commons/src/main/java/io/flowinquiry/modules/ai/config/ChatModelConfiguration.java new file mode 100644 index 00000000..21a75abd --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/ai/config/ChatModelConfiguration.java @@ -0,0 +1,30 @@ +package io.flowinquiry.modules.ai.config; + +import io.flowinquiry.modules.ai.service.ChatModelService; +import io.flowinquiry.modules.ai.service.OllamaChatModelService; +import io.flowinquiry.modules.ai.service.OpenAiChatModelService; +import java.util.Optional; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class ChatModelConfiguration { + + @Bean + @Primary + @ConditionalOnBean({OllamaChatModelService.class, OpenAiChatModelService.class}) + public ChatModelService chatModel( + Optional ollamaChatModelService, + Optional openAiChatModelService) { + if (ollamaChatModelService.isPresent()) { + return ollamaChatModelService.get(); + } else if (openAiChatModelService.isPresent()) { + return openAiChatModelService.get(); + } + + // If no chat models are present, this block won't execute due to @ConditionalOnBean + return null; + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/ai/service/ChatModelService.java b/commons/src/main/java/io/flowinquiry/modules/ai/service/ChatModelService.java new file mode 100644 index 00000000..9d30ea86 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/ai/service/ChatModelService.java @@ -0,0 +1,46 @@ +package io.flowinquiry.modules.ai.service; + +/** + * Interface representing a generic chat model service. + * + *

This interface defines the contract for interacting with different chat models, such as OpenAI + * or Ollama, by providing a method to process input and return a response. Implementations of this + * interface should encapsulate the logic specific to each chat model's API or processing. + * + *

Usage Example

+ * + *
{@code
+ * ChatModelService chatModelService = new OpenAiChatModelService();
+ * String response = chatModelService.call("Summarize this text.");
+ * System.out.println(response);
+ * }
+ * + *

Implementations

+ * + *
    + *
  • {@link OpenAiChatModelService} + *
  • {@link OllamaChatModelService} + *
+ * + *

Thread-Safety

+ * + * Implementations of this interface should ensure thread-safety if they are intended to be used in + * concurrent environments. + * + * @author Hai Nguyen + * @version 1.0 + */ +public interface ChatModelService { + + /** + * Processes the given input using the chat model and returns the model's response. + * + *

The input string can be any text to be processed by the chat model, such as a question, a + * request for summarization, or any prompt supported by the model. + * + * @param input the text input to be processed by the chat model + * @return the response generated by the chat model + * @throws IllegalArgumentException if the input is null or empty + */ + String call(String input); +} diff --git a/commons/src/main/java/io/flowinquiry/modules/ai/service/OllamaChatModelService.java b/commons/src/main/java/io/flowinquiry/modules/ai/service/OllamaChatModelService.java new file mode 100644 index 00000000..7ca8d279 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/ai/service/OllamaChatModelService.java @@ -0,0 +1,16 @@ +package io.flowinquiry.modules.ai.service; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnProperty( + name = {"OLLAMA_CHAT_MODEL", "OLLAMA_API_KEY"}, + matchIfMissing = false) +public class OllamaChatModelService implements ChatModelService { + + @Override + public String call(String input) { + return "Response from Ollama: " + input; + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/ai/service/OpenAiChatModelService.java b/commons/src/main/java/io/flowinquiry/modules/ai/service/OpenAiChatModelService.java new file mode 100644 index 00000000..bdf1c6b3 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/ai/service/OpenAiChatModelService.java @@ -0,0 +1,25 @@ +package io.flowinquiry.modules.ai.service; + +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnProperty( + name = {"OPEN_AI_CHAT_MODEL", "OPEN_AI_API_KEY"}, + matchIfMissing = false) +@ConditionalOnBean(OpenAiChatModel.class) +public class OpenAiChatModelService implements ChatModelService { + + private final OpenAiChatModel openAiChatModel; + + public OpenAiChatModelService(OpenAiChatModel openAiChatModel) { + this.openAiChatModel = openAiChatModel; + } + + @Override + public String call(String input) { + return openAiChatModel.call(input); + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/collab/service/CommentService.java b/commons/src/main/java/io/flowinquiry/modules/collab/service/CommentService.java index 0688317e..5fa71f19 100644 --- a/commons/src/main/java/io/flowinquiry/modules/collab/service/CommentService.java +++ b/commons/src/main/java/io/flowinquiry/modules/collab/service/CommentService.java @@ -5,7 +5,9 @@ import io.flowinquiry.modules.collab.repository.CommentRepository; import io.flowinquiry.modules.collab.service.dto.CommentDTO; import io.flowinquiry.modules.collab.service.mapper.CommentMapper; +import io.flowinquiry.modules.teams.service.event.TeamRequestNewCommentEvent; import java.util.List; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,17 +15,28 @@ @Transactional public class CommentService { + private final ApplicationEventPublisher eventPublisher; + private final CommentRepository commentRepository; private final CommentMapper commentMapper; - public CommentService(CommentRepository commentRepository, CommentMapper commentMapper) { + public CommentService( + ApplicationEventPublisher eventPublisher, + CommentRepository commentRepository, + CommentMapper commentMapper) { + this.eventPublisher = eventPublisher; this.commentRepository = commentRepository; this.commentMapper = commentMapper; } public CommentDTO saveComment(CommentDTO comment) { - return commentMapper.toDTO(commentRepository.save(commentMapper.toEntity(comment))); + CommentDTO savedComment = + commentMapper.toDTO(commentRepository.save(commentMapper.toEntity(comment))); + if (savedComment.getEntityType() == EntityType.Team_Request) { + eventPublisher.publishEvent(new TeamRequestNewCommentEvent(this, savedComment)); + } + return savedComment; } public Comment getCommentById(Long id) { diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/controller/AiChatController.java b/commons/src/main/java/io/flowinquiry/modules/teams/controller/AiChatController.java new file mode 100644 index 00000000..19b59630 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/controller/AiChatController.java @@ -0,0 +1,26 @@ +package io.flowinquiry.modules.teams.controller; + +import io.flowinquiry.modules.teams.service.TeamRequestHealthEvalService; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/ai") +@ConditionalOnBean(OpenAiChatModel.class) +public class AiChatController { + + private final TeamRequestHealthEvalService teamRequestHealthEvalService; + + public AiChatController(TeamRequestHealthEvalService teamRequestHealthEvalService) { + this.teamRequestHealthEvalService = teamRequestHealthEvalService; + } + + @PostMapping + public String createRequestSummary(@RequestBody String requestDescription) { + return teamRequestHealthEvalService.summarizeTeamRequest(requestDescription); + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequest.java b/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequest.java index 7d444d56..5ca1ae3e 100644 --- a/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequest.java +++ b/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequest.java @@ -2,6 +2,7 @@ import io.flowinquiry.modules.audit.AbstractAuditingEntity; import io.flowinquiry.modules.usermanagement.domain.User; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; @@ -13,6 +14,7 @@ import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import java.time.LocalDate; import java.util.Set; @@ -98,4 +100,11 @@ public class TeamRequest extends AbstractAuditingEntity { @Formula( "(SELECT COUNT(a.id) FROM fw_entity_attachment a WHERE a.entity_type = 'Team_Request' AND a.entity_id = id)") private int numberAttachments; + + @OneToOne( + mappedBy = "teamRequest", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + orphanRemoval = true) + private TeamRequestConversationHealth conversationHealth; } diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequestConversationHealth.java b/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequestConversationHealth.java new file mode 100644 index 00000000..203cb9fb --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/domain/TeamRequestConversationHealth.java @@ -0,0 +1,51 @@ +package io.flowinquiry.modules.teams.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "fw_team_request_conversation_health") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TeamRequestConversationHealth { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "summary") + private String summary; + + @Column(name = "conversation_health") + private Float conversationHealth; + + @Column(name = "cumulative_sentiment", nullable = false) + private Float cumulativeSentiment = 0.0f; + + @Column(name = "total_messages", nullable = false) + private Integer totalMessages = 0; + + @Column(name = "total_questions", nullable = false) + private Integer totalQuestions = 0; + + @Column(name = "resolved_questions", nullable = false) + private Integer resolvedQuestions = 0; + + @OneToOne + @JoinColumn(name = "team_request_id", nullable = false) + private TeamRequest teamRequest; +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestConversationHealthRepository.java b/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestConversationHealthRepository.java new file mode 100644 index 00000000..4110b29d --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestConversationHealthRepository.java @@ -0,0 +1,13 @@ +package io.flowinquiry.modules.teams.repository; + +import io.flowinquiry.modules.teams.domain.TeamRequestConversationHealth; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TeamRequestConversationHealthRepository + extends JpaRepository { + + Optional findByTeamRequestId(Long teamRequestId); +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestRepository.java b/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestRepository.java index 5746d623..140aa9e6 100644 --- a/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestRepository.java +++ b/commons/src/main/java/io/flowinquiry/modules/teams/repository/TeamRequestRepository.java @@ -35,11 +35,19 @@ public interface TeamRequestRepository "modifiedByUser", "workflow", "currentState", - "watchers" + "watchers", + "conversationHealth" }) Optional findById(@Param("id") Long id); - @EntityGraph(attributePaths = {"team", "requestUser", "assignUser", "workflow"}) + @EntityGraph( + attributePaths = { + "team", + "requestUser", + "assignUser", + "workflow", + "conversationHealth" + }) @Query( value = """ @@ -56,7 +64,14 @@ public interface TeamRequestRepository """) Optional findPreviousEntity(@Param("requestId") Long requestId); - @EntityGraph(attributePaths = {"team", "requestUser", "assignUser", "workflow"}) + @EntityGraph( + attributePaths = { + "team", + "requestUser", + "assignUser", + "workflow", + "conversationHealth" + }) @Query( value = """ diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/TeamRequestHealthEvalService.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/TeamRequestHealthEvalService.java new file mode 100644 index 00000000..777e5d4b --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/TeamRequestHealthEvalService.java @@ -0,0 +1,153 @@ +package io.flowinquiry.modules.teams.service; + +import io.flowinquiry.modules.ai.service.ChatModelService; +import io.flowinquiry.modules.teams.domain.TeamRequest; +import io.flowinquiry.modules.teams.domain.TeamRequestConversationHealth; +import io.flowinquiry.modules.teams.repository.TeamRequestConversationHealthRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@ConditionalOnBean(ChatModelService.class) +public class TeamRequestHealthEvalService { + + private final TeamRequestConversationHealthRepository teamRequestConversationHealthRepository; + private final ChatModelService chatModelService; + + public TeamRequestHealthEvalService( + ChatModelService chatModelService, + TeamRequestConversationHealthRepository teamRequestConversationHealthRepository) { + this.chatModelService = chatModelService; + this.teamRequestConversationHealthRepository = teamRequestConversationHealthRepository; + } + + public String summarizeTeamRequest(String description) { + return chatModelService.call("Summarize this text: " + description); + } + + /** + * Evaluates the conversation health incrementally by updating metrics. + * + * @param teamRequestId The ID of the team request. + * @param newMessage The new message in the conversation. + */ + @Transactional + public void evaluateConversationHealth( + Long teamRequestId, String newMessage, boolean isCustomerResponse) { + // Retrieve existing health record or create a new one + TeamRequestConversationHealth health = + teamRequestConversationHealthRepository + .findByTeamRequestId(teamRequestId) + .orElseGet(() -> createNewConversationHealth(teamRequestId, newMessage)); + + // Evaluate the sentiment of the new message + float sentimentScore = evaluateSentiment(newMessage); + + // Check if the new message resolves the issue (only for customer responses) + boolean resolvesIssue = isCustomerResponse && determineIfResolved(newMessage); + + // Increment metrics + health.setTotalMessages(health.getTotalMessages() + 1); + if (isCustomerResponse) { + health.setTotalQuestions(health.getTotalQuestions() + 1); + if (resolvesIssue) { + health.setResolvedQuestions(health.getResolvedQuestions() + 1); + } + } + + // Update cumulative sentiment (weigh customer responses more heavily) + float sentimentWeight = isCustomerResponse ? 1.5f : 1.0f; + health.setCumulativeSentiment( + (health.getCumulativeSentiment() * (health.getTotalMessages() - 1) + + sentimentScore * sentimentWeight) + / health.getTotalMessages()); + + // Calculate clarity score + float clarityScore = + health.getTotalQuestions() > 0 + ? (float) health.getResolvedQuestions() / health.getTotalQuestions() + : 1.0f; + + // Recalculate conversation health + health.setConversationHealth( + (0.4f * health.getCumulativeSentiment()) + + // Sentiment contribution + (0.4f * clarityScore) + + // Clarity contribution + (0.2f * (resolvesIssue ? 1.0f : 0.0f)) // Resolution contribution + ); + + // Save the updated health record + teamRequestConversationHealthRepository.save(health); + } + + /** + * Determines if the new message resolves an issue based on its content. + * + * @param newMessage The content of the new message. + * @return True if the message resolves an issue; otherwise, false. + */ + private boolean determineIfResolved(String newMessage) { + // Use OpenAI to analyze if the message resolves an issue + String prompt = + "Does this message resolve an issue? Respond with 'yes' or 'no': " + newMessage; + String response = chatModelService.call(prompt); + + // Interpret the response + return response.trim().equalsIgnoreCase("yes"); + } + + /** + * Calls OpenAI to evaluate the sentiment of a message. + * + * @param newMessage The message to evaluate. + * @return A sentiment score (0.0 - 1.0). + */ + private float evaluateSentiment(String newMessage) { + // Use OpenAI to analyze sentiment + String response = + chatModelService.call( + "Evaluate sentiment for this message (return a score between 0 and 1): " + + newMessage); + + // Extract the sentiment score from the response (assuming response contains a parsable + // float) + try { + return Float.parseFloat(response.trim()); + } catch (NumberFormatException e) { + throw new IllegalStateException( + "Unable to parse sentiment score from OpenAI response: " + response); + } + } + + /** + * Generates a summary for the team request content using OpenAI. + * + * @param description The initial description of the team request. + * @return The generated summary. + */ + private String generateSummary(String description) { + String prompt = "Summarize this text: " + description; + return chatModelService.call(prompt); + } + + /** + * Creates a new conversation health record for a team request. + * + * @param teamRequestId The ID of the team request. + * @return The newly created conversation health entity. + */ + private TeamRequestConversationHealth createNewConversationHealth( + Long teamRequestId, String firstMessage) { + TeamRequestConversationHealth health = new TeamRequestConversationHealth(); + health.setTeamRequest(TeamRequest.builder().id(teamRequestId).build()); + health.setCumulativeSentiment(0.0f); + health.setTotalMessages(0); + health.setTotalQuestions(0); + health.setResolvedQuestions(0); + health.setConversationHealth(0.0f); + health.setSummary(generateSummary(firstMessage)); + return teamRequestConversationHealthRepository.save(health); + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestConversationHealthDTO.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestConversationHealthDTO.java new file mode 100644 index 00000000..51358a95 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestConversationHealthDTO.java @@ -0,0 +1,59 @@ +package io.flowinquiry.modules.teams.service.dto; + +import static io.flowinquiry.modules.teams.service.dto.TicketHealthLevel.CRITICAL; +import static io.flowinquiry.modules.teams.service.dto.TicketHealthLevel.EXCELLENT; +import static io.flowinquiry.modules.teams.service.dto.TicketHealthLevel.FAIR; +import static io.flowinquiry.modules.teams.service.dto.TicketHealthLevel.GOOD; +import static io.flowinquiry.modules.teams.service.dto.TicketHealthLevel.POOR; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder +public class TeamRequestConversationHealthDTO { + private Long id; + private Long teamRequestId; + private Float conversationHealth; + private Float cumulativeSentiment; + private Integer totalMessages; + private Integer totalQuestions; + private Integer resolvedQuestions; + + /** + * Calculates the ticket health level based on conversation health, cumulative sentiment, and + * clarity ratio. + * + * @return The ticket health level (EXCELLENT, GOOD, FAIR, POOR, CRITICAL). + */ + @JsonProperty("healthLevel") + public TicketHealthLevel getHealthLevel() { + if (conversationHealth == null || cumulativeSentiment == null) { + return EXCELLENT; // Default to EXCELLENT if data is missing + } + + // Calculate clarity ratio + float clarityRatio = + totalQuestions != null && totalQuestions > 0 + ? (float) resolvedQuestions / totalQuestions + : 0; + + // Determine health level based on thresholds + if (conversationHealth > 0.9 && clarityRatio > 0.9 && cumulativeSentiment > 0.9) { + return EXCELLENT; + } else if (conversationHealth > 0.8 && clarityRatio > 0.8 && cumulativeSentiment > 0.8) { + return GOOD; + } else if (conversationHealth > 0.6 && clarityRatio > 0.6 && cumulativeSentiment > 0.6) { + return FAIR; + } else if (conversationHealth > 0.4 && clarityRatio > 0.4 && cumulativeSentiment > 0.4) { + return POOR; + } else { + return CRITICAL; + } + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestDTO.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestDTO.java index a830a451..a3d181f1 100644 --- a/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestDTO.java +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TeamRequestDTO.java @@ -40,4 +40,5 @@ public class TeamRequestDTO { private Instant modifiedAt; private Set watchers; private int numberAttachments; + private TeamRequestConversationHealthDTO conversationHealth; } diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TicketHealthLevel.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TicketHealthLevel.java new file mode 100644 index 00000000..25e77fe1 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/dto/TicketHealthLevel.java @@ -0,0 +1,23 @@ +package io.flowinquiry.modules.teams.service.dto; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum TicketHealthLevel { + EXCELLENT, + GOOD, + FAIR, + POOR, + CRITICAL; + + /** + * The ticket health level at frontend has the first character is upper case only such as + * Excellent, Good, etc. + * + * @return the json value for the enum + */ + @JsonValue + public String toJson() { + String name = this.name().toLowerCase(); + return Character.toUpperCase(name.charAt(0)) + name.substring(1); + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/event/TeamRequestNewCommentEvent.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/event/TeamRequestNewCommentEvent.java new file mode 100644 index 00000000..9caf4c5c --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/event/TeamRequestNewCommentEvent.java @@ -0,0 +1,15 @@ +package io.flowinquiry.modules.teams.service.event; + +import io.flowinquiry.modules.collab.service.dto.CommentDTO; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class TeamRequestNewCommentEvent extends ApplicationEvent { + private final CommentDTO commentDTO; + + public TeamRequestNewCommentEvent(Object source, CommentDTO commentDTO) { + super(source); + this.commentDTO = commentDTO; + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedAiSummaryEventListener.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedAiSummaryEventListener.java new file mode 100644 index 00000000..50d7a5c0 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedAiSummaryEventListener.java @@ -0,0 +1,36 @@ +package io.flowinquiry.modules.teams.service.listener; + +import io.flowinquiry.modules.teams.service.TeamRequestHealthEvalService; +import io.flowinquiry.modules.teams.service.dto.TeamRequestDTO; +import io.flowinquiry.modules.teams.service.event.NewTeamRequestCreatedEvent; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnBean(TeamRequestHealthEvalService.class) +public class NewTeamRequestCreatedAiSummaryEventListener { + + private final TeamRequestHealthEvalService teamRequestHealthEvalService; + + public NewTeamRequestCreatedAiSummaryEventListener( + TeamRequestHealthEvalService teamRequestHealthEvalService) { + this.teamRequestHealthEvalService = teamRequestHealthEvalService; + } + + @Async("asyncTaskExecutor") + @EventListener + public void onNewTeamRequestCreated(NewTeamRequestCreatedEvent event) { + TeamRequestDTO teamRequestDTO = event.getTeamRequest(); + teamRequestHealthEvalService.evaluateConversationHealth( + teamRequestDTO.getId(), + "Title: " + + teamRequestDTO.getRequestTitle() + + "\n" + + "Description: " + + teamRequestDTO.getRequestDescription() + + "\n", + true); + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedMailEventListener.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedMailEventListener.java index e4591081..78223ffc 100644 --- a/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedMailEventListener.java +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/NewTeamRequestCreatedMailEventListener.java @@ -20,7 +20,7 @@ public class NewTeamRequestCreatedMailEventListener { private final WatcherMapper watcherMapper; private final TeamRequestWatcherRepository teamRequestWatcherRepository; - private TeamRequestService teamRequestService; + private final TeamRequestService teamRequestService; private final MailService mailService; public NewTeamRequestCreatedMailEventListener( diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/TeamRequestNewCommentAiEvaluateConversationHealthEventListener.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/TeamRequestNewCommentAiEvaluateConversationHealthEventListener.java new file mode 100644 index 00000000..e677bf4b --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/listener/TeamRequestNewCommentAiEvaluateConversationHealthEventListener.java @@ -0,0 +1,41 @@ +package io.flowinquiry.modules.teams.service.listener; + +import io.flowinquiry.modules.collab.service.dto.CommentDTO; +import io.flowinquiry.modules.teams.service.TeamRequestHealthEvalService; +import io.flowinquiry.modules.teams.service.TeamRequestService; +import io.flowinquiry.modules.teams.service.dto.TeamRequestDTO; +import io.flowinquiry.modules.teams.service.event.TeamRequestNewCommentEvent; +import java.util.Objects; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnBean(TeamRequestHealthEvalService.class) +public class TeamRequestNewCommentAiEvaluateConversationHealthEventListener { + + private final TeamRequestService teamRequestService; + + private final TeamRequestHealthEvalService teamRequestHealthEvalService; + + public TeamRequestNewCommentAiEvaluateConversationHealthEventListener( + TeamRequestService teamRequestService, + TeamRequestHealthEvalService teamRequestHealthEvalService) { + this.teamRequestHealthEvalService = teamRequestHealthEvalService; + this.teamRequestService = teamRequestService; + } + + @Async("asyncTaskExecutor") + @EventListener + public void onTeamRequestNewCommentAiEvaluateConversationHealthEvent( + TeamRequestNewCommentEvent event) { + CommentDTO comment = event.getCommentDTO(); + TeamRequestDTO teamRequestDTO = + teamRequestService.getTeamRequestById(comment.getEntityId()); + teamRequestHealthEvalService.evaluateConversationHealth( + comment.getEntityId(), + comment.getContent(), + Objects.equals(teamRequestDTO.getRequestUserId(), comment.getCreatedById())); + } +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestConversationHealthMapper.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestConversationHealthMapper.java new file mode 100644 index 00000000..ea542a19 --- /dev/null +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestConversationHealthMapper.java @@ -0,0 +1,27 @@ +package io.flowinquiry.modules.teams.service.mapper; + +import io.flowinquiry.modules.teams.domain.TeamRequestConversationHealth; +import io.flowinquiry.modules.teams.service.dto.TeamRequestConversationHealthDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface TeamRequestConversationHealthMapper { + /** + * Converts a TeamRequestConversationHealth entity to its DTO. + * + * @param entity the TeamRequestConversationHealth entity + * @return the TeamRequestConversationHealthDTO + */ + @Mapping(source = "teamRequest.id", target = "teamRequestId") + TeamRequestConversationHealthDTO toDTO(TeamRequestConversationHealth entity); + + /** + * Converts a TeamRequestConversationHealthDTO to its entity. + * + * @param dto the TeamRequestConversationHealthDTO + * @return the TeamRequestConversationHealth entity + */ + @Mapping(source = "teamRequestId", target = "teamRequest.id") + TeamRequestConversationHealth toEntity(TeamRequestConversationHealthDTO dto); +} diff --git a/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestMapper.java b/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestMapper.java index 23d4f9bd..64682941 100644 --- a/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestMapper.java +++ b/commons/src/main/java/io/flowinquiry/modules/teams/service/mapper/TeamRequestMapper.java @@ -16,7 +16,9 @@ import org.mapstruct.Named; import org.mapstruct.factory.Mappers; -@Mapper(componentModel = "spring") +@Mapper( + componentModel = "spring", + uses = {TeamRequestConversationHealthMapper.class}) public interface TeamRequestMapper { @Mapping(target = "teamId", source = "team.id") @@ -36,6 +38,7 @@ public interface TeamRequestMapper { @Mapping(target = "currentStateId", source = "currentState.id") @Mapping(target = "currentStateName", source = "currentState.stateName") @Mapping(target = "watchers", source = "watchers") + @Mapping(target = "conversationHealth", source = "conversationHealth") TeamRequestDTO toDto(TeamRequest teamRequest); @Mapping(target = "team", source = "teamId", qualifiedByName = "toTeam") @@ -46,8 +49,20 @@ public interface TeamRequestMapper { target = "currentState", source = "currentStateId", qualifiedByName = "toWorkflowState") + @Mapping(target = "conversationHealth", source = "conversationHealth") TeamRequest toEntity(TeamRequestDTO teamRequestDTO); + @Mapping(target = "team", source = "teamId", qualifiedByName = "toTeam") + @Mapping(target = "workflow", source = "workflowId", qualifiedByName = "toWorkflow") + @Mapping(target = "assignUser", source = "assignUserId", qualifiedByName = "toUser") + @Mapping(target = "requestUser", source = "requestUserId", qualifiedByName = "toUser") + @Mapping( + target = "currentState", + source = "currentStateId", + qualifiedByName = "toWorkflowState") + @Mapping(target = "conversationHealth", source = "conversationHealth") + void updateEntity(TeamRequestDTO dto, @MappingTarget TeamRequest entity); + @Named("toTeam") default Team toTeam(Long teamId) { return (teamId == null) ? null : Team.builder().id(teamId).build(); @@ -80,16 +95,6 @@ default String mapUserFullName(User user) { return (firstName + " " + lastName).trim(); } - @Mapping(target = "team", source = "teamId", qualifiedByName = "toTeam") - @Mapping(target = "workflow", source = "workflowId", qualifiedByName = "toWorkflow") - @Mapping(target = "assignUser", source = "assignUserId", qualifiedByName = "toUser") - @Mapping(target = "requestUser", source = "requestUserId", qualifiedByName = "toUser") - @Mapping( - target = "currentState", - source = "currentStateId", - qualifiedByName = "toWorkflowState") - void updateEntity(TeamRequestDTO dto, @MappingTarget TeamRequest entity); - default Set mapWatchers(Set watchers) { if (watchers == null || watchers.isEmpty()) { return Collections.emptySet(); diff --git a/commons/src/main/java/io/flowinquiry/modules/usermanagement/service/DomainUserDetailsService.java b/commons/src/main/java/io/flowinquiry/modules/usermanagement/service/DomainUserDetailsService.java index 0a095706..3b1aeb72 100644 --- a/commons/src/main/java/io/flowinquiry/modules/usermanagement/service/DomainUserDetailsService.java +++ b/commons/src/main/java/io/flowinquiry/modules/usermanagement/service/DomainUserDetailsService.java @@ -8,6 +8,7 @@ import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.Cacheable; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -28,6 +29,7 @@ public DomainUserDetailsService(UserRepository userRepository) { @Override @Transactional(readOnly = true) + @Cacheable(value = "authenticatedUsers") public UserDetails loadUserByUsername(final String email) { LOG.debug("Authenticating {}", email); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0945f96b..91db01cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,13 +12,17 @@ mockitoJunitVersion = "5.14.1" jsonApiVersion = "2.1.3" parssonVersion="1.1.7" springbootVersion = "3.4.1" +springAiVersion="1.0.0-M5" springDependencyManagementVersion="1.1.7" jhisterVersion = "8.7.1" j2HtmlVersion = "1.6.0" shedlockVersion="6.0.1" jibVersion="3.4.4" +caffeineVersion="3.2.0" [libraries] +caffeine = {module="com.github.ben-manes.caffeine:caffeine", version.ref="caffeineVersion"} +caffeine-jcache = {module="com.github.ben-manes.caffeine:jcache", version.ref="caffeineVersion"} jhipster-framework = { module = "tech.jhipster:jhipster-framework", version.ref = "jhisterVersion" } lombok = { module = "org.projectlombok:lombok", version.ref = "lombokVersion" } liquibase = { module = "org.liquibase:liquibase-core", version.ref = "liquibaseVersion" } @@ -42,6 +46,8 @@ shedlock = {module="net.javacrumbs.shedlock:shedlock-spring", version.ref="shedl shedlock-jdbc-provider = {module="net.javacrumbs.shedlock:shedlock-provider-jdbc-template", version.ref="shedlockVersion"} spring-boot-dependencies= {module="org.springframework.boot:spring-boot-dependencies", version.ref="springbootVersion"} splotless-plugin = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "7.0.0.BETA4" } +spring-ai-openai = {module = "org.springframework.ai:spring-ai-openai-spring-boot-starter", version.ref="springAiVersion"} +spring-ai-ollama = {module = "org.springframework.ai:spring-ai-ollama-spring-boot-starter", version.ref="springAiVersion"} [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "springbootVersion" } @@ -53,3 +59,5 @@ junit = ["junit-jupiter-api", "junit-jupiter-engine"] mockito = ["mockito", "mockito-junit"] json = ["json-api", "parsson"] shedlock = ["shedlock", "shedlock-jdbc-provider"] +spring-ai = ["spring-ai-openai", "spring-ai-ollama"] +caffeine-cache = ["caffeine", "caffeine-jcache"] \ No newline at end of file diff --git a/server/build.gradle b/server/build.gradle index 59d73911..e0744564 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -4,7 +4,6 @@ plugins { id "com.gorylenko.gradle-git-properties" alias(libs.plugins.spring.boot) alias(libs.plugins.spring.dependency.management) - id "flowinquiry.spring-cache-conventions" id "flowinquiry.docker-conventions" } diff --git a/server/src/main/resources/config/application-dev.yml b/server/src/main/resources/config/application-dev.yml index b5668bca..090a34d4 100644 --- a/server/src/main/resources/config/application-dev.yml +++ b/server/src/main/resources/config/application-dev.yml @@ -13,6 +13,15 @@ logging: # org.springframework.web: DEBUG spring: + ai: + openai: + api-key: ${OPEN_AI_API_KEY} + chat: + options: + model: ${OPEN_AI_CHAT_MODEL} + ollama: + chat: + enabled: true devtools: restart: enabled: true diff --git a/server/src/main/resources/config/application-prod.yml b/server/src/main/resources/config/application-prod.yml index 7e0ad9e0..9739abfd 100644 --- a/server/src/main/resources/config/application-prod.yml +++ b/server/src/main/resources/config/application-prod.yml @@ -17,6 +17,12 @@ management: enabled: true spring: + ai: + openai: + api-key: ${OPEN_AI_API_KEY} + chat: + options: + model: ${OPEN_AI_CHAT_MODEL} devtools: restart: enabled: false diff --git a/server/src/main/resources/config/application.yml b/server/src/main/resources/config/application.yml index 2de61354..f431f068 100644 --- a/server/src/main/resources/config/application.yml +++ b/server/src/main/resources/config/application.yml @@ -36,8 +36,9 @@ spring: hibernate.type.preferred_instant_jdbc_type: TIMESTAMP hibernate.id.new_generator_mappings: true hibernate.connection.provider_disables_autocommit: true - hibernate.cache.use_second_level_cache: true - hibernate.cache.use_query_cache: false + hibernate.cache: + use_second_level_cache: true + use_query_cache: false hibernate.generate_statistics: false # modify batch size as necessary hibernate.jdbc.batch_size: 25 diff --git a/server/src/test/resources/config/application-test.yml b/server/src/test/resources/config/application-test.yml index 7dfb5401..abb30920 100644 --- a/server/src/test/resources/config/application-test.yml +++ b/server/src/test/resources/config/application-test.yml @@ -9,6 +9,11 @@ # =================================================================== spring: + ai: + openai: + api-key: ${OPEN_AI_API_KEY} + chat: + enabled: false datasource: type: com.zaxxer.hikari.HikariDataSource hikari: diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/001_request_workflow_tables.xml b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/001_request_workflow_tables.xml index acacde8d..a2568279 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/001_request_workflow_tables.xml +++ b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/001_request_workflow_tables.xml @@ -215,6 +215,51 @@ constraintName="team_request_workflow_state" referencedTableName="fw_workflow_state" referencedColumnNames="id" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +