Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@

package com.github.copilot.rpc;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.github.copilot.CopilotExperimental;

/**
* Defines a tool that can be invoked by the AI assistant.
Expand Down Expand Up @@ -163,4 +173,101 @@ public static ToolDefinition createWithDefer(String name, String description, Ma
ToolHandler handler, ToolDefer defer) {
return new ToolDefinition(name, description, schema, handler, null, null, defer);
}

/**
* Discovers tool definitions from an object whose methods are annotated with
* {@code @CopilotTool}. Requires that the {@code CopilotToolProcessor}
* annotation processor ran at compile time (generating the
* {@code $$CopilotToolMeta} companion class).
*
* @param instance
* the object containing {@code @CopilotTool}-annotated methods
* @return list of tool definitions with working invocation handlers
* @throws IllegalStateException
* if the generated {@code $$CopilotToolMeta} class is not found
* (annotation processor did not run)
* @since 1.0.2
*/
@CopilotExperimental
public static List<ToolDefinition> fromObject(Object instance) {
if (instance == null) {
throw new IllegalArgumentException("instance must not be null");
}
Class<?> clazz = instance.getClass();
return loadDefinitions(clazz, instance);
}

/**
* Discovers tool definitions from a class with static
* {@code @CopilotTool}-annotated methods. Requires that the
* {@code CopilotToolProcessor} annotation processor ran at compile time
* (generating the {@code $$CopilotToolMeta} companion class).
*
* @param clazz
* the class containing static {@code @CopilotTool}-annotated methods
* @return list of tool definitions with working invocation handlers
* @throws IllegalStateException
* if the generated {@code $$CopilotToolMeta} class is not found
* (annotation processor did not run)
* @since 1.0.2
*/
@CopilotExperimental
public static List<ToolDefinition> fromClass(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("clazz must not be null");
}
List<String> instanceMethods = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(com.github.copilot.tool.CopilotTool.class))
.filter(m -> !Modifier.isStatic(m.getModifiers())).map(Method::getName).collect(Collectors.toList());
if (!instanceMethods.isEmpty()) {
throw new IllegalArgumentException(
"fromClass() requires all @CopilotTool methods to be static, but found instance methods: "
+ instanceMethods + ". Use fromObject(new " + clazz.getSimpleName() + "()) instead.");
}
return loadDefinitions(clazz, null);
}

@SuppressWarnings("unchecked")
private static List<ToolDefinition> loadDefinitions(Class<?> clazz, Object instance) {
String metaClassName = clazz.getName() + "$$CopilotToolMeta";
try {
Class<?> metaClass = Class.forName(metaClassName, true, clazz.getClassLoader());
var provider = (com.github.copilot.tool.CopilotToolMetadataProvider<Object>) metaClass
.getDeclaredConstructor().newInstance();
return provider.definitions(instance, getConfiguredMapper());
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Generated class " + metaClassName + " not found. "
+ "Ensure the CopilotToolProcessor annotation processor ran during compilation. "
+ "Add the copilot-sdk-java dependency to your annotation processor path.", e);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Failed to invoke " + metaClassName + ".definitions()", e);
}
}

/**
* Returns the SDK-configured ObjectMapper for tool argument/result
* serialization. Configuration mirrors
* {@code JsonRpcClient.createObjectMapper()}.
*/
private static ObjectMapper getConfiguredMapper() {
return ConfiguredMapperHolder.INSTANCE;
}

/**
* Lazy holder for the configured ObjectMapper (thread-safe, initialized on
* first access).
*/
private static final class ConfiguredMapperHolder {
static final ObjectMapper INSTANCE = createMapper();

private static ObjectMapper createMapper() {
// Configuration must match JsonRpcClient.createObjectMapper()
var mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.tool;

import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.copilot.CopilotExperimental;
import com.github.copilot.rpc.ToolDefinition;

/**
* Contract for classes that provide {@link ToolDefinition} metadata for
* {@code @CopilotTool}-annotated methods.
*
* <p>
* The {@link CopilotToolProcessor} annotation processor generates an
* implementation of this interface as a {@code $$CopilotToolMeta} companion
* class. Users may also implement this interface directly for full manual
* control over tool registration without using annotation processing.
*
* @param <T>
* the tool class whose methods are described by this provider
* @since 1.0.2
*/
@CopilotExperimental
public interface CopilotToolMetadataProvider<T> {

/**
* Returns tool definitions for the given instance.
*
* @param instance
* the object containing tool methods, or {@code null} for static
* methods
* @param mapper
* the SDK-configured {@link ObjectMapper} for argument
* deserialization
* @return list of tool definitions with working invocation handlers
*/
List<ToolDefinition> definitions(T instance, ObjectMapper mapper);
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ private void writeMetaClass(PrintWriter out, String packageName, String simpleCl

out.println("import com.github.copilot.rpc.ToolDefinition;");
out.println("import com.github.copilot.rpc.ToolDefer;");
out.println("import com.github.copilot.tool.CopilotToolMetadataProvider;");
out.println("import com.fasterxml.jackson.databind.ObjectMapper;");
out.println("import java.util.*;");
out.println("import java.util.concurrent.CompletableFuture;");
out.println();

out.println("final class " + metaClassName + " {");
out.println("public final class " + metaClassName + " implements CopilotToolMetadataProvider<" + simpleClassName
+ "> {");
out.println();

// Helper method for adding description/default to schema maps
Expand All @@ -144,9 +146,10 @@ private void writeMetaClass(PrintWriter out, String packageName, String simpleCl
}

// definitions method
out.println(" @Override");
out.println(" @SuppressWarnings({\"unchecked\", \"rawtypes\"})");
out.println(
" static List<ToolDefinition> definitions(" + simpleClassName + " instance, ObjectMapper mapper) {");
" public List<ToolDefinition> definitions(" + simpleClassName + " instance, ObjectMapper mapper) {");
out.println(" return List.of(");

for (int i = 0; i < methods.size(); i++) {
Expand Down
Loading
Loading