diff --git a/.gitignore b/.gitignore
index 1885cccd64..1af13a9b9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
server-product/docker/build/
server-product/docker/onedev-*/
**/target/
+**/bin/
**/.classpath
**/.gitignore
**/.settings
diff --git a/pom.xml b/pom.xml
index 6746367289..d9d0872c2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -627,6 +627,11 @@
fastexcel0.15.7
+
+ dev.langchain4j
+ langchain4j-open-ai
+ ${langchain4j.version}
+
@@ -663,5 +668,6 @@
3.1.02.15.01.28.3
+ 1.8.0
diff --git a/server-core/pom.xml b/server-core/pom.xml
index f4dfe35097..d8a3d81678 100644
--- a/server-core/pom.xml
+++ b/server-core/pom.xml
@@ -394,6 +394,10 @@
kotlin-stdlib-jdk7${kotlin.version}
+
+ dev.langchain4j
+ langchain4j-open-ai
+ 1.9.23
diff --git a/server-core/src/main/java/io/onedev/server/CoreModule.java b/server-core/src/main/java/io/onedev/server/CoreModule.java
index 1b7313561c..8071e254f8 100644
--- a/server-core/src/main/java/io/onedev/server/CoreModule.java
+++ b/server-core/src/main/java/io/onedev/server/CoreModule.java
@@ -75,6 +75,7 @@ import io.onedev.commons.utils.ExceptionUtils;
import io.onedev.commons.utils.StringUtils;
import io.onedev.k8shelper.KubernetesHelper;
import io.onedev.k8shelper.OsInfo;
+import io.onedev.server.ai.McpHelperResource;
import io.onedev.server.annotation.Shallow;
import io.onedev.server.attachment.AttachmentService;
import io.onedev.server.attachment.DefaultAttachmentService;
@@ -144,11 +145,11 @@ import io.onedev.server.persistence.HibernateInterceptor;
import io.onedev.server.persistence.IdService;
import io.onedev.server.persistence.PersistListener;
import io.onedev.server.persistence.PrefixedNamingStrategy;
-import io.onedev.server.persistence.SessionFactoryService;
import io.onedev.server.persistence.SessionFactoryProvider;
+import io.onedev.server.persistence.SessionFactoryService;
import io.onedev.server.persistence.SessionInterceptor;
-import io.onedev.server.persistence.SessionService;
import io.onedev.server.persistence.SessionProvider;
+import io.onedev.server.persistence.SessionService;
import io.onedev.server.persistence.TransactionInterceptor;
import io.onedev.server.persistence.TransactionService;
import io.onedev.server.persistence.annotation.Sessional;
@@ -160,7 +161,6 @@ import io.onedev.server.rest.DefaultServletContainer;
import io.onedev.server.rest.JerseyConfigurator;
import io.onedev.server.rest.ResourceConfigProvider;
import io.onedev.server.rest.WebApplicationExceptionHandler;
-import io.onedev.server.rest.resource.McpHelperResource;
import io.onedev.server.rest.resource.ProjectResource;
import io.onedev.server.search.code.CodeIndexService;
import io.onedev.server.search.code.CodeSearchService;
@@ -197,10 +197,10 @@ import io.onedev.server.service.BuildMetricService;
import io.onedev.server.service.BuildParamService;
import io.onedev.server.service.BuildQueryPersonalizationService;
import io.onedev.server.service.BuildService;
-import io.onedev.server.service.CodeCommentService;
import io.onedev.server.service.CodeCommentMentionService;
import io.onedev.server.service.CodeCommentQueryPersonalizationService;
import io.onedev.server.service.CodeCommentReplyService;
+import io.onedev.server.service.CodeCommentService;
import io.onedev.server.service.CodeCommentStatusChangeService;
import io.onedev.server.service.CodeCommentTouchService;
import io.onedev.server.service.CommitQueryPersonalizationService;
@@ -215,9 +215,9 @@ import io.onedev.server.service.GroupAuthorizationService;
import io.onedev.server.service.GroupService;
import io.onedev.server.service.IssueAuthorizationService;
import io.onedev.server.service.IssueChangeService;
-import io.onedev.server.service.IssueCommentService;
import io.onedev.server.service.IssueCommentReactionService;
import io.onedev.server.service.IssueCommentRevisionService;
+import io.onedev.server.service.IssueCommentService;
import io.onedev.server.service.IssueDescriptionRevisionService;
import io.onedev.server.service.IssueFieldService;
import io.onedev.server.service.IssueLinkService;
@@ -248,9 +248,9 @@ import io.onedev.server.service.ProjectLastEventDateService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.PullRequestAssignmentService;
import io.onedev.server.service.PullRequestChangeService;
-import io.onedev.server.service.PullRequestCommentService;
import io.onedev.server.service.PullRequestCommentReactionService;
import io.onedev.server.service.PullRequestCommentRevisionService;
+import io.onedev.server.service.PullRequestCommentService;
import io.onedev.server.service.PullRequestDescriptionRevisionService;
import io.onedev.server.service.PullRequestLabelService;
import io.onedev.server.service.PullRequestMentionService;
@@ -285,10 +285,10 @@ import io.onedev.server.service.impl.DefaultBuildMetricService;
import io.onedev.server.service.impl.DefaultBuildParamService;
import io.onedev.server.service.impl.DefaultBuildQueryPersonalizationService;
import io.onedev.server.service.impl.DefaultBuildService;
-import io.onedev.server.service.impl.DefaultCodeCommentService;
import io.onedev.server.service.impl.DefaultCodeCommentMentionService;
import io.onedev.server.service.impl.DefaultCodeCommentQueryPersonalizationService;
import io.onedev.server.service.impl.DefaultCodeCommentReplyService;
+import io.onedev.server.service.impl.DefaultCodeCommentService;
import io.onedev.server.service.impl.DefaultCodeCommentStatusChangeService;
import io.onedev.server.service.impl.DefaultCodeCommentTouchService;
import io.onedev.server.service.impl.DefaultCommitQueryPersonalizationService;
@@ -303,9 +303,9 @@ import io.onedev.server.service.impl.DefaultGroupAuthorizationService;
import io.onedev.server.service.impl.DefaultGroupService;
import io.onedev.server.service.impl.DefaultIssueAuthorizationService;
import io.onedev.server.service.impl.DefaultIssueChangeService;
-import io.onedev.server.service.impl.DefaultIssueCommentService;
import io.onedev.server.service.impl.DefaultIssueCommentReactionService;
import io.onedev.server.service.impl.DefaultIssueCommentRevisionService;
+import io.onedev.server.service.impl.DefaultIssueCommentService;
import io.onedev.server.service.impl.DefaultIssueDescriptionRevisionService;
import io.onedev.server.service.impl.DefaultIssueFieldService;
import io.onedev.server.service.impl.DefaultIssueLinkService;
@@ -336,9 +336,9 @@ import io.onedev.server.service.impl.DefaultProjectLastEventDateService;
import io.onedev.server.service.impl.DefaultProjectService;
import io.onedev.server.service.impl.DefaultPullRequestAssignmentService;
import io.onedev.server.service.impl.DefaultPullRequestChangeService;
-import io.onedev.server.service.impl.DefaultPullRequestCommentService;
import io.onedev.server.service.impl.DefaultPullRequestCommentReactionService;
import io.onedev.server.service.impl.DefaultPullRequestCommentRevisionService;
+import io.onedev.server.service.impl.DefaultPullRequestCommentService;
import io.onedev.server.service.impl.DefaultPullRequestDescriptionRevisionService;
import io.onedev.server.service.impl.DefaultPullRequestLabelService;
import io.onedev.server.service.impl.DefaultPullRequestMentionService;
diff --git a/server-core/src/main/java/io/onedev/server/rest/resource/McpHelperResource.java b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java
similarity index 81%
rename from server-core/src/main/java/io/onedev/server/rest/resource/McpHelperResource.java
rename to server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java
index f399e94d9e..91064ae14f 100644
--- a/server-core/src/main/java/io/onedev/server/rest/resource/McpHelperResource.java
+++ b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java
@@ -1,6 +1,8 @@
-package io.onedev.server.rest.resource;
+package io.onedev.server.ai;
+import static io.onedev.server.ai.QueryDescriptions.getBuildQueryDescription;
import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE;
+import static org.unbescape.html.HtmlEscape.escapeHtml5;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@@ -16,7 +18,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
-import org.jspecify.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.persistence.EntityNotFoundException;
@@ -37,7 +38,7 @@ import javax.ws.rs.core.Response;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.eclipse.jgit.lib.ObjectId;
-import org.unbescape.html.HtmlEscape;
+import org.jspecify.annotations.Nullable;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -165,8 +166,6 @@ public class McpHelperResource {
private final BuildService buildService;
- private final BuildParamService buildParamService;
-
private final JobService jobService;
private final GitService gitService;
@@ -215,243 +214,11 @@ public class McpHelperResource {
this.urlService = urlService;
this.pullRequestCommentService = pullRequestCommentService;
this.buildService = buildService;
- this.buildParamService = buildParamService;
this.jobService = jobService;
this.validator = validator;
this.serverConfig = serverConfig;
}
- private String getIssueQueryStringDescription() {
- var stateNames = new StringBuilder();
- for (var state: settingService.getIssueSetting().getStateSpecs()) {
- stateNames.append(" - ");
- stateNames.append(state.getName());
- if (state.getDescription() != null) {
- stateNames.append(": ").append(state.getDescription().replace("\n", " "));
- }
- stateNames.append("\n");
- }
- var fieldCriterias = new StringBuilder();
- for (var field: settingService.getIssueSetting().getFieldSpecs()) {
- if (field instanceof ChoiceField) {
- var choiceField = (ChoiceField) field;
- fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
- + field.getName() + "\" is \"<" + field.getName().toLowerCase()
- + " value>\" (quotes are required), where <" + field.getName().toLowerCase()
- + " value> is one of below:\n");
- for (var choice : choiceField.getPossibleValues())
- fieldCriterias.append(" - " + choice).append("\n");
- } else if (field instanceof UserChoiceField) {
- fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
- + field.getName() + "\" is \"\" (quotes are required)\n");
- fieldCriterias.append(
- "- " + field.getName().toLowerCase() + " criteria for current user in form of: \""
- + field.getName() + "\" is me (quotes are required)\n");
- } else if (field instanceof GroupChoiceField) {
- fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
- + field.getName() + "\" is \"\" (quotes are required)\n");
- } else if (field instanceof BooleanField) {
- fieldCriterias.append("- " + field.getName().toLowerCase() + " is true criteria in form of: \""
- + field.getName() + "\" is \"true\" (quotes are required)\n");
- fieldCriterias.append("- " + field.getName().toLowerCase() + " is false criteria in form of: \""
- + field.getName() + "\" is \"false\" (quotes are required)\n");
- } else if (field instanceof DateField) {
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is before certain date criteria in form of: \"" + field.getName()
- + "\" is before \"\" (quotes are required), where is of format YYYY-MM-DD\n");
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is after certain date criteria in form of: \"" + field.getName()
- + "\" is after \"\" (quotes are required), where is of format YYYY-MM-DD\n");
- } else if (field instanceof DateTimeField) {
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is before certain date time criteria in form of: \"" + field.getName()
- + "\" is before \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n");
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is after certain date time criteria in form of: \"" + field.getName()
- + "\" is after \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n");
- } else if (field instanceof IntegerField) {
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is equal to certain integer criteria in form of: \"" + field.getName()
- + "\" is \"\" (quotes are required), where is an integer\n");
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is greater than certain integer criteria in form of: \"" + field.getName()
- + "\" is greater than \"\" (quotes are required), where is an integer\n");
- fieldCriterias.append("- " + field.getName().toLowerCase()
- + " is less than certain integer criteria in form of: \"" + field.getName()
- + "\" is less than \"\" (quotes are required), where is an integer\n");
- }
- fieldCriterias.append("- " + field.getName().toLowerCase() + " is not set criteria in form of: \""
- + field.getName() + "\" is empty (quotes are required)\n");
- }
- var linkCriterias = new StringBuilder();
- for (var linkSpec: linkSpecService.query()) {
- linkCriterias.append("- criteria to list issues with any " + linkSpec.getName().toLowerCase()
- + " issues matching certain criteria in form of: any \"" + linkSpec.getName()
- + "\" matching(another criteria) (quotes are required)\n");
- linkCriterias.append("- criteria to list issues with all " + linkSpec.getName().toLowerCase()
- + " issues matching certain criteria in form of: all \"" + linkSpec.getName()
- + "\" matching(another criteria) (quotes are required)\n");
- linkCriterias.append("- criteria to list issues with some " + linkSpec.getName().toLowerCase()
- + " issues in form of: has any \"" + linkSpec.getName() + "\" (quotes are required)\n");
- if (linkSpec.getOpposite() != null) {
- linkCriterias.append("- criteria to list issues with any "
- + linkSpec.getOpposite().getName().toLowerCase()
- + " issues matching certain criteria in form of: any \"" + linkSpec.getOpposite().getName()
- + "\" matching(another criteria) (quotes are required)\n");
- linkCriterias.append("- criteria to list issues with all "
- + linkSpec.getOpposite().getName().toLowerCase()
- + " issues matching certain criteria in form of: all \"" + linkSpec.getOpposite().getName()
- + "\" matching(another criteria) (quotes are required)\n");
- linkCriterias.append("- criteria to list issues with some " + linkSpec.getOpposite().getName().toLowerCase()
- + " issues in form of: has any \"" + linkSpec.getOpposite().getName() + "\" (quotes are required)\n");
- }
- }
- var orderFields = new StringBuilder();
- for (var field: Issue.SORT_FIELDS.keySet()) {
- orderFields.append("- ").append(field).append("\n");
- }
-
- var description =
- "A query string is one of below criteria:\n" +
- "- issue with specified number in form of: \"Number\" is \"#\", or in form of: \"Number\" is \"-\" (quotes are required)\n" +
- "- criteria to check if title/description/comment contains specified text in form of: ~~\n" +
- "- state criteria in form of: \"State\" is \"\" (quotes are required), where is one of below:\n" +
- stateNames +
- fieldCriterias +
- linkCriterias +
- "- submitted by specified user criteria in form of: submitted by \"\" (quotes are required)\n" +
- "- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
- "- submitted before certain date criteria in form of: \"Submit Date\" is until \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" +
- "- submitted after certain date criteria in form of: \"Submit Date\" is since \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" +
- "- updated before certain date criteria in form of: \"Last Activity Date\" is until \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" +
- "- updated after certain date criteria in form of: \"Last Activity Date\" is since \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" +
- "- confidential criteria in form of: confidential\n" +
- "- iteration criteria in form of: \"Iteration\" is \"\" (quotes are required)\n" +
- "- and criteria in form of and \n" +
- "- or criteria in form of or . Note that \"and criteria\" takes precedence over \"or criteria\", use braces to group \"or criteria\" like \"(criteria1 or criteria2) and criteria3\" if you want to override precedence\n" +
- "- not criteria in form of not()\n" +
- "\n" +
- "And can optionally add order clause at end of query string in form of: order by \"\" ,\"\" ,... (quotes are required), where is one of below:\n" +
- orderFields +
- "\n" +
- "Leave empty to list all accessible issues";
-
- return HtmlEscape.escapeHtml5(description);
- }
-
- private String getPullRequestQueryStringDescription() {
- var orderFields = new StringBuilder();
- for (var field : PullRequest.SORT_FIELDS.keySet()) {
- orderFields.append("- ").append(field).append("\n");
- }
-
- var labelNames = labelSpecService.query().stream().map(LabelSpec::getName).collect(Collectors.joining(", "));
- var mergeStrategyNames = Arrays.stream(MergeStrategy.values()).map(MergeStrategy::name).collect(Collectors.joining(", "));
-
- var description =
- "A query string is one of below criteria:\n" +
- "- pull request with specified number in form of: \"Number\" is \"#\", or in form of: \"Number\" is \"-\" (quotes are required)\n" +
- "- criteria to check if title/description/comment contains specified text in form of: ~~\n" +
- "- open criteria in form of: open\n" +
- "- merged criteria in form of: merged\n" +
- "- discarded criteria in form of: discarded\n" +
- "- source branch criteria in form of: \"Source Branch\" is \"\" (quotes are required)\n" +
- "- target branch criteria in form of: \"Target Branch\" is \"\" (quotes are required)\n" +
- "- merge strategy criteria in form of: \"Merge Strategy\" is \"\" (quotes are required), where is one of: " + mergeStrategyNames + "\n" +
- "- label criteria in form of: \"Label\" is \"