-
Notifications
You must be signed in to change notification settings - Fork 625
33462 generic tool paramter schema #33688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature-mcp-server
Are you sure you want to change the base?
33462 generic tool paramter schema #33688
Conversation
...iberty.mcp.internal/src/io/openliberty/mcp/internal/schemas/GenericParameterDescription.java
Outdated
Show resolved
Hide resolved
dev/io.openliberty.mcp.internal/src/io/openliberty/mcp/internal/schemas/SchemaGenerator.java
Outdated
Show resolved
Hide resolved
devalibm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments, other than that logic seems fine to me
...openliberty.mcp.internal/src/io/openliberty/mcp/internal/typeimpl/ParameterizedTypeImpl.java
Outdated
Show resolved
Hide resolved
aa2f3fc to
ed3d6a7
Compare
| return false; | ||
| } | ||
|
|
||
| public static Type createCustomType(Type type, Map<TypeVariable<?>, Type> genericMap) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename this (maybe to createResolvedType) and add Javadoc.
| if (type instanceof TypeVariable<?> tv) { | ||
| if (genericMap.get(tv) == null) { | ||
| return type; | ||
| } else { | ||
| return genericMap.get(tv); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't recall what can go in the generic map, do we need to call it in a loop or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we do (based on TypeUtility.populateGenericMap)
| /** | ||
| * | ||
| */ | ||
| public class GenericArrayTypeImpl implements GenericArrayType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we ever use this as a key in a map? If so we need to implement hashcode and equals.
| /** | ||
| * | ||
| */ | ||
| public class ParameterizedTypeImpl implements ParameterizedType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we might use this as a key in a map we need hashcode and equals.
| public JsonObject getToolInputSchema(AnnotatedMethod<?> toolMethod, Map<TypeVariable<?>, Type> genericMap) { | ||
| ToolKey key = new ToolKey(toolMethod, SchemaDirection.INPUT); | ||
| return schemaCache.computeIfAbsent(key, k -> SchemaGenerator.generateToolInputSchema(toolMethod, blueprintRegistry)); | ||
| return schemaCache.computeIfAbsent(key, k -> SchemaGenerator.generateToolInputSchema(toolMethod, blueprintRegistry, genericMap)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is questionable - the key should incorporate all the information required to create the schema, otherwise we will return a schema from the cache if we generate a schema for the same method but a different generic map.
I think we get away with it here because it's not valid to have two tools with the same method, since they'd automatically have the same name.
When we support multiple modules however, it might be possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I add generic Map to tool key or if you agree with this comment #33688 (comment) I could add in the argumentMaps instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think either of those would be fine. However, we must make sure that anything that's part of the key has equals and hashcode implemented (records have these automatically) and won't change after it's put in the map.
| private static GenericParameterDescription checkConcreteGeneric(Type baseType, Map<TypeVariable<?>, Type> genericMap, ArrayList<String> genericParams, | ||
| GenericParameterDescription genericParamDescription) { | ||
|
|
||
| List<Type> genericTypes; | ||
| if (baseType instanceof ParameterizedType pt) { | ||
| genericTypes = List.of(pt.getActualTypeArguments()); | ||
|
|
||
| } else if (baseType instanceof GenericArrayType gat) { | ||
| genericTypes = List.of(gat.getGenericComponentType()); | ||
|
|
||
| } else if (baseType instanceof TypeVariable<?> tv) { | ||
| genericTypes = List.of(tv); | ||
|
|
||
| } else if (baseType.getClass().isArray()) { | ||
| Type elementType = ((Class<?>) baseType).getComponentType(); | ||
| genericTypes = List.of(elementType); | ||
|
|
||
| } else | ||
| genericTypes = List.of(); | ||
|
|
||
| for (Type genericType : genericTypes) { | ||
| if (genericType instanceof TypeVariable<?> tv) { | ||
| if (genericMap.get(tv) == null) { | ||
| genericParamDescription = GenericParameterDescription.NONCONCRETE_GENERIC; | ||
| } else { | ||
| genericParamDescription = GenericParameterDescription.CONCRETE_GENERIC; | ||
| } | ||
| } else { | ||
| checkConcreteGeneric(genericType, genericMap, genericParams, genericParamDescription); | ||
| } | ||
| } | ||
| return genericParamDescription; | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest we change this to boolean hasUnresolvableTypeVariables(Type baseType, Map<TypeVariable<?>, Type> genericMap)
genericParams is unused and genericParamDescription is not necessary.
| MockAnnotatedMethod<Object> toolMethod = TestUtils.findMethod(SchemaTest.class, "updateWidget"); | ||
|
|
||
| String toolInputSchema = registry.getToolInputSchema(toolMethod).toString(); | ||
| String toolInputSchema = registry.getToolInputSchema(toolMethod, new HashMap<>()).toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Collections.emptyMap() when you need an empty map that shouldn't change.
| @Tool(name = "addGenericSingleBoundToList", title = "adds generic to generic list", description = "adds person to employee list, returns nothing") | ||
| public @Schema(description = "Returns list of person object") <T extends Number> List<T> addGenericSingleBoundToList(@ToolArg(name = "generic list", | ||
| description = "List of generics") List<T> list, | ||
| @ToolArg(name = "generic", | ||
| description = "Generic object") T item) { | ||
| list.add(item); | ||
| return list; | ||
| //comment | ||
| } | ||
|
|
||
| @Test | ||
| @Test(expected = GenericArgumentException.class) | ||
| public void testGenericSingleBoundToolArg() { | ||
| MockAnnotatedMethod<Object> toolMethod = TestUtils.findMethod(SchemaTest.class, "addGenericSingleBoundToList"); | ||
| String response = registry.getToolInputSchema(toolMethod).toString(); | ||
| String expectedResponseString = """ | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "generic list": { | ||
| "type": "array", | ||
| "items": { | ||
| "$ref": "#/$defs/T" | ||
| }, | ||
| "description": "List of generics" | ||
| }, | ||
| "generic": { | ||
| "$ref": "#/$defs/T", | ||
| "description": "Generic object" | ||
| } | ||
| }, | ||
| "required": [ | ||
| "generic list", | ||
| "generic" | ||
| ], | ||
| "$defs": { | ||
| "T": { | ||
| "type": "number" | ||
| } | ||
| } | ||
| } | ||
| """; | ||
| JSONAssert.assertEquals(expectedResponseString, response, true); | ||
| String response = registry.getToolInputSchema(toolMethod, new HashMap<>()).toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, this used to work but now doesn't...
I think it's reasonable not to allow this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In FAT test I got rid of all tool tests that had non concrete generic params, I forgot to remove the corresponding test in units tests.
The unit test still passed because there was different versions of getArgumentMap() calls were done, one version threw generic exception (called in ToolMetadat.createFrom())the other didnt (This one was callled in getInputTool) but in this new version they both call the same getArgumentMap which throws the exception.
There exists test which tests generic fields in objects directly using getSchema but not in getToolInput.
| } else if (baseType.getClass().isArray()) { | ||
| Type elementType = ((Class<?>) baseType).getComponentType(); | ||
| genericTypes = List.of(elementType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| } else if (baseType.getClass().isArray()) { | |
| Type elementType = ((Class<?>) baseType).getComponentType(); | |
| genericTypes = List.of(elementType); | |
| } else if (baseType instanceof Class clazz && clazz.isArray()) { | |
| Type elementType = clazz.getComponentType(); | |
| genericTypes = List.of(elementType); |
Should be this I think? baseType.getClass() would return one of the subtypes of Type, whereas I think you want baseType as a Class
| } else | ||
| genericTypes = List.of(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| } else | |
| genericTypes = List.of(); | |
| } else { | |
| genericTypes = List.of(); | |
| } |
release buglabel if applicable: https://github.com/OpenLiberty/open-liberty/wiki/Open-Liberty-Conventions).Resolves Support MCP tool calls that contain concrete generic parameters #33462