diff --git a/pom.xml b/pom.xml index e8f7a953a4..69b5fd5d03 100644 --- a/pom.xml +++ b/pom.xml @@ -629,7 +629,7 @@ dev.langchain4j - langchain4j-core + langchain4j ${langchain4j.version} diff --git a/server-core/pom.xml b/server-core/pom.xml index 0d5111f288..66ca4eb1e0 100644 --- a/server-core/pom.xml +++ b/server-core/pom.xml @@ -385,7 +385,7 @@ dev.langchain4j - langchain4j-core + langchain4j diff --git a/server-core/src/main/java/io/onedev/server/ai/IssueHelper.java b/server-core/src/main/java/io/onedev/server/ai/IssueHelper.java new file mode 100644 index 0000000000..95729abfbd --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/ai/IssueHelper.java @@ -0,0 +1,90 @@ +package io.onedev.server.ai; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.onedev.server.OneDev; +import io.onedev.server.model.Issue; +import io.onedev.server.model.IssueComment; +import io.onedev.server.model.Project; +import io.onedev.server.web.UrlService; + +public class IssueHelper { + + private static ObjectMapper getObjectMapper() { + return OneDev.getInstance(ObjectMapper.class); + } + + private static UrlService getUrlService() { + return OneDev.getInstance(UrlService.class); + } + + public static Map getSummary(Project currentProject, Issue issue) { + var typeReference = new TypeReference>() {}; + var summary = getObjectMapper().convertValue(issue, typeReference); + summary.remove("id"); + summary.remove("stateOrdinal"); + summary.remove("uuid"); + summary.remove("messageId"); + summary.remove("pinDate"); + summary.remove("boardPosition"); + summary.remove("numberScopeId"); + summary.put("reference", issue.getReference().toString(currentProject)); + summary.remove("submitterId"); + summary.put("submitter", issue.getSubmitter().getName()); + summary.put("Project", issue.getProject().getPath()); + summary.remove("lastActivity"); + for (var it = summary.entrySet().iterator(); it.hasNext();) { + var entry = it.next(); + if (entry.getKey().endsWith("Count")) + it.remove(); + } + return summary; + } + + public static List> getComments(Issue issue) { + var comments = new ArrayList>(); + issue.getComments().stream().sorted(Comparator.comparing(IssueComment::getId)).forEach(comment -> { + var commentMap = new HashMap(); + commentMap.put("user", comment.getUser().getName()); + commentMap.put("date", comment.getDate()); + commentMap.put("content", comment.getContent()); + comments.add(commentMap); + }); + return comments; + } + + public static Map getDetail(Project currentProject, Issue issue) { + var detail = getSummary(currentProject, issue); + for (var entry : issue.getFieldInputs().entrySet()) { + detail.put(entry.getKey(), entry.getValue().getValues()); + } + + Map> linkedIssues = new HashMap<>(); + for (var link: issue.getTargetLinks()) { + linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>()) + .add(link.getTarget().getReference().toString(currentProject)); + } + for (var link : issue.getSourceLinks()) { + if (link.getSpec().getOpposite() != null) { + linkedIssues.computeIfAbsent(link.getSpec().getOpposite().getName(), k -> new ArrayList<>()) + .add(link.getSource().getReference().toString(currentProject)); + } else { + linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>()) + .add(link.getSource().getReference().toString(currentProject)); + } + } + detail.putAll(linkedIssues); + detail.put("link", getUrlService().urlFor(issue, true)); + + return detail; + } +} diff --git a/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java index 7685f847f4..657fd6ba52 100644 --- a/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java +++ b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java @@ -10,7 +10,6 @@ import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -709,16 +708,16 @@ public class McpHelperResource { parsedQuery = new IssueQuery(); } - var issues = new ArrayList>(); + var summaries = new ArrayList>(); for (var issue : issueService.query(subject, new ProjectScope(projectInfo.project, true, false), parsedQuery, true, offset, count)) { - var issueMap = getIssueMap(projectInfo.currentProject, issue); + var summary = IssueHelper.getSummary(projectInfo.currentProject, issue); for (var entry: issue.getFieldInputs().entrySet()) { - issueMap.put(entry.getKey(), entry.getValue().getValues()); + summary.put(entry.getKey(), entry.getValue().getValues()); } - issueMap.put("link", urlService.urlFor(issue, true)); - issues.add(issueMap); + summary.put("link", urlService.urlFor(issue, true)); + summaries.add(summary); } - return issues; + return summaries; } private Issue getIssue(Project currentProject, String referenceString) { @@ -732,29 +731,6 @@ public class McpHelperResource { throw new NotFoundException("Issue not found: " + referenceString); } } - - private Map getIssueMap(Project currentProject, Issue issue) { - var typeReference = new TypeReference>() {}; - var issueMap = objectMapper.convertValue(issue, typeReference); - issueMap.remove("id"); - issueMap.remove("stateOrdinal"); - issueMap.remove("uuid"); - issueMap.remove("messageId"); - issueMap.remove("pinDate"); - issueMap.remove("boardPosition"); - issueMap.remove("numberScopeId"); - issueMap.put("reference", issue.getReference().toString(currentProject)); - issueMap.remove("submitterId"); - issueMap.put("submitter", issue.getSubmitter().getName()); - issueMap.put("Project", issue.getProject().getPath()); - issueMap.remove("lastActivity"); - for (var it = issueMap.entrySet().iterator(); it.hasNext();) { - var entry = it.next(); - if (entry.getKey().endsWith("Count")) - it.remove(); - } - return issueMap; - } @Path("/get-issue") @GET @@ -765,31 +741,8 @@ public class McpHelperResource { throw new UnauthenticatedException(); var currentProject = getProject(currentProjectPath); - var issue = getIssue(currentProject, issueReference); - - var issueMap = getIssueMap(currentProject, issue); - for (var entry : issue.getFieldInputs().entrySet()) { - issueMap.put(entry.getKey(), entry.getValue().getValues()); - } - - Map> linkedIssues = new HashMap<>(); - for (var link: issue.getTargetLinks()) { - linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>()) - .add(link.getTarget().getReference().toString(currentProject)); - } - for (var link : issue.getSourceLinks()) { - if (link.getSpec().getOpposite() != null) { - linkedIssues.computeIfAbsent(link.getSpec().getOpposite().getName(), k -> new ArrayList<>()) - .add(link.getSource().getReference().toString(currentProject)); - } else { - linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>()) - .add(link.getSource().getReference().toString(currentProject)); - } - } - issueMap.putAll(linkedIssues); - issueMap.put("link", urlService.urlFor(issue, true)); - - return issueMap; + var issue = getIssue(currentProject, issueReference); + return IssueHelper.getDetail(currentProject, issue); } @Path("/get-issue-comments") @@ -799,20 +752,9 @@ public class McpHelperResource { @QueryParam("reference") @NotNull String issueReference) { if (SecurityUtils.getUser() == null) throw new UnauthenticatedException(); - var currentProject = getProject(currentProjectPath); - var issue = getIssue(currentProject, issueReference); - - var comments = new ArrayList>(); - for (var comment : issue.getComments()) { - var commentMap = new HashMap(); - commentMap.put("user", comment.getUser().getName()); - commentMap.put("date", comment.getDate()); - commentMap.put("content", comment.getContent()); - comments.add(commentMap); - } - return comments; + return IssueHelper.getComments(issue); } @Path("/add-issue-comment") @@ -1093,40 +1035,6 @@ public class McpHelperResource { } } - private Map getPullRequestMap(Project currentProject, - PullRequest pullRequest, boolean checkMergeConditionIfOpen) { - var typeReference = new TypeReference>() {}; - var pullRequestMap = objectMapper.convertValue(pullRequest, typeReference); - pullRequestMap.remove("id"); - if (pullRequest.isOpen() && checkMergeConditionIfOpen) { - var errorMessage = pullRequest.checkMergeCondition(); - if (errorMessage != null) - pullRequestMap.put("status", PullRequest.Status.OPEN.name() + " (" + errorMessage + ")"); - else - pullRequestMap.put("status", PullRequest.Status.OPEN.name() + " (ready to merge)"); - } - pullRequestMap.remove("uuid"); - pullRequestMap.remove("buildCommitHash"); - pullRequestMap.remove("submitTimeGroups"); - pullRequestMap.remove("closeTimeGroups"); - pullRequestMap.remove("checkError"); - pullRequestMap.remove("numberScopeId"); - pullRequestMap.put("reference", pullRequest.getReference().toString(currentProject)); - pullRequestMap.remove("submitterId"); - pullRequestMap.put("submitter", pullRequest.getSubmitter().getName()); - pullRequestMap.put("targetProject", pullRequest.getTarget().getProject().getPath()); - if (pullRequest.getSourceProject() != null) - pullRequestMap.put("sourceProject", pullRequest.getSourceProject().getPath()); - pullRequestMap.remove("codeCommentsUpdateDate"); - pullRequestMap.remove("lastActivity"); - for (var it = pullRequestMap.entrySet().iterator(); it.hasNext();) { - var entry = it.next(); - if (entry.getKey().endsWith("Count")) - it.remove(); - } - return pullRequestMap; - } - @Path("/query-pull-requests") @GET public List> queryPullRequests( @@ -1154,13 +1062,13 @@ public class McpHelperResource { parsedQuery = new PullRequestQuery(); } - var pullRequests = new ArrayList>(); + var summaries = new ArrayList>(); for (var pullRequest : pullRequestService.query(subject, projectInfo.project, parsedQuery, false, offset, count)) { - var pullRequestMap = getPullRequestMap(projectInfo.currentProject, pullRequest, false); - pullRequestMap.put("link", urlService.urlFor(pullRequest, true)); - pullRequests.add(pullRequestMap); + var summary = PullRequestHelper.getSummary(projectInfo.currentProject, pullRequest, false); + summary.put("link", urlService.urlFor(pullRequest, true)); + summaries.add(summary); } - return pullRequests; + return summaries; } @Path("/query-builds") @@ -1249,31 +1157,8 @@ public class McpHelperResource { throw new UnauthenticatedException(); var currentProject = getProject(currentProjectPath); - var pullRequest = getPullRequest(currentProject, pullRequestReference); - - var pullRequestMap = getPullRequestMap(currentProject, pullRequest, true); - pullRequestMap.put("headCommitHash", pullRequest.getLatestUpdate().getHeadCommitHash()); - pullRequestMap.put("assignees", pullRequest.getAssignees().stream().map(it->it.getName()).collect(Collectors.toList())); - var reviews = new ArrayList>(); - for (var review : pullRequest.getReviews()) { - if (review.getStatus() == PullRequestReview.Status.EXCLUDED) - continue; - var reviewMap = new HashMap(); - reviewMap.put("reviewer", review.getUser().getName()); - reviewMap.put("status", review.getStatus()); - reviews.add(reviewMap); - } - pullRequestMap.put("reviews", reviews); - var builds = new ArrayList(); - for (var build : pullRequest.getBuilds()) { - builds.add(build.getReference().toString(currentProject) + " (job: " + build.getJobName() + ", status: " + build.getStatus() + ")"); - } - pullRequestMap.put("builds", builds); - pullRequestMap.put("labels", pullRequest.getLabels().stream().map(it->it.getSpec().getName()).collect(Collectors.toList())); - pullRequestMap.put("link", urlService.urlFor(pullRequest, true)); - - return pullRequestMap; + return PullRequestHelper.getDetail(currentProject, pullRequest); } @Path("/get-pull-request-comments") @@ -1287,15 +1172,7 @@ public class McpHelperResource { var currentProject = getProject(currentProjectPath); var pullRequest = getPullRequest(currentProject, pullRequestReference); - var comments = new ArrayList>(); - for (var comment : pullRequest.getComments()) { - var commentMap = new HashMap(); - commentMap.put("user", comment.getUser().getName()); - commentMap.put("date", comment.getDate()); - commentMap.put("content", comment.getContent()); - comments.add(commentMap); - } - return comments; + return PullRequestHelper.getComments(pullRequest); } @Path("/get-pull-request-code-comments") diff --git a/server-core/src/main/java/io/onedev/server/ai/PullRequestHelper.java b/server-core/src/main/java/io/onedev/server/ai/PullRequestHelper.java new file mode 100644 index 0000000000..e237170be8 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/ai/PullRequestHelper.java @@ -0,0 +1,102 @@ +package io.onedev.server.ai; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.onedev.server.OneDev; +import io.onedev.server.model.Project; +import io.onedev.server.model.PullRequest; +import io.onedev.server.model.PullRequestComment; +import io.onedev.server.model.PullRequestReview; +import io.onedev.server.web.UrlService; + +public class PullRequestHelper { + + private static ObjectMapper getObjectMapper() { + return OneDev.getInstance(ObjectMapper.class); + } + + private static UrlService getUrlService() { + return OneDev.getInstance(UrlService.class); + } + + public static Map getSummary(Project currentProject, + PullRequest pullRequest, boolean checkMergeConditionIfOpen) { + var typeReference = new TypeReference>() {}; + var summary = getObjectMapper().convertValue(pullRequest, typeReference); + summary.remove("id"); + if (pullRequest.isOpen() && checkMergeConditionIfOpen) { + var errorMessage = pullRequest.checkMergeCondition(); + if (errorMessage != null) + summary.put("status", PullRequest.Status.OPEN.name() + " (" + errorMessage + ")"); + else + summary.put("status", PullRequest.Status.OPEN.name() + " (ready to merge)"); + } + summary.remove("uuid"); + summary.remove("buildCommitHash"); + summary.remove("submitTimeGroups"); + summary.remove("closeTimeGroups"); + summary.remove("checkError"); + summary.remove("numberScopeId"); + summary.put("reference", pullRequest.getReference().toString(currentProject)); + summary.remove("submitterId"); + summary.put("submitter", pullRequest.getSubmitter().getName()); + summary.put("targetProject", pullRequest.getTarget().getProject().getPath()); + if (pullRequest.getSourceProject() != null) + summary.put("sourceProject", pullRequest.getSourceProject().getPath()); + summary.remove("codeCommentsUpdateDate"); + summary.remove("lastActivity"); + for (var it = summary.entrySet().iterator(); it.hasNext();) { + var entry = it.next(); + if (entry.getKey().endsWith("Count")) + it.remove(); + } + return summary; + } + + public static Map getDetail(Project currentProject, PullRequest pullRequest) { + var detail = getSummary(currentProject, pullRequest, true); + detail.put("headCommitHash", pullRequest.getLatestUpdate().getHeadCommitHash()); + detail.put("assignees", pullRequest.getAssignees().stream().map(it->it.getName()).collect(Collectors.toList())); + var reviews = new ArrayList>(); + for (var review : pullRequest.getReviews()) { + if (review.getStatus() == PullRequestReview.Status.EXCLUDED) + continue; + var reviewMap = new HashMap(); + reviewMap.put("reviewer", review.getUser().getName()); + reviewMap.put("status", review.getStatus()); + reviews.add(reviewMap); + } + detail.put("reviews", reviews); + var builds = new ArrayList(); + for (var build : pullRequest.getBuilds()) { + builds.add(build.getReference().toString(currentProject) + " (job: " + build.getJobName() + ", status: " + build.getStatus() + ")"); + } + detail.put("builds", builds); + detail.put("labels", pullRequest.getLabels().stream().map(it->it.getSpec().getName()).collect(Collectors.toList())); + detail.put("link", getUrlService().urlFor(pullRequest, true)); + + return detail; + } + + public static List> getComments(PullRequest pullRequest) { + var comments = new ArrayList>(); + pullRequest.getComments().stream().sorted(Comparator.comparing(PullRequestComment::getId)).forEach(comment -> { + var commentMap = new HashMap(); + commentMap.put("user", comment.getUser().getName()); + commentMap.put("date", comment.getDate()); + commentMap.put("content", comment.getContent()); + comments.add(commentMap); + }); + return comments; + } + +} diff --git a/server-core/src/main/java/io/onedev/server/model/Chat.java b/server-core/src/main/java/io/onedev/server/model/Chat.java index 46c8bf88f5..c772a14911 100644 --- a/server-core/src/main/java/io/onedev/server/model/Chat.java +++ b/server-core/src/main/java/io/onedev/server/model/Chat.java @@ -102,15 +102,15 @@ public class Chat extends AbstractEntity { } public static String getChangeObservable(Long chatId) { - return "chat:" + chatId; + return Chat.class.getName() + ":" + chatId; } public static String getPartialResponseObservable(Long chatId) { - return "chat:" + chatId + ":partialResponse"; + return Chat.class.getName() + ":partialResponse:" + chatId; } public static String getNewMessagesObservable(Long chatId) { - return "chat:" + chatId + ":newMessages"; + return Chat.class.getName() + ":newMessages:" + chatId; } } diff --git a/server-core/src/main/java/io/onedev/server/service/ChatService.java b/server-core/src/main/java/io/onedev/server/service/ChatService.java index 83a680848c..7c7d04d640 100644 --- a/server-core/src/main/java/io/onedev/server/service/ChatService.java +++ b/server-core/src/main/java/io/onedev/server/service/ChatService.java @@ -8,6 +8,8 @@ import io.onedev.server.model.Chat; import io.onedev.server.model.ChatMessage; import io.onedev.server.model.User; import io.onedev.server.service.support.ChatResponding; +import io.onedev.server.service.support.ChatTool; +import io.onedev.server.web.WebSession; public interface ChatService extends EntityService { @@ -15,9 +17,13 @@ public interface ChatService extends EntityService { void createOrUpdate(Chat chat); - void sendRequest(String sessionId, ChatMessage request); + void sendRequest(WebSession session, ChatMessage request, List tools, int timeoutSeconds); @Nullable - ChatResponding getResponding(String sessionId, Chat chat); + ChatResponding getResponding(WebSession session, Chat chat); + + long nextAnonymousChatId(); + + long nextAnonymousChatMessageId(); } diff --git a/server-core/src/main/java/io/onedev/server/service/impl/DefaultChatService.java b/server-core/src/main/java/io/onedev/server/service/impl/DefaultChatService.java index 41c5814c92..0bdb061cc4 100644 --- a/server-core/src/main/java/io/onedev/server/service/impl/DefaultChatService.java +++ b/server-core/src/main/java/io/onedev/server/service/impl/DefaultChatService.java @@ -1,25 +1,38 @@ package io.onedev.server.service.impl; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hazelcast.cp.IAtomicLong; + import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.ToolExecutionResultMessage; import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.PartialResponse; import dev.langchain4j.model.chat.response.PartialResponseContext; @@ -28,18 +41,26 @@ import dev.langchain4j.model.chat.response.PartialThinkingContext; import dev.langchain4j.model.chat.response.PartialToolCall; import dev.langchain4j.model.chat.response.PartialToolCallContext; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import io.onedev.commons.utils.ExplicitException; import io.onedev.commons.utils.StringUtils; +import io.onedev.server.cluster.ClusterService; +import io.onedev.server.event.Listen; +import io.onedev.server.event.system.SystemStarting; import io.onedev.server.exception.ExceptionUtils; import io.onedev.server.model.Chat; import io.onedev.server.model.ChatMessage; import io.onedev.server.model.User; +import io.onedev.server.persistence.SessionService; import io.onedev.server.persistence.TransactionService; import io.onedev.server.persistence.annotation.Sessional; import io.onedev.server.persistence.annotation.Transactional; import io.onedev.server.persistence.dao.EntityCriteria; +import io.onedev.server.security.SecurityUtils; import io.onedev.server.service.ChatService; import io.onedev.server.service.support.ChatResponding; +import io.onedev.server.service.support.ChatTool; import io.onedev.server.web.SessionListener; +import io.onedev.server.web.WebSession; import io.onedev.server.web.websocket.WebSocketService; @Singleton @@ -62,6 +83,23 @@ public class DefaultChatService extends BaseEntityService implements ChatS @Inject private WebSocketService webSocketService; + @Inject + private ObjectMapper objectMapper; + + @Inject + private SessionService sessionService; + + @Inject + private ClusterService clusterService; + + /* + * Use cluster wide id for anonymous chats and messages as we use id to route + * websocket observable changes to correct connections + */ + private volatile IAtomicLong nextAnonymousChatId; + + private volatile IAtomicLong nextAnonymousChatMessageId; + private final Map> respondings = new ConcurrentHashMap<>(); @Override @@ -70,7 +108,7 @@ public class DefaultChatService extends BaseEntityService implements ChatS criteria.add(Restrictions.eq(Chat.PROP_USER, user)); criteria.add(Restrictions.eq(Chat.PROP_AI, ai)); criteria.add(Restrictions.ilike(Chat.PROP_TITLE, "%" + term + "%")); - criteria.addOrder(Order.desc(Chat.PROP_DATE)); + criteria.addOrder(Order.desc(Chat.PROP_ID)); return query(criteria); } @@ -81,16 +119,22 @@ public class DefaultChatService extends BaseEntityService implements ChatS @Sessional @Override - public ChatResponding getResponding(String sessionId, Chat chat) { - return getResponding(sessionId, chat.getId()); + public ChatResponding getResponding(WebSession session, Chat chat) { + return getResponding(session.getId(), chat.getId()); } @Transactional @Override - public void sendRequest(String sessionId, ChatMessage request) { + public void sendRequest(WebSession session, ChatMessage request, List tools, int timeoutSeconds) { + var sessionId = session.getId(); var requestId = request.getId(); var chatId = request.getChat().getId(); + var anonymous = request.getChat().getUser() == null; + var modelSetting = request.getChat().getAi().getAiSetting().getModelSetting(); + var streamingChatModel = modelSetting.getStreamingChatModel(); + var chatModel = modelSetting.getChatModel(); + var messages = request.getChat().getSortedMessages() .stream() .filter(it->!it.isError()) @@ -99,123 +143,221 @@ public class DefaultChatService extends BaseEntityService implements ChatS if (messages.size() > MAX_HISTORY_MESSAGES) messages = messages.subList(messages.size()-MAX_HISTORY_MESSAGES, messages.size()); - var langchain4jMessages = messages.stream() + var langchain4jMessages = new ArrayList(); + langchain4jMessages.addAll(messages.stream() .map(it -> { var content = it.getContent(); if (!it.equals(request)) content = StringUtils.abbreviate(content, MAX_HISTORY_MESSAGE_LEN); if (it.isRequest()) - return new UserMessage(content); + return (dev.langchain4j.data.message.ChatMessage) new UserMessage(content); else - return new AiMessage(content); + return (dev.langchain4j.data.message.ChatMessage) new AiMessage(content); }) + .collect(Collectors.toList())); + + var toolSpecifications = tools.stream() + .map(ChatTool::getSpecification) .collect(Collectors.toList()); + var completableFuture = new CompletableFuture(); + var executableFuture = executorService.submit(new Runnable() { - var future = executorService.submit(() -> { - var latch = new CountDownLatch(1); - var handler = new StreamingChatResponseHandler() { + private StreamingChatResponseHandler newResponseHandler(Subject subject) { + return new StreamingChatResponseHandler() { - private long lastPartialResponseNotificationTime = System.currentTimeMillis(); + private long lastPartialResponseNotificationTime = System.currentTimeMillis(); - @Override - public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) { - if (latch.getCount() == 0) { - context.streamingHandle().cancel(); - } else { - var responding = getResponding(sessionId, chatId, requestId); - if (responding != null) { - var content = responding.getContent(); - if (content == null) - content = ""; - content += partialResponse.text(); - responding.content = content; - if (System.currentTimeMillis() - lastPartialResponseNotificationTime > PARTIAL_RESPONSE_NOTIFICATION_INTERVAL) { - lastPartialResponseNotificationTime = System.currentTimeMillis(); - webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null); + @Override + public void onPartialResponse(PartialResponse partialResponse, PartialResponseContext context) { + ThreadContext.bind(subject); + if (completableFuture.isDone()) { + context.streamingHandle().cancel(); + } else { + var responding = getResponding(sessionId, chatId, requestId); + if (responding != null) { + var content = responding.getContent(); + if (content == null) + content = ""; + content += partialResponse.text(); + responding.content = content; + if (System.currentTimeMillis() - lastPartialResponseNotificationTime > PARTIAL_RESPONSE_NOTIFICATION_INTERVAL) { + lastPartialResponseNotificationTime = System.currentTimeMillis(); + webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null); + } + } + } + } + + @Override + public void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context) { + ThreadContext.bind(subject); + if (completableFuture.isDone()) + context.streamingHandle().cancel(); + } + + @Override + public void onPartialToolCall(PartialToolCall partialToolCall, PartialToolCallContext context) { + ThreadContext.bind(subject); + if (completableFuture.isDone()) { + context.streamingHandle().cancel(); + } + } + + @Override + public void onCompleteResponse(ChatResponse completeResponse) { + ThreadContext.bind(subject); + sessionService.runAsync(() -> { + try { + var aiMessage = completeResponse.aiMessage(); + if (aiMessage.hasToolExecutionRequests()) { + langchain4jMessages.add(aiMessage); + var toolRequests = aiMessage.toolExecutionRequests(); + for (int i = 0; i < toolRequests.size(); i++) { + var toolRequest = toolRequests.get(i); + String toolName = toolRequest.name(); + String toolArgs = toolRequest.arguments(); + var toolResult = tools.stream() + .filter(it->it.getSpecification().name().equals(toolName)) + .findFirst() + .orElseThrow(() -> new ExplicitException("Unknown tool: " + toolName)) + .execute(objectMapper.readTree(toolArgs)); + ToolExecutionResultMessage toolResultMessage = ToolExecutionResultMessage.from( + toolRequest.id(), toolName, toolResult); + langchain4jMessages.add(toolResultMessage); + } + + var handler = newResponseHandler(SecurityUtils.getSubject()); + var langchain4jChatRequest = ChatRequest.builder() + .messages(new ArrayList<>(langchain4jMessages)) + .toolSpecifications(toolSpecifications) + .build(); + streamingChatModel.chat(langchain4jChatRequest, handler); + } else { + completableFuture.complete(aiMessage.text()); + } + } catch (Throwable t) { + completableFuture.completeExceptionally(t); } - } - } - } - - @Override - public void onPartialThinking(PartialThinking partialThinking, PartialThinkingContext context) { - if (latch.getCount() == 0) - context.streamingHandle().cancel(); - } - - @Override - public void onPartialToolCall(PartialToolCall partialToolCall, PartialToolCallContext context) { - if (latch.getCount() == 0) - context.streamingHandle().cancel(); - } - - @Override - public void onCompleteResponse(ChatResponse completeResponse) { - try { - createResponseIfNecessary(sessionId, chatId, requestId, completeResponse.aiMessage().text(), null); - } finally { - latch.countDown(); - } - } - - @Override - public void onError(Throwable error) { - try { - createResponseIfNecessary(sessionId, chatId, requestId, "Error getting chat response, check server log for details", error); - } finally { - latch.countDown(); - } - } - - }; - try { - modelSetting.getStreamingChatModel().chat(langchain4jMessages, handler); - transactionService.run(() -> { - var chat = load(chatId); - var requests = chat.getMessages().stream().filter(it->it.isRequest()).collect(Collectors.toList()); - if (requests.size() == 1) { - var systemPrompt = String.format(""" - Summarize provided message to get a compact title with below requirements: - 1. It should be within %d characters - 2. Only title is returned, no other text or comments - """, Chat.MAX_TITLE_LEN); - var title = modelSetting.getChatModel().chat(new SystemMessage(systemPrompt), new UserMessage(requests.get(0).getContent())).aiMessage().text(); - chat.setTitle(title); - webSocketService.notifyObservableChange(Chat.getChangeObservable(chatId), null); - } - }); - latch.await(); - } catch (Exception e) { - if (ExceptionUtils.find(e, InterruptedException.class) == null) { - logger.error("Error getting chat response", e); - String errorMessage = e.getMessage(); - if (errorMessage == null) - errorMessage = "Error getting chat response, check server log for details"; - createResponseIfNecessary(sessionId, chatId, requestId, errorMessage, e); - } else { - var responding = getResponding(sessionId, chatId, requestId); - if (responding != null && responding.getContent() != null) - createResponseIfNecessary(sessionId, chatId, requestId, responding.getContent(), null); - } - } finally { - latch.countDown(); - var respondingsOfSession = respondings.get(sessionId); - if (respondingsOfSession != null) { - var responding = respondingsOfSession.get(chatId); - if (responding != null) { - respondingsOfSession.computeIfPresent(chatId, (k, v)-> { - if (v.requestId.equals(requestId)) - return null; - else - return v; }); - webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null); + } + + @Override + public void onError(Throwable error) { + ThreadContext.bind(subject); + completableFuture.completeExceptionally(error); + } + + }; + } + + private void createResponse(String content, @Nullable Throwable throwable) { + var responding = getResponding(session.getId(), chatId, requestId); + if (responding != null) { + transactionService.run(() -> { + if (throwable != null) + logger.error("Error getting chat response", throwable); + var response = new ChatMessage(); + response.setError(throwable != null); + response.setContent(content); + if (anonymous) { + var chat = SerializationUtils.clone(checkNotNull(session.getAnonymousChats().get(chatId))); + response.setChat(chat); + response.setId(nextAnonymousChatMessageId()); + chat.getMessages().add(response); + session.getAnonymousChats().put(chatId, chat); + } else { + response.setChat(load(chatId)); + dao.persist(response); + } + webSocketService.notifyObservableChange(Chat.getNewMessagesObservable(chatId), null); + }); + }; + } + + private void createIncompleteResponse(String reason) { + var responding = getResponding(sessionId, chatId, requestId); + if (responding != null) { + var content = responding.getContent(); + if (content == null) + content = ""; + else + content += "\n\n"; + content += "" + reason + ""; + createResponse(content, null); + } + } + + @Override + public void run() { + var handler = newResponseHandler(SecurityUtils.getSubject()); + try { + var langchain4jChatRequest = ChatRequest.builder() + .messages(langchain4jMessages.stream() + .map(msg -> (dev.langchain4j.data.message.ChatMessage) msg) + .collect(Collectors.toList())) + .toolSpecifications(toolSpecifications) + .build(); + + streamingChatModel.chat(langchain4jChatRequest, handler); + transactionService.run(() -> { + var chat = anonymous ? checkNotNull(session.getAnonymousChats().get(chatId)) : load(chatId); + var requests = chat.getMessages().stream().filter(it->it.isRequest()).collect(Collectors.toList()); + if (requests.size() == 1) { + var systemPrompt = String.format(""" + Summarize provided message to get a compact title with below requirements: + 1. Just summarize the message itself, no need to ask user for clarification or confirmation + 2. It should be within %d characters + 3. Only title is returned, no other text or comments + 4. Display in %s + """, Chat.MAX_TITLE_LEN, session.getLocale().getDisplayLanguage()); + var title = chatModel.chat(new SystemMessage(systemPrompt), new UserMessage(requests.get(0).getContent())).aiMessage().text(); + if (anonymous) { + chat = SerializationUtils.clone(chat); + chat.setTitle(title); + session.getAnonymousChats().put(chatId, chat); + } else { + chat.setTitle(title); + dao.persist(chat); + } + webSocketService.notifyObservableChange(Chat.getChangeObservable(chatId), null); + } + }); + var response = completableFuture.get(timeoutSeconds, TimeUnit.SECONDS); + if (StringUtils.isBlank(response)) + throw new ExplicitException("Received empty response"); + createResponse(response, null); + } catch (Throwable e) { + if (ExceptionUtils.find(e, InterruptedException.class) != null) { + createIncompleteResponse("Conversation cancelled"); + } else if (ExceptionUtils.find(e, TimeoutException.class) != null) { + createIncompleteResponse("Conversation timed out"); + } else { + logger.error("Error getting chat response", e); + String errorMessage = e.getMessage(); + if (errorMessage == null) + errorMessage = "Error getting chat response, check server log for details"; + createResponse(errorMessage, e); + } + } finally { + completableFuture.cancel(false); + var respondingsOfSession = respondings.get(sessionId); + if (respondingsOfSession != null) { + var responding = respondingsOfSession.get(chatId); + if (responding != null) { + respondingsOfSession.computeIfPresent(chatId, (k, v)-> { + if (v.requestId.equals(requestId)) + return null; + else + return v; + }); + webSocketService.notifyObservableChange(Chat.getPartialResponseObservable(chatId), null); + } } } } }); - var previousResponding = respondings.computeIfAbsent(sessionId, it->new ConcurrentHashMap<>()).put(chatId, new ChatRespondingImpl(requestId, future)); + var previousResponding = respondings.computeIfAbsent(sessionId, it->new ConcurrentHashMap<>()).put(chatId, new ChatRespondingImpl(requestId, executableFuture)); if (previousResponding != null) previousResponding.cancel(); } @@ -236,21 +378,24 @@ public class DefaultChatService extends BaseEntityService implements ChatS return null; } - private void createResponseIfNecessary(String sessionId, Long chatId, Long requestId, String content, @Nullable Throwable throwable) { - var responding = getResponding(sessionId, chatId, requestId); - if (responding != null) { - transactionService.run(() -> { - var chat = load(chatId); - if (throwable != null) - logger.error("Error getting chat response", throwable); - var response = new ChatMessage(); - response.setChat(chat); - response.setError(throwable != null); - response.setContent(content); - dao.persist(response); - webSocketService.notifyObservableChange(Chat.getNewMessagesObservable(chatId), null); - }); - }; + @Listen + public void on(SystemStarting event) { + nextAnonymousChatId = clusterService.getHazelcastInstance().getCPSubsystem().getAtomicLong("nextAnonymousChatId"); + nextAnonymousChatMessageId = clusterService.getHazelcastInstance().getCPSubsystem().getAtomicLong("nextAnonymousChatMessageId"); + if (clusterService.isLeaderServer()) { + nextAnonymousChatId.set(1L); + nextAnonymousChatMessageId.set(1L); + } + } + + @Override + public long nextAnonymousChatId() { + return nextAnonymousChatId.getAndIncrement(); + } + + @Override + public long nextAnonymousChatMessageId() { + return nextAnonymousChatMessageId.getAndIncrement(); } @Override diff --git a/server-core/src/main/java/io/onedev/server/service/support/ChatTool.java b/server-core/src/main/java/io/onedev/server/service/support/ChatTool.java new file mode 100644 index 0000000000..52ad0ba602 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/service/support/ChatTool.java @@ -0,0 +1,21 @@ +package io.onedev.server.service.support; + +import com.fasterxml.jackson.databind.JsonNode; + +import dev.langchain4j.agent.tool.ToolSpecification; + +public interface ChatTool { + + ToolSpecification getSpecification(); + + /** + * This method should not rely on any Wicket facilities, such as page, component, model etc. + * If you need to access some data from these facilities, make sure to prepare them while + * creating the tool + * + * @param arguments + * @return + */ + String execute(JsonNode arguments); + +} diff --git a/server-core/src/main/java/io/onedev/server/web/WebSession.java b/server-core/src/main/java/io/onedev/server/web/WebSession.java index 7487c68abd..bb95f7ee34 100644 --- a/server-core/src/main/java/io/onedev/server/web/WebSession.java +++ b/server-core/src/main/java/io/onedev/server/web/WebSession.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.jspecify.annotations.Nullable; import javax.servlet.http.HttpSession; import org.apache.shiro.SecurityUtils; @@ -14,8 +13,10 @@ import org.apache.shiro.subject.Subject; import org.apache.wicket.protocol.http.WicketServlet; import org.apache.wicket.request.Request; import org.apache.wicket.util.collections.ConcurrentHashSet; +import org.jspecify.annotations.Nullable; import io.onedev.server.OneDev; +import io.onedev.server.model.Chat; import io.onedev.server.web.util.Cursor; public class WebSession extends org.apache.wicket.protocol.http.WebSession { @@ -36,11 +37,13 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession { private Set expandedProjectIds = new ConcurrentHashSet<>(); - private boolean chatVisible; + private volatile boolean chatVisible; - private Long activeChatId; + private volatile Long activeChatId; - private String chatInput; + private volatile Map anonymousChats = new ConcurrentHashMap<>(); + + private volatile String chatInput; public WebSession(Request request) { super(request); @@ -141,6 +144,10 @@ public class WebSession extends org.apache.wicket.protocol.http.WebSession { this.activeChatId = activeChatId; } + public Map getAnonymousChats() { + return anonymousChats; + } + @Nullable public String getChatInput() { return chatInput; diff --git a/server-core/src/main/java/io/onedev/server/web/asset/icon/ai-summarize.svg b/server-core/src/main/java/io/onedev/server/web/asset/icon/ai-summarize.svg new file mode 100644 index 0000000000..2832245118 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/web/asset/icon/ai-summarize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.html b/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.html index 2684c5fd56..f8cfc10643 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.html +++ b/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.html @@ -1,8 +1,8 @@
-
-
Chat with
- +
+
Chat with
+
diff --git a/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.java b/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.java index bd90669e2d..1644b93e12 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/component/ai/chat/ChatPanel.java @@ -6,6 +6,7 @@ import static io.onedev.server.web.translation.Translation._T; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -13,6 +14,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.servlet.http.Cookie; +import org.apache.commons.lang3.SerializationUtils; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; @@ -31,10 +33,13 @@ import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.repeater.RepeatingView; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.http.WebRequest; import org.apache.wicket.request.http.WebResponse; +import org.apache.wicket.util.visit.IVisit; +import org.apache.wicket.util.visit.IVisitor; import org.json.JSONException; import org.json.JSONWriter; import org.jspecify.annotations.Nullable; @@ -46,6 +51,8 @@ import io.onedev.server.persistence.dao.Dao; import io.onedev.server.service.ChatService; import io.onedev.server.service.UserService; import io.onedev.server.service.support.ChatResponding; +import io.onedev.server.service.support.ChatTool; +import io.onedev.server.util.facade.UserFacade; import io.onedev.server.web.WebConstants; import io.onedev.server.web.WebSession; import io.onedev.server.web.behavior.ChangeObserver; @@ -59,11 +66,14 @@ import io.onedev.server.web.component.select2.ChoiceProvider; import io.onedev.server.web.component.select2.ResponseFiller; import io.onedev.server.web.component.select2.Select2Choice; import io.onedev.server.web.component.user.UserAvatar; +import io.onedev.server.web.util.ChatToolAware; public class ChatPanel extends Panel { private static final long serialVersionUID = 1L; + private static final int TIMEOUT_SECONDS = 120; + private static final String COOKIE_ACTIVE_AI = "active-ai"; @Inject @@ -80,6 +90,23 @@ public class ChatPanel extends Panel { private RepeatingView messagesView; private WebMarkupContainer respondingContainer; + + private final IModel> entitledAisModel = new LoadableDetachableModel>() { + + @Override + protected List load() { + if (getUser() != null) { + return getUser().getEntitledAis(); + } else { + return userService.cloneCache().values().stream() + .filter(it -> it.getType() == User.Type.AI && !it.isDisabled() && it.isEntitleToAll()) + .sorted(Comparator.comparing(UserFacade::getDisplayName)) + .map(it->userService.load(it.getId())) + .collect(Collectors.toList()); + } + } + + }; public ChatPanel(String componentId) { super(componentId); @@ -100,7 +127,7 @@ public class ChatPanel extends Panel { protected List getMenuItems(FloatingPanel dropdown) { var activeAI = getActiveAI(); var menuItems = new ArrayList(); - for (var ai : getUser().getEntitledAis()) { + for (var ai : getEntitledAis()) { menuItems.add(new MenuItem() { @Override @@ -150,7 +177,7 @@ public class ChatPanel extends Panel { }); var chatSelectorContainer = new WebMarkupContainer("chatSelectorContainer"); - chatSelectorContainer.setOutputMarkupId(true); + chatSelectorContainer.setOutputMarkupId(true); chatSelectorContainer.add(new Select2Choice("chatSelector", new IModel() { @Override @@ -172,7 +199,16 @@ public class ChatPanel extends Panel { @Override public void query(String term, int page, io.onedev.server.web.component.select2.Response response) { var count = (page+1) * WebConstants.PAGE_SIZE + 1; - var chats = chatService.query(getUser(), getActiveAI(), term, count); + List chats; + if (getUser() != null) { + chats = chatService.query(getUser(), getActiveAI(), term, count); + } else { + chats = WebSession.get().getAnonymousChats().values().stream() + .filter(it -> it.getAi().equals(getActiveAI()) && it.getTitle().toLowerCase().contains(term.toLowerCase())) + .sorted(Comparator.comparing(Chat::getId).reversed()) + .limit(count) + .collect(Collectors.toList()); + } new ResponseFiller<>(response).fill(chats, page, WebConstants.PAGE_SIZE); } @@ -205,10 +241,6 @@ public class ChatPanel extends Panel { return Collections.emptySet(); } - public void onObservableChanged(IPartialPageRequestHandler handler, Collection changedObservables) { - handler.add(chatSelectorContainer); - } - }); chatSelectorContainer.add(new ChangeObserver() { @@ -223,10 +255,11 @@ public class ChatPanel extends Panel { @Override public void onObservableChanged(IPartialPageRequestHandler handler, Collection changedObservables) { - showNewMessages(handler); + if (isVisible()) + showNewMessages(handler); } - }); + }); chatSelectorContainer.add(new AjaxLink("newChat") { @@ -242,7 +275,10 @@ public class ChatPanel extends Panel { @Override public void onClick(AjaxRequestTarget target) { getSession().success(_T("Chat deleted")); - chatService.delete(getActiveChat()); + if (getUser() != null) + chatService.delete(getActiveChat()); + else + WebSession.get().getAnonymousChats().remove(getActiveChat().getId()); WebSession.get().setActiveChatId(null); target.add(ChatPanel.this); } @@ -299,7 +335,7 @@ public class ChatPanel extends Panel { else return Collections.emptySet(); } - + }); respondingContainer.setOutputMarkupPlaceholderTag(true); @@ -337,23 +373,55 @@ public class ChatPanel extends Panel { protected void onSubmit(AjaxRequestTarget target, Form form) { var input = WebSession.get().getChatInput().trim(); var chat = getActiveChat(); + ChatMessage request = new ChatMessage(); + request.setRequest(true); + request.setContent(input); if (chat == null) { chat = new Chat(); chat.setUser(getUser()); chat.setAi(getActiveAI()); chat.setTitle(_T("New chat")); chat.setDate(new Date()); - chatService.createOrUpdate(chat); + if (getUser() != null) { + chatService.createOrUpdate(chat); + request.setChat(chat); + chat.getMessages().add(request); + dao.persist(request); + } else { + chat.setId(chatService.nextAnonymousChatId()); + request.setId(chatService.nextAnonymousChatMessageId()); + request.setChat(chat); + chat.getMessages().add(request); + WebSession.get().getAnonymousChats().put(chat.getId(), chat); + } WebSession.get().setActiveChatId(chat.getId()); - target.add(chatSelectorContainer); + target.add(chatSelectorContainer); + } else { + if (getUser() != null) { + request.setChat(chat); + chat.getMessages().add(request); + dao.persist(request); + } else { + request.setId(chatService.nextAnonymousChatMessageId()); + chat = SerializationUtils.clone(chat); + request.setChat(chat); + chat.getMessages().add(request); + WebSession.get().getAnonymousChats().put(chat.getId(), chat); + } } - var request = new ChatMessage(); - request.setChat(chat); - request.setRequest(true); - request.setContent(input); - chat.getMessages().add(request); - dao.persist(request); - chatService.sendRequest(getSession().getId(), request); + + List chatTools = new ArrayList<>(); + if (getPage() instanceof ChatToolAware) + chatTools.addAll(((ChatToolAware) getPage()).getChatTools()); + getPage().visitChildren(ChatToolAware.class, new IVisitor() { + + @Override + public void component(Component component, IVisit visit) { + chatTools.addAll(((ChatToolAware) component).getChatTools()); + } + + }); + chatService.sendRequest(WebSession.get(), request, chatTools, TIMEOUT_SECONDS); showNewMessages(target); target.add(respondingContainer); @@ -388,10 +456,10 @@ public class ChatPanel extends Panel { private User getActiveAI() { if (activeAiId != null) { var ai = userService.get(activeAiId); - if (ai != null && getUser().getEntitledAis().contains(ai)) + if (ai != null && getEntitledAis().contains(ai)) return ai; } - return getUser().getEntitledAis().get(0); + return getEntitledAis().get(0); } private void setActiveAI(User ai) { @@ -408,7 +476,11 @@ public class ChatPanel extends Panel { private Chat getActiveChat() { var activeChatId = WebSession.get().getActiveChatId(); if (activeChatId != null) { - var chat = chatService.get(activeChatId); + Chat chat; + if (getUser() != null) + chat = chatService.get(activeChatId); + else + chat = WebSession.get().getAnonymousChats().get(activeChatId); if (chat != null && chat.getAi().equals(getActiveAI())) return chat; } @@ -419,7 +491,7 @@ public class ChatPanel extends Panel { private ChatResponding getResponding() { var chat = getActiveChat(); if (chat != null) - return chatService.getResponding(getSession().getId(), chat); + return chatService.getResponding(WebSession.get(), chat); else return null; } @@ -434,12 +506,13 @@ public class ChatPanel extends Panel { @SuppressWarnings("deprecation") private void showNewMessages(IPartialPageRequestHandler handler) { - long lastMessageId; + long prevLastMessageId; if (messagesView.size() != 0) - lastMessageId = (Long) messagesView.get(messagesView.size() - 1).getDefaultModelObject(); + prevLastMessageId = (Long) messagesView.get(messagesView.size() - 1).getDefaultModelObject(); else - lastMessageId = 0; - getMessages().stream().filter(it -> it.getId() > lastMessageId).forEach(it -> { + prevLastMessageId = 0; + + getMessages().stream().filter(it -> it.getId() > prevLastMessageId).forEach(it -> { var messageContainer = newMessageContainer(messagesView.newChildId(), it); messagesView.add(messageContainer); handler.prependJavaScript(String.format(""" @@ -447,10 +520,13 @@ public class ChatPanel extends Panel { """, respondingContainer.getMarkupId(), messageContainer.getMarkupId())); handler.add(messageContainer); }); + var lastMessage = messagesView.get(messagesView.size() - 1); - handler.appendJavaScript(String.format(""" - $('#%s')[0].scrollIntoView({ block: "end" }); - """, lastMessage.getMarkupId())); + if ((Long) lastMessage.getDefaultModelObject() > prevLastMessageId) { + handler.appendJavaScript(String.format(""" + $('#%s')[0].scrollIntoView({ block: "end" }); + """, lastMessage.getMarkupId())); + } } private Component newMessageContainer(String containerId, ChatMessage message) { @@ -493,7 +569,7 @@ public class ChatPanel extends Panel { @Override protected void onConfigure() { super.onConfigure(); - setVisible(WebSession.get().isChatVisible()); + setVisible(WebSession.get().isChatVisible() && !getEntitledAis().isEmpty()); } @Override @@ -504,16 +580,25 @@ public class ChatPanel extends Panel { } @Override - public boolean isVisible() { - return WebSession.get().isChatVisible(); + protected void onDetach() { + super.onDetach(); + entitledAisModel.detach(); } - public void show(AjaxRequestTarget target) { - if (!isVisible()) { - WebSession.get().setChatVisible(true); - WebSession.get().setActiveChatId(null); - target.add(this); - } + public List getEntitledAis() { + return entitledAisModel.getObject(); + } + + public void show(AjaxRequestTarget target, @Nullable String prompt) { + WebSession.get().setChatVisible(true); + WebSession.get().setActiveChatId(null); + if (prompt != null) { + WebSession.get().setChatInput(prompt); + target.appendJavaScript(""" + $(".chat>.body>.send .submit").click(); + """); + } + target.add(this); } public void hide(AjaxRequestTarget target) { diff --git a/server-core/src/main/java/io/onedev/server/web/component/ai/chat/chat.css b/server-core/src/main/java/io/onedev/server/web/component/ai/chat/chat.css index 6e6a6c5a21..c026e1b488 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/ai/chat/chat.css +++ b/server-core/src/main/java/io/onedev/server/web/component/ai/chat/chat.css @@ -1,25 +1,20 @@ .chat { position: fixed; - top: calc(var(--topbar-height) + 1rem); + top: 0; bottom: 0; right: 0; - z-index: 1000; - border-radius: 0.42rem 0 0 0; + z-index: 2000; background: white; - box-shadow: 0 0 12px rgba(0,0,0,0.2), 0 -2px 8px rgba(0,0,0,0.1); -} -.chat>.head { - border-radius: 0.42rem 0 0 0; + box-shadow: 0 0 12px rgba(0,0,0,0.2); } .dark-mode .chat { background-color: var(--dark-mode-dark); - box-shadow: 0 0 20px rgb(0 0 0 / 50%), 0 -2px 15px rgb(0 0 0 / 30%); + box-shadow: 0 0 20px rgb(0 0 0 / 50%); } .chat .ui-resizable-handle { position: absolute; left: 0; - top: 0.42rem; bottom: 0; width: 3px; cursor: col-resize; diff --git a/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.html b/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.html index 916fee015a..ceb946a89a 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.html +++ b/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.html @@ -18,10 +18,11 @@ -
- - - + +
+ + +
\ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.java b/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.java index 46a07b4236..e3262ec02a 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/component/issue/activities/IssueActivitiesPanel.java @@ -39,7 +39,6 @@ import com.google.common.collect.Lists; import io.onedev.server.OneDev; import io.onedev.server.attachment.AttachmentSupport; import io.onedev.server.attachment.ProjectAttachmentSupport; -import io.onedev.server.service.IssueCommentService; import io.onedev.server.entityreference.ReferencedFromAware; import io.onedev.server.model.Issue; import io.onedev.server.model.IssueChange; @@ -53,6 +52,7 @@ import io.onedev.server.model.support.issue.changedata.IssueOwnSpentTimeChangeDa import io.onedev.server.model.support.issue.changedata.IssueTotalEstimatedTimeChangeData; import io.onedev.server.model.support.issue.changedata.IssueTotalSpentTimeChangeData; import io.onedev.server.security.SecurityUtils; +import io.onedev.server.service.IssueCommentService; import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener; import io.onedev.server.web.behavior.ChangeObserver; import io.onedev.server.web.component.comment.CommentInput; @@ -61,6 +61,7 @@ import io.onedev.server.web.component.issue.activities.activity.IssueChangeActiv import io.onedev.server.web.component.issue.activities.activity.IssueCommentActivity; import io.onedev.server.web.component.issue.activities.activity.IssueWorkActivity; import io.onedev.server.web.page.base.BasePage; +import io.onedev.server.web.page.layout.LayoutPage; import io.onedev.server.web.page.security.LoginPage; import io.onedev.server.web.util.WicketUtils; @@ -327,6 +328,23 @@ public abstract class IssueActivitiesPanel extends Panel { public Component renderOptions(String componentId) { Fragment fragment = new Fragment(componentId, "optionsFrag", this); + + fragment.add(new AjaxLink("aiSummarize") { + + @Override + public void onClick(AjaxRequestTarget target) { + var page = (LayoutPage)getPage(); + page.getChatter().show(target, "Summarize comments of current issue. Display summary in " + getSession().getLocale().getDisplayLanguage()); + } + + @Override + protected void onConfigure() { + super.onConfigure(); + var page = (LayoutPage)getPage(); + setVisible(!page.getChatter().getEntitledAis().isEmpty()); + } + }); + fragment.add(showCommentsLink = new AjaxLink("showComments") { @Override diff --git a/server-core/src/main/java/io/onedev/server/web/page/base/BasePage.java b/server-core/src/main/java/io/onedev/server/web/page/base/BasePage.java index 3a77cfb0c6..25c9970386 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/base/BasePage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/base/BasePage.java @@ -474,5 +474,5 @@ public abstract class BasePage extends WebPage { removeAutosaveKeys = new HashSet<>(); return removeAutosaveKeys; } - + } diff --git a/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java b/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java index 5c372cb53e..e6ffd5c881 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/layout/LayoutPage.java @@ -176,6 +176,8 @@ public abstract class LayoutPage extends BasePage { private AbstractDefaultAjaxBehavior newVersionStatusBehavior; + private ChatPanel chatter; + public LayoutPage(PageParameters params) { super(params); } @@ -811,20 +813,20 @@ public abstract class LayoutPage extends BasePage { }); - var chat = new ChatPanel("chat"); - add(chat); + chatter = new ChatPanel("chat"); + add(chatter); topbar.add(new AjaxLink("showChat") { @Override public void onClick(AjaxRequestTarget target) { - chat.show(target); + chatter.show(target, null); } @Override protected void onConfigure() { super.onConfigure(); - setVisible(getLoginUser() != null && !getLoginUser().getEntitledAis().isEmpty()); + setVisible(!chatter.getEntitledAis().isEmpty()); } }); @@ -1287,6 +1289,10 @@ public abstract class LayoutPage extends BasePage { response.render(OnLoadHeaderItem.forScript("onedev.server.layout.onLoad();")); } + public ChatPanel getChatter() { + return chatter; + } + protected List getSidebarMenus() { return Lists.newArrayList(); } diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/issues/detail/IssueDetailPage.java b/server-core/src/main/java/io/onedev/server/web/page/project/issues/detail/IssueDetailPage.java index dc38693bd8..7a7d23b541 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/issues/detail/IssueDetailPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/issues/detail/IssueDetailPage.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javax.inject.Inject; import javax.persistence.EntityNotFoundException; import javax.validation.ValidationException; @@ -31,14 +32,15 @@ import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.flow.RedirectToUrlException; import org.apache.wicket.request.mapper.parameter.PageParameters; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; -import io.onedev.server.OneDev; +import dev.langchain4j.agent.tool.ToolSpecification; +import io.onedev.server.ai.IssueHelper; import io.onedev.server.buildspecmodel.inputspec.InputContext; import io.onedev.server.data.migration.VersionedXmlDoc; -import io.onedev.server.service.IssueLinkService; -import io.onedev.server.service.IssueService; -import io.onedev.server.service.SettingService; import io.onedev.server.model.Issue; import io.onedev.server.model.Project; import io.onedev.server.model.support.issue.field.spec.FieldSpec; @@ -46,6 +48,10 @@ import io.onedev.server.search.entity.EntityQuery; import io.onedev.server.search.entity.issue.IssueQuery; import io.onedev.server.search.entity.issue.IssueQueryParseOption; import io.onedev.server.security.SecurityUtils; +import io.onedev.server.service.IssueLinkService; +import io.onedev.server.service.IssueService; +import io.onedev.server.service.SettingService; +import io.onedev.server.service.support.ChatTool; import io.onedev.server.util.ProjectScope; import io.onedev.server.web.WebSession; import io.onedev.server.web.behavior.ChangeObserver; @@ -65,18 +71,34 @@ import io.onedev.server.web.page.project.ProjectPage; import io.onedev.server.web.page.project.dashboard.ProjectDashboardPage; import io.onedev.server.web.page.project.issues.ProjectIssuesPage; import io.onedev.server.web.page.project.issues.list.ProjectIssueListPage; +import io.onedev.server.web.util.ChatToolAware; import io.onedev.server.web.util.ConfirmClickModifier; import io.onedev.server.web.util.Cursor; import io.onedev.server.web.util.CursorSupport; import io.onedev.server.xodus.VisitInfoService; -public abstract class IssueDetailPage extends ProjectIssuesPage implements InputContext { +public abstract class IssueDetailPage extends ProjectIssuesPage implements InputContext, ChatToolAware { public static final String PARAM_ISSUE = "issue"; private static final String KEY_SCROLL_TOP = "onedev.issue.scrollTop"; protected final IModel issueModel; + + @Inject + private ObjectMapper objectMapper; + + @Inject + private IssueService issueService; + + @Inject + private IssueLinkService issueLinkService; + + @Inject + private VisitInfoService visitInfoService; + + @Inject + private SettingService settingService; public IssueDetailPage(PageParameters params) { super(params); @@ -98,11 +120,11 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input throw new ValidationException(MessageFormat.format(_T("Invalid issue number: {0}"), issueNumberString)); } - Issue issue = getIssueService().find(getProject(), issueNumber); + Issue issue = issueService.find(getProject(), issueNumber); if (issue == null) { throw new EntityNotFoundException(MessageFormat.format(_T("Unable to find issue #{0} in project {1}"), issueNumber, getProject())); } else { - OneDev.getInstance(IssueLinkService.class).loadDeepLinks(issue); + issueLinkService.loadDeepLinks(issue); if (!issue.getProject().equals(getProject())) throw new RestartResponseException(getPageClass(), paramsOf(issue)); return issue; @@ -293,7 +315,7 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input @Override public void onClick() { - getIssueService().delete(getIssue()); + issueService.delete(getIssue()); var oldAuditContent = VersionedXmlDoc.fromBean(getIssue()).toXML(); auditService.audit(getIssue().getProject(), "deleted issue \"" + getIssue().getReference().toString(getIssue().getProject()) + "\"", oldAuditContent, null); @@ -331,7 +353,7 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input @Override protected List query(EntityQuery query, int offset, int count, ProjectScope projectScope) { - return getIssueService().query(SecurityUtils.getSubject(), projectScope, query, false, offset, count); + return issueService.query(SecurityUtils.getSubject(), projectScope, query, false, offset, count); } @Override @@ -362,12 +384,58 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input @Override public void onEndRequest(RequestCycle cycle) { if (SecurityUtils.getAuthUser() != null) - OneDev.getInstance(VisitInfoService.class).visitIssue(SecurityUtils.getAuthUser(), getIssue()); + visitInfoService.visitIssue(SecurityUtils.getAuthUser(), getIssue()); } }); } + + @Override + public List getChatTools() { + var issueId = getIssue().getId(); + return List.of(new ChatTool() { + + @Override + public ToolSpecification getSpecification() { + return ToolSpecification.builder() + .name("getCurrentIssue") + .description("Get info of current issue in json format") + .build(); + } + + @Override + public String execute(JsonNode arguments) { + var issue = issueService.load(issueId); + try { + return objectMapper.writeValueAsString(IssueHelper.getDetail(issue.getProject(), issue)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + }, new ChatTool() { + + @Override + public ToolSpecification getSpecification() { + return ToolSpecification.builder() + .name("getCurrentIssueComments") + .description("Get comments of current issue in json format") + .build(); + } + + @Override + public String execute(JsonNode arguments) { + var issue = issueService.load(issueId); + try { + return objectMapper.writeValueAsString(IssueHelper.getComments(issue)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + }); + } @Override protected void onDetach() { @@ -392,13 +460,9 @@ public abstract class IssueDetailPage extends ProjectIssuesPage implements Input @Override public FieldSpec getInputSpec(String inputName) { - return OneDev.getInstance(SettingService.class).getIssueSetting().getFieldSpec(inputName); + return settingService.getIssueSetting().getFieldSpec(inputName); } - - private IssueService getIssueService() { - return OneDev.getInstance(IssueService.class); - } - + @Override protected Component newProjectTitle(String componentId) { Fragment fragment = new Fragment(componentId, "projectTitleFrag", this); diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java index 877d5ba951..7ce3774f32 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/PullRequestDetailPage.java @@ -17,9 +17,11 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import javax.inject.Inject; import javax.persistence.EntityNotFoundException; import javax.validation.ValidationException; @@ -64,21 +66,16 @@ import org.apache.wicket.request.mapper.parameter.PageParameters; import org.eclipse.jgit.lib.ObjectId; import org.jetbrains.annotations.Nullable; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; -import io.onedev.server.OneDev; +import dev.langchain4j.agent.tool.ToolSpecification; +import io.onedev.server.ai.PullRequestHelper; import io.onedev.server.attachment.AttachmentSupport; import io.onedev.server.attachment.ProjectAttachmentSupport; import io.onedev.server.data.migration.VersionedXmlDoc; -import io.onedev.server.service.PullRequestAssignmentService; -import io.onedev.server.service.PullRequestChangeService; -import io.onedev.server.service.PullRequestLabelService; -import io.onedev.server.service.PullRequestService; -import io.onedev.server.service.PullRequestReactionService; -import io.onedev.server.service.PullRequestReviewService; -import io.onedev.server.service.PullRequestWatchService; -import io.onedev.server.service.SettingService; -import io.onedev.server.service.UserService; import io.onedev.server.entityreference.EntityReference; import io.onedev.server.entityreference.LinkTransformer; import io.onedev.server.git.GitUtils; @@ -106,6 +103,16 @@ import io.onedev.server.search.entity.pullrequest.PullRequestQuery; import io.onedev.server.security.SecurityUtils; import io.onedev.server.security.permission.ProjectPermission; import io.onedev.server.security.permission.ReadCode; +import io.onedev.server.service.PullRequestAssignmentService; +import io.onedev.server.service.PullRequestChangeService; +import io.onedev.server.service.PullRequestLabelService; +import io.onedev.server.service.PullRequestReactionService; +import io.onedev.server.service.PullRequestReviewService; +import io.onedev.server.service.PullRequestService; +import io.onedev.server.service.PullRequestWatchService; +import io.onedev.server.service.SettingService; +import io.onedev.server.service.UserService; +import io.onedev.server.service.support.ChatTool; import io.onedev.server.util.DateUtils; import io.onedev.server.util.ProjectScope; import io.onedev.server.web.WebSession; @@ -159,6 +166,7 @@ import io.onedev.server.web.page.project.pullrequests.detail.changes.PullRequest import io.onedev.server.web.page.project.pullrequests.detail.codecomments.PullRequestCodeCommentsPage; import io.onedev.server.web.page.project.pullrequests.detail.operationdlg.MergePullRequestOptionPanel; import io.onedev.server.web.page.project.pullrequests.detail.operationdlg.OperationCommentPanel; +import io.onedev.server.web.util.ChatToolAware; import io.onedev.server.web.util.ConfirmClickModifier; import io.onedev.server.web.util.Cursor; import io.onedev.server.web.util.CursorSupport; @@ -169,7 +177,7 @@ import io.onedev.server.web.util.editbean.CommitMessageBean; import io.onedev.server.web.util.editbean.LabelsBean; import io.onedev.server.xodus.VisitInfoService; -public abstract class PullRequestDetailPage extends ProjectPage implements PullRequestAware { +public abstract class PullRequestDetailPage extends ProjectPage implements PullRequestAware, ChatToolAware { public static final String PARAM_REQUEST = "request"; @@ -185,6 +193,45 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR private MergeStrategy mergeStrategy; + @Inject + private ObjectMapper objectMapper; + + @Inject + private PullRequestService pullRequestService; + + @Inject + private GitService gitService; + + @Inject + private PullRequestReviewService pullRequestReviewService; + + @Inject + private PullRequestChangeService pullRequestChangeService; + + @Inject + private PullRequestAssignmentService pullRequestAssignmentService; + + @Inject + private PullRequestLabelService pullRequestLabelService; + + @Inject + private PullRequestWatchService pullRequestWatchService; + + @Inject + private PullRequestReactionService pullRequestReactionService; + + @Inject + private UserService userService; + + @Inject + private SettingService settingService; + + @Inject + private VisitInfoService visitInfoService; + + @Inject + private Set summaryContributions; + public PullRequestDetailPage(PageParameters params) { super(params); @@ -205,7 +252,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR throw new ValidationException(MessageFormat.format(_T("Invalid pull request number: {0}"), requestNumberString)); } - PullRequest request = getPullRequestService().find(getProject(), requestNumber); + PullRequest request = pullRequestService.find(getProject(), requestNumber); if (request == null) { throw new EntityNotFoundException(MessageFormat.format(_T("Unable to find pull request #{0} in project {1}"), requestNumber, getProject().getPath())); } else if (!request.getTargetProject().equals(getProject())) { @@ -225,22 +272,6 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR latestUpdateId = requestModel.getObject().getLatestUpdate().getId(); } - private GitService getGitService() { - return OneDev.getInstance(GitService.class); - } - - private PullRequestService getPullRequestService() { - return OneDev.getInstance(PullRequestService.class); - } - - private PullRequestReviewService getPullRequestReviewService() { - return OneDev.getInstance(PullRequestReviewService.class); - } - - private PullRequestChangeService getPullRequestChangeService() { - return OneDev.getInstance(PullRequestChangeService.class); - } - private WebMarkupContainer newRequestHead() { WebMarkupContainer requestHead = new WebMarkupContainer("requestHeader"); requestHead.setOutputMarkupId(true); @@ -340,7 +371,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR super.onSubmit(target, form); var user = SecurityUtils.getUser(); - OneDev.getInstance(PullRequestChangeService.class).changeTitle(user, getPullRequest(), title); + pullRequestChangeService.changeTitle(user, getPullRequest(), title); notifyPullRequestChange(target); isEditingTitle = false; @@ -919,7 +950,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override public void onEndRequest(RequestCycle cycle) { if (SecurityUtils.getAuthUser() != null) - OneDev.getInstance(VisitInfoService.class).visitPullRequest(SecurityUtils.getAuthUser(), getPullRequest()); + visitInfoService.visitPullRequest(SecurityUtils.getAuthUser(), getPullRequest()); } }); @@ -980,7 +1011,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override public void onClick(AjaxRequestTarget target) { var user = SecurityUtils.getUser(); - OneDev.getInstance(PullRequestChangeService.class).changeTargetBranch(user, getPullRequest(), branch); + pullRequestChangeService.changeTargetBranch(user, getPullRequest(), branch); notifyPullRequestChange(target); dropdown.close(); } @@ -1087,7 +1118,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR var assignment = new PullRequestAssignment(); assignment.setRequest(getPullRequest()); assignment.setUser(SecurityUtils.getUser()); - OneDev.getInstance(PullRequestAssignmentService.class).create(assignment); + pullRequestAssignmentService.create(assignment); ((BasePage)getPage()).notifyObservableChange(target, PullRequest.getChangeObservable(getPullRequest().getId())); } @@ -1137,7 +1168,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected void onUpdated(IPartialPageRequestHandler handler, Serializable bean, String propertyName) { LabelsBean labelsBean = (LabelsBean) bean; - OneDev.getInstance(PullRequestLabelService.class).sync(getPullRequest(), labelsBean.getLabels()); + pullRequestLabelService.sync(getPullRequest(), labelsBean.getLabels()); handler.add(labelsContainer); } @@ -1184,12 +1215,12 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected void onSaveWatch(EntityWatch watch) { - OneDev.getInstance(PullRequestWatchService.class).createOrUpdate((PullRequestWatch) watch); + pullRequestWatchService.createOrUpdate((PullRequestWatch) watch); } @Override protected void onDeleteWatch(EntityWatch watch) { - OneDev.getInstance(PullRequestWatchService.class).delete((PullRequestWatch) watch); + pullRequestWatchService.delete((PullRequestWatch) watch); } @Override @@ -1217,7 +1248,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override public void onClick(AjaxRequestTarget target) { - getPullRequestService().checkAsync(getPullRequest(), false, true); + pullRequestService.checkAsync(getPullRequest(), false, true); Session.get().success(_T("Pull request synchronization submitted")); } @@ -1227,7 +1258,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override public void onClick() { PullRequest request = getPullRequest(); - getPullRequestService().delete(request); + pullRequestService.delete(request); var oldAuditContent = VersionedXmlDoc.fromBean(request).toXML(); auditService.audit(request.getProject(), "deleted pull request \"" + request.getReference().toString(request.getProject()) + "\"", oldAuditContent, null); @@ -1278,7 +1309,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected List query(EntityQuery query, int offset, int count, ProjectScope projectScope) { - return getPullRequestService().query(SecurityUtils.getSubject(), projectScope!=null?projectScope.getProject():null, query, false, offset, count); + return pullRequestService.query(SecurityUtils.getSubject(), projectScope!=null?projectScope.getProject():null, query, false, offset, count); } @Override @@ -1347,7 +1378,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected void onUpdate(AjaxRequestTarget target) { var user = SecurityUtils.getUser(); - OneDev.getInstance(PullRequestChangeService.class).changeMergeStrategy(user, getPullRequest(), mergeStrategy); + pullRequestChangeService.changeMergeStrategy(user, getPullRequest(), mergeStrategy); notifyPullRequestChange(target); } @@ -1420,7 +1451,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String onSave(AjaxRequestTarget target, CommitMessageBean bean) { var request = getPullRequest(); - var system = OneDev.getInstance(UserService.class).getSystem(); + var system = userService.getSystem(); var branchProtection = getProject().getBranchProtection(request.getTargetBranch(), system); var errorMessage = branchProtection.checkCommitMessage(bean.getCommitMessage(), request.getMergeStrategy() != SQUASH_SOURCE_BRANCH_COMMITS); @@ -1429,10 +1460,10 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR } else { autoMerge.setCommitMessage(bean.getCommitMessage()); var user = SecurityUtils.getUser(); - getPullRequestChangeService().changeAutoMerge(user, request, autoMerge); + pullRequestChangeService.changeAutoMerge(user, request, autoMerge); Session.get().success(_T("Preset commit message updated")); close(); - getPullRequestService().checkAutoMerge(request); + pullRequestService.checkAutoMerge(request); return null; } } @@ -1530,7 +1561,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR var autoMerge = new AutoMerge(); autoMerge.setEnabled(true); autoMerge.setCommitMessage(bean.getCommitMessage()); - getPullRequestChangeService().changeAutoMerge(user, getPullRequest(), autoMerge); + pullRequestChangeService.changeAutoMerge(user, getPullRequest(), autoMerge); target.add(autoMergeContainer); close(); return null; @@ -1552,13 +1583,13 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR var autoMerge = new AutoMerge(); autoMerge.setEnabled(true); autoMerge.setCommitMessage(request.getAutoMerge().getCommitMessage()); - getPullRequestChangeService().changeAutoMerge(user, request, autoMerge); + pullRequestChangeService.changeAutoMerge(user, request, autoMerge); target.add(autoMergeContainer); } } else { var autoMerge = new AutoMerge(); autoMerge.setCommitMessage(request.getAutoMerge().getCommitMessage()); - getPullRequestChangeService().changeAutoMerge(user, request, autoMerge); + pullRequestChangeService.changeAutoMerge(user, request, autoMerge); target.add(autoMergeContainer); } } @@ -1683,7 +1714,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected void onSaveComment(AjaxRequestTarget target, String comment) { var user = SecurityUtils.getUser(); - OneDev.getInstance(PullRequestChangeService.class).changeDescription(user, getPullRequest(), comment); + pullRequestChangeService.changeDescription(user, getPullRequest(), comment); ((BasePage)getPage()).notifyObservableChange(target, PullRequest.getChangeObservable(getPullRequest().getId())); } @@ -1753,10 +1784,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override public void onToggleEmoji(AjaxRequestTarget target, String emoji) { - OneDev.getInstance(PullRequestReactionService.class).toggleEmoji( - SecurityUtils.getUser(), - getPullRequest(), - emoji); + pullRequestReactionService.toggleEmoji(SecurityUtils.getUser(), getPullRequest(), emoji); } }; } @@ -1799,8 +1827,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected List load() { - List contributions = - new ArrayList<>(OneDev.getExtensions(PullRequestSummaryContribution.class)); + List contributions = new ArrayList<>(summaryContributions); contributions.sort(Comparator.comparing(PullRequestSummaryContribution::getOrder)); List parts = new ArrayList<>(); @@ -1889,7 +1916,6 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR return errorMessage; if (targetHead.equals(request.getTarget().getObjectId()) && sourceHead.equals(request.getSourceHead())) { - var gitService = OneDev.getInstance(GitService.class); var amendedCommitId = gitService.amendCommit(request.getProject(), mergeCommit.copy(), mergeCommit.getAuthorIdent(), mergeCommit.getCommitterIdent(), bean.getCommitMessage()); @@ -2001,7 +2027,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String operate(AjaxRequestTarget target) { if (canOperate()) { - getPullRequestReviewService().review(SecurityUtils.getUser(), getPullRequest(), true, getComment()); + pullRequestReviewService.review(SecurityUtils.getUser(), getPullRequest(), true, getComment()); notifyPullRequestChange(target); Session.get().success(_T("Approved")); return null; @@ -2044,7 +2070,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String operate(AjaxRequestTarget target) { if (canOperate()) { - getPullRequestReviewService().review(SecurityUtils.getUser(), getPullRequest(), false, getComment()); + pullRequestReviewService.review(SecurityUtils.getUser(), getPullRequest(), false, getComment()); notifyPullRequestChange(target); Session.get().success(_T("Requested For changes")); return null; @@ -2091,7 +2117,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR if (errorMessage != null) return errorMessage; } - getPullRequestService().merge(SecurityUtils.getUser(), getPullRequest(), commitMessage); + pullRequestService.merge(SecurityUtils.getUser(), getPullRequest(), commitMessage); notifyPullRequestChange(target); return null; } else { @@ -2123,7 +2149,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String operate(AjaxRequestTarget target) { if (canOperate()) { - getPullRequestService().discard(SecurityUtils.getUser(), getPullRequest(), getComment()); + pullRequestService.discard(SecurityUtils.getUser(), getPullRequest(), getComment()); notifyPullRequestChange(target); return null; } else { @@ -2160,7 +2186,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String operate(AjaxRequestTarget target) { if (canOperate()) { - getPullRequestService().reopen(SecurityUtils.getUser(), getPullRequest(), getComment()); + pullRequestService.reopen(SecurityUtils.getUser(), getPullRequest(), getComment()); notifyPullRequestChange(target); return null; } else { @@ -2199,7 +2225,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String operate(AjaxRequestTarget target) { if (canOperate()) { - getPullRequestService().deleteSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment()); + pullRequestService.deleteSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment()); notifyPullRequestChange(target); Session.get().success(_T("Deleted source branch")); return null; @@ -2239,7 +2265,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR @Override protected String operate(AjaxRequestTarget target) { if (canOperate()) { - getPullRequestService().restoreSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment()); + pullRequestService.restoreSourceBranch(SecurityUtils.getUser(), getPullRequest(), getComment()); notifyPullRequestChange(target); Session.get().success(_T("Restored source branch")); return null; @@ -2274,7 +2300,6 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR ObjectId targetHead = request.getTarget().getObjectId(); ObjectId mergeCommitId; - var gitService = getGitService(); if (updateByMerge) { String commitMessage; if (!request.getSourceProject().equals(project)) { @@ -2300,8 +2325,7 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR && !project.hasValidCommitSignature(project.getRevCommit(targetHead, true))) { return _T("No valid signature for head commit of target branch"); } - if (protection.isCommitSignatureRequired() - && OneDev.getInstance(SettingService.class).getGpgSetting().getSigningKey() == null) { + if (protection.isCommitSignatureRequired() && settingService.getGpgSetting().getSigningKey() == null) { return _T("Commit signature required but no GPG signing key specified"); } var error = gitService.checkCommitMessages(protection, project, sourceHead, mergeCommitId, new HashMap<>()); @@ -2313,11 +2337,10 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR private void updateSourceBranch(ObjectId commitId) { var request = getPullRequest(); - var gitService = getGitService(); if (!request.getSourceProject().equals(request.getTargetProject())) gitService.fetch(request.getSourceProject(), request.getTargetProject(), commitId.name()); var oldCommitId = request.getSourceHead(); - getGitService().updateRef(request.getSourceProject(), request.getSourceRef(), commitId, oldCommitId); + gitService.updateRef(request.getSourceProject(), request.getSourceRef(), commitId, oldCommitId); } @Override @@ -2326,6 +2349,52 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR super.onDetach(); } + @Override + public List getChatTools() { + var pullRequestId = getPullRequest().getId(); + return List.of(new ChatTool() { + + @Override + public ToolSpecification getSpecification() { + return ToolSpecification.builder() + .name("getCurrentPullRequest") + .description("Get info of current pull request in json format") + .build(); + } + + @Override + public String execute(JsonNode arguments) { + var pullRequest = pullRequestService.load(pullRequestId); + try { + return objectMapper.writeValueAsString(PullRequestHelper.getDetail(pullRequest.getProject(), pullRequest)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + }, new ChatTool() { + + @Override + public ToolSpecification getSpecification() { + return ToolSpecification.builder() + .name("getCurrentPullRequestComments") + .description("Get comments of current pull request in json format") + .build(); + } + + @Override + public String execute(JsonNode arguments) { + var pullRequest = pullRequestService.load(pullRequestId); + try { + return objectMapper.writeValueAsString(PullRequestHelper.getComments(pullRequest)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + }); + } + public static PageParameters paramsOf(PullRequest pullRequest) { return paramsOf(pullRequest.getProject(), pullRequest.getNumber()); } diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.html b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.html index c1d3a7ed45..ce23db8ccd 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.html +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.html @@ -22,10 +22,11 @@
-
- - - + +
+ + +
\ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.java b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.java index b145232cff..7ea8e101ba 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/detail/activities/PullRequestActivitiesPage.java @@ -61,6 +61,7 @@ import io.onedev.server.web.ajaxlistener.ConfirmLeaveListener; import io.onedev.server.web.behavior.ChangeObserver; import io.onedev.server.web.component.comment.CommentInput; import io.onedev.server.web.page.base.BasePage; +import io.onedev.server.web.page.layout.LayoutPage; import io.onedev.server.web.page.project.pullrequests.detail.PullRequestDetailPage; import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestChangeActivity; import io.onedev.server.web.page.project.pullrequests.detail.activities.activity.PullRequestCommentActivity; @@ -409,6 +410,24 @@ public class PullRequestActivitiesPage extends PullRequestDetailPage { public Component renderOptions(String componentId) { Fragment fragment = new Fragment(componentId, "optionsFrag", this); + + fragment.add(new AjaxLink("aiSummarize") { + + @Override + public void onClick(AjaxRequestTarget target) { + var page = (LayoutPage)getPage(); + page.getChatter().show(target, "Summarize comments of current pull request. Display summary in " + getSession().getLocale().getDisplayLanguage()); + } + + @Override + protected void onConfigure() { + super.onConfigure(); + var page = (LayoutPage)getPage(); + setVisible(!page.getChatter().getEntitledAis().isEmpty()); + } + + }); + fragment.add(showCommentsLink = new AjaxLink("showComments") { @Override diff --git a/server-core/src/main/java/io/onedev/server/web/util/ChatToolAware.java b/server-core/src/main/java/io/onedev/server/web/util/ChatToolAware.java new file mode 100644 index 0000000000..f24186569b --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/web/util/ChatToolAware.java @@ -0,0 +1,11 @@ +package io.onedev.server.web.util; + +import java.util.List; + +import io.onedev.server.service.support.ChatTool; + +public interface ChatToolAware { + + List getChatTools(); + +}