Skip to content

Commit 0460014

Browse files
Copilotedburns
authored andcommitted
feat(java): create @copilotTool and @PARAM annotations with tests
- Add NONE constant to ToolDefer enum for annotation default value - Create com.github.copilot.tool.CopilotTool annotation - Create com.github.copilot.tool.Param annotation - Export com.github.copilot.tool package in module-info.java - Add CopilotToolAnnotationTest verifying retention, targets, defaults Closes #1758
1 parent 767a29b commit 0460014

5 files changed

Lines changed: 256 additions & 0 deletions

File tree

‎java/src/main/java/com/github/copilot/rpc/ToolDefer.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
*/
2222
public enum ToolDefer {
2323

24+
/**
25+
* No deferral preference set. Used as the default for annotation-based
26+
* tool definitions where the user has not explicitly chosen a mode.
27+
*/
28+
NONE(""),
29+
2430
/** The tool can be deferred and surfaced through tool search. */
2531
AUTO("auto"),
2632

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.tool;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
import com.github.copilot.CopilotExperimental;
14+
import com.github.copilot.rpc.ToolDefer;
15+
16+
/**
17+
* Marks a method as a Copilot tool. The annotated method will be exposed to the
18+
* model as a callable tool during a session.
19+
*
20+
* <p>
21+
* Example usage:
22+
*
23+
* <pre>
24+
* &#64;CopilotTool("Get weather for a location")
25+
* public CompletableFuture&lt;String&gt; getWeather(
26+
* &#64;Param(value = "City name", required = true) String location) {
27+
* return CompletableFuture.completedFuture("Sunny in " + location);
28+
* }
29+
* </pre>
30+
*
31+
* @since 1.0.0
32+
*/
33+
@Documented
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target(ElementType.METHOD)
36+
@CopilotExperimental
37+
public @interface CopilotTool {
38+
39+
/** Tool description (sent to the model). */
40+
String value();
41+
42+
/** Tool name. Defaults to method name converted to snake_case. */
43+
String name() default "";
44+
45+
/** Whether this tool overrides a built-in tool. */
46+
boolean overridesBuiltInTool() default false;
47+
48+
/** Whether to skip permission checks. */
49+
boolean skipPermission() default false;
50+
51+
/** Defer configuration for this tool. */
52+
ToolDefer defer() default ToolDefer.NONE;
53+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.tool;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Annotates a parameter of a {@link CopilotTool}-annotated method to provide
15+
* metadata about the parameter that is sent to the model.
16+
*
17+
* <p>
18+
* Example usage:
19+
*
20+
* <pre>
21+
* &#64;CopilotTool("Search for issues")
22+
* public CompletableFuture&lt;String&gt; searchIssues(
23+
* &#64;Param(value = "Search query", required = true) String query,
24+
* &#64;Param(value = "Max results", required = false, defaultValue = "10") int limit) {
25+
* // ...
26+
* }
27+
* </pre>
28+
*
29+
* @since 1.0.0
30+
*/
31+
@Documented
32+
@Retention(RetentionPolicy.RUNTIME)
33+
@Target(ElementType.PARAMETER)
34+
public @interface Param {
35+
36+
/** Parameter description (sent to the model). */
37+
String value() default "";
38+
39+
/** Parameter name override. Defaults to the actual parameter name. */
40+
String name() default "";
41+
42+
/** Whether this parameter is required. Default true. */
43+
boolean required() default true;
44+
45+
/** Optional default value when the argument is omitted. */
46+
String defaultValue() default "";
47+
}

‎java/src/main/java/module-info.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
exports com.github.copilot.generated;
2020
exports com.github.copilot.generated.rpc;
2121
exports com.github.copilot.rpc;
22+
exports com.github.copilot.tool;
2223

2324
opens com.github.copilot to com.fasterxml.jackson.databind;
2425
opens com.github.copilot.generated to com.fasterxml.jackson.databind;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.tool;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
import java.lang.reflect.Method;
14+
import java.lang.reflect.Parameter;
15+
import java.util.concurrent.CompletableFuture;
16+
17+
import org.junit.jupiter.api.Test;
18+
19+
import com.github.copilot.CopilotExperimental;
20+
import com.github.copilot.rpc.ToolDefer;
21+
22+
/**
23+
* Unit tests for {@link CopilotTool} and {@link Param} annotations.
24+
*/
25+
public class CopilotToolAnnotationTest {
26+
27+
// --- @CopilotTool attribute verification ---
28+
29+
@Test
30+
void copilotToolHasRuntimeRetention() {
31+
Retention retention = CopilotTool.class.getAnnotation(Retention.class);
32+
assertNotNull(retention);
33+
assertEquals(RetentionPolicy.RUNTIME, retention.value());
34+
}
35+
36+
@Test
37+
void copilotToolTargetsMethod() {
38+
Target target = CopilotTool.class.getAnnotation(Target.class);
39+
assertNotNull(target);
40+
assertArrayEquals(new ElementType[] {ElementType.METHOD}, target.value());
41+
}
42+
43+
@Test
44+
void copilotToolIsAnnotatedWithCopilotExperimental() {
45+
// @CopilotExperimental has CLASS retention so it is not visible via
46+
// reflection at runtime. However, we can confirm:
47+
// 1. The annotation type targets TYPE (which includes @interface declarations).
48+
// 2. Compilation succeeded with @CopilotExperimental on @CopilotTool
49+
// (the CopilotExperimentalProcessor would reject usage otherwise).
50+
Target expTarget = CopilotExperimental.class.getAnnotation(Target.class);
51+
assertNotNull(expTarget);
52+
boolean includesType = false;
53+
for (ElementType et : expTarget.value()) {
54+
if (et == ElementType.TYPE) {
55+
includesType = true;
56+
break;
57+
}
58+
}
59+
assertTrue(includesType,
60+
"@CopilotExperimental must target TYPE to be applicable to annotation declarations");
61+
}
62+
63+
@Test
64+
void copilotToolDefaultValues() throws Exception {
65+
Method nameMethod = CopilotTool.class.getDeclaredMethod("name");
66+
assertEquals("", nameMethod.getDefaultValue());
67+
68+
Method overridesMethod = CopilotTool.class.getDeclaredMethod("overridesBuiltInTool");
69+
assertEquals(false, overridesMethod.getDefaultValue());
70+
71+
Method skipMethod = CopilotTool.class.getDeclaredMethod("skipPermission");
72+
assertEquals(false, skipMethod.getDefaultValue());
73+
74+
Method deferMethod = CopilotTool.class.getDeclaredMethod("defer");
75+
assertEquals(ToolDefer.NONE, deferMethod.getDefaultValue());
76+
}
77+
78+
// --- @Param attribute verification ---
79+
80+
@Test
81+
void paramHasRuntimeRetention() {
82+
Retention retention = Param.class.getAnnotation(Retention.class);
83+
assertNotNull(retention);
84+
assertEquals(RetentionPolicy.RUNTIME, retention.value());
85+
}
86+
87+
@Test
88+
void paramTargetsParameter() {
89+
Target target = Param.class.getAnnotation(Target.class);
90+
assertNotNull(target);
91+
assertArrayEquals(new ElementType[] {ElementType.PARAMETER}, target.value());
92+
}
93+
94+
@Test
95+
void paramDefaultValues() throws Exception {
96+
Method valueMethod = Param.class.getDeclaredMethod("value");
97+
assertEquals("", valueMethod.getDefaultValue());
98+
99+
Method nameMethod = Param.class.getDeclaredMethod("name");
100+
assertEquals("", nameMethod.getDefaultValue());
101+
102+
Method requiredMethod = Param.class.getDeclaredMethod("required");
103+
assertEquals(true, requiredMethod.getDefaultValue());
104+
105+
Method defaultValueMethod = Param.class.getDeclaredMethod("defaultValue");
106+
assertEquals("", defaultValueMethod.getDefaultValue());
107+
}
108+
109+
// --- Applicability test ---
110+
111+
@SuppressWarnings("unused")
112+
static class SampleToolHolder {
113+
114+
@CopilotTool(value = "Get weather for a location", name = "get_weather", defer = ToolDefer.AUTO)
115+
public CompletableFuture<String> getWeather(
116+
@Param(value = "City name", required = true) String location,
117+
@Param(value = "Temperature unit", required = false, defaultValue = "celsius") String unit) {
118+
return CompletableFuture.completedFuture("Sunny in " + location);
119+
}
120+
}
121+
122+
@Test
123+
void annotationsAreAccessibleViaReflection() throws Exception {
124+
Method method = SampleToolHolder.class.getDeclaredMethod("getWeather", String.class, String.class);
125+
126+
CopilotTool toolAnnotation = method.getAnnotation(CopilotTool.class);
127+
assertNotNull(toolAnnotation);
128+
assertEquals("Get weather for a location", toolAnnotation.value());
129+
assertEquals("get_weather", toolAnnotation.name());
130+
assertFalse(toolAnnotation.overridesBuiltInTool());
131+
assertFalse(toolAnnotation.skipPermission());
132+
assertEquals(ToolDefer.AUTO, toolAnnotation.defer());
133+
134+
Parameter[] params = method.getParameters();
135+
assertEquals(2, params.length);
136+
137+
Param locationParam = params[0].getAnnotation(Param.class);
138+
assertNotNull(locationParam);
139+
assertEquals("City name", locationParam.value());
140+
assertTrue(locationParam.required());
141+
assertEquals("", locationParam.defaultValue());
142+
143+
Param unitParam = params[1].getAnnotation(Param.class);
144+
assertNotNull(unitParam);
145+
assertEquals("Temperature unit", unitParam.value());
146+
assertFalse(unitParam.required());
147+
assertEquals("celsius", unitParam.defaultValue());
148+
}
149+
}

0 commit comments

Comments
 (0)