Skip to content

Commit 3a74f3f

Browse files
openAIFunction from struct
Add creation from struct or cell array of struct This is for interaction with MCP tool definitions --------- Co-authored-by: Miriam Scharnke <[email protected]>
1 parent 3b7d1ed commit 3a74f3f

File tree

6 files changed

+161
-17
lines changed

6 files changed

+161
-17
lines changed

+llms/+utils/errorMessageCatalog.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
catalog = dictionary("string", "string");
4545
catalog("llms:mustBeUnique") = "Values must be unique.";
4646
catalog("llms:mustBeVarName") = "Parameter name must begin with a letter and contain not more than 'namelengthmax' characters.";
47+
catalog("llms:mustBeNonzeroLengthTextOrStruct") = "Value must be a character vector, string scalar, scalar cell array of character vectors, struct, or cell array of structs.";
48+
catalog("llms:mustOnlyBeStruct") = "When creating an openAIFunction object from tools provided by MCP servers, additional parameters are not supported.";
4749
catalog("llms:parameterMustBeUnique") = "A parameter name equivalent to '{1}' already exists in Parameters. Redefining a parameter is not allowed.";
4850
catalog("llms:mustBeAssistantCall") = "Input struct must contain field 'role' with value 'assistant', and field 'content'.";
4951
catalog("llms:mustBeAssistantWithContent") = "Input struct must contain field 'content' containing text with one or more characters.";

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Using this add-on, you can:
1818
- Generate responses to natural language prompts.
1919
- Manage chat history.
2020
- Generate JSON\-formatted and structured output.
21-
- Use tool calling.
21+
- Use tool calling.
22+
- Call external tools provided by MCP servers (requires the MATLAB MCP HTTP Client add\-on).
2223
- Generate, edit, and describe images.
2324

2425
For more information about the features in this add-on, see the documentation in the [`doc`](/doc) directory.

doc/functions/openAIFunction.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ Use Function Calls from MATLAB®
88

99
`f = openAIFunction(name,description)`
1010

11+
`f = openAIFunction(mcpTool)`
12+
1113
## Description
1214

1315
An `openAIFunction` object represents a tool that you have, such as a MATLAB function. It includes information about the name, syntax, and behavior of the tool. If you pass an `openAIFunction` object to a large language model (LLM), then the LLM can suggest calls to the tool in its generated output. The LLM does not execute the tool itself. However, you can write scripts that automate the tool calls suggested by the LLM.
1416

17+
If you have the MATLAB MCP HTTP Client add\-on installed, then you can also use tools provided by external MCP servers.
18+
1519
Use `openAIFunction` objects to call tools using OpenAI® or Ollama™.
1620

1721

@@ -21,7 +25,9 @@ For example:
2125
![Diagram illustrating how to incorporate function calling in text generation.](images/openAIFunction1.png)
2226

2327

24-
`f = openAIFunction(name,description)` creates an `openAIFunction` object.
28+
`f = openAIFunction(name,description)` creates an `openAIFunction` object with a name `name` and description `description`.
29+
30+
`f = openAIFunction(mcpTool)` creates an `openAIFunction` object from one or more tools provided by an MCP server specified as a cell array of struct. For example, `mcpTool = client.Tools` for an `mcpHTTPClient` object `client` (requires the MATLAB MCP HTTP Client add\-on).
2531

2632
# Input Arguments
2733
### `name` — Function name
@@ -38,6 +44,13 @@ Specify the function name and set the `FunctionName` property.
3844

3945
Describe the function using natural language and set the `Description` property.
4046

47+
### `mcpTool` — Tool provided by MCP server
48+
cell array of struct
49+
50+
Tool provided by an external MCP server, specified as a cell array of struct (requires the MATLAB MCP HTTP Client add\-on).
51+
52+
For example, if you have an `mcpHTTPClient` (MATLAB MCP HTTP Client) object `client`, then you can specify `mcpTool` as `client.Tools`.
53+
4154
# Properties
4255
### `FunctionName` — Tool name
4356

functionSignatures.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,28 @@
153153
{"name":"message","type":"struct"},
154154
{"name":"response","type":"matlab.net.http.ResponseMessage"}
155155
]
156+
},
157+
"openAIFunction.openAIFunction":
158+
{
159+
"inputs":
160+
[
161+
{"name":"name","kind":"required","type":["string","scalar"]},
162+
{"name":"description","kind":"positional","type":["string","scalar"]}
163+
],
164+
"outputs":
165+
[
166+
{"name":"this","type":"openAIFunction"}
167+
]
168+
},
169+
"openAIFunction.openAIFunction":
170+
{
171+
"inputs":
172+
[
173+
{"name":"definition","kind":"required","type":[["struct"],["cell"]]}
174+
],
175+
"outputs":
176+
[
177+
{"name":"this","type":"openAIFunction"}
178+
]
156179
}
157180
}

