diff --git a/agents/semantickernel-agents-core/pom.xml b/agents/semantickernel-agents-core/pom.xml new file mode 100644 index 000000000..4eec1b768 --- /dev/null +++ b/agents/semantickernel-agents-core/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + com.microsoft.semantic-kernel + semantickernel-parent + 1.4.4-SNAPSHOT + ../../pom.xml + + + semantickernel-agents-core + + Semantic Kernel Chat Completion Agent + Chat Completion Agent for Semantic Kernel + + + + com.microsoft.semantic-kernel + semantickernel-api + + + + \ No newline at end of file diff --git a/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java new file mode 100644 index 000000000..f8423f3d7 --- /dev/null +++ b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatCompletionAgent.java @@ -0,0 +1,303 @@ +package com.microsoft.semantickernel.agents.chatcompletion; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.agents.AgentInvokeOptions; +import com.microsoft.semantickernel.agents.AgentResponseItem; +import com.microsoft.semantickernel.agents.AgentThread; +import com.microsoft.semantickernel.agents.KernelAgent; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.InvocationReturnMode; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory; +import com.microsoft.semantickernel.services.ServiceNotFoundException; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.stream.Collectors; + +public class ChatCompletionAgent extends KernelAgent { + + private ChatCompletionAgent( + String id, + String name, + String description, + Kernel kernel, + KernelArguments kernelArguments, + InvocationContext context, + String instructions, + PromptTemplate template + ) { + super( + id, + name, + description, + kernel, + kernelArguments, + context, + instructions, + template + ); + } + + /** + * Invoke the agent with the given chat history. + * + * @param messages The chat history to process + * @param thread The agent thread to use + * @param options The options for invoking the agent + * @return A Mono containing the agent response + */ + @Override + public Mono>>> invokeAsync( + List> messages, + AgentThread thread, + @Nullable AgentInvokeOptions options + ) { + return ensureThreadExistsWithMessagesAsync(messages, thread, ChatHistoryAgentThread::new) + .cast(ChatHistoryAgentThread.class) + .flatMap(agentThread -> { + // Extract the chat history from the thread + ChatHistory history = new ChatHistory( + agentThread.getChatHistory().getMessages() + ); + + // Invoke the agent with the chat history + return internalInvokeAsync( + history, + options + ) + .flatMapMany(Flux::fromIterable) + // notify on the new thread instance + .concatMap(agentMessage -> this.notifyThreadOfNewMessageAsync(agentThread, agentMessage).thenReturn(agentMessage)) + .collectList() + .map(chatMessageContents -> + chatMessageContents.stream() + .map(message -> new AgentResponseItem>(message, agentThread)) + .collect(Collectors.toList()) + ); + }); + } + + private Mono>> internalInvokeAsync( + ChatHistory history, + @Nullable AgentInvokeOptions options + ) { + if (options == null) { + options = new AgentInvokeOptions(); + } + + final Kernel kernel = options.getKernel() != null ? options.getKernel() : this.kernel; + final KernelArguments arguments = mergeArguments(options.getKernelArguments()); + final String additionalInstructions = options.getAdditionalInstructions(); + final InvocationContext invocationContext = options.getInvocationContext() != null ? options.getInvocationContext() : this.invocationContext; + + try { + ChatCompletionService chatCompletionService = kernel.getService(ChatCompletionService.class, arguments); + + PromptExecutionSettings executionSettings = invocationContext != null && invocationContext.getPromptExecutionSettings() != null + ? invocationContext.getPromptExecutionSettings() + : kernelArguments.getExecutionSettings().get(chatCompletionService.getServiceId()); + + ToolCallBehavior toolCallBehavior = invocationContext != null + ? invocationContext.getToolCallBehavior() + : ToolCallBehavior.allowAllKernelFunctions(true); + + // Build base invocation context + InvocationContext.Builder builder = InvocationContext.builder() + .withPromptExecutionSettings(executionSettings) + .withToolCallBehavior(toolCallBehavior) + .withReturnMode(InvocationReturnMode.NEW_MESSAGES_ONLY); + + if (invocationContext != null) { + builder = builder + .withTelemetry(invocationContext.getTelemetry()) + .withContextVariableConverter(invocationContext.getContextVariableTypes()) + .withKernelHooks(invocationContext.getKernelHooks()); + } + + InvocationContext agentInvocationContext = builder.build(); + + return renderInstructionsAsync(kernel, arguments, agentInvocationContext).flatMap( + instructions -> { + // Create a new chat history with the instructions + ChatHistory chat = new ChatHistory( + instructions + ); + + // Add agent additional instructions + if (additionalInstructions != null) { + chat.addMessage(new ChatMessageContent<>( + AuthorRole.SYSTEM, + additionalInstructions + )); + } + + // Add the chat history to the new chat + chat.addAll(history); + + return chatCompletionService.getChatMessageContentsAsync(chat, kernel, agentInvocationContext); + } + ); + + } catch (ServiceNotFoundException e) { + return Mono.error(e); + } + } + + + @Override + public Mono notifyThreadOfNewMessageAsync(AgentThread thread, ChatMessageContent message) { + return Mono.defer(() -> { + return thread.onNewMessageAsync(message); + }); + } + + /** + * Builder for creating instances of ChatCompletionAgent. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SemanticKernelBuilder { + private String id; + private String name; + private String description; + private Kernel kernel; + private KernelArguments kernelArguments; + private InvocationContext invocationContext; + private String instructions; + private PromptTemplate template; + + /** + * Set the ID of the agent. + * + * @param id The ID of the agent. + */ + public Builder withId(String id) { + this.id = id; + return this; + } + + /** + * Set the name of the agent. + * + * @param name The name of the agent. + */ + public Builder withName(String name) { + this.name = name; + return this; + } + + /** + * Set the description of the agent. + * + * @param description The description of the agent. + */ + public Builder withDescription(String description) { + this.description = description; + return this; + } + + /** + * Set the kernel to use for the agent. + * + * @param kernel The kernel to use. + */ + public Builder withKernel(Kernel kernel) { + this.kernel = kernel; + return this; + } + + /** + * Set the kernel arguments to use for the agent. + * + * @param KernelArguments The kernel arguments to use. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withKernelArguments(KernelArguments KernelArguments) { + this.kernelArguments = KernelArguments; + return this; + } + + /** + * Set the instructions for the agent. + * + * @param instructions The instructions for the agent. + */ + public Builder withInstructions(String instructions) { + this.instructions = instructions; + return this; + } + + /** + * Set the invocation context for the agent. + * + * @param invocationContext The invocation context to use. + */ + public Builder withInvocationContext(InvocationContext invocationContext) { + this.invocationContext = invocationContext; + return this; + } + + /** + * Set the template for the agent. + * + * @param template The template to use. + */ + public Builder withTemplate(PromptTemplate template) { + this.template = template; + return this; + } + + /** + * Build the ChatCompletionAgent instance. + * + * @return The ChatCompletionAgent instance. + */ + public ChatCompletionAgent build() { + return new ChatCompletionAgent( + id, + name, + description, + kernel, + kernelArguments, + invocationContext, + instructions, + template + ); + } + + /** + * Build the ChatCompletionAgent instance with the given prompt template config and factory. + * + * @param promptTemplateConfig The prompt template config to use. + * @param promptTemplateFactory The prompt template factory to use. + * @return The ChatCompletionAgent instance. + */ + public ChatCompletionAgent build(PromptTemplateConfig promptTemplateConfig, PromptTemplateFactory promptTemplateFactory) { + return new ChatCompletionAgent( + id, + name, + description, + kernel, + kernelArguments, + invocationContext, + promptTemplateConfig.getTemplate(), + promptTemplateFactory.tryCreate(promptTemplateConfig) + ); + } + } +} diff --git a/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java new file mode 100644 index 000000000..1a68f8c44 --- /dev/null +++ b/agents/semantickernel-agents-core/src/main/java/com/microsoft/semantickernel/agents/chatcompletion/ChatHistoryAgentThread.java @@ -0,0 +1,116 @@ +package com.microsoft.semantickernel.agents.chatcompletion; + +import com.microsoft.semantickernel.agents.AgentThread; +import com.microsoft.semantickernel.agents.BaseAgentThread; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.UUID; + +public class ChatHistoryAgentThread extends BaseAgentThread { + private ChatHistory chatHistory; + + public ChatHistoryAgentThread() { + this(UUID.randomUUID().toString(), new ChatHistory()); + } + + /** + * Constructor for com.microsoft.semantickernel.agents.chatcompletion.ChatHistoryAgentThread. + * + * @param id The ID of the thread. + * @param chatHistory The chat history. + */ + public ChatHistoryAgentThread(String id, @Nullable ChatHistory chatHistory) { + super(id); + this.chatHistory = chatHistory != null ? chatHistory : new ChatHistory(); + } + + /** + * Get the chat history. + * + * @return The chat history. + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public ChatHistory getChatHistory() { + return chatHistory; + } + + @Override + public Mono createAsync() { + if (this.id == null) { + this.id = UUID.randomUUID().toString(); + chatHistory = new ChatHistory(); + } + return Mono.just(id); + } + + @Override + public Mono deleteAsync() { + return Mono.fromRunnable(chatHistory::clear); + } + + /** + * Create a copy of the thread. + * + * @return A new instance of the thread. + */ + @Override + public ChatHistoryAgentThread copy() { + return new ChatHistoryAgentThread(this.id, new ChatHistory(chatHistory.getMessages())); + } + + @Override + public Mono onNewMessageAsync(ChatMessageContent newMessage) { + return Mono.fromRunnable(() -> { + chatHistory.addMessage(newMessage); + }); + } + + public List> getMessages() { + return chatHistory.getMessages(); + } + + + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SemanticKernelBuilder { + private String id; + private ChatHistory chatHistory; + + /** + * Set the ID of the thread. + * + * @param id The ID of the thread. + * @return The builder instance. + */ + public Builder withId(String id) { + this.id = id; + return this; + } + + /** + * Set the chat history. + * + * @param chatHistory The chat history. + * @return The builder instance. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withChatHistory(ChatHistory chatHistory) { + this.chatHistory = chatHistory; + return this; + } + + @Override + public ChatHistoryAgentThread build() { + return new ChatHistoryAgentThread(id, chatHistory); + } + } +} diff --git a/pom.xml b/pom.xml index 30963f54b..0d31715dd 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,7 @@ data/semantickernel-data-azureaisearch data/semantickernel-data-jdbc data/semantickernel-data-redis + agents/semantickernel-agents-core diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml b/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml index 1f6bc1811..3ccc114c1 100644 --- a/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/pom.xml @@ -49,6 +49,11 @@ semantickernel-data-redis + + com.microsoft.semantic-kernel + semantickernel-agents-core + + com.microsoft.semantic-kernel semantickernel-experimental diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java new file mode 100644 index 000000000..180ec8ed2 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubModel.java @@ -0,0 +1,219 @@ +package com.microsoft.semantickernel.samples.plugins.github; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +public abstract class GitHubModel { + public final static ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + @Override + public String toString() { + try { + return objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static class User extends GitHubModel { + @JsonProperty("login") + private String login; + @JsonProperty("id") + private long id; + @JsonProperty("name") + private String name; + @JsonProperty("company") + private String company; + @JsonProperty("html_url") + private String url; + @JsonCreator + public User(@JsonProperty("login") String login, + @JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("company") String company, + @JsonProperty("html_url") String url) { + this.login = login; + this.id = id; + this.name = name; + this.company = company; + this.url = url; + } + + public String getLogin() { + return login; + } + public long getId() { + return id; + } + public String getName() { + return name; + } + public String getCompany() { + return company; + } + public String getUrl() { + return url; + } + } + + public static class Repository extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("full_name") + private String name; + @JsonProperty("description") + private String description; + @JsonProperty("html_url") + private String url; + @JsonCreator + public Repository(@JsonProperty("id") long id, + @JsonProperty("full_name") String name, + @JsonProperty("description") String description, + @JsonProperty("html_url") String url) { + this.id = id; + this.name = name; + this.description = description; + this.url = url; + } + + public long getId() { + return id; + } + public String getName() { + return name; + } + public String getDescription() { + return description; + } + public String getUrl() { + return url; + } + + @Override + public String toString() { + try { + return objectMapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + public static class Issue extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("number") + private long number; + @JsonProperty("title") + private String title; + @JsonProperty("state") + private String state; + @JsonProperty("html_url") + private String url; + @JsonProperty("labels") + private Label[] labels; + @JsonProperty("created_at") + private String createdAt; + @JsonProperty("closed_at") + private String closedAt; + + @JsonCreator + public Issue(@JsonProperty("id") long id, + @JsonProperty("number") long number, + @JsonProperty("title") String title, + @JsonProperty("state") String state, + @JsonProperty("html_url") String url, + @JsonProperty("labels") Label[] labels, + @JsonProperty("created_at") String createdAt, + @JsonProperty("closed_at") String closedAt) { + this.id = id; + this.number = number; + this.title = title; + this.state = state; + this.url = url; + this.labels = labels; + this.createdAt = createdAt; + this.closedAt = closedAt; + } + + public long getId() { + return id; + } + public long getNumber() { + return number; + } + public String getTitle() { + return title; + } + public String getState() { + return state; + } + public String getUrl() { + return url; + } + public Label[] getLabels() { + return labels; + } + public String getCreatedAt() { + return createdAt; + } + public String getClosedAt() { + return closedAt; + } + } + + public static class IssueDetail extends Issue { + @JsonProperty("body") + private String body; + + @JsonCreator + public IssueDetail(@JsonProperty("id") long id, + @JsonProperty("number") long number, + @JsonProperty("title") String title, + @JsonProperty("state") String state, + @JsonProperty("html_url") String url, + @JsonProperty("labels") Label[] labels, + @JsonProperty("created_at") String createdAt, + @JsonProperty("closed_at") String closedAt, + @JsonProperty("body") String body) { + super(id, number, title, state, url, labels, createdAt, closedAt); + this.body = body; + } + + public String getBody() { + return body; + } + } + + public static class Label extends GitHubModel { + @JsonProperty("id") + private long id; + @JsonProperty("name") + private String name; + @JsonProperty("description") + private String description; + + @JsonCreator + public Label(@JsonProperty("id") long id, + @JsonProperty("name") String name, + @JsonProperty("description") String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public long getId() { + return id; + } + public String getName() { + return name; + } + public String getDescription() { + return description; + } + } +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java new file mode 100644 index 000000000..d3c59a152 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/plugins/github/GitHubPlugin.java @@ -0,0 +1,165 @@ +package com.microsoft.semantickernel.samples.plugins.github; + +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction; +import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter; + +import java.io.IOException; +import java.util.List; + +public class GitHubPlugin { + public static final String baseUrl = "https://api.github.com"; + private final String token; + + public GitHubPlugin(String token) { + this.token = token; + } + + @DefineKernelFunction(name = "get_user_info", description = "Get user information from GitHub", + returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$User") + public Mono getUserProfileAsync() { + HttpClient client = createClient(); + + return makeRequestAsync(client, "/user") + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.User.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubUser", e); + } + }); + } + + @DefineKernelFunction(name = "get_repo_info", description = "Get repository information from GitHub", + returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$Repository") + public Mono getRepositoryAsync( + @KernelFunctionParameter( + name = "organization", + description = "The name of the repository to retrieve information for" + ) String organization, + @KernelFunctionParameter( + name = "repo_name", + description = "The name of the repository to retrieve information for" + ) String repoName + ) { + HttpClient client = createClient(); + + return makeRequestAsync(client, String.format("/repos/%s/%s", organization, repoName)) + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.Repository.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubRepository", e); + } + }); + } + + @DefineKernelFunction(name = "get_issues", description = "Get issues from GitHub", + returnType = "java.util.List") + public Mono> getIssuesAsync( + @KernelFunctionParameter( + name = "organization", + description = "The name of the organization to retrieve issues for" + ) String organization, + @KernelFunctionParameter( + name = "repo_name", + description = "The name of the repository to retrieve issues for" + ) String repoName, + @KernelFunctionParameter( + name = "max_results", + description = "The maximum number of issues to retrieve", + required = false, + defaultValue = "10", + type = int.class + ) int maxResults, + @KernelFunctionParameter( + name = "state", + description = "The state of the issues to retrieve", + required = false, + defaultValue = "open" + ) String state, + @KernelFunctionParameter( + name = "assignee", + description = "The assignee of the issues to retrieve", + required = false + ) String assignee + ) { + HttpClient client = createClient(); + + String query = String.format("/repos/%s/%s/issues", organization, repoName); + query = buildQueryString(query, "state", state); + query = buildQueryString(query, "assignee", assignee); + query = buildQueryString(query, "per_page", String.valueOf(maxResults)); + + return makeRequestAsync(client, query) + .flatMap(json -> { + try { + GitHubModel.Issue[] issues = GitHubModel.objectMapper.readValue(json, GitHubModel.Issue[].class); + return Mono.just(List.of(issues)); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubIssues", e); + } + }); + } + + @DefineKernelFunction(name = "get_issue_detail_info", description = "Get detail information of a single issue from GitHub", + returnType = "com.microsoft.semantickernel.samples.plugins.github.GitHubModel$IssueDetail") + public GitHubModel.IssueDetail getIssueDetailAsync( + @KernelFunctionParameter( + name = "organization", + description = "The name of the repository to retrieve information for" + ) String organization, + @KernelFunctionParameter( + name = "repo_name", + description = "The name of the repository to retrieve information for" + ) String repoName, + @KernelFunctionParameter( + name = "issue_number", + description = "The issue number to retrieve information for", + type = int.class + ) int issueNumber + ) { + HttpClient client = createClient(); + + return makeRequestAsync(client, String.format("/repos/%s/%s/issues/%d", organization, repoName, issueNumber)) + .map(json -> { + try { + return GitHubModel.objectMapper.readValue(json, GitHubModel.IssueDetail.class); + } catch (IOException e) { + throw new IllegalStateException("Failed to deserialize GitHubIssue", e); + } + }).block(); + } + + private HttpClient createClient() { + return HttpClient.create() + .baseUrl(baseUrl) + .headers(headers -> { + headers.add("User-Agent", "request"); + headers.add("Accept", "application/vnd.github+json"); + headers.add("Authorization", "Bearer " + token); + headers.add("X-GitHub-Api-Version", "2022-11-28"); + }); + } + + private static String buildQueryString(String path, String param, String value) { + if (value == null || value.isEmpty() || value.equals(KernelFunctionParameter.NO_DEFAULT_VALUE)) { + return path; + } + + return path + (path.contains("?") ? "&" : "?") + param + "=" + value; + } + + private Mono makeRequestAsync(HttpClient client, String path) { + return client + .get() + .uri(path) + .responseSingle((res, content) -> { + if (res.status().code() != 200) { + return Mono.error(new IllegalStateException("Request failed: " + res.status())); + } + return content.asString(); + }); + } +} diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/agents/CompletionAgent.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/agents/CompletionAgent.java new file mode 100644 index 000000000..1e5a665a9 --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/agents/CompletionAgent.java @@ -0,0 +1,140 @@ +package com.microsoft.semantickernel.samples.syntaxexamples.agents; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.agents.AgentInvokeOptions; +import com.microsoft.semantickernel.agents.chatcompletion.ChatCompletionAgent; +import com.microsoft.semantickernel.agents.chatcompletion.ChatHistoryAgentThread; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.implementation.templateengine.tokenizer.DefaultPromptTemplate; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.plugin.KernelPluginFactory; +import com.microsoft.semantickernel.samples.plugins.github.GitHubModel; +import com.microsoft.semantickernel.samples.plugins.github.GitHubPlugin; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; + +import java.util.List; +import java.util.Scanner; + +public class CompletionAgent { + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("MODEL_ID", "gpt-4o"); + + private static final String GITHUB_PAT = System.getenv("GITHUB_PAT"); + public static void main(String[] args) { + System.out.println("======== ChatCompletion Agent ========"); + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + System.out.println("------------------------"); + + ChatCompletionService chatCompletion = OpenAIChatCompletion.builder() + .withModelId(MODEL_ID) + .withOpenAIAsyncClient(client) + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletion) + .withPlugin(KernelPluginFactory.createFromObject(new GitHubPlugin(GITHUB_PAT), + "GitHubPlugin")) + .build(); + + InvocationContext invocationContext = InvocationContext.builder() + .withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true)) + .withContextVariableConverter(new ContextVariableTypeConverter<>( + GitHubModel.Issue.class, + o -> (GitHubModel.Issue) o, + o -> o.toString(), + s -> null + )) + .build(); + + ChatCompletionAgent agent = ChatCompletionAgent.builder() + .withKernel(kernel) + .withKernelArguments( + KernelArguments.builder() + .withVariable("repository", "microsoft/semantic-kernel-java") + .withExecutionSettings(PromptExecutionSettings.builder() + .build()) + .build() + ) + .withInvocationContext(invocationContext) + .withTemplate( + DefaultPromptTemplate.build( + PromptTemplateConfig.builder() + .withTemplate( + """ + You are an agent designed to query and retrieve information from a single GitHub repository in a read-only manner. + You are also able to access the profile of the active user. + + Use the current date and time to provide up-to-date details or time-sensitive responses. + + The repository you are querying is a public repository with the following name: {{$repository}} + + The current date and time is: {{$now}}. + """ + ) + .build() + ) + ).build(); + + ChatHistoryAgentThread agentThread = new ChatHistoryAgentThread(); + Scanner scanner = new Scanner(System.in); + + while (true) { + System.out.print("> "); + + String input = scanner.nextLine(); + if (input.equalsIgnoreCase("exit")) { + break; + } + + var message = new ChatMessageContent<>(AuthorRole.USER, input); + KernelArguments arguments = KernelArguments.builder() + .withVariable("now", System.currentTimeMillis()) + .build(); + + var response = agent.invokeAsync( + List.of(message), + agentThread, + AgentInvokeOptions.builder() + .withKernel(kernel) + .withKernelArguments(arguments) + .build() + ).block(); + + var lastResponse = response.get(response.size() - 1); + + System.out.println("> " + lastResponse.getMessage()); + agentThread = (ChatHistoryAgentThread) lastResponse.getThread(); + } + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/Agent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/Agent.java new file mode 100644 index 000000000..3a82550c6 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/Agent.java @@ -0,0 +1,62 @@ +package com.microsoft.semantickernel.agents; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Interface for a semantic kernel agent. + */ +public interface Agent { + + /** + * Gets the agent's ID. + * + * @return The agent's ID + */ + String getId(); + + /** + * Gets the agent's name. + * + * @return The agent's name + */ + String getName(); + + /** + * Gets the agent's description. + * + * @return The agent's description + */ + String getDescription(); + + /** + * Invoke the agent with the given chat history. + * + * @param messages The chat history to process + * @param thread The agent thread to use + * @param options The options for invoking the agent + * @return A Mono containing the agent response + */ + Mono>>> invokeAsync(List> messages, AgentThread thread, AgentInvokeOptions options); + + /** + * Notifies the agent of a new message. + * + * @param thread The agent thread to use + */ + Mono notifyThreadOfNewMessageAsync(AgentThread thread, ChatMessageContent newMessage); +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentInvokeOptions.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentInvokeOptions.java new file mode 100644 index 000000000..3fb4cba18 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentInvokeOptions.java @@ -0,0 +1,159 @@ +package com.microsoft.semantickernel.agents; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.builders.SemanticKernelBuilder; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import javax.annotation.Nullable; + +/** + * Options for invoking an agent. + */ +public class AgentInvokeOptions { + + private final KernelArguments kernelArguments; + private final Kernel kernel; + private final String additionalInstructions; + private final InvocationContext invocationContext; + + /** + * Default constructor for AgentInvokeOptions. + */ + public AgentInvokeOptions() { + this(null, null, null, null); + } + + /** + * Constructor for AgentInvokeOptions. + * + * @param kernelArguments The arguments for the kernel function. + * @param kernel The kernel to use. + * @param additionalInstructions Additional instructions for the agent. + * @param invocationContext The invocation context. + */ + public AgentInvokeOptions(@Nullable KernelArguments kernelArguments, + @Nullable Kernel kernel, + @Nullable String additionalInstructions, + @Nullable InvocationContext invocationContext) { + this.kernelArguments = kernelArguments != null ? kernelArguments.copy() : null; + this.kernel = kernel; + this.additionalInstructions = additionalInstructions; + this.invocationContext = invocationContext; + } + + /** + * Get the kernel arguments. + * + * @return The kernel arguments. + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public KernelArguments getKernelArguments() { + return kernelArguments; + } + + /** + * Get the kernel. + * + * @return The kernel. + */ + public Kernel getKernel() { + return kernel; + } + + /** + * Get additional instructions. + * + * @return The additional instructions. + */ + public String getAdditionalInstructions() { + return additionalInstructions; + } + + /** + * Get the invocation context. + * + * @return The invocation context. + */ + public InvocationContext getInvocationContext() { + return invocationContext; + } + + + + /** + * Builder for AgentInvokeOptions. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SemanticKernelBuilder { + + private KernelArguments kernelArguments; + private Kernel kernel; + private String additionalInstructions; + private InvocationContext invocationContext; + + /** + * Set the kernel arguments. + * + * @param kernelArguments The kernel arguments. + * @return The builder. + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public Builder withKernelArguments(KernelArguments kernelArguments) { + this.kernelArguments = kernelArguments; + return this; + } + + /** + * Set the kernel. + * + * @param kernel The kernel. + * @return The builder. + */ + public Builder withKernel(Kernel kernel) { + this.kernel = kernel; + return this; + } + + /** + * Set additional instructions. + * + * @param additionalInstructions The additional instructions. + * @return The builder. + */ + public Builder withAdditionalInstructions(String additionalInstructions) { + this.additionalInstructions = additionalInstructions; + return this; + } + + /** + * Set the invocation context. + * + * @param invocationContext The invocation context. + * @return The builder. + */ + public Builder withInvocationContext(InvocationContext invocationContext) { + this.invocationContext = invocationContext; + return this; + } + + /** + * Build the object. + * + * @return a constructed object. + */ + @Override + public AgentInvokeOptions build() { + return new AgentInvokeOptions( + kernelArguments, + kernel, + additionalInstructions, + invocationContext + ); + } + } +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentResponseItem.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentResponseItem.java new file mode 100644 index 000000000..f585bfeab --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentResponseItem.java @@ -0,0 +1,33 @@ +package com.microsoft.semantickernel.agents; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class AgentResponseItem { + private final T message; + private final AgentThread thread; + + @SuppressFBWarnings("EI_EXPOSE_REP2") + public AgentResponseItem(T message, AgentThread thread) { + this.message = message; + this.thread = thread; + } + + /** + * Gets the agent response message. + * + * @return The message. + */ + public T getMessage() { + return message; + } + + /** + * Gets the thread. + * + * @return The thread. + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public AgentThread getThread() { + return thread; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentThread.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentThread.java new file mode 100644 index 000000000..d369d999d --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/AgentThread.java @@ -0,0 +1,52 @@ +package com.microsoft.semantickernel.agents; + +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import reactor.core.publisher.Mono; + +/** + * Interface for an agent thread. + */ +public interface AgentThread { + /** + * Get the thread ID. + * + * @return The thread ID. + */ + String getId(); + + /** + * Create a new thread. + * + * @return A Mono containing the thread ID. + */ + Mono createAsync(); + + /** + * Delete the thread. + * + * @return A Mono indicating completion. + */ + Mono deleteAsync(); + + /** + * Check if the thread is deleted. + * + * @return A Mono containing true if the thread is deleted, false otherwise. + */ + boolean isDeleted(); + + /** + * Create a copy of the thread. + * + * @return A new instance of the thread. + */ + AgentThread copy(); + + /** + * Handle a new message in the thread. + * + * @param newMessage The new message to handle. + * @return A Mono indicating completion. + */ + Mono onNewMessageAsync(ChatMessageContent newMessage); +} \ No newline at end of file diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/BaseAgentThread.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/BaseAgentThread.java new file mode 100644 index 000000000..b7c97eea9 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/BaseAgentThread.java @@ -0,0 +1,23 @@ +package com.microsoft.semantickernel.agents; + +public abstract class BaseAgentThread implements AgentThread { + + protected String id; + protected boolean isDeleted; + + public BaseAgentThread() { + } + + public BaseAgentThread(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + @Override + public boolean isDeleted() { + return isDeleted; + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/KernelAgent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/KernelAgent.java new file mode 100644 index 000000000..71e81951d --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/agents/KernelAgent.java @@ -0,0 +1,170 @@ +package com.microsoft.semantickernel.agents; + +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.InvocationReturnMode; +import com.microsoft.semantickernel.orchestration.PromptExecutionSettings; +import com.microsoft.semantickernel.semanticfunctions.KernelArguments; +import com.microsoft.semantickernel.semanticfunctions.PromptTemplate; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +public abstract class KernelAgent implements Agent { + + protected final String id; + protected final String name; + protected final String description; + protected final Kernel kernel; + protected final KernelArguments kernelArguments; + protected final InvocationContext invocationContext; + protected final String instructions; + protected final PromptTemplate template; + + protected KernelAgent( + String id, + String name, + String description, + Kernel kernel, + KernelArguments kernelArguments, + InvocationContext invocationContext, + String instructions, + PromptTemplate template + ) { + this.id = id != null ? id : UUID.randomUUID().toString(); + this.name = name; + this.description = description; + this.kernel = kernel; + this.kernelArguments = kernelArguments != null + ? kernelArguments.copy() : KernelArguments.builder().build(); + this.invocationContext = invocationContext != null + ? invocationContext : InvocationContext.builder().build(); + this.instructions = instructions; + this.template = template; + } + + /** + * Gets the agent's ID. + * + * @return The agent's ID + */ + public String getId() { + return id; + } + + /** + * Gets the agent's name. + * + * @return The agent's name + */ + public String getName() { + return name; + } + + /** + * Gets the agent's description. + * + * @return The agent's description + */ + public String getDescription() { + return description; + } + + /** + * Gets the kernel used by the agent. + * + * @return The kernel used by the agent + */ + public Kernel getKernel() { + return kernel; + } + + /** + * Gets the invocation context used by the agent. + * + * @return The invocation context used by the agent + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + public KernelArguments getKernelArguments() { + return kernelArguments; + } + + /** + * Gets the invocation context used by the agent. + * + * @return The invocation context used by the agent + */ + public String getInstructions() { + return instructions; + } + + /** + * Gets the invocation context used by the agent. + * + * @return The invocation context used by the agent + */ + public PromptTemplate getTemplate() { + return template; + } + + + /** + * Merges the provided arguments with the current arguments. + * Provided arguments will override the current arguments. + * + * @param arguments The arguments to merge with the current arguments. + */ + protected KernelArguments mergeArguments(KernelArguments arguments) { + if (arguments == null) { + return kernelArguments; + } + + Map executionSettings = new HashMap<>(kernelArguments.getExecutionSettings()); + executionSettings.putAll(arguments.getExecutionSettings()); + + return KernelArguments.builder() + .withVariables(kernelArguments) + .withVariables(arguments) + .withExecutionSettings(executionSettings) + .build(); + } + + /** + * Formats the instructions using the provided kernel, arguments, and context. + * + * @param kernel The kernel to use for formatting. + * @param arguments The arguments to use for formatting. + * @param context The context to use for formatting. + * @return A Mono that resolves to the formatted instructions. + */ + protected Mono renderInstructionsAsync(Kernel kernel, KernelArguments arguments, InvocationContext context) { + if (template != null) { + return template.renderAsync(kernel, arguments, context); + } else { + return Mono.just(instructions); + } + } + + protected Mono ensureThreadExistsWithMessagesAsync(List> messages, AgentThread thread, Supplier threadSupplier) { + return Mono.defer(() -> { + // Check if the thread already exists + // If it does, we can work with a copy of it + AgentThread newThread = thread == null ? threadSupplier.get() : thread.copy(); + + return newThread.createAsync() + .thenMany(Flux.fromIterable(messages)) + .concatMap(message -> { + return notifyThreadOfNewMessageAsync(newThread, message) + .then(Mono.just(message)); + }) + .then(Mono.just((T) newThread)); + }); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java index 25cd8ea89..df5f18322 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java @@ -187,6 +187,13 @@ public ChatHistory addSystemMessage(String content) { return addMessage(AuthorRole.SYSTEM, content); } + /** + * Clear the chat history + */ + public void clear() { + chatMessageContents.clear(); + } + /** * Add all messages to the chat history * @param messages The messages to add to the chat history diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java index 9784648b3..0408860e9 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatMessageContent.java @@ -23,7 +23,6 @@ * @param the type of the inner content within the messages */ public class ChatMessageContent extends KernelContentImpl { - private final AuthorRole authorRole; @Nullable private final String content; @@ -52,6 +51,28 @@ public ChatMessageContent( null); } + /** + * Creates a new instance of the {@link ChatMessageContent} class. Defaults to + * {@link ChatMessageContentType#TEXT} content type. + * + * @param authorRole the author role that generated the content + * @param authorName the author name + * @param content the content + */ + public ChatMessageContent( + AuthorRole authorRole, + String authorName, + String content) { + this( + authorRole, + authorName, + content, + null, + null, + null, + null); + } + /** * Creates a new instance of the {@link ChatMessageContent} class. Defaults to * {@link ChatMessageContentType#TEXT} content type. diff --git a/semantickernel-bom/pom.xml b/semantickernel-bom/pom.xml index bb4d766fd..32ca102b1 100644 --- a/semantickernel-bom/pom.xml +++ b/semantickernel-bom/pom.xml @@ -112,6 +112,12 @@ ${project.version} + + com.microsoft.semantic-kernel + semantickernel-agents-core + ${project.version} + + com.azure azure-ai-openai