diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java index 23b7fe30d..df84b9e1f 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java @@ -4,11 +4,18 @@ package com.github.copilot.rpc; +import java.lang.reflect.Method; +import java.util.List; import java.util.Map; 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. @@ -163,4 +170,93 @@ 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 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 fromClass(Class clazz) { + if (clazz == null) { + throw new IllegalArgumentException("clazz must not be null"); + } + return loadDefinitions(clazz, null); + } + + @SuppressWarnings("unchecked") + private static List loadDefinitions(Class clazz, Object instance) { + String metaClassName = clazz.getName() + "$$CopilotToolMeta"; + try { + Class metaClass = Class.forName(metaClassName, true, clazz.getClassLoader()); + Method defs = metaClass.getDeclaredMethod("definitions", clazz, ObjectMapper.class); + defs.setAccessible(true); + return (List) defs.invoke(null, 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; + } + } } diff --git a/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java new file mode 100644 index 000000000..4070665ee --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.copilot.AllowCopilotExperimental; +import com.github.copilot.rpc.fixtures.ArgCoercionTools; +import com.github.copilot.rpc.fixtures.DateTimeTools; +import com.github.copilot.rpc.fixtures.DefaultValueTools; +import com.github.copilot.rpc.fixtures.MultiReturnTools; +import com.github.copilot.rpc.fixtures.OverrideTools; +import com.github.copilot.rpc.fixtures.SimpleTools; + +/** + * End-to-end tests for {@link ToolDefinition#fromObject(Object)}. + *

+ * The annotation processor generates {@code $$CopilotToolMeta} companion + * classes for the fixture classes during test compilation. + */ +@AllowCopilotExperimental +class ToolDefinitionFromObjectTest { + + // ── Test 1: Basic end-to-end ──────────────────────────────────────────────── + + @Test + void fromObject_returnsCorrectNumberOfTools() { + var tools = ToolDefinition.fromObject(new SimpleTools()); + assertEquals(2, tools.size()); + } + + @Test + void fromObject_toolNamesAndDescriptions() { + var tools = ToolDefinition.fromObject(new SimpleTools()); + var tool1 = findTool(tools, "greet_user"); + assertNotNull(tool1); + assertEquals("Greets a user by name", tool1.description()); + + var tool2 = findTool(tools, "add_numbers"); + assertNotNull(tool2); + assertEquals("Adds two numbers together", tool2.description()); + } + + @Test + void fromObject_toolParameterSchema() { + var tools = ToolDefinition.fromObject(new SimpleTools()); + var tool = findTool(tools, "greet_user"); + assertNotNull(tool); + @SuppressWarnings("unchecked") + var schema = (Map) tool.parameters(); + assertEquals("object", schema.get("type")); + @SuppressWarnings("unchecked") + var properties = (Map) schema.get("properties"); + assertTrue(properties.containsKey("name")); + @SuppressWarnings("unchecked") + var required = (List) schema.get("required"); + assertTrue(required.contains("name")); + } + + @Test + void fromObject_handlerInvocation() throws Exception { + var instance = new SimpleTools(); + var tools = ToolDefinition.fromObject(instance); + var tool = findTool(tools, "greet_user"); + assertNotNull(tool); + + var result = tool.handler().invoke(createInvocation("greet_user", Map.of("name", "Alice"))).get(); + assertEquals("Hello, Alice!", result); + } + + // ── Test 2: Handler return type patterns ──────────────────────────────────── + + @Test + void fromObject_stringReturn() throws Exception { + var tools = ToolDefinition.fromObject(new MultiReturnTools()); + var tool = findTool(tools, "string_method"); + assertNotNull(tool); + var result = tool.handler().invoke(createInvocation("string_method", Map.of())).get(); + assertEquals("hello", result); + } + + @Test + void fromObject_voidReturn() throws Exception { + var tools = ToolDefinition.fromObject(new MultiReturnTools()); + var tool = findTool(tools, "void_method"); + assertNotNull(tool); + var result = tool.handler().invoke(createInvocation("void_method", Map.of())).get(); + assertEquals("Success", result); + } + + @Test + void fromObject_asyncReturn() throws Exception { + var tools = ToolDefinition.fromObject(new MultiReturnTools()); + var tool = findTool(tools, "async_method"); + assertNotNull(tool); + var result = tool.handler().invoke(createInvocation("async_method", Map.of())).get(); + assertEquals("async result", result); + } + + // ── Test 3: Argument coercion ─────────────────────────────────────────────── + + @Test + void fromObject_argumentCoercion() throws Exception { + var instance = new ArgCoercionTools(); + var tools = ToolDefinition.fromObject(instance); + var tool = findTool(tools, "mixed_args"); + assertNotNull(tool); + + var result = tool.handler().invoke( + createInvocation("mixed_args", Map.of("text", "hello", "count", 5, "flag", true, "color", "RED"))) + .get(); + assertEquals("hello-5-true-RED", result); + } + + // ── Test 4: Default value ─────────────────────────────────────────────────── + + @Test + void fromObject_defaultValue() throws Exception { + var instance = new DefaultValueTools(); + var tools = ToolDefinition.fromObject(instance); + var tool = findTool(tools, "with_default"); + assertNotNull(tool); + + // Omit "count" key — should use default value 42 + var result = tool.handler().invoke(createInvocation("with_default", Map.of("label", "test"))).get(); + assertEquals("test:42", result); + } + + // ── Test 5: Error case — missing generated class ──────────────────────────── + + @Test + void fromObject_throwsOnMissingMetaClass() { + // A class that was never processed by CopilotToolProcessor + var ex = assertThrows(IllegalStateException.class, () -> ToolDefinition.fromObject("a plain String")); + assertTrue(ex.getMessage().contains("not found")); + assertTrue(ex.getMessage().contains("CopilotToolProcessor")); + } + + // ── Test 6: java.time argument ────────────────────────────────────────────── + + @Test + void fromObject_javaTimeArgument() throws Exception { + var instance = new DateTimeTools(); + var tools = ToolDefinition.fromObject(instance); + var tool = findTool(tools, "schedule_event"); + assertNotNull(tool); + + var result = tool.handler().invoke(createInvocation("schedule_event", Map.of("when", "2024-06-15T10:30:00"))) + .get(); + assertEquals("Scheduled at 2024-06-15T10:30", result); + } + + // ── Test 7: Override tool ──────────────────────────────────────────────────── + + @Test + void fromObject_overrideTool() { + var tools = ToolDefinition.fromObject(new OverrideTools()); + var tool = findTool(tools, "grep"); + assertNotNull(tool); + assertEquals(Boolean.TRUE, tool.overridesBuiltInTool()); + } + + // ── Test 8: ToolDefer.NONE → null mapping (defer absent from JSON) ────────── + + @Test + void fromObject_deferNone_absentFromJson() throws Exception { + var tools = ToolDefinition.fromObject(new SimpleTools()); + var tool = findTool(tools, "greet_user"); + assertNotNull(tool); + // The defer field should be null (NONE maps to null) + assertNull(tool.defer()); + + // Serialize to JSON and verify "defer" key is absent + 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); + + String json = mapper.writeValueAsString(tool); + assertFalse(json.contains("\"defer\""), "defer key should be absent from JSON, got: " + json); + } + + // ── Helpers ───────────────────────────────────────────────────────────────── + + private static ToolDefinition findTool(List tools, String name) { + return tools.stream().filter(t -> name.equals(t.name())).findFirst().orElse(null); + } + + private static ToolInvocation createInvocation(String toolName, Map args) { + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + ObjectMapper mapper = new ObjectMapper(); + argsNode.setAll((ObjectNode) mapper.valueToTree(args)); + return new ToolInvocation().setToolName(toolName).setArguments(argsNode); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java new file mode 100644 index 000000000..8e1aaedb7 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java @@ -0,0 +1,48 @@ +// GENERATED by CopilotToolProcessor — do not edit (hand-written test fixture) +package com.github.copilot.rpc.fixtures; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.rpc.ToolDefinition; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +final class ArgCoercionTools$$CopilotToolMeta { + + private static Map withMeta(Map base, String description, Object defaultValue) { + var result = new LinkedHashMap(base); + if (description != null) + result.put("description", description); + if (defaultValue != null) + result.put("default", defaultValue); + return Collections.unmodifiableMap(result); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List definitions(ArgCoercionTools instance, ObjectMapper mapper) { + return List + .of(new ToolDefinition("mixed_args", "Method with mixed argument types", Map.of( + "type", "object", "properties", Map + .ofEntries( + Map.entry("text", + (Map) (Map) withMeta(Map.of("type", "string"), + "Text input", null)), + Map.entry("count", + (Map) (Map) withMeta(Map.of("type", "integer"), + "A count", null)), + Map.entry("flag", + (Map) (Map) withMeta(Map.of("type", "boolean"), + "A flag", null)), + Map.entry("color", + (Map) (Map) withMeta(Map.of("type", "string", "enum", + List.of("RED", "GREEN", "BLUE")), "A color", null))), + "required", List.of("text", "count", "flag", "color")), invocation -> { + Map args = invocation.getArguments(); + String text = (String) args.get("text"); + int count = ((Number) args.get("count")).intValue(); + boolean flag = (Boolean) args.get("flag"); + ArgCoercionTools.Color color = ArgCoercionTools.Color.valueOf((String) args.get("color")); + return CompletableFuture.completedFuture(instance.mixedArgs(text, count, flag, color)); + }, null, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools.java new file mode 100644 index 000000000..7f85bd2c7 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc.fixtures; + +import com.github.copilot.tool.CopilotTool; +import com.github.copilot.tool.Param; + +/** + * Fixture testing argument coercion with multiple types including an enum. + */ +public class ArgCoercionTools { + + public enum Color { + RED, GREEN, BLUE + } + + @CopilotTool("Method with mixed argument types") + public String mixedArgs(@Param("Text input") String text, @Param("A count") int count, + @Param("A flag") boolean flag, @Param("A color") Color color) { + return text + "-" + count + "-" + flag + "-" + color.name(); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java new file mode 100644 index 000000000..3627a5c30 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java @@ -0,0 +1,36 @@ +// GENERATED by CopilotToolProcessor — do not edit (hand-written test fixture) +package com.github.copilot.rpc.fixtures; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.rpc.ToolDefinition; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +final class DateTimeTools$$CopilotToolMeta { + + private static Map withMeta(Map base, String description, Object defaultValue) { + var result = new LinkedHashMap(base); + if (description != null) + result.put("description", description); + if (defaultValue != null) + result.put("default", defaultValue); + return Collections.unmodifiableMap(result); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List definitions(DateTimeTools instance, ObjectMapper mapper) { + return List.of(new ToolDefinition("schedule_event", "Schedule an event at a given time", + Map.of("type", "object", "properties", + Map.ofEntries(Map.entry("when", + (Map) (Map) withMeta(Map.of("type", "string", "format", "date-time"), + "When to schedule", null))), + "required", List.of("when")), + invocation -> { + Map args = invocation.getArguments(); + LocalDateTime when = mapper.convertValue(args.get("when"), LocalDateTime.class); + return CompletableFuture.completedFuture(instance.scheduleEvent(when)); + }, null, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools.java new file mode 100644 index 000000000..541c2c6d8 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc.fixtures; + +import java.time.LocalDateTime; + +import com.github.copilot.tool.CopilotTool; +import com.github.copilot.tool.Param; + +/** + * Fixture testing java.time argument deserialization via ObjectMapper with + * JavaTimeModule. + */ +public class DateTimeTools { + + @CopilotTool("Schedule an event at a given time") + public String scheduleEvent(@Param(value = "When to schedule", required = true) LocalDateTime when) { + return "Scheduled at " + when.getYear() + "-" + String.format("%02d", when.getMonthValue()) + "-" + + String.format("%02d", when.getDayOfMonth()) + "T" + String.format("%02d", when.getHour()) + ":" + + String.format("%02d", when.getMinute()); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java new file mode 100644 index 000000000..53d8b6337 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java @@ -0,0 +1,43 @@ +// GENERATED by CopilotToolProcessor — do not edit (hand-written test fixture) +package com.github.copilot.rpc.fixtures; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.rpc.ToolDefinition; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +final class DefaultValueTools$$CopilotToolMeta { + + private static Map withMeta(Map base, String description, Object defaultValue) { + var result = new LinkedHashMap(base); + if (description != null) + result.put("description", description); + if (defaultValue != null) + result.put("default", defaultValue); + return Collections.unmodifiableMap(result); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List definitions(DefaultValueTools instance, ObjectMapper mapper) { + return List + .of(new ToolDefinition( + "with_default", "Method with a default value parameter", Map + .of("type", "object", "properties", + Map.ofEntries( + Map.entry("label", + (Map) (Map) withMeta(Map.of("type", "string"), + "A label", null)), + Map.entry("count", + (Map) (Map) withMeta(Map.of("type", "integer"), + "A count", 42))), + "required", List.of("label")), + invocation -> { + Map args = invocation.getArguments(); + String label = (String) args.get("label"); + Object countRaw = args.containsKey("count") ? args.get("count") : 42; + int count = ((Number) countRaw).intValue(); + return CompletableFuture.completedFuture(instance.withDefault(label, count)); + }, null, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools.java new file mode 100644 index 000000000..6e2c3106e --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools.java @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc.fixtures; + +import com.github.copilot.tool.CopilotTool; +import com.github.copilot.tool.Param; + +/** + * Fixture testing default parameter values. + */ +public class DefaultValueTools { + + @CopilotTool("Method with a default value parameter") + public String withDefault(@Param(value = "A label", required = true) String label, + @Param(value = "A count", required = false, defaultValue = "42") int count) { + return label + ":" + count; + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java new file mode 100644 index 000000000..eb6615abb --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java @@ -0,0 +1,27 @@ +// GENERATED by CopilotToolProcessor — do not edit (hand-written test fixture) +package com.github.copilot.rpc.fixtures; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.rpc.ToolDefinition; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +final class MultiReturnTools$$CopilotToolMeta { + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List definitions(MultiReturnTools instance, ObjectMapper mapper) { + return List.of(new ToolDefinition("string_method", "Returns a string", + Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { + return CompletableFuture.completedFuture(instance.stringMethod()); + }, null, null, null), new ToolDefinition("void_method", "Void method", + Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { + instance.voidMethod(); + return CompletableFuture.completedFuture("Success"); + }, null, null, null), + new ToolDefinition("async_method", "Async method", + Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> { + return instance.asyncMethod().thenApply(r -> (Object) r); + }, null, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools.java new file mode 100644 index 000000000..62a6a2500 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc.fixtures; + +import java.util.concurrent.CompletableFuture; + +import com.github.copilot.tool.CopilotTool; + +/** + * Fixture testing different return type patterns. + */ +public class MultiReturnTools { + + @CopilotTool("Returns a string") + public String stringMethod() { + return "hello"; + } + + @CopilotTool("Void method") + public void voidMethod() { + // side-effect only + } + + @CopilotTool("Async method") + public CompletableFuture asyncMethod() { + return CompletableFuture.completedFuture("async result"); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java new file mode 100644 index 000000000..88bdfbb5d --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java @@ -0,0 +1,37 @@ +// GENERATED by CopilotToolProcessor — do not edit (hand-written test fixture) +package com.github.copilot.rpc.fixtures; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.rpc.ToolDefinition; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +final class OverrideTools$$CopilotToolMeta { + + private static Map withMeta(Map base, String description, Object defaultValue) { + var result = new LinkedHashMap(base); + if (description != null) + result.put("description", description); + if (defaultValue != null) + result.put("default", defaultValue); + return Collections.unmodifiableMap(result); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List definitions(OverrideTools instance, ObjectMapper mapper) { + return List + .of(new ToolDefinition( + "grep", "Custom grep implementation", Map + .of("type", "object", "properties", + Map.ofEntries(Map.entry("pattern", + (Map) (Map) withMeta(Map.of("type", "string"), + "Search pattern", null))), + "required", List.of("pattern")), + invocation -> { + Map args = invocation.getArguments(); + String pattern = (String) args.get("pattern"); + return CompletableFuture.completedFuture(instance.customGrep(pattern)); + }, Boolean.TRUE, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools.java new file mode 100644 index 000000000..5fbb432f9 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools.java @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc.fixtures; + +import com.github.copilot.tool.CopilotTool; +import com.github.copilot.tool.Param; + +/** + * Fixture testing tool override flag. + */ +public class OverrideTools { + + @CopilotTool(value = "Custom grep implementation", name = "grep", overridesBuiltInTool = true) + public String customGrep(@Param(value = "Search pattern", required = true) String pattern) { + return "Found: " + pattern; + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java new file mode 100644 index 000000000..8ee2740a9 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java @@ -0,0 +1,49 @@ +// GENERATED by CopilotToolProcessor — do not edit (hand-written test fixture) +package com.github.copilot.rpc.fixtures; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.rpc.ToolDefinition; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +final class SimpleTools$$CopilotToolMeta { + + private static Map withMeta(Map base, String description, Object defaultValue) { + var result = new LinkedHashMap(base); + if (description != null) + result.put("description", description); + if (defaultValue != null) + result.put("default", defaultValue); + return Collections.unmodifiableMap(result); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + static List definitions(SimpleTools instance, ObjectMapper mapper) { + return List.of(new ToolDefinition("greet_user", "Greets a user by name", + Map.of("type", "object", "properties", Map.ofEntries(Map.entry("name", + (Map) (Map) withMeta(Map.of("type", "string"), "The user's name", null))), + "required", List.of("name")), + invocation -> { + Map args = invocation.getArguments(); + String name = (String) args.get("name"); + return CompletableFuture.completedFuture(instance.greetUser(name)); + }, null, null, null), + new ToolDefinition("add_numbers", "Adds two numbers together", + Map.of("type", "object", "properties", + Map.ofEntries( + Map.entry("a", + (Map) (Map) withMeta(Map.of("type", "integer"), + "First number", null)), + Map.entry("b", + (Map) (Map) withMeta(Map.of("type", "integer"), + "Second number", null))), + "required", List.of("a", "b")), + invocation -> { + Map args = invocation.getArguments(); + int a = ((Number) args.get("a")).intValue(); + int b = ((Number) args.get("b")).intValue(); + return CompletableFuture.completedFuture(instance.addNumbers(a, b)); + }, null, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java new file mode 100644 index 000000000..5bdee36e5 --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc.fixtures; + +import com.github.copilot.tool.CopilotTool; +import com.github.copilot.tool.Param; + +/** + * Simple tool fixture with basic String-returning methods. + */ +public class SimpleTools { + + @CopilotTool("Greets a user by name") + public String greetUser(@Param(value = "The user's name", required = true) String name) { + return "Hello, " + name + "!"; + } + + @CopilotTool("Adds two numbers together") + public String addNumbers(@Param(value = "First number") int a, @Param(value = "Second number") int b) { + return String.valueOf(a + b); + } +}