openAIFunction.m

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,19 @@
3737
methods
3838
function this = openAIFunction(name, description)
3939
arguments
40-
name (1,1) {mustBeNonzeroLengthText}
40+
name {mustBeNonzeroLengthTextOrStruct}
4141
description {llms.utils.mustBeTextOrEmpty} = []
4242
end
4343

44+
if isstruct(name) || (iscell(name) && ~isempty(name) && isstruct(name{1}))
45+
if nargin > 1
46+
error("llms:mustOnlyBeStruct", ...
47+
llms.utils.errorMessageCatalog.getMessage("llms:mustOnlyBeStruct"));
48+
end
49+
this = structToFunction(name);
50+
return
51+
end
52+
4453
this.FunctionName = name;
4554
this.Description = description;
4655
end
@@ -157,6 +166,18 @@ function mustBeValidVariableName(value)
157166
end
158167
end
159168

169+
function mustBeNonzeroLengthTextOrStruct(value)
170+
if isstruct(value) || (iscell(value) && all(cellfun(@isstruct,value)))
171+
return
172+
end
173+
try
174+
mustBeNonzeroLengthText(value)
175+
mustBeTextScalar(value)
176+
catch
177+
error("llms:mustBeNonzeroLengthTextOrStruct", llms.utils.errorMessageCatalog.getMessage("llms:mustBeNonzeroLengthTextOrStruct"));
178+
end
179+
end
180+
160181
function validatePropertyValue(value,name)
161182
switch(name)
162183
case "type"
@@ -181,4 +202,43 @@ function validatePropertyEnum(value)
181202
if ~llms.utils.isUnique(value)
182203
error("llms:mustBeUnique", llms.utils.errorMessageCatalog.getMessage("llms:mustBeUnique"));
183204
end
205+
end
206+
207+
function fns = structToFunction(fnStruct)
208+
if iscell(fnStruct)
209+
fns = cellfun(@structToFunction,fnStruct,"UniformOutput",false);
210+
fns = reshape([fns{:}],size(fnStruct));
211+
return
212+
end
213+
if ~isscalar(fnStruct)
214+
fns = arrayfun(@structToFunction,fnStruct,"UniformOutput",false);
215+
fns = reshape([fns{:}],size(fnStruct));
216+
return
217+
end
218+
219+
if isfield(fnStruct,'description')
220+
fns = openAIFunction(string(fnStruct.name),string(fnStruct.description));
221+
else
222+
fns = openAIFunction(string(fnStruct.name));
223+
end
224+
argStruct = fnStruct.inputSchema;
225+
props = argStruct.properties;
226+
names = string(fieldnames(props));
227+
for name = names(:).'
228+
optargs = {};
229+
if isfield(props.(name),"type")
230+
type = string(props.(name).type);
231+
% openAIFunction only supports a subset of types
232+
if ismember(type,["string","number","integer","object","boolean","null"])
233+
optargs = [optargs, {"type",type}];%#ok<AGROW>
234+
end
235+
end
236+
if isfield(props.(name),"description")
237+
description = string(props.(name).description);
238+
optargs = [optargs, {"description", description}];%#ok<AGROW>
239+
end
240+
required = isfield(argStruct,"required") && ismember(name,argStruct.required);
241+
fns = addParameter(fns,name,optargs{:}, ...
242+
RequiredParameter=required);
243+
end
184244
end

tests/topenAIFunction.m

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,50 @@ function testCreateFunctionWithDescription(testCase)
2020
end
2121

2222
function testCreateFunctionWithoutDescription(testCase)
23-
name = "functionName";
23+
name = 'functionName';
2424
funObj = openAIFunction(name);
2525
testCase.verifyEqual(funObj.FunctionName, name);
2626
testCase.verifyEmpty(funObj.Description);
2727
end
2828

