mirror of
https://github.com/theonedev/onedev.git
synced 2025-12-08 18:26:30 +00:00
feat: Natural language query for issues/builds/pull requests via AI (OD-2600)
This commit is contained in:
parent
2b9237b8b1
commit
b465a1fd78
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
server-product/docker/build/
|
||||
server-product/docker/onedev-*/
|
||||
**/target/
|
||||
**/bin/
|
||||
**/.classpath
|
||||
**/.gitignore
|
||||
**/.settings
|
||||
|
||||
6
pom.xml
6
pom.xml
@ -627,6 +627,11 @@
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>0.15.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai</artifactId>
|
||||
<version>${langchain4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<repositories>
|
||||
@ -663,5 +668,6 @@
|
||||
<servlet.version>3.1.0</servlet.version>
|
||||
<jackson.version>2.15.0</jackson.version>
|
||||
<tika.version>1.28.3</tika.version>
|
||||
<langchain4j.version>1.8.0</langchain4j.version>
|
||||
</properties>
|
||||
</project>
|
||||
|
||||
@ -394,6 +394,10 @@
|
||||
<artifactId>kotlin-stdlib-jdk7</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<kotlin.version>1.9.23</kotlin.version>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 \"<login name of a user>\" (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 \"<group name>\" (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 \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is after certain date criteria in form of: \"" + field.getName()
|
||||
+ "\" is after \"<date>\" (quotes are required), where <date> 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 \"<date time>\" (quotes are required), where <date time> 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 \"<date time>\" (quotes are required), where <date time> 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 \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is greater than certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is greater than \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is less than certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is less than \"<integer>\" (quotes are required), where <integer> 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 \"#<issue number>\", or in form of: \"Number\" is \"<project key>-<issue number>\" (quotes are required)\n" +
|
||||
"- criteria to check if title/description/comment contains specified text in form of: ~<containing text>~\n" +
|
||||
"- state criteria in form of: \"State\" is \"<state name>\" (quotes are required), where <state name> is one of below:\n" +
|
||||
stateNames +
|
||||
fieldCriterias +
|
||||
linkCriterias +
|
||||
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (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 \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- confidential criteria in form of: confidential\n" +
|
||||
"- iteration criteria in form of: \"Iteration\" is \"<iteration name>\" (quotes are required)\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>. 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(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> 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 \"#<pull request number>\", or in form of: \"Number\" is \"<project key>-<pull request number>\" (quotes are required)\n" +
|
||||
"- criteria to check if title/description/comment contains specified text in form of: ~<containing text>~\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 \"<branch name>\" (quotes are required)\n" +
|
||||
"- target branch criteria in form of: \"Target Branch\" is \"<branch name>\" (quotes are required)\n" +
|
||||
"- merge strategy criteria in form of: \"Merge Strategy\" is \"<merge strategy>\" (quotes are required), where <merge strategy> is one of: " + mergeStrategyNames + "\n" +
|
||||
"- label criteria in form of: \"Label\" is \"<label name>\" (quotes are required), where <label name> is one of: " + labelNames + "\n" +
|
||||
"- ready to merge criteria in form of: ready to merge\n" +
|
||||
"- waiting for someone to review criteria in form of: has pending reviews\n" +
|
||||
"- some builds are unsuccessful criteria in form of: has unsuccessful builds\n" +
|
||||
"- some builds are not finished criteria in form of: has unfinished builds\n" +
|
||||
"- has merge conflicts criteria in form of: has merge conflicts\n" +
|
||||
"- assigned to specified user criteria in form of: assigned to \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- approved by specified user criteria in form of: approved by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- to be reviewed by specified user criteria in form of: to be reviewed by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- to be changed by specified user criteria in form of: to be changed by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- to be merged by specified user criteria in form of: to be merged by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- requested for changes by specified user in form of: requested for changes by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- need action of specified user criteria in form of: need action by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- assigned to current user criteria in form of: assigned to me\n" +
|
||||
"- approved by current user criteria in form of: approved by me\n" +
|
||||
"- to be reviewed by current user criteria in form of: to be reviewed by me\n" +
|
||||
"- to be changed by current user criteria in form of: to be changed by me\n" +
|
||||
"- to be merged by current user criteria in form of: to be merged by me\n" +
|
||||
"- requested for changes by current user in form of: requested for changes by me\n" +
|
||||
"- requested for changes by any user criteria in form of: someone requested for changes\n" +
|
||||
"- need action of current user criteria in form of: need my action\n" +
|
||||
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (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 \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- closed (merged or discarded) before certain date criteria in form of: \"Close Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- closed (merged or discarded) after certain date criteria in form of: \"Close Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- includes specified issue criteria in form of: includes issue \"<issue reference>\" (quotes are required)\n" +
|
||||
"- includes specified commit criteria in form of: includes commit \"<commit hash>\" (quotes are required)\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>. 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(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n" +
|
||||
orderFields +
|
||||
"\n" +
|
||||
"Leave empty to list all pull requests";
|
||||
|
||||
return HtmlEscape.escapeHtml5(description);
|
||||
}
|
||||
|
||||
private String getBuildQueryStringDescription() {
|
||||
var orderFields = new StringBuilder();
|
||||
for (var field : Build.SORT_FIELDS.keySet()) {
|
||||
orderFields.append("- ").append(field).append("\n");
|
||||
}
|
||||
|
||||
var jobNames = buildService.getJobNames(null).stream().collect(Collectors.joining(", "));
|
||||
var paramNames = buildParamService.getParamNames(null).stream().collect(Collectors.joining(", "));
|
||||
var labelNames = labelSpecService.query().stream().map(LabelSpec::getName).collect(Collectors.joining(", "));
|
||||
|
||||
var description =
|
||||
"A query string is one of below criteria:\n" +
|
||||
"- build with specified number in form of: \"Number\" is \"#<build number>\", or in form of: \"Number\" is \"<project key>-<build number>\" (quotes are required)\n" +
|
||||
"- criteria to check if version/job contains specified text in form of: ~<containing text>~\n" +
|
||||
"- sucessful criteria in form of: sucessful\n" +
|
||||
"- failed criteria in form of: failed\n" +
|
||||
"- cancelled criteria in form of: cancelled\n" +
|
||||
"- timed out criteria in form of: timed out\n" +
|
||||
"- finished criteria in form of: finished\n" +
|
||||
"- running criteria in form of: running\n" +
|
||||
"- waiting criteria in form of: waiting\n" +
|
||||
"- pending criteria in form of: pending\n" +
|
||||
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
|
||||
"- cancelled by specified user criteria in form of: cancelled by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- cancelled by current user criteria in form of: cancelled by me (quotes are required)\n" +
|
||||
"- depends on specified build criteria in form of: depends on \"<build reference>\" (quotes are required)\n" +
|
||||
"- dependencies of specified build criteria in form of: dependencies of \"<build reference>\" (quotes are required)\n" +
|
||||
"- fixed specified issue criteria in form of: fixed issue \"<issue reference>\" (quotes are required)\n" +
|
||||
"- job criteria in form of: \"Job\" is \"<job name>\" (quotes are required), where <job name> is one of: " + jobNames + "\n" +
|
||||
"- version criteria in form of: \"Version\" is \"<version>\" (quotes are required)\n" +
|
||||
"- branch criteria in form of: \"Branch\" is \"<branch name>\" (quotes are required)\n" +
|
||||
"- tag criteria in form of: \"Tag\" is \"<tag name>\" (quotes are required)\n" +
|
||||
"- param criteria in form of: \"<param name>\" is \"<param value>\" (quotes are required), where <param name> is one of: " + paramNames + "\n" +
|
||||
"- label criteria in form of: \"Label\" is \"<label name>\" (quotes are required), where <label name> is one of: " + labelNames + "\n" +
|
||||
"- pull request criteria in form of: \"Pull Request\" is \"<pull request reference>\" (quotes are required)\n" +
|
||||
"- commit criteria in form of: \"Commit\" is \"<commit hash>\" (quotes are required)\n" +
|
||||
"- before certain date criteria in form of: \"Submit Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>. 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(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n" +
|
||||
orderFields +
|
||||
"\n" +
|
||||
"Leave empty to list all pull requests";
|
||||
|
||||
return HtmlEscape.escapeHtml5(description);
|
||||
}
|
||||
|
||||
private String getToolParamName(String fieldName) {
|
||||
return fieldName.replace(" ", "_");
|
||||
}
|
||||
@ -521,7 +288,7 @@ public class McpHelperResource {
|
||||
"Expects unix timestamp in milliseconds since epoch");
|
||||
}
|
||||
|
||||
fieldDescription = HtmlEscape.escapeHtml5(fieldDescription);
|
||||
fieldDescription = escapeHtml5(fieldDescription);
|
||||
|
||||
var fieldProperties = new HashMap<String, Object>();
|
||||
if (field.isAllowMultiple()) {
|
||||
@ -556,7 +323,7 @@ public class McpHelperResource {
|
||||
"description", "Project to query issues in. Leave empty to query in current project"));
|
||||
queryIssuesProperties.put("query", Map.of(
|
||||
"type", "string",
|
||||
"description", getIssueQueryStringDescription()));
|
||||
"description", escapeHtml5(QueryDescriptions.getIssueQueryDescription())));
|
||||
queryIssuesProperties.put("offset", Map.of(
|
||||
"type", "integer",
|
||||
"description", "start position for the query (optional, defaults to 0)"));
|
||||
@ -716,7 +483,7 @@ public class McpHelperResource {
|
||||
"description", "Project to query pull requests in. Leave empty to query in current project"));
|
||||
queryPullRequestsProperties.put("query", Map.of(
|
||||
"type", "string",
|
||||
"description", getPullRequestQueryStringDescription()));
|
||||
"description", escapeHtml5(QueryDescriptions.getPullRequestQueryDescription())));
|
||||
queryPullRequestsProperties.put("offset", Map.of(
|
||||
"type", "integer",
|
||||
"description", "start position for the query (optional, defaults to 0)"));
|
||||
@ -738,7 +505,7 @@ public class McpHelperResource {
|
||||
"description", "Project to query builds in. Leave empty to query in current project"));
|
||||
queryBuildsProperties.put("query", Map.of(
|
||||
"type", "string",
|
||||
"description", getBuildQueryStringDescription()));
|
||||
"description", escapeHtml5(getBuildQueryDescription())));
|
||||
queryBuildsProperties.put("offset", Map.of(
|
||||
"type", "integer",
|
||||
"description", "start position for the query (optional, defaults to 0)"));
|
||||
@ -0,0 +1,300 @@
|
||||
package io.onedev.server.ai;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.model.Build;
|
||||
import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.LabelSpec;
|
||||
import io.onedev.server.model.PullRequest;
|
||||
import io.onedev.server.model.support.issue.field.spec.BooleanField;
|
||||
import io.onedev.server.model.support.issue.field.spec.DateField;
|
||||
import io.onedev.server.model.support.issue.field.spec.DateTimeField;
|
||||
import io.onedev.server.model.support.issue.field.spec.GroupChoiceField;
|
||||
import io.onedev.server.model.support.issue.field.spec.IntegerField;
|
||||
import io.onedev.server.model.support.issue.field.spec.choicefield.ChoiceField;
|
||||
import io.onedev.server.model.support.issue.field.spec.userchoicefield.UserChoiceField;
|
||||
import io.onedev.server.model.support.pullrequest.MergeStrategy;
|
||||
import io.onedev.server.service.BuildParamService;
|
||||
import io.onedev.server.service.BuildService;
|
||||
import io.onedev.server.service.LabelSpecService;
|
||||
import io.onedev.server.service.LinkSpecService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
|
||||
public class QueryDescriptions {
|
||||
|
||||
private static SettingService getSettingService() {
|
||||
return OneDev.getInstance(SettingService.class);
|
||||
}
|
||||
|
||||
private static LinkSpecService getLinkSpecService() {
|
||||
return OneDev.getInstance(LinkSpecService.class);
|
||||
}
|
||||
|
||||
private static LabelSpecService getLabelSpecService() {
|
||||
return OneDev.getInstance(LabelSpecService.class);
|
||||
}
|
||||
|
||||
private static BuildService getBuildService() {
|
||||
return OneDev.getInstance(BuildService.class);
|
||||
}
|
||||
|
||||
private static BuildParamService getBuildParamService() {
|
||||
return OneDev.getInstance(BuildParamService.class);
|
||||
}
|
||||
|
||||
public static String getIssueQueryDescription() {
|
||||
var settingService = getSettingService();
|
||||
var linkSpecService = getLinkSpecService();
|
||||
|
||||
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 \"<login name of a user>\" (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 \"<group name>\" (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 \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is after certain date criteria in form of: \"" + field.getName()
|
||||
+ "\" is after \"<date>\" (quotes are required), where <date> 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 \"<date time>\" (quotes are required), where <date time> 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 \"<date time>\" (quotes are required), where <date time> 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 \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is greater than certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is greater than \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is less than certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is less than \"<integer>\" (quotes are required), where <integer> 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()
|
||||
+ " 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()
|
||||
+ " 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()
|
||||
+ " 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()
|
||||
+ " 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()
|
||||
+ " 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()
|
||||
+ " 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\n" +
|
||||
"- issue with specified number in form of: \"Number\" is \"#<issue number>\", or in form of: \"Number\" is \"<project key>-<issue number>\" (quotes are required)\n" +
|
||||
"- state criteria in form of: \"State\" is \"<state name>\" (quotes are required), where <state name> is one of below:\n" +
|
||||
stateNames +
|
||||
fieldCriterias +
|
||||
linkCriterias +
|
||||
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (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 \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- confidential criteria in form of: confidential\n" +
|
||||
"- iteration criteria in form of: \"Iteration\" is \"<iteration name>\" (quotes are required)\n" +
|
||||
"- title contains specified text criteria in form of: \"Title\" contains \"<containing text>\" (quotes are required)\n" +
|
||||
"- description contains specified text criteria in form of: \"Description\" contains \"<containing text>\" (quotes are required)\n" +
|
||||
"- comment contains specified text criteria in form of: \"Comment\" contains \"<containing text>\" (quotes are required)\n" +
|
||||
"- project criteria in form of: \"Project\" is \"<project path pattern>\" (quotes are required)\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>\n" +
|
||||
"- operator 'and' takes precedence over 'or' when used together, unless parentheses are used to group 'or' criterias\n" +
|
||||
"- not criteria in form of not(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n\n" +
|
||||
orderFields + "\n" +
|
||||
"Issue, build or pull request can be referenced by their number in form of: #<number>, <project path>#<number> or <project key>-<number>\n" +
|
||||
"\n" +
|
||||
"Leave empty to list all accessible issues";
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
public static String getPullRequestQueryDescription() {
|
||||
var labelSpecService = getLabelSpecService();
|
||||
|
||||
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\n" +
|
||||
"- pull request with specified number in form of: \"Number\" is \"#<pull request number>\", or in form of: \"Number\" is \"<project key>-<pull request number>\" (quotes are required)\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 \"<branch name>\" (quotes are required)\n" +
|
||||
"- target branch criteria in form of: \"Target Branch\" is \"<branch name>\" (quotes are required)\n" +
|
||||
"- merge strategy criteria in form of: \"Merge Strategy\" is \"<merge strategy>\" (quotes are required), where <merge strategy> is one of: " + mergeStrategyNames + "\n" +
|
||||
"- label criteria in form of: \"Label\" is \"<label name>\" (quotes are required), where <label name> is one of: " + labelNames + "\n" +
|
||||
"- ready to merge criteria in form of: ready to merge\n" +
|
||||
"- waiting for someone to review criteria in form of: has pending reviews\n" +
|
||||
"- some builds are unsuccessful criteria in form of: has unsuccessful builds\n" +
|
||||
"- some builds are not finished criteria in form of: has unfinished builds\n" +
|
||||
"- has merge conflicts criteria in form of: has merge conflicts\n" +
|
||||
"- assigned to specified user criteria in form of: assigned to \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- approved by specified user criteria in form of: approved by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- to be reviewed by specified user criteria in form of: to be reviewed by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- to be changed by specified user criteria in form of: to be changed by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- to be merged by specified user criteria in form of: to be merged by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- requested for changes by specified user in form of: requested for changes by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- need action of specified user criteria in form of: need action by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- assigned to current user criteria in form of: assigned to me\n" +
|
||||
"- approved by current user criteria in form of: approved by me\n" +
|
||||
"- to be reviewed by current user criteria in form of: to be reviewed by me\n" +
|
||||
"- to be changed by current user criteria in form of: to be changed by me\n" +
|
||||
"- to be merged by current user criteria in form of: to be merged by me\n" +
|
||||
"- requested for changes by current user in form of: requested for changes by me\n" +
|
||||
"- requested for changes by any user criteria in form of: someone requested for changes\n" +
|
||||
"- need action of current user criteria in form of: need my action\n" +
|
||||
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (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 \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- closed (merged or discarded) before certain date criteria in form of: \"Close Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- closed (merged or discarded) after certain date criteria in form of: \"Close Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- includes specified issue criteria in form of: includes issue \"<issue reference>\" (quotes are required)\n" +
|
||||
"- includes specified commit criteria in form of: includes commit \"<commit hash>\" (quotes are required)\n" +
|
||||
"- title contains specified text criteria in form of: \"Title\" contains \"<containing text>\" (quotes are required)\n" +
|
||||
"- description contains specified text criteria in form of: \"Description\" contains \"<containing text>\" (quotes are required)\n" +
|
||||
"- comment contains specified text criteria in form of: \"Comment\" contains \"<containing text>\" (quotes are required)\n" +
|
||||
"- project criteria in form of: \"Project\" is \"<project path pattern>\" (quotes are required)\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>\n" +
|
||||
"- operator 'and' takes precedence over 'or' when used together, unless parentheses are used to group 'or' criterias\n" +
|
||||
"- not criteria in form of not(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n\n" +
|
||||
orderFields + "\n" +
|
||||
"Issue, build or pull request can be referenced by their number in form of: #<number>, <project path>#<number> or <project key>-<number>\n" +
|
||||
"\n" +
|
||||
"Leave empty to list all accessible pull requests";
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
public static String getBuildQueryDescription() {
|
||||
var buildService = getBuildService();
|
||||
var buildParamService = getBuildParamService();
|
||||
var labelSpecService = getLabelSpecService();
|
||||
|
||||
var orderFields = new StringBuilder();
|
||||
for (var field : Build.SORT_FIELDS.keySet()) {
|
||||
orderFields.append("- ").append(field).append("\n");
|
||||
}
|
||||
|
||||
var jobNames = buildService.getJobNames(null).stream().collect(Collectors.joining(", "));
|
||||
var paramNames = buildParamService.getParamNames(null).stream().collect(Collectors.joining(", "));
|
||||
var labelNames = labelSpecService.query().stream().map(LabelSpec::getName).collect(Collectors.joining(", "));
|
||||
|
||||
var description =
|
||||
"A query string is one of below criteria:\n\n" +
|
||||
"- build with specified number in form of: \"Number\" is \"#<build number>\", or in form of: \"Number\" is \"<project key>-<build number>\" (quotes are required)\n" +
|
||||
"- criteria to check if version/job contains specified text in form of: ~<containing text>~\n" +
|
||||
"- sucessful criteria in form of: sucessful\n" +
|
||||
"- failed criteria in form of: failed\n" +
|
||||
"- cancelled criteria in form of: cancelled\n" +
|
||||
"- timed out criteria in form of: timed out\n" +
|
||||
"- finished criteria in form of: finished\n" +
|
||||
"- running criteria in form of: running\n" +
|
||||
"- waiting criteria in form of: waiting\n" +
|
||||
"- pending criteria in form of: pending\n" +
|
||||
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
|
||||
"- cancelled by specified user criteria in form of: cancelled by \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- cancelled by current user criteria in form of: cancelled by me (quotes are required)\n" +
|
||||
"- depends on specified build criteria in form of: depends on \"<build reference>\" (quotes are required)\n" +
|
||||
"- dependencies of specified build criteria in form of: dependencies of \"<build reference>\" (quotes are required)\n" +
|
||||
"- fixed specified issue criteria in form of: fixed issue \"<issue reference>\" (quotes are required)\n" +
|
||||
"- job criteria in form of: \"Job\" is \"<job name>\" (quotes are required), where <job name> is one of: " + jobNames + "\n" +
|
||||
"- version criteria in form of: \"Version\" is \"<version>\" (quotes are required)\n" +
|
||||
"- branch criteria in form of: \"Branch\" is \"<branch name>\" (quotes are required)\n" +
|
||||
"- tag criteria in form of: \"Tag\" is \"<tag name>\" (quotes are required)\n" +
|
||||
"- param criteria in form of: \"<param name>\" is \"<param value>\" (quotes are required), where <param name> is one of: " + paramNames + "\n" +
|
||||
"- label criteria in form of: \"Label\" is \"<label name>\" (quotes are required), where <label name> is one of: " + labelNames + "\n" +
|
||||
"- pull request criteria in form of: \"Pull Request\" is \"<pull request reference>\" (quotes are required)\n" +
|
||||
"- commit criteria in form of: \"Commit\" is \"<commit hash>\" (quotes are required)\n" +
|
||||
"- before certain date criteria in form of: \"Submit Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- project criteria in form of: \"Project\" is \"<project path pattern>\" (quotes are required)\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>\n" +
|
||||
"- operator 'and' takes precedence over 'or' when used together, unless parentheses are used to group 'or' criterias\n" +
|
||||
"- not criteria in form of not(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n\n" +
|
||||
orderFields + "\n" +
|
||||
"Issue, build or pull request can be referenced by their number in form of: #<number>, <project path>#<number> or <project key>-<number>\n" +
|
||||
"\n" +
|
||||
"Leave empty to list all accessible builds";
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
}
|
||||
@ -6,9 +6,9 @@ import static io.onedev.server.model.User.PROP_SERVICE_ACCOUNT;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_CURL_LOCATION;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_DISABLE_AUTO_UPDATE_CHECK;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_GIT_LOCATION;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_SESSION_TIMEOUT;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_SSH_ROOT_URL;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_USE_AVATAR_SERVICE;
|
||||
import static io.onedev.server.model.support.administration.SystemSetting.PROP_SESSION_TIMEOUT;
|
||||
import static io.onedev.server.persistence.PersistenceUtils.tableExists;
|
||||
import static org.unbescape.html.HtmlEscape.escapeHtml5;
|
||||
|
||||
@ -74,19 +74,13 @@ import io.onedev.commons.utils.FileUtils;
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.commons.utils.ZipUtils;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.cluster.ClusterService;
|
||||
import io.onedev.server.cluster.ClusterRunnable;
|
||||
import io.onedev.server.cluster.ClusterService;
|
||||
import io.onedev.server.cluster.ClusterTask;
|
||||
import io.onedev.server.commandhandler.Upgrade;
|
||||
import io.onedev.server.data.migration.DataMigrator;
|
||||
import io.onedev.server.data.migration.MigrationHelper;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.service.AlertService;
|
||||
import io.onedev.server.service.EmailAddressService;
|
||||
import io.onedev.server.service.LinkSpecService;
|
||||
import io.onedev.server.service.RoleService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.service.UserService;
|
||||
import io.onedev.server.event.Listen;
|
||||
import io.onedev.server.event.entity.EntityPersisted;
|
||||
import io.onedev.server.event.system.SystemStarted;
|
||||
@ -98,6 +92,7 @@ import io.onedev.server.model.Role;
|
||||
import io.onedev.server.model.Setting;
|
||||
import io.onedev.server.model.Setting.Key;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.model.support.administration.AISetting;
|
||||
import io.onedev.server.model.support.administration.AgentSetting;
|
||||
import io.onedev.server.model.support.administration.AlertSetting;
|
||||
import io.onedev.server.model.support.administration.AuditSetting;
|
||||
@ -124,6 +119,12 @@ 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.Dao;
|
||||
import io.onedev.server.service.AlertService;
|
||||
import io.onedev.server.service.EmailAddressService;
|
||||
import io.onedev.server.service.LinkSpecService;
|
||||
import io.onedev.server.service.RoleService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.service.UserService;
|
||||
import io.onedev.server.ssh.SshKeyUtils;
|
||||
import io.onedev.server.taskschedule.SchedulableTask;
|
||||
import io.onedev.server.taskschedule.TaskScheduler;
|
||||
@ -878,10 +879,12 @@ public class DefaultDataService implements DataService, Serializable {
|
||||
}
|
||||
|
||||
setting = settingService.findSetting(Key.AUDIT);
|
||||
if (setting == null) {
|
||||
AuditSetting auditSetting = new AuditSetting();
|
||||
settingService.saveAuditSetting(auditSetting);
|
||||
}
|
||||
if (setting == null)
|
||||
settingService.saveAuditSetting(new AuditSetting());
|
||||
|
||||
setting = settingService.findSetting(Key.AI);
|
||||
if (setting == null)
|
||||
settingService.saveAISetting(new AISetting());
|
||||
|
||||
if (roleService.get(Role.OWNER_ID) == null) {
|
||||
Role owner = new Role();
|
||||
|
||||
@ -8378,4 +8378,8 @@ public class DataMigrator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void migrate214(File dataDir, Stack<Integer> versions) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ public class Setting extends AbstractEntity {
|
||||
GROOVY_SCRIPTS, PULL_REQUEST, BUILD, PACK, PROJECT, SSH, GPG,
|
||||
EMAIL_TEMPLATES, CONTRIBUTED_SETTINGS, SERVICE_DESK_SETTING,
|
||||
AGENT, PERFORMANCE, BRANDING, CLUSTER_SETTING, SUBSCRIPTION_DATA, ALERT,
|
||||
SYSTEM_UUID, AUDIT
|
||||
SYSTEM_UUID, AUDIT, AI
|
||||
};
|
||||
|
||||
@Column(nullable=false, unique=true)
|
||||
|
||||
@ -0,0 +1,135 @@
|
||||
package io.onedev.server.model.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import io.onedev.server.annotation.ChoiceProvider;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.util.EditContext;
|
||||
|
||||
@Editable(order=100)
|
||||
public class AIModelSetting implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
|
||||
|
||||
private static final int DEFAULT_READ_TIMEOUT_SECONDS = 30;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AIModelSetting.class);
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
private String apiKey;
|
||||
|
||||
private String name;
|
||||
|
||||
@Editable(order=200, name="Base URL", placeholder="https://api.openai.com/v1", description="Base URL of <b class='text-info'>OpenAI compatible</b> API endpoint. Leave empty to use OpenAI official endpoint")
|
||||
@Pattern(regexp="https?://.+", message="Base URL should be a valid http/https URL")
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
@Editable(order=300, name="API Key")
|
||||
@NotEmpty
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
@Editable(order=400)
|
||||
@ChoiceProvider("getModels")
|
||||
@NotEmpty
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static List<String> getModels() {
|
||||
var baseUrl = (String) EditContext.get().getInputValue("baseUrl");
|
||||
if (baseUrl == null)
|
||||
baseUrl = "https://api.openai.com/v1";
|
||||
|
||||
var apiKey = (String) EditContext.get().getInputValue("apiKey");
|
||||
|
||||
if (apiKey != null) {
|
||||
try {
|
||||
var modelsUrl = baseUrl.endsWith("/") ? baseUrl + "models" : baseUrl + "/models";
|
||||
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(DEFAULT_CONNECT_TIMEOUT_SECONDS))
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(modelsUrl))
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.GET()
|
||||
.timeout(Duration.ofSeconds(DEFAULT_READ_TIMEOUT_SECONDS))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
var models = new ArrayList<String>();
|
||||
var jsonResponse = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||
var dataArray = jsonResponse.getAsJsonArray("data");
|
||||
|
||||
for (var element : dataArray) {
|
||||
models.add(element.getAsJsonObject().get("id").getAsString());
|
||||
}
|
||||
|
||||
models.sort(String::compareTo);
|
||||
return models;
|
||||
} else {
|
||||
logger.error("Error getting models (status code: {}, response body: {})",
|
||||
response.statusCode(), response.body());
|
||||
return List.of("<Error getting models, check server log for details>");
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
logger.error("Error getting models", e);
|
||||
return List.of("<Error getting models, check server log for details>");
|
||||
}
|
||||
} else {
|
||||
return List.of("<Specify API key to get models>");
|
||||
}
|
||||
}
|
||||
|
||||
public ChatModel getChatModel() {
|
||||
return OpenAiChatModel.builder()
|
||||
.apiKey(apiKey)
|
||||
.baseUrl(baseUrl)
|
||||
.modelName(name)
|
||||
.timeout(Duration.ofSeconds(DEFAULT_READ_TIMEOUT_SECONDS))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package io.onedev.server.model.support.administration;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.model.support.AIModelSetting;
|
||||
|
||||
@Editable
|
||||
public class AISetting implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private AIModelSetting naturalLanguageQueryModelSetting;
|
||||
|
||||
@Editable(order=100, name="Natural Language Query Model", description=
|
||||
"""
|
||||
If specified, one will be able to query issues, pull requests and builds via natural language. Suggested models in terms of performance and cost for this task:
|
||||
<ul>
|
||||
<li>Google/gemini-2.5-flash</li>
|
||||
<li>OpenAI/gpt-4.1-mini</li>
|
||||
<li>Qwen/Qwen-2.5-72B-instruct</li>
|
||||
</ul>
|
||||
""")
|
||||
@Nullable
|
||||
public AIModelSetting getNaturalLanguageQueryModelSetting() {
|
||||
return naturalLanguageQueryModelSetting;
|
||||
}
|
||||
|
||||
public void setNaturalLanguageQueryModelSetting(AIModelSetting naturalLanguageQueryModelSetting) {
|
||||
this.naturalLanguageQueryModelSetting = naturalLanguageQueryModelSetting;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ChatModel getNaturalLanguageQueryModel() {
|
||||
return naturalLanguageQueryModelSetting != null ? naturalLanguageQueryModelSetting.getChatModel() : null;
|
||||
}
|
||||
|
||||
}
|
||||
@ -9,6 +9,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import io.onedev.server.annotation.NoDBAccess;
|
||||
import io.onedev.server.model.Setting;
|
||||
import io.onedev.server.model.Setting.Key;
|
||||
import io.onedev.server.model.support.administration.AISetting;
|
||||
import io.onedev.server.model.support.administration.AgentSetting;
|
||||
import io.onedev.server.model.support.administration.AlertSetting;
|
||||
import io.onedev.server.model.support.administration.AuditSetting;
|
||||
@ -172,6 +173,10 @@ public interface SettingService extends EntityService<Setting> {
|
||||
|
||||
PerformanceSetting getPerformanceSetting();
|
||||
|
||||
AISetting getAISetting();
|
||||
|
||||
void saveAISetting(AISetting aiSetting);
|
||||
|
||||
void saveSshSetting(SshSetting sshSetting);
|
||||
|
||||
void saveGpgSetting(GpgSetting gpgSetting);
|
||||
|
||||
@ -13,7 +13,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.validation.Validator;
|
||||
@ -21,6 +20,7 @@ import javax.validation.Validator;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.shiro.crypto.AesCipherService;
|
||||
import org.apache.shiro.crypto.CipherService;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.onedev.commons.loader.ManagedSerializedForm;
|
||||
import io.onedev.server.OneDev;
|
||||
@ -30,6 +30,7 @@ import io.onedev.server.event.entity.EntityPersisted;
|
||||
import io.onedev.server.event.system.SystemStarting;
|
||||
import io.onedev.server.model.Setting;
|
||||
import io.onedev.server.model.Setting.Key;
|
||||
import io.onedev.server.model.support.administration.AISetting;
|
||||
import io.onedev.server.model.support.administration.AgentSetting;
|
||||
import io.onedev.server.model.support.administration.AlertSetting;
|
||||
import io.onedev.server.model.support.administration.AuditSetting;
|
||||
@ -166,6 +167,11 @@ public class DefaultSettingService extends BaseEntityService<Setting> implements
|
||||
return (AuditSetting) getSettingValue(Key.AUDIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AISetting getAISetting() {
|
||||
return (AISetting) getSettingValue(Key.AI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecuritySetting getSecuritySetting() {
|
||||
return (SecuritySetting) getSettingValue(Key.SECURITY);
|
||||
@ -296,6 +302,12 @@ public class DefaultSettingService extends BaseEntityService<Setting> implements
|
||||
saveSetting(Key.AUDIT, auditSetting);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void saveAISetting(AISetting aiSetting) {
|
||||
saveSetting(Key.AI, aiSetting);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void saveSecuritySetting(SecuritySetting securitySetting) {
|
||||
|
||||
@ -1,7 +1,18 @@
|
||||
package io.onedev.server.web.behavior;
|
||||
|
||||
import static io.onedev.server.web.translation.Translation._T;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.model.IModel;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Collections;
|
||||
import io.onedev.commons.codeassist.FenceAware;
|
||||
import io.onedev.commons.codeassist.InputCompletion;
|
||||
@ -12,26 +23,19 @@ import io.onedev.commons.codeassist.parser.ParseExpect;
|
||||
import io.onedev.commons.codeassist.parser.TerminalExpect;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.service.BuildParamService;
|
||||
import io.onedev.server.ai.QueryDescriptions;
|
||||
import io.onedev.server.model.Build;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.search.entity.build.BuildQuery;
|
||||
import io.onedev.server.search.entity.build.BuildQueryLexer;
|
||||
import io.onedev.server.search.entity.build.BuildQueryParser;
|
||||
import io.onedev.server.service.BuildParamService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
|
||||
import io.onedev.server.web.behavior.inputassist.InputAssistBehavior;
|
||||
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
|
||||
import io.onedev.server.web.util.SuggestionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.model.IModel;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import static io.onedev.server.web.translation.Translation._T;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BuildQueryBehavior extends ANTLRAssistBehavior {
|
||||
|
||||
@ -254,9 +258,32 @@ public class BuildQueryBehavior extends ANTLRAssistBehavior {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null)
|
||||
hints.add(_T("Set up AI to use natural language query"));
|
||||
return hints;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
|
||||
var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel();
|
||||
if (naturalLanguageQueryModel != null) {
|
||||
return new NaturalLanguageTranslator(naturalLanguageQueryModel) {
|
||||
|
||||
@Override
|
||||
public String getQueryDescription() {
|
||||
return QueryDescriptions.getBuildQueryDescription();
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SettingService getSettingService() {
|
||||
return OneDev.getInstance(SettingService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
|
||||
return suggestion.getDescription() != null
|
||||
|
||||
@ -57,11 +57,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.model.IModel;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
@ -75,9 +74,7 @@ import io.onedev.commons.codeassist.parser.ParseExpect;
|
||||
import io.onedev.commons.codeassist.parser.TerminalExpect;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.service.GroupService;
|
||||
import io.onedev.server.service.LinkSpecService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.ai.QueryDescriptions;
|
||||
import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.IssueSchedule;
|
||||
import io.onedev.server.model.LinkSpec;
|
||||
@ -100,8 +97,12 @@ import io.onedev.server.model.support.issue.field.spec.userchoicefield.UserChoic
|
||||
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
|
||||
import io.onedev.server.search.entity.issue.IssueQueryParser;
|
||||
import io.onedev.server.search.entity.project.ProjectQuery;
|
||||
import io.onedev.server.service.GroupService;
|
||||
import io.onedev.server.service.LinkSpecService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
|
||||
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
|
||||
import io.onedev.server.web.behavior.inputassist.InputAssistBehavior;
|
||||
import io.onedev.server.web.util.SuggestionUtils;
|
||||
@ -339,8 +340,21 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
|
||||
}
|
||||
|
||||
}.suggest(terminalExpect);
|
||||
} else if (spec.getRuleName().equals("Fuzzy")) {
|
||||
} else if (spec.getRuleName().equals("Ai")) {
|
||||
return new FenceAware(codeAssist.getGrammar(), '`', '`') {
|
||||
|
||||
@Override
|
||||
protected List<InputSuggestion> match(String matchWith) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFencingDescription() {
|
||||
return _T("enclose with ` to query with AI");
|
||||
}
|
||||
|
||||
}.suggest(terminalExpect);
|
||||
} else if (spec.getRuleName().equals("Fuzzy")) {
|
||||
return new FenceAware(codeAssist.getGrammar(), '~', '~') {
|
||||
|
||||
@Override
|
||||
@ -397,7 +411,7 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
|
||||
@Override
|
||||
protected List<String> getHints(TerminalExpect terminalExpect) {
|
||||
List<String> hints = new ArrayList<>();
|
||||
GlobalIssueSetting issueSetting = OneDev.getInstance(SettingService.class).getIssueSetting();
|
||||
GlobalIssueSetting issueSetting = getSettingService().getIssueSetting();
|
||||
if (terminalExpect.getElementSpec() instanceof LexerRuleRefElementSpec) {
|
||||
LexerRuleRefElementSpec spec = (LexerRuleRefElementSpec) terminalExpect.getElementSpec();
|
||||
if ("criteriaValue".equals(spec.getLabel())) {
|
||||
@ -418,13 +432,35 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null)
|
||||
hints.add(_T("Set up AI to use natural language query"));
|
||||
return hints;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
|
||||
return suggestion.getDescription() != null
|
||||
&& suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX);
|
||||
return suggestion.getDescription() != null && (suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
|
||||
var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel();
|
||||
if (naturalLanguageQueryModel != null) {
|
||||
return new NaturalLanguageTranslator(naturalLanguageQueryModel) {
|
||||
|
||||
@Override
|
||||
public String getQueryDescription() {
|
||||
return QueryDescriptions.getIssueQueryDescription();
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SettingService getSettingService() {
|
||||
return OneDev.getInstance(SettingService.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +1,38 @@
|
||||
package io.onedev.server.web.behavior;
|
||||
|
||||
import static io.onedev.server.model.AbstractEntity.NAME_NUMBER;
|
||||
import static io.onedev.server.search.entity.EntityQuery.getValue;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQuery.checkField;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQuery.getOperator;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQuery.getRuleName;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.ApprovedByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.AssignedToMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.CommentedByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.IgnoredByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.IncludesCommit;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.IncludesIssue;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.MentionedMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.NeedMyAction;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.OrderBy;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.RequestedForChangesByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.SubmittedByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.ToBeChangedByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.ToBeMergedByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.ToBeReviewedByMe;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.WatchedByMe;
|
||||
import static io.onedev.server.web.translation.Translation._T;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.model.IModel;
|
||||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.onedev.commons.codeassist.FenceAware;
|
||||
import io.onedev.commons.codeassist.InputCompletion;
|
||||
import io.onedev.commons.codeassist.InputSuggestion;
|
||||
@ -10,28 +41,19 @@ import io.onedev.commons.codeassist.parser.Element;
|
||||
import io.onedev.commons.codeassist.parser.ParseExpect;
|
||||
import io.onedev.commons.codeassist.parser.TerminalExpect;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.ai.QueryDescriptions;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.PullRequest;
|
||||
import io.onedev.server.model.support.pullrequest.MergeStrategy;
|
||||
import io.onedev.server.search.entity.project.ProjectQuery;
|
||||
import io.onedev.server.search.entity.pullrequest.PullRequestQueryParser;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
|
||||
import io.onedev.server.web.behavior.inputassist.InputAssistBehavior;
|
||||
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
|
||||
import io.onedev.server.web.util.SuggestionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.model.IModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static io.onedev.server.model.AbstractEntity.NAME_NUMBER;
|
||||
import static io.onedev.server.search.entity.EntityQuery.getValue;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQuery.*;
|
||||
import static io.onedev.server.search.entity.pullrequest.PullRequestQueryLexer.*;
|
||||
import static io.onedev.server.web.translation.Translation._T;
|
||||
|
||||
public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
|
||||
|
||||
@ -241,6 +263,8 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getSettingService().getAISetting().getNaturalLanguageQueryModelSetting() == null)
|
||||
hints.add(_T("Set up AI to use natural language query"));
|
||||
return hints;
|
||||
}
|
||||
|
||||
@ -250,4 +274,25 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior {
|
||||
&& suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
|
||||
var naturalLanguageQueryModel = getSettingService().getAISetting().getNaturalLanguageQueryModel();
|
||||
if (naturalLanguageQueryModel != null) {
|
||||
return new NaturalLanguageTranslator(naturalLanguageQueryModel) {
|
||||
|
||||
@Override
|
||||
public String getQueryDescription() {
|
||||
return QueryDescriptions.getPullRequestQueryDescription();
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SettingService getSettingService() {
|
||||
return OneDev.getInstance(SettingService.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@ package io.onedev.server.web.behavior.inputassist;
|
||||
|
||||
import static io.onedev.server.web.translation.Translation._T;
|
||||
import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
|
||||
import static org.unbescape.javascript.JavaScriptEscape.escapeJavaScript;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.Session;
|
||||
import org.apache.wicket.ajax.AjaxChannel;
|
||||
import org.apache.wicket.ajax.AjaxRequestTarget;
|
||||
import org.apache.wicket.ajax.attributes.AjaxRequestAttributes;
|
||||
@ -17,6 +19,9 @@ import org.apache.wicket.markup.head.JavaScriptHeaderItem;
|
||||
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
|
||||
import org.apache.wicket.request.IRequestParameters;
|
||||
import org.apache.wicket.request.cycle.RequestCycle;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.unbescape.javascript.JavaScriptEscape;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@ -27,7 +32,10 @@ import com.google.common.base.Splitter;
|
||||
import io.onedev.commons.codeassist.InputCompletion;
|
||||
import io.onedev.commons.codeassist.InputStatus;
|
||||
import io.onedev.commons.loader.AppLoader;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.commons.utils.LinearRange;
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.exception.ExceptionUtils;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.RangeUtils;
|
||||
import io.onedev.server.web.behavior.AbstractPostAjaxBehavior;
|
||||
@ -39,6 +47,8 @@ import io.onedev.server.web.component.floating.FloatingPanel;
|
||||
|
||||
public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(InputAssistBehavior.class);
|
||||
|
||||
public static final int MAX_SUGGESTIONS = 1000;
|
||||
|
||||
private FloatingPanel dropdown;
|
||||
@ -113,9 +123,27 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
|
||||
if (type.equals("close")) {
|
||||
if (dropdown != null)
|
||||
dropdown.close();
|
||||
} else if (type.equals("translate")) {
|
||||
String toTranslate = params.getParameterValue("input").toString();
|
||||
String translated;
|
||||
try {
|
||||
translated = Preconditions.checkNotNull(getNaturalLanguageTranslator()).translate(toTranslate);
|
||||
} catch (Exception e) {
|
||||
translated = toTranslate;
|
||||
var explicitException = ExceptionUtils.find(e, ExplicitException.class);
|
||||
if (explicitException != null) {
|
||||
Session.get().error(explicitException.getMessage());
|
||||
} else {
|
||||
logger.error("Error translating natural language input", e);
|
||||
Session.get().error("Error translating natural language input. Check server log for details");
|
||||
}
|
||||
}
|
||||
target.appendJavaScript(
|
||||
String.format("onedev.server.inputassist.naturalLanguageTranslated('%s', '%s');",
|
||||
getComponent().getMarkupId(), escapeJavaScript(translated)));
|
||||
} else {
|
||||
String inputContent = params.getParameterValue("input").toString();
|
||||
Integer inputCaret = params.getParameterValue("caret").toOptionalInteger();
|
||||
int inputCaret = params.getParameterValue("caret").toInt();
|
||||
|
||||
Preconditions.checkArgument(inputContent.indexOf('\r') == -1);
|
||||
|
||||
@ -133,7 +161,7 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
|
||||
getComponent().getMarkupId(), json);
|
||||
target.appendJavaScript(script);
|
||||
|
||||
if (inputCaret != null) {
|
||||
if (inputCaret != -1) {
|
||||
InputStatus inputStatus = new InputStatus(inputContent, inputCaret);
|
||||
List<InputCompletion> suggestions = new ArrayList<>();
|
||||
ComponentContext.push(new ComponentContext(getComponent()));
|
||||
@ -165,64 +193,85 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
|
||||
String.format("onedev.server.inputassist.appendSpace('%s');",
|
||||
getComponent().getMarkupId()));
|
||||
} else {
|
||||
int anchor = getAnchor(inputContent.substring(0, inputCaret));
|
||||
if (dropdown == null) {
|
||||
dropdown = new FloatingPanel(target, new Alignment(new ComponentTarget(getComponent(), anchor), AlignPlacement.bottom(0))) {
|
||||
|
||||
@Override
|
||||
protected Component newContent(String id) {
|
||||
return new AssistPanel(id, getComponent(), suggestions, getHints(inputStatus)) {
|
||||
|
||||
@Override
|
||||
protected void onClose(AjaxRequestTarget target) {
|
||||
close();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClosed() {
|
||||
super.onClosed();
|
||||
dropdown = null;
|
||||
}
|
||||
|
||||
};
|
||||
script = String.format("onedev.server.inputassist.assistOpened('%s', '%s', '%s');",
|
||||
getComponent().getMarkupId(), dropdown.getMarkupId(), JavaScriptEscape.escapeJavaScript(inputContent));
|
||||
target.appendJavaScript(script);
|
||||
} else {
|
||||
Component content = dropdown.getContent();
|
||||
Component newContent = new AssistPanel(content.getId(), getComponent(), suggestions, getHints(inputStatus)) {
|
||||
|
||||
@Override
|
||||
protected void onClose(AjaxRequestTarget target) {
|
||||
close();
|
||||
}
|
||||
|
||||
};
|
||||
content.replaceWith(newContent);
|
||||
target.add(newContent);
|
||||
|
||||
AlignTarget alignTarget = new ComponentTarget(getComponent(), anchor);
|
||||
script = String.format("$('#%s').data('alignment').target=%s;", dropdown.getMarkupId(), alignTarget);
|
||||
target.prependJavaScript(script);
|
||||
|
||||
script = String.format("onedev.server.inputassist.assistUpdated('%s', '%s', '%s');",
|
||||
getComponent().getMarkupId(), dropdown.getMarkupId(), JavaScriptEscape.escapeJavaScript(inputContent));
|
||||
target.appendJavaScript(script);
|
||||
}
|
||||
addTranslationSuggestion(inputStatus, suggestions);
|
||||
showSuggestions(target, inputStatus, suggestions);
|
||||
}
|
||||
} else if (addTranslationSuggestion(inputStatus, suggestions)) {
|
||||
showSuggestions(target, inputStatus, suggestions);
|
||||
} else if (dropdown != null) {
|
||||
dropdown.close();
|
||||
}
|
||||
} else if (dropdown != null) {
|
||||
dropdown.close();
|
||||
}
|
||||
|
||||
onInput(target, inputContent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean addTranslationSuggestion(InputStatus status, List<InputCompletion> suggestions) {
|
||||
var queryTranslator = getNaturalLanguageTranslator();
|
||||
if (queryTranslator != null) {
|
||||
var contentBeforeCaret = status.getContentBeforeCaret();
|
||||
if (StringUtils.isNotBlank(contentBeforeCaret) && suggestions.stream().noneMatch(it->!isFuzzySuggestion(it))) {
|
||||
contentBeforeCaret += "🤖";
|
||||
suggestions.add(0, new InputCompletion(contentBeforeCaret, contentBeforeCaret + status.getContentAfterCaret(), contentBeforeCaret.length(), _T("Natural language query via AI"), null));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showSuggestions(AjaxRequestTarget target, InputStatus status, List<InputCompletion> suggestions) {
|
||||
int anchor = getAnchor(status.getContent().substring(0, status.getCaret()));
|
||||
if (dropdown == null) {
|
||||
dropdown = new FloatingPanel(target, new Alignment(new ComponentTarget(getComponent(), anchor), AlignPlacement.bottom(0))) {
|
||||
|
||||
@Override
|
||||
protected Component newContent(String id) {
|
||||
return new AssistPanel(id, getComponent(), suggestions, getHints(status)) {
|
||||
|
||||
@Override
|
||||
protected void onClose(AjaxRequestTarget target) {
|
||||
close();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClosed() {
|
||||
super.onClosed();
|
||||
dropdown = null;
|
||||
}
|
||||
|
||||
};
|
||||
var script = String.format("onedev.server.inputassist.assistOpened('%s', '%s', '%s');",
|
||||
getComponent().getMarkupId(), dropdown.getMarkupId(), JavaScriptEscape.escapeJavaScript(status.getContent()));
|
||||
target.appendJavaScript(script);
|
||||
} else {
|
||||
Component content = dropdown.getContent();
|
||||
Component newContent = new AssistPanel(content.getId(), getComponent(), suggestions, getHints(status)) {
|
||||
|
||||
@Override
|
||||
protected void onClose(AjaxRequestTarget target) {
|
||||
close();
|
||||
}
|
||||
|
||||
};
|
||||
content.replaceWith(newContent);
|
||||
target.add(newContent);
|
||||
|
||||
AlignTarget alignTarget = new ComponentTarget(getComponent(), anchor);
|
||||
var script = String.format("$('#%s').data('alignment').target=%s;", dropdown.getMarkupId(), alignTarget);
|
||||
target.prependJavaScript(script);
|
||||
|
||||
script = String.format("onedev.server.inputassist.assistUpdated('%s', '%s', '%s');",
|
||||
getComponent().getMarkupId(), dropdown.getMarkupId(), JavaScriptEscape.escapeJavaScript(status.getContent()));
|
||||
target.appendJavaScript(script);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (dropdown != null)
|
||||
dropdown.close();
|
||||
@ -242,10 +291,10 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
|
||||
translations.put("inactiveHelp", _T("<span class='keycap'>Tab</span> to complete."));
|
||||
String script;
|
||||
try {
|
||||
script = String.format("onedev.server.inputassist.onDomReady('%s', %s, %s);",
|
||||
script = String.format("onedev.server.inputassist.onDomReady('%s', %s, %b, %s);",
|
||||
getComponent().getMarkupId(true),
|
||||
getCallbackFunction(explicit("type"), explicit("input"), explicit("caret"), explicit("event")),
|
||||
AppLoader.getInstance(ObjectMapper.class).writeValueAsString(translations));
|
||||
getNaturalLanguageTranslator() != null, AppLoader.getInstance(ObjectMapper.class).writeValueAsString(translations));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@ -274,4 +323,10 @@ public abstract class InputAssistBehavior extends AbstractPostAjaxBehavior {
|
||||
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package io.onedev.server.web.behavior.inputassist;
|
||||
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
|
||||
public abstract class NaturalLanguageTranslator {
|
||||
|
||||
private static final int MAX_QUERY_LENGTH = 512;
|
||||
|
||||
private final ChatModel model;
|
||||
|
||||
public NaturalLanguageTranslator(ChatModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public abstract String getQueryDescription();
|
||||
|
||||
public String translate(String naturalLanguage) {
|
||||
if (naturalLanguage.length() > MAX_QUERY_LENGTH) {
|
||||
throw new ExplicitException("Query is too long. Max " + MAX_QUERY_LENGTH + " characters");
|
||||
}
|
||||
var systemMessage = new SystemMessage("""
|
||||
You are a query translator that converts natural language into structured query string described as below:
|
||||
|
||||
""" + getQueryDescription() + """
|
||||
|
||||
|
||||
Also note that the user input might also be a structure query but with some syntax errors. In that case,
|
||||
fix syntax errors and return the corrected query string.
|
||||
|
||||
IMPORTANT: only structured query string should be returned, no other text or comments.
|
||||
""");
|
||||
|
||||
var userMessage = new UserMessage(naturalLanguage);
|
||||
|
||||
return model.chat(systemMessage, userMessage).aiMessage().text();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
onedev.server.inputassist = {
|
||||
onDomReady: function(inputId, callback, translations) {
|
||||
onDomReady: function(inputId, callback, supportNaturalLanguageInput, translations) {
|
||||
var $input = $("#" + inputId);
|
||||
$input.data("supportNaturalLanguageInput", supportNaturalLanguageInput);
|
||||
onedev.server.inputassist.translations = translations;
|
||||
|
||||
onedev.server.inputassist.markErrors(inputId, []);
|
||||
@ -21,8 +22,20 @@ onedev.server.inputassist = {
|
||||
if (value != $input.data("prevValue") || caret != $input.data("prevCaret") || !$input.data("dropdown")) {
|
||||
$input.data("prevValue", value);
|
||||
$input.data("prevCaret", caret);
|
||||
if ($input.is(":focus") && e.keyCode != 27 && e.keyCode != 13) // ignore esc, enter
|
||||
callback("input", value, caret, e.type);
|
||||
if ($input.is(":focus") && e.keyCode != 27 && e.keyCode != 13) { // ignore esc, enter
|
||||
if (caret != -1 && $input.data("supportNaturalLanguageInput")) {
|
||||
var contentBeforeCaret = value.substring(0, caret);
|
||||
if (contentBeforeCaret.endsWith("🤖")) {
|
||||
var contentToTranslate = contentBeforeCaret.substring(0, contentBeforeCaret.length-2);
|
||||
onedev.server.inputassist.showNaturalLanguageTranslatingIndicator($input, caret);
|
||||
callback("translate", contentToTranslate, caret);
|
||||
} else {
|
||||
callback("input", value, caret, e.type);
|
||||
}
|
||||
} else {
|
||||
callback("input", value, caret, e.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.trim().length == 0)
|
||||
onedev.server.inputassist.markErrors(inputId, []);
|
||||
@ -133,28 +146,28 @@ onedev.server.inputassist = {
|
||||
markErrors: function(inputId, errors) {
|
||||
var $input = $("#" + inputId);
|
||||
$input.data("errors", errors);
|
||||
var $parent = $input.closest("form");
|
||||
$parent.css("position", "relative");
|
||||
$parent.find(">.input-error-mark").remove();
|
||||
var $form = $input.closest("form");
|
||||
$form.css("position", "relative");
|
||||
$form.find(">.input-error-mark").remove();
|
||||
if ($input.val().length != 0) {
|
||||
for (var i in errors) {
|
||||
var error = errors[i];
|
||||
var fromCoord = getCaretCoordinates($input[0], error.from);
|
||||
var toCoord = getCaretCoordinates($input[0], error.to+1);
|
||||
var $error = $("<div class='input-error-mark'></div>");
|
||||
$error.appendTo($parent);
|
||||
$error.appendTo($form);
|
||||
var inputCoord = $input.offset();
|
||||
var parentCoord = $parent.offset();
|
||||
var parentCoord = $form.offset();
|
||||
var textHeight = 16;
|
||||
var errorHeight = 5;
|
||||
var minWidth = 5;
|
||||
var textMargin = 10;
|
||||
var left = fromCoord.left + inputCoord.left - parentCoord.left - $input.scrollLeft();
|
||||
if (left < $input.offset().left - $parent.offset().left + textMargin)
|
||||
left = $input.offset().left - $parent.offset().left + textMargin;
|
||||
if (left < $input.offset().left - $form.offset().left + textMargin)
|
||||
left = $input.offset().left - $form.offset().left + textMargin;
|
||||
var top = fromCoord.top + inputCoord.top - parentCoord.top + textHeight - $input.scrollTop();
|
||||
if (top < $input.offset().top - $parent.offset().top + textMargin)
|
||||
top = $input.offset().top - $parent.offset().top + textMargin;
|
||||
if (top < $input.offset().top - $form.offset().top + textMargin)
|
||||
top = $input.offset().top - $form.offset().top + textMargin;
|
||||
$error.css({left: left, top: top});
|
||||
$error.outerWidth(Math.max(toCoord.left-fromCoord.left, minWidth));
|
||||
$error.outerHeight(errorHeight);
|
||||
@ -172,6 +185,19 @@ onedev.server.inputassist = {
|
||||
$input.trigger("assist");
|
||||
},
|
||||
|
||||
naturalLanguageTranslated: function(inputId, translatedInput) {
|
||||
var $input = $("#" + inputId);
|
||||
onedev.server.inputassist.hideNaturalLanguageTranslatingIndicator($input);
|
||||
var caret = $input.caret();
|
||||
var content = translatedInput + $input.val().substring(caret);
|
||||
$input.val(content);
|
||||
$input.caret(translatedInput.length);
|
||||
$input.blur();
|
||||
$input.focus();
|
||||
$input.trigger("input");
|
||||
$input.trigger("assist");
|
||||
},
|
||||
|
||||
assistOpened: function(inputId, dropdownId, inputContent) {
|
||||
var $input = $("#" + inputId);
|
||||
var $dropdown = $("#" + dropdownId);
|
||||
@ -217,5 +243,36 @@ onedev.server.inputassist = {
|
||||
} else {
|
||||
$dropdown.find(".help .complete").empty().append(onedev.server.inputassist.translations["inactiveHelp"]);
|
||||
}
|
||||
},
|
||||
|
||||
showNaturalLanguageTranslatingIndicator: function($input, caret) {
|
||||
$input.prop("readonly", true);
|
||||
var $form = $input.closest("form");
|
||||
$form.css("position", "relative");
|
||||
|
||||
var coord = getCaretCoordinates($input[0], caret);
|
||||
var $indicator;
|
||||
if (onedev.server.isDarkMode())
|
||||
$indicator = $("<div class='ajax-loading-indicator natural-language-translating-indicator'><img src='/~img/dark-ajax-indicator.gif' width='16' height='16'></div>");
|
||||
else
|
||||
$indicator = $("<div class='ajax-loading-indicator natural-language-translating-indicator'><img src='/~img/ajax-indicator.gif' width='16' height='16'></div>");
|
||||
$indicator.appendTo($form);
|
||||
|
||||
var inputCoord = $input.offset();
|
||||
var parentCoord = $form.offset();
|
||||
var left = coord.left + inputCoord.left - parentCoord.left - $input.scrollLeft() + 5;
|
||||
var top = coord.top + inputCoord.top - parentCoord.top - $input.scrollTop() - 3;
|
||||
|
||||
$indicator.css({
|
||||
position: "absolute",
|
||||
left: left + "px",
|
||||
top: top + "px",
|
||||
zIndex: 1000
|
||||
});
|
||||
},
|
||||
|
||||
hideNaturalLanguageTranslatingIndicator: function($input) {
|
||||
$input.prop("readonly", false);
|
||||
$input.closest("form").children(".natural-language-translating-indicator").remove();
|
||||
}
|
||||
};
|
||||
@ -8,6 +8,7 @@ import org.apache.wicket.request.mapper.CompoundRequestMapper;
|
||||
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.web.asset.icon.IconScope;
|
||||
import io.onedev.server.web.page.admin.aisetting.AISettingPage;
|
||||
import io.onedev.server.web.page.admin.alertsettings.AlertSettingPage;
|
||||
import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage;
|
||||
import io.onedev.server.web.page.admin.brandingsetting.BrandingSettingPage;
|
||||
@ -346,6 +347,7 @@ public class BaseUrlMapper extends CompoundRequestMapper {
|
||||
add(new BasePageMapper("~administration/labels", LabelManagementPage.class));
|
||||
add(new BasePageMapper("~administration/settings/alert", AlertSettingPage.class));
|
||||
add(new BasePageMapper("~administration/settings/performance", PerformanceSettingPage.class));
|
||||
add(new BasePageMapper("~administration/settings/ai", AISettingPage.class));
|
||||
add(new BasePageMapper("~administration/settings/backup", DatabaseBackupPage.class));
|
||||
add(new BasePageMapper("~administration/settings/authenticator", AuthenticatorPage.class));
|
||||
add(new BasePageMapper("~administration/settings/sso-providers", SsoProviderListPage.class));
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<wicket:extend>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form wicket:id="form" class="leave-confirm">
|
||||
<div wicket:id="editor" class="mb-4"></div>
|
||||
<input type="submit" class="btn btn-primary dirty-aware" t:value="Save Settings">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</wicket:extend>
|
||||
@ -0,0 +1,58 @@
|
||||
package io.onedev.server.web.page.admin.aisetting;
|
||||
|
||||
import static io.onedev.server.web.translation.Translation._T;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.markup.html.basic.Label;
|
||||
import org.apache.wicket.markup.html.form.Form;
|
||||
import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.model.support.administration.AISetting;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.web.editable.BeanContext;
|
||||
import io.onedev.server.web.page.admin.AdministrationPage;
|
||||
|
||||
public class AISettingPage extends AdministrationPage {
|
||||
|
||||
@Inject
|
||||
private SettingService settingService;
|
||||
|
||||
public AISettingPage(PageParameters params) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInitialize() {
|
||||
super.onInitialize();
|
||||
|
||||
AISetting aiSetting = settingService.getAISetting();
|
||||
var oldAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML();
|
||||
|
||||
Form<?> form = new Form<Void>("form") {
|
||||
|
||||
@Override
|
||||
protected void onSubmit() {
|
||||
super.onSubmit();
|
||||
var newAuditContent = VersionedXmlDoc.fromBean(aiSetting).toXML();
|
||||
settingService.saveAISetting(aiSetting);
|
||||
auditService.audit(null, "changed AI settings", oldAuditContent, newAuditContent);
|
||||
getSession().success(_T("AI settings have been saved"));
|
||||
|
||||
setResponsePage(AISettingPage.class);
|
||||
}
|
||||
|
||||
};
|
||||
form.add(BeanContext.edit("editor", aiSetting));
|
||||
|
||||
add(form);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Component newTopbarTitle(String componentId) {
|
||||
return new Label(componentId, _T("AI Settings"));
|
||||
}
|
||||
|
||||
}
|
||||
@ -177,7 +177,7 @@ onedev.server = {
|
||||
setupAjaxLoadingIndicator: function() {
|
||||
var ongoingAjaxRequests = 0;
|
||||
Wicket.Event.subscribe('/ajax/call/beforeSend', function(e, attributes) {
|
||||
if (ongoingAjaxRequests == 0) {
|
||||
if (ongoingAjaxRequests == 0 && $(".ajax-loading-indicator:visible").length == 0) {
|
||||
var $ajaxLoadingIndicator = $("#ajax-loading-indicator");
|
||||
if ($ajaxLoadingIndicator[0].timer)
|
||||
clearTimeout($ajaxLoadingIndicator[0].timer);
|
||||
|
||||
@ -65,12 +65,12 @@ import io.onedev.server.OneDev;
|
||||
import io.onedev.server.ServerConfig;
|
||||
import io.onedev.server.SubscriptionService;
|
||||
import io.onedev.server.cluster.ClusterService;
|
||||
import io.onedev.server.service.AlertService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.model.Alert;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.persistence.dao.EntityCriteria;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.service.AlertService;
|
||||
import io.onedev.server.service.SettingService;
|
||||
import io.onedev.server.updatecheck.UpdateCheckService;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.web.WebConstants;
|
||||
@ -91,6 +91,7 @@ import io.onedev.server.web.component.svg.SpriteImage;
|
||||
import io.onedev.server.web.component.user.UserAvatar;
|
||||
import io.onedev.server.web.editable.EditableUtils;
|
||||
import io.onedev.server.web.page.HomePage;
|
||||
import io.onedev.server.web.page.admin.aisetting.AISettingPage;
|
||||
import io.onedev.server.web.page.admin.alertsettings.AlertSettingPage;
|
||||
import io.onedev.server.web.page.admin.authenticator.AuthenticatorPage;
|
||||
import io.onedev.server.web.page.admin.brandingsetting.BrandingSettingPage;
|
||||
@ -338,6 +339,9 @@ public abstract class LayoutPage extends BasePage {
|
||||
administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("Groovy Scripts"),
|
||||
GroovyScriptListPage.class, new PageParameters()));
|
||||
|
||||
administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("AI Settings"),
|
||||
AISettingPage.class, new PageParameters()));
|
||||
|
||||
administrationMenuItems.add(new SidebarMenuItem.Page(null, _T("Branding"),
|
||||
BrandingSettingPage.class, new PageParameters()));
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ public class TestPage extends BasePage {
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
System.out.println("bb");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_de extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "AI-Modell");
|
||||
m.put("AI Model Provider", "AI-Modell-Anbieter");
|
||||
m.put("Model", "Modell");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_es extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "Modelo de IA");
|
||||
m.put("AI Model Provider", "Proveedor de Modelo de IA");
|
||||
m.put("Model", "Modelo");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_fr extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "Modèle d'IA");
|
||||
m.put("AI Model Provider", "Fournisseur de modèle d'IA");
|
||||
m.put("Model", "Modèle");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_it extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "Modello AI");
|
||||
m.put("AI Model Provider", "Fornitore di Modelli AI");
|
||||
m.put("Model", "Modello");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_ja extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "AIモデル");
|
||||
m.put("AI Model Provider", "AIモデルプロバイダー");
|
||||
m.put("Model", "モデル");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_ko extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "AI 모델");
|
||||
m.put("AI Model Provider", "AI 모델 제공자");
|
||||
m.put("Model", "모델");
|
||||
m.put("Open AI", "오픈 AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4635,6 +4635,10 @@ public class Translation_pt extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "{javax.validation.constraints.NotEmpty.message}");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "{javax.validation.constraints.NotNull.message}");
|
||||
m.put("{javax.validation.constraints.Size.message}", "{javax.validation.constraints.Size.message}");
|
||||
m.put("AI Model", "Modelo de IA");
|
||||
m.put("AI Model Provider", "Provedor de Modelo de IA");
|
||||
m.put("Model", "Modelo");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -4653,6 +4653,10 @@ public class Translation_zh extends TranslationResourceBundle {
|
||||
m.put("{javax.validation.constraints.NotEmpty.message}", "不能为空");
|
||||
m.put("{javax.validation.constraints.NotNull.message}", "不能为空");
|
||||
m.put("{javax.validation.constraints.Size.message}", "至少需要指定一个值");
|
||||
m.put("AI Model", "AI 模型");
|
||||
m.put("AI Model Provider", "AI 模型提供方");
|
||||
m.put("Model", "模型");
|
||||
m.put("Open AI", "Open AI");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user