29+
function testCreateFromScalarStruct(testCase)
30+
toolDef = hFcnStruct;
31+
% difference between openAI and MCP:
32+
toolDef = renameStructField(toolDef,'parameters','inputSchema');
33+
funObj = openAIFunction(toolDef);
34+
testCase.verifyEqual(funObj.FunctionName,toolDef.name);
35+
testCase.verifyEqual(funObj.Description,[]);
36+
end
37+
38+
function testCreateFromStructArray(testCase)
39+
toolDef = hFcnStruct;
40+
% difference between openAI and MCP:
41+
toolDef = renameStructField(toolDef,'parameters','inputSchema');
42+
toolDef2 = toolDef;
43+
toolDef2.name = "otherTool";
44+
funObj = openAIFunction([toolDef,toolDef2]);
45+
testCase.assertSize(funObj,[1,2]);
46+
testCase.verifyEqual(funObj(1).FunctionName,toolDef.name);
47+
testCase.verifyEqual(funObj(1).Description,[]);
48+
testCase.verifyEqual(funObj(2).FunctionName,"otherTool");
49+
testCase.verifyEqual(funObj(2).Description,[]);
50+
end
51+
52+
function testCreateFromCellOfStructs(testCase)
53+
toolDef = hFcnStruct;
54+
% difference between openAI and MCP:
55+
toolDef = renameStructField(toolDef,'parameters','inputSchema');
56+
toolDef2 = toolDef;
57+
toolDef2.name = "otherTool";
58+
toolDef2.description = "desc";
59+
funObj = openAIFunction({toolDef,toolDef2});
60+
testCase.assertSize(funObj,[1,2]);
61+
testCase.verifyEqual(funObj(1).FunctionName,toolDef.name);
62+
testCase.verifyEqual(funObj(1).Description,[]);
63+
testCase.verifyEqual(funObj(2).FunctionName,"otherTool");
64+
testCase.verifyEqual(funObj(2).Description,"desc");
65+
end
66+
2967
function testParametersAreAdded(testCase)
3068
funObj = openAIFunction("getCurrentWeather");
3169

@@ -45,16 +83,7 @@ function testValidProperties(testCase, ValidProperties)
4583

4684
function testValidOutputStructWithParameters(testCase)
4785
% Format expected from OpenAI
48-
expectedStruct = struct("name", "getCurrentWeather",...
49-
"parameters", struct("type", "object", ...
50-
"properties", struct( ...
51-
"location", struct("type", "string", ...
52-
"description", "City and state."), ...
53-
"format", struct("type", "string",...
54-
"enum", ["celsius", "fahrenheit"], ...
55-
"description", "Temperature unit to use.")), ...
56-
"required", ["location", "format"]));
57-
86+
expectedStruct = hFcnStruct;
5887
funObj = openAIFunction("getCurrentWeather");
5988

6089
funObj = addParameter(funObj, "location", type="string", ...
@@ -201,17 +230,33 @@ function testInvalidInputsAddParameter(testCase, InvalidInputsaddParameter)
201230
invalidInputsConstructor = struct( ...
202231
"NonTextName", ...
203232
struct("Input", {{123}}, ...
204-
"Error", "MATLAB:validators:mustBeNonzeroLengthText"), ...
233+
"Error", "llms:mustBeNonzeroLengthTextOrStruct"), ...
205234
...
206235
"NonTextDescription", ...
207236
struct("Input", {{"functionName", 123}}, ...
208237
"Error", "MATLAB:validators:mustBeTextScalar"), ...
209238
...
210239
"NonScalarName", ...
211240
struct("Input", {{["name1" "name2"]}}, ...
212-
"Error", "MATLAB:validation:IncompatibleSize"), ...
241+
"Error", "llms:mustBeNonzeroLengthTextOrStruct"), ...
213242
...
214243
"NonScalarDescription", ...
215244
struct("Input", {{"functionName", ["desc1" "desc2"]}}, ...
216-
"Error", "MATLAB:validators:mustBeTextScalar"));
245+
"Error", "MATLAB:validators:mustBeTextScalar"), ...
246+
...
247+
"StructPlusNVPs", ...
248+
struct("Input", {{hFcnStruct, "description"}}, ...
249+
"Error", "llms:mustOnlyBeStruct"));
250+
end
251+
252+
function structEnc = hFcnStruct
253+
structEnc = struct("name", "getCurrentWeather",...
254+
"parameters", struct("type", "object", ...
255+
"properties", struct( ...
256+
"location", struct("type", "string", ...
257+
"description", "City and state."), ...
258+
"format", struct("type", "string",...
259+
"enum", ["celsius", "fahrenheit"], ...
260+
"description", "Temperature unit to use.")), ...
261+
"required", ["location", "format"]));
217262
end

0 commit comments

Comments
 (0)