aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsigoden <sigoden@gmail.com>2024-06-07 15:16:31 +0800
committerGitHub <noreply@github.com>2024-06-07 15:16:31 +0800
commit739a832d87c00e3b5977a24bba5654fa5ea7a702 (patch)
tree2bb4c102a3e04b9c8c1ecd61bdb6c92f84ca27cb
parent2b07fc2c7e4e6311d35ae72c17b25e47680d61f6 (diff)
downloadllm-functions-docker-739a832d87c00e3b5977a24bba5654fa5ea7a702.tar.gz
feat: js/py generate declarations from comments (#30)
-rw-r--r--Argcfile.sh102
-rw-r--r--README.md76
-rw-r--r--scripts/build-declarations.js203
-rw-r--r--scripts/build-declarations.py177
-rwxr-xr-xscripts/build-declarations.sh43
-rwxr-xr-xscripts/create-tool.sh98
-rwxr-xr-xscripts/run-tool.js31
-rwxr-xr-xscripts/run-tool.py42
-rwxr-xr-xscripts/run-tool.sh98
-rw-r--r--tools/demo_tool.js16
-rw-r--r--tools/demo_tool.py32
-rwxr-xr-xtools/demo_tool.sh15
-rw-r--r--tools/may_execute_js_code.js29
-rw-r--r--tools/may_execute_py_code.py27
14 files changed, 717 insertions, 272 deletions
diff --git a/Argcfile.sh b/Argcfile.sh
index 8b0317d..382cd88 100644
--- a/Argcfile.sh
+++ b/Argcfile.sh
@@ -22,26 +22,32 @@ call() {
}
# @cmd Build the project
-# @option --names-file=functions.txt Path to a file containing function filenames, one per line.
+# @option --names-file=functions.txt Path to a file containing tool filenames, one per line.
+# @option --declarations-file=functions.json <FILE> Path to a json file to save function declarations
# This file specifies which function files to build.
# Example:
# get_current_weather.sh
# may_execute_js_code.js
build() {
- argc build-declarations-json --names-file "${argc_names_file}"
+ argc build-declarations-json --names-file "${argc_names_file}" --declarations-file "${argc_declarations_file}"
argc build-bin --names-file "${argc_names_file}"
}
-# @cmd Build bin dir
-# @option --names-file=functions.txt Path to a file containing function filenames, one per line.
+# @cmd Build tool binaries
+# @option --names-file=functions.txt Path to a file containing tool filenames, one per line.
+# @arg tools*[`_choice_tool`] The tool filenames
build-bin() {
- if [[ ! -f "$argc_names_file" ]]; then
- _die "no found "$argc_names_file""
+ if [[ "${#argc_tools[@]}" -gt 0 ]]; then
+ names=("${argc_tools[@]}" )
+ elif [[ -f "$argc_names_file" ]]; then
+ names=($(cat "$argc_names_file"))
+ fi
+ if [[ -z "$names" ]]; then
+ _die "error: no tools selected"
fi
mkdir -p "$BIN_DIR"
rm -rf "$BIN_DIR"/*
- names=($(cat "$argc_names_file"))
- not_found_funcs=()
+ not_found_tools=()
for name in "${names[@]}"; do
basename="${name%.*}"
lang="${name##*.}"
@@ -52,14 +58,14 @@ build-bin() {
_build_win_shim $lang > "$bin_file"
else
bin_file="$BIN_DIR/$basename"
- ln -s "$PWD/scripts/run-tool.$lang" "$bin_file"
+ ln -s -f "$PWD/scripts/run-tool.$lang" "$bin_file"
fi
else
- not_found_funcs+=("$name")
+ not_found_tools+=("$name")
fi
done
- if [[ -n "$not_found_funcs" ]]; then
- _die "error: not founds functions: ${not_found_funcs[*]}"
+ if [[ -n "$not_found_tools" ]]; then
+ _die "error: not found tools: ${not_found_tools[*]}"
fi
for name in "$BIN_DIR"/*; do
echo "Build $name"
@@ -67,73 +73,71 @@ build-bin() {
}
# @cmd Build declarations.json
-# @option --output=functions.json <FILE> Path to a json file to save function declarations
-# @option --names-file=functions.txt Path to a file containing function filenames, one per line.
-# @arg funcs*[`_choice_func`] The function filenames
+# @option --names-file=functions.txt Path to a file containing tool filenames, one per line.
+# @option --declarations-file=functions.json <FILE> Path to a json file to save function declarations
+# @arg tools*[`_choice_tool`] The tool filenames
build-declarations-json() {
- if [[ "${#argc_funcs[@]}" -gt 0 ]]; then
- names=("${argc_funcs[@]}" )
+ if [[ "${#argc_tools[@]}" -gt 0 ]]; then
+ names=("${argc_tools[@]}" )
elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file"))
fi
if [[ -z "$names" ]]; then
- _die "error: no function for building declarations.json"
+ _die "error: no tools selected"
fi
json_list=()
- not_found_funcs=()
- build_failed_funcs=()
+ not_found_tools=()
+ build_failed_tools=()
for name in "${names[@]}"; do
lang="${name##*.}"
func_file="tools/$name"
if [[ ! -f "$func_file" ]]; then
- not_found_funcs+=("$name")
+ not_found_tools+=("$name")
continue;
fi
- json_data="$("build-single-declaration" "$name")" || {
- build_failed_funcs+=("$name")
+ json_data="$(build-tool-declaration "$name")" || {
+ build_failed_tools+=("$name")
}
json_list+=("$json_data")
done
- if [[ -n "$not_found_funcs" ]]; then
- _die "error: not found functions: ${not_found_funcs[*]}"
+ if [[ -n "$not_found_tools" ]]; then
+ _die "error: not found tools: ${not_found_tools[*]}"
fi
- if [[ -n "$build_failed_funcs" ]]; then
- _die "error: invalid functions: ${build_failed_funcs[*]}"
+ if [[ -n "$build_failed_tools" ]]; then
+ _die "error: invalid tools: ${build_failed_tools[*]}"
fi
- echo "Build $argc_output"
- echo "["$(IFS=,; echo "${json_list[*]}")"]" | jq '.' > "$argc_output"
+ echo "Build $argc_declarations_file"
+ echo "["$(IFS=,; echo "${json_list[*]}")"]" | jq '.' > "$argc_declarations_file"
}
-# @cmd Build single declaration
-# @arg func![`_choice_func`] The function name
-build-single-declaration() {
- func="$1"
- lang="${func##*.}"
+# @cmd Build function declaration for a tool
+# @arg tool![`_choice_tool`] The function name
+build-tool-declaration() {
+ lang="${1##*.}"
cmd="$(_lang_to_cmd "$lang")"
- LLM_FUNCTION_ACTION=declarate "$cmd" "scripts/run-tool.$lang" "$func"
+ "$cmd" "scripts/build-declarations.$lang" "tools/$1" | jq '.[0]'
}
-# @cmd List functions that can be put into functions.txt
+# @cmd List tools that can be put into functions.txt
# Examples:
-# argc --list-functions > functions.txt
-# argc --list-functions search_duckduckgo.sh >> functions.txt
-# @arg funcs*[`_choice_func`] The function filenames, list all available functions if not provided
-list-functions() {
- _choice_func
+# argc list-tools > functions.txt
+list-tools() {
+ _choice_tool
}
# @cmd Test the project
test() {
- func_names_file=functions.txt.test
- argc list-functions > "$func_names_file"
- argc build --names-file "$func_names_file"
- argc test-functions
- rm -rf "$func_names_file"
+ mkdir -p tmp/tests
+ names_file=tmp/tests/functions.txt
+ declarations_file=tmp/tests/functions.json
+ argc list-tools > "$names_file"
+ argc build --names-file "$names_file" --declarations-file "$declarations_file"
+ argc test-tools
}
# @cmd Test call functions
-test-functions() {
+test-tools() {
if _is_win; then
ext=".cmd"
fi
@@ -172,7 +176,7 @@ install() {
fi
}
-# @cmd Create a boilplate tool script file.
+# @cmd Create a boilplate tool scriptfile.
# @arg args~
create() {
./scripts/create-tool.sh "$@"
@@ -239,7 +243,7 @@ _is_win() {
fi
}
-_choice_func() {
+_choice_tool() {
for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}"
cmd="${item#*:}"
diff --git a/README.md b/README.md
index 435e005..acdd681 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,9 @@ AIChat will ask permission before running the function.
## Writing Your Own Functions
-The project supports write functions in bash/js/python.
+You can write functions in bash/javascript/python.
+
+`llm-functions` will automatic generate function declarations from comments. Refer to `demo_tool.{sh,js,py}` for examples of how to use comments for autogeneration of declarations.
### Bash
@@ -92,47 +94,19 @@ main() {
eval "$(argc --argc-eval "$0" "$@")"
```
-`llm-functions` will automatic generate function declaration.json from [comment tags](https://github.com/sigoden/argc?tab=readme-ov-file#comment-tags).
-
-The relationship between comment tags and parameters in function declarations is as follows:
-
-```sh
-# @flag --boolean Parameter `{"type": "boolean"}`
-# @option --string Parameter `{"type": "string"}`
-# @option --string-enum[foo|bar] Parameter `{"type": "string", "enum": ["foo", "bar"]}`
-# @option --integer <INT> Parameter `{"type": "integer"}`
-# @option --number <NUM> Parameter `{"type": "number"}`
-# @option --array* <VALUE> Parameter `{"type": "array", "items": {"type":"string"}}`
-# @option --scalar-required! Use `!` to mark a scalar parameter as required.
-# @option --array-required+ Use `+` to mark a array parameter as required
-```
-
### Javascript
Create a new javascript in the [./tools/](./tools/) directory (.e.g. `may_execute_js_code.js`).
```js
-exports.declarate = function declarate() {
- return {
- "name": "may_execute_js_code",
- "description": "Runs the javascript code in node.js.",
- "parameters": {
- "type": "object",
- "properties": {
- "code": {
- "type": "string",
- "description": "Javascript code to execute, such as `console.log(\"hello world\")`"
- }
- },
- "required": [
- "code"
- ]
- }
- }
-}
-
-exports.execute = function execute(data) {
- eval(data.code)
+/**
+ * Runs the javascript code in node.js.
+ * @typedef {Object} Args
+ * @property {string} code - Javascript code to execute, such as `console.log("hello world")`
+ * @param {Args} args
+ */
+exports.main = function main({ code }) {
+ eval(code);
}
```
@@ -142,27 +116,13 @@ exports.execute = function execute(data) {
Create a new python script in the [./tools/](./tools/) directory (e.g., `may_execute_py_code.py`).
```py
-def declarate():
- return {
- "name": "may_execute_py_code",
- "description": "Runs the python code.",
- "parameters": {
- "type": "object",
- "properties": {
- "code": {
- "type": "string",
- "description": "python code to execute, such as `print(\"hello world\")`"
- }
- },
- "required": [
- "code"
- ]
- }
- }
-
-
-def execute(data):
- exec(data["code"])
+def main(code: str):
+ """Runs the python code.
+ Args:
+ code: Python code to execute, such as `print("hello world")`
+ """
+ exec(code)
+
```
## License
diff --git a/scripts/build-declarations.js b/scripts/build-declarations.js
new file mode 100644
index 0000000..e1a2892
--- /dev/null
+++ b/scripts/build-declarations.js
@@ -0,0 +1,203 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+
+function main() {
+ const scriptfile = process.argv[2];
+ const contents = fs.readFileSync(process.argv[2], "utf8");
+ const functions = extractFunctions(contents);
+ let declarations = functions.map(({ funcName, jsdoc }) => {
+ const { description, params } = parseJsDoc(jsdoc, funcName);
+ const declaration = buildDeclaration(funcName, description, params);
+ return declaration;
+ });
+ const name = getBasename(scriptfile);
+ if (declarations.length > 0) {
+ declarations = declarations.slice(0, 1);
+ declarations[0].name = name;
+ }
+ console.log(JSON.stringify(declarations, null, 2));
+}
+
+/**
+ * @param {string} contents
+ * @param {bool} isTool
+ */
+function extractFunctions(contents, isTool = true) {
+ const output = [];
+ const lines = contents.split("\n");
+ let isInComment = false;
+ let jsdoc = "";
+ let incompleteComment = "";
+ for (let line of lines) {
+ if (/^\s*\/\*/.test(line)) {
+ isInComment = true;
+ incompleteComment += `\n${line}`;
+ } else if (/^\s*\*\//.test(line)) {
+ isInComment = false;
+ incompleteComment += `\n${line}`;
+ jsdoc = incompleteComment;
+ incompleteComment = "";
+ } else if (isInComment) {
+ incompleteComment += `\n${line}`;
+ } else {
+ if (!jsdoc || line.trim() === "") {
+ continue;
+ }
+ if (isTool) {
+ if (/function main/.test(line)) {
+ output.push({
+ funcName: "main",
+ jsdoc,
+ });
+ }
+ } else {
+ const match = /function *([_A-Za-z]+)/.exec(line);
+ if (match) {
+ const funcName = match[1];
+ if (!funcName.startsWith("_")) {
+ output.push({ funcName, jsdoc });
+ }
+ }
+ }
+ jsdoc = "";
+ }
+ }
+ return output;
+}
+
+/**
+ * @param {string} jsdoc
+ * @param {string} funcName,
+ */
+function parseJsDoc(jsdoc, funcName) {
+ const lines = jsdoc.split("\n");
+ let description = "";
+ const rawParams = [];
+ let tag = "";
+ for (let line of lines) {
+ line = line.replace(/^\s*(\/\*\*|\*\/|\*)/, "").trim();
+ let match = /^@(\w+)/.exec(line);
+ if (match) {
+ tag = match[1];
+ }
+ if (!tag) {
+ description += `\n${line}`;
+ } else if (tag == "property") {
+ if (match) {
+ rawParams.push(line.slice(tag.length + 1).trim());
+ } else {
+ rawParams[rawParams.length - 1] += `\n${line}`;
+ }
+ }
+ }
+ const params = [];
+ for (const rawParam of rawParams) {
+ try {
+ params.push(parseParam(rawParam));
+ } catch (err) {
+ throw new Error(
+ `Unable to parse function '${funcName}' of jsdoc '@property ${rawParam}'`,
+ );
+ }
+ }
+ return {
+ description: description.trim(),
+ params,
+ };
+}
+
+/**
+ * @typedef {ReturnType<parseParam>} Param
+ */
+
+/**
+ * @param {string} rawParam
+ */
+function parseParam(rawParam) {
+ const regex = /^{([^}]+)} +(\S+)( *- +| +)?/;
+ const match = regex.exec(rawParam);
+ if (!match) {
+ throw new Error(`Invalid jsdoc comment`);
+ }
+ const type = match[1];
+ let name = match[2];
+ const description = rawParam.replace(regex, "");
+
+ let required = true;
+ if (/^\[.*\]$/.test(name)) {
+ name = name.slice(1, -1);
+ required = false;
+ }
+ let property = buildProperty(type, description);
+ return { name, property, required };
+}
+
+/**
+ * @param {string} type
+ * @param {string} description
+ */
+function buildProperty(type, description) {
+ type = type.toLowerCase();
+ const property = {};
+ if (type.includes("|")) {
+ property.type = "string";
+ property.enum = type.replace(/'/g, "").split("|");
+ } else if (type === "boolean") {
+ property.type = "boolean";
+ } else if (type === "string") {
+ property.type = "string";
+ } else if (type === "integer") {
+ property.type = "integer";
+ } else if (type === "number") {
+ property.type = "number";
+ } else if (type === "string[]") {
+ property.type = "array";
+ property.items = { type: "string" };
+ } else {
+ throw new Error(`Unsupported type '${type}'`);
+ }
+ property.description = description;
+ return property;
+}
+
+/**
+ * @param {string} filePath
+ */
+function getBasename(filePath) {
+ const filenameWithExt = filePath.split(/[/\\]/).pop();
+
+ const lastDotIndex = filenameWithExt.lastIndexOf(".");
+
+ if (lastDotIndex === -1) {
+ return filenameWithExt;
+ }
+
+ return filenameWithExt.substring(0, lastDotIndex);
+}
+
+/**
+ * @param {string} name
+ * @param {string} description
+ * @param {Param[]} params
+ */
+function buildDeclaration(name, description, params) {
+ const schema = {
+ name,
+ description,
+ properties: {},
+ };
+ const requiredParams = [];
+ for (const { name, property, required } of params) {
+ schema.properties[name] = property;
+ if (required) {
+ requiredParams.push(name);
+ }
+ }
+ if (requiredParams.length > 0) {
+ schema.required = requiredParams;
+ }
+ return schema;
+}
+
+main();
diff --git a/scripts/build-declarations.py b/scripts/build-declarations.py
new file mode 100644
index 0000000..17a27b6
--- /dev/null
+++ b/scripts/build-declarations.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+
+import ast
+import os
+import json
+import re
+import sys
+from collections import OrderedDict
+
+
+def main():
+ scriptfile = sys.argv[1]
+ with open(scriptfile, "r", encoding="utf-8") as f:
+ contents = f.read()
+
+ functions = extract_functions(contents)
+ declarations = []
+ for function in functions:
+ func_name, docstring, func_args = function
+ description, params = parse_docstring(docstring)
+ declarations.append(
+ build_declaration(func_name, description, params, func_args)
+ )
+
+ name = os.path.splitext(os.path.basename(scriptfile))[0]
+ if declarations:
+ declarations = declarations[0:1]
+ declarations[0]["name"] = name
+
+ print(json.dumps(declarations, indent=2))
+
+
+def extract_functions(contents: str):
+ tree = ast.parse(contents)
+ output = []
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.FunctionDef):
+ continue
+ func_name = node.name
+ if func_name.startswith("_"):
+ continue
+ docstring = ast.get_docstring(node) or ""
+ func_args = OrderedDict()
+ for arg in node.args.args:
+ arg_name = arg.arg
+ arg_type = get_arg_type(arg.annotation)
+ func_args[arg_name] = arg_type
+ output.append((func_name, docstring, func_args))
+ return output
+
+
+def get_arg_type(annotation) -> str:
+ if annotation is None:
+ return ""
+ elif isinstance(annotation, ast.Name):
+ return annotation.id
+ elif isinstance(annotation, ast.Subscript):
+ if isinstance(annotation.value, ast.Name):
+ type_name = annotation.value.id
+ if type_name == "List":
+ child = get_arg_type(annotation.slice)
+ return f"list[{child}]"
+ if type_name == "Literal":
+ literals = [ast.unparse(el) for el in annotation.slice.elts]
+ return f"{'|'.join(literals)}"
+ if type_name == "Optional":
+ child = get_arg_type(annotation.slice)
+ return f"{child}?"
+ return "any"
+
+
+def parse_docstring(docstring: str):
+ lines = docstring.splitlines()
+ description = ""
+ rawParams = []
+ is_in_args = False
+ for line in lines:
+ if not is_in_args:
+ if line.startswith("Args:"):
+ is_in_args = True
+ else:
+ description += f"\n{line}"
+ continue
+ else:
+ if re.search(r"^\s+", line):
+ rawParams.append(line.strip())
+ else:
+ break
+ params = {}
+ for rawParam in rawParams:
+ name, type_, description = parse_param(rawParam)
+ params[name] = (type_, description)
+ return (description.strip(), params)
+
+
+def parse_param(raw_param: str):
+ name = ""
+ description = ""
+ type_from_comment = ""
+ if ":" in raw_param:
+ name, description = raw_param.split(":", 1)
+ name = name.strip()
+ description = description.strip()
+ else:
+ name = raw_param
+ if " " in name:
+ name, type_from_comment = name.split(" ", 1)
+ type_from_comment = type_from_comment.strip()
+
+ if type_from_comment.startswith("(") and type_from_comment.endswith(")"):
+ type_from_comment = type_from_comment[1:-1]
+ type_parts = [value.strip() for value in type_from_comment.split(",")]
+ type_ = type_parts[0]
+ if "optional" in type_parts[1:]:
+ type_ = f"{type_}?"
+
+ return (name, type_, description)
+
+
+def build_declaration(
+ name: str, description: str, params: dict, args: OrderedDict[str, str]
+) -> dict[str, dict]:
+ schema = {
+ "name": name,
+ "description": description,
+ "properties": {},
+ }
+ required_params = []
+ for arg_name, arg_type in args.items():
+ type_ = arg_type
+ description = ""
+ required = True
+ if params.get(arg_name):
+ param_type, description = params[arg_name]
+ if not type_:
+ type_ = param_type
+ if type_.endswith("?"):
+ type_ = type_[:-1]
+ required = False
+ try:
+ property = build_property(type_, description)
+ except:
+ raise ValueError(f"Unable to parse arg '{arg_name}' of function '{name}'")
+ schema["properties"][arg_name] = property
+ if required:
+ required_params.append(arg_name)
+ if required_params:
+ schema["required"] = required_params
+ return schema
+
+
+def build_property(type_: str, description: str):
+ property = {}
+ if "|" in type_:
+ property["type"] = "string"
+ property["enum"] = type_.replace("'", "").split("|")
+ elif type_ == "bool":
+ property["type"] = "boolean"
+ elif type_ == "str":
+ property["type"] = "string"
+ elif type_ == "int":
+ property["type"] = "integer"
+ elif type_ == "float":
+ property["type"] = "number"
+ elif type_ == "list[str]":
+ property["type"] = "array"
+ property["items"] = {"type": "string"}
+ elif type_ == "":
+ property["type"] = "string"
+ else:
+ raise ValueError(f"Unsupported type `{type_}`")
+ property["description"] = description
+ return property
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/build-declarations.sh b/scripts/build-declarations.sh
new file mode 100755
index 0000000..1f5786a
--- /dev/null
+++ b/scripts/build-declarations.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+
+argc --argc-export "$1" | \
+jq -r '
+ def parse_description(flag_option):
+ if flag_option.describe == "" then
+ {}
+ else
+ { "description": flag_option.describe }
+ end;
+
+ def parse_enum(flag_option):
+ if flag_option.choice.type == "Values" then
+ { "enum": flag_option.choice.data }
+ else
+ {}
+ end;
+
+ def parse_property(flag_option):
+ [
+ { condition: (flag_option.flag == true), result: { type: "boolean" } },
+ { condition: (flag_option.multiple_occurs == true), result: { type: "array", items: { type: "string" } } },
+ { condition: (flag_option.notations[0] == "INT"), result: { type: "integer" } },
+ { condition: (flag_option.notations[0] == "NUM"), result: { type: "number" } },
+ { condition: true, result: { type: "string" } } ]
+ | map(select(.condition) | .result) | first
+ | (. + parse_description(flag_option))
+ | (. + parse_enum(flag_option))
+ ;
+
+
+ def parse_parameter(flag_options):
+ {
+ type: "object",
+ properties: (reduce flag_options[] as $item ({}; . + { ($item.id | sub("-"; "_"; "g")): parse_property($item) })),
+ required: [flag_options[] | select(.required == true) | .id | sub("-"; "_"; "g")],
+ };
+
+ [{
+ name: (.name | sub("-"; "_"; "g")),
+ description: .describe,
+ parameters: parse_parameter([.flag_options[] | select(.id != "help" and .id != "version")])
+ }]' \ No newline at end of file
diff --git a/scripts/create-tool.sh b/scripts/create-tool.sh
index 40d2ef3..b6d56dc 100755
--- a/scripts/create-tool.sh
+++ b/scripts/create-tool.sh
@@ -1,17 +1,20 @@
#!/usr/bin/env bash
set -e
-# @describe Create a boilplate tool script file.
+# @describe Create a boilplate tool scriptfile.
+#
# It automatically generate declaration json for `*.py` and `*.js` and generate `@option` tags for `.sh`.
# Examples:
# argc create abc.sh foo bar! baz+ qux*
# ./scripts/create-tool.sh test.py foo bar! baz+ qux*
+#
+# @flag --force Override the exist tool file
# @arg name! The script file name.
# @arg params* The script parameters
main() {
output="tools/$argc_name"
- if [[ -f "$output" ]]; then
+ if [[ -f "$output" ]] && [[ -z "$argc_force" ]]; then
_die "$output already exists"
fi
ext="${argc_name##*.}"
@@ -25,6 +28,7 @@ main() {
py) create_py ;;
*) _die "Invalid extension name: $ext, must be one of ${support_exts[*]}" ;;
esac
+ _die "$output generated"
}
create_sh() {
@@ -49,25 +53,91 @@ EOF
}
create_js() {
+ properties=''
+ for param in "${argc_params[@]}"; do
+ if [[ "$param" == *'!' ]]; then
+ param="${param:0:$((${#param}-1))}"
+ property=" * @property {string} $param - "
+ elif [[ "$param" == *'+' ]]; then
+ param="${param:0:$((${#param}-1))}"
+ property=" * @property {string[]} $param - "
+ elif [[ "$param" == *'*' ]]; then
+ param="${param:0:$((${#param}-1))}"
+ property=" * @property {string[]} [$param] - "
+ else
+ property=" * @property {string} [$param] - "
+ fi
+ properties+=$'\n'"$property"
+ done
cat <<EOF > "$output"
-exports.declarate = function declarate() {
- return $(build_schema)
-}
-
-exports.execute = function execute(data) {
- console.log(data)
+/**
+ *
+ * @typedef {Object} Args${properties}
+ * @param {Args} args
+ */
+exports.main = function main(args) {
+ console.log(args);
}
EOF
}
create_py() {
+ has_array_param=false
+ has_optional_pram=false
+ required_properties=''
+ optional_properties=''
+ required_arguments=()
+ optional_arguments=()
+ indent=" "
+ for param in "${argc_params[@]}"; do
+ optional=false
+ if [[ "$param" == *'!' ]]; then
+ param="${param:0:$((${#param}-1))}"
+ type="str"
+ elif [[ "$param" == *'+' ]]; then
+ param="${param:0:$((${#param}-1))}"
+ type="List[str]"
+ has_array_param=true
+ elif [[ "$param" == *'*' ]]; then
+ param="${param:0:$((${#param}-1))}"
+ type="Optional[List[str]] = None"
+ optional=true
+ has_array_param=true
+ else
+ optional=true
+ type="Optional[str] = None"
+ fi
+ if [[ "$optional" == "true" ]]; then
+ has_optional_pram=true
+ optional_arguments+="$param: $type, "
+ optional_properties+=$'\n'"$indent$indent$param: -"
+ else
+ required_arguments+="$param: $type, "
+ required_properties+=$'\n'"$indent$indent$param: -"
+ fi
+ done
+ import_typing_members=()
+ if [[ "$has_array_param" == "true" ]]; then
+ import_typing_members+=("List")
+ fi
+ if [[ "$has_optional_pram" == "true" ]]; then
+ import_typing_members+=("Optional")
+ fi
+ imports=""
+ if [[ -n "$import_typing_members" ]]; then
+ members="$(echo "${import_typing_members[*]}" | sed 's/ /, /')"
+ imports="from typing import $members"$'\n'
+ fi
+ if [[ -n "$imports" ]]; then
+ imports="$imports"$'\n'
+ fi
cat <<EOF > "$output"
-def declarate():
- return $(build_schema)
-
-
-def execute(data):
- print(data)
+${imports}
+def main(${required_arguments}${optional_arguments}):
+ """
+ Args:${required_properties}${optional_properties}
+ """
+ pass
EOF
}
diff --git a/scripts/run-tool.js b/scripts/run-tool.js
index 43a6587..f99b2e0 100755
--- a/scripts/run-tool.js
+++ b/scripts/run-tool.js
@@ -58,23 +58,18 @@ const [funcName, funcData] = parseArgv();
process.env["LLM_FUNCTION_NAME"] = funcName;
-if (process.env["LLM_FUNCTION_ACTION"] == "declarate") {
- const { declarate } = loadFunc(funcName);
- console.log(JSON.stringify(declarate(), null, 2));
-} else {
- if (!funcData) {
- console.log("No json data");
- process.exit(1);
- }
-
- let args;
- try {
- args = JSON.parse(funcData);
- } catch {
- console.log("Invalid json data");
- process.exit(1);
- }
+if (!funcData) {
+ console.log("No json data");
+ process.exit(1);
+}
- const { execute } = loadFunc(funcName);
- execute(args);
+let args;
+try {
+ args = JSON.parse(funcData);
+} catch {
+ console.log("Invalid json data");
+ process.exit(1);
}
+
+const { main } = loadFunc(funcName);
+main(args);
diff --git a/scripts/run-tool.py b/scripts/run-tool.py
index 30968a1..97d1d25 100755
--- a/scripts/run-tool.py
+++ b/scripts/run-tool.py
@@ -5,6 +5,7 @@ import json
import sys
import importlib.util
+
def parse_argv():
func_name = sys.argv[0]
func_data = None
@@ -21,6 +22,7 @@ def parse_argv():
return func_name, func_data
+
def load_func(func_name):
func_file_name = f"{func_name}.py"
func_path = os.path.join(os.environ["LLM_FUNCTIONS_DIR"], f"tools/{func_file_name}")
@@ -33,20 +35,24 @@ def load_func(func_name):
print(f"Invalid function: {func_file_name}")
sys.exit(1)
+
def load_env(file_path):
try:
- with open(file_path, 'r') as f:
+ with open(file_path, "r") as f:
for line in f:
line = line.strip()
- if line.startswith('#') or line == '':
+ if line.startswith("#") or line == "":
continue
- key, *value = line.split('=')
- os.environ[key.strip()] = '='.join(value).strip()
+ key, *value = line.split("=")
+ os.environ[key.strip()] = "=".join(value).strip()
except FileNotFoundError:
pass
-os.environ["LLM_FUNCTIONS_DIR"] = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
+os.environ["LLM_FUNCTIONS_DIR"] = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), "..")
+)
load_env(os.path.join(os.environ["LLM_FUNCTIONS_DIR"], ".env"))
@@ -54,20 +60,16 @@ func_name, func_data = parse_argv()
os.environ["LLM_FUNCTION_NAME"] = func_name
-if os.getenv("LLM_FUNCTION_ACTION") == "declarate":
- module = load_func(func_name)
- print(json.dumps(module.declarate(), indent=2))
-else:
- if not func_data:
- print("No json data")
- sys.exit(1)
+if not func_data:
+ print("No json data")
+ sys.exit(1)
- args = None
- try:
- args = json.loads(func_data)
- except (json.JSONDecodeError, TypeError):
- print("Invalid json data")
- sys.exit(1)
+args = None
+try:
+ args = json.loads(func_data)
+except (json.JSONDecodeError, TypeError):
+ print("Invalid json data")
+ sys.exit(1)
- module = load_func(func_name)
- module.execute(args) \ No newline at end of file
+module = load_func(func_name)
+module.main(**args)
diff --git a/scripts/run-tool.sh b/scripts/run-tool.sh
index 6f5befc..bd32a49 100755
--- a/scripts/run-tool.sh
+++ b/scripts/run-tool.sh
@@ -27,76 +27,32 @@ if [[ "$OS" == "Windows_NT" ]]; then
func_file="$(cygpath -w "$func_file")"
fi
-if [[ "$LLM_FUNCTION_ACTION" == "declarate" ]]; then
- argc --argc-export "$func_file" | \
- $JQ -r '
- def parse_description(flag_option):
- if flag_option.describe == "" then
- {}
- else
- { "description": flag_option.describe }
- end;
-
- def parse_enum(flag_option):
- if flag_option.choice.type == "Values" then
- { "enum": flag_option.choice.data }
- else
- {}
- end;
-
- def parse_property(flag_option):
- [
- { condition: (flag_option.flag == true), result: { type: "boolean" } },
- { condition: (flag_option.multiple_occurs == true), result: { type: "array", items: { type: "string" } } },
- { condition: (flag_option.notations[0] == "INT"), result: { type: "integer" } },
- { condition: (flag_option.notations[0] == "NUM"), result: { type: "number" } },
- { condition: true, result: { type: "string" } } ]
- | map(select(.condition) | .result) | first
- | (. + parse_description(flag_option))
- | (. + parse_enum(flag_option))
- ;
-
-
- def parse_parameter(flag_options):
- {
- type: "object",
- properties: (reduce flag_options[] as $item ({}; . + { ($item.id | sub("-"; "_"; "g")): parse_property($item) })),
- required: [flag_options[] | select(.required == true) | .id],
- };
+if [[ -z "$func_data" ]]; then
+ echo "No json data"
+ exit 1
+fi
- {
- name: (.name | sub("-"; "_"; "g")),
- description: .describe,
- parameters: parse_parameter([.flag_options[] | select(.id != "help" and .id != "version")])
- }'
-else
- if [[ -z "$func_data" ]]; then
- echo "No json data"
- exit 1
+data="$(
+ echo "$func_data" | \
+ $JQ -r '
+ to_entries | .[] |
+ (.key | split("_") | join("-")) as $key |
+ if .value | type == "array" then
+ .value | .[] | "--\($key)\n\(. | @json)"
+ elif .value | type == "boolean" then
+ if .value then "--\($key)" else "" end
+ else
+ "--\($key)\n\(.value | @json)"
+ end'
+)" || {
+ echo "Invalid json data"
+ exit 1
+}
+while IFS= read -r line; do
+ if [[ "$line" == '--'* ]]; then
+ args+=("$line")
+ else
+ args+=("$(echo "$line" | $JQ -r '.')")
fi
-
- data="$(
- echo "$func_data" | \
- $JQ -r '
- to_entries | .[] |
- (.key | split("_") | join("-")) as $key |
- if .value | type == "array" then
- .value | .[] | "--\($key)\n\(. | @json)"
- elif .value | type == "boolean" then
- if .value then "--\($key)" else "" end
- else
- "--\($key)\n\(.value | @json)"
- end'
- )" || {
- echo "Invalid json data"
- exit 1
- }
- while IFS= read -r line; do
- if [[ "$line" == '--'* ]]; then
- args+=("$line")
- else
- args+=("$(echo "$line" | $JQ -r '.')")
- fi
- done <<< "$data"
- "$func_file" "${args[@]}"
-fi \ No newline at end of file
+done <<< "$data"
+"$func_file" "${args[@]}" \ No newline at end of file
diff --git a/tools/demo_tool.js b/tools/demo_tool.js
new file mode 100644
index 0000000..f8fd283
--- /dev/null
+++ b/tools/demo_tool.js
@@ -0,0 +1,16 @@
+/**
+ * Demonstrate how to create a tool using Javascript and how to use comments.
+ * @typedef {Object} Args
+ * @property {string} string - Define a required string property
+ * @property {'foo'|'bar'} string_enum - Define a required string property with enum
+ * @property {string} [string_optional] - Define a optional string property
+ * @property {boolean} boolean - Define a required boolean property
+ * @property {Integer} integer - Define a required integer property
+ * @property {number} number - Define a required number property
+ * @property {string[]} array - Define a required string array property
+ * @property {string[]} [array_optional] - Define a optional string array property
+ * @param {Args} args
+ */
+exports.main = function main(args) {
+ console.log(args);
+}
diff --git a/tools/demo_tool.py b/tools/demo_tool.py
new file mode 100644
index 0000000..bd353ad
--- /dev/null
+++ b/tools/demo_tool.py
@@ -0,0 +1,32 @@
+from typing import List, Literal, Optional
+
+
+def main(
+ boolean: bool,
+ string: str,
+ string_enum: Literal["foo", "bar"],
+ integer: int,
+ number: float,
+ array: List[str],
+ string_optional: Optional[str] = None,
+ array_optional: Optional[List[str]] = None,
+) -> None:
+ """Demonstrate how to create a tool using Python and how to use comments.
+ Args:
+ boolean: Define a required boolean property
+ string: Define a required string property
+ string_enum: Define a required string property with enum
+ integer: Define a required integer property
+ number: Define a required number property
+ array: Define a required string array property
+ string_optional: Define a optional string property
+ array_optional: Define a optional string array property
+ """
+ print(f"boolean: {boolean}")
+ print(f"string: {string}")
+ print(f"string_enum: {string_enum}")
+ print(f"integer: {integer}")
+ print(f"number: {number}")
+ print(f"array: {array}")
+ print(f"string_optional: {string_optional}")
+ print(f"array_optional: {array_optional}")
diff --git a/tools/demo_tool.sh b/tools/demo_tool.sh
new file mode 100755
index 0000000..0d24c75
--- /dev/null
+++ b/tools/demo_tool.sh
@@ -0,0 +1,15 @@
+# @describe Demonstrate how to create a tool using Bash and how to use comment tags.
+# @option --string! Define a required string property
+# @option --string-enum![foo|bar] Define a required string property with enum
+# @option --string-optional Define a optional string property
+# @flag --boolean Define a boolean property
+# @option --integer! <INT> Define a required integer property
+# @option --number! <NUM> Define a required number property
+# @option --array+ <VALUE> Define a required string array property
+# @option --array-optional* Define a optional string array property
+
+main() {
+ ( set -o posix ; set ) | grep ^argc_ # inspect all argc variables
+}
+
+eval "$(argc --argc-eval "$0" "$@")" \ No newline at end of file
diff --git a/tools/may_execute_js_code.js b/tools/may_execute_js_code.js
index 9575582..2dec177 100644
--- a/tools/may_execute_js_code.js
+++ b/tools/may_execute_js_code.js
@@ -1,22 +1,9 @@
-exports.declarate = function declarate() {
- return {
- "name": "may_execute_js_code",
- "description": "Runs the javascript code in node.js.",
- "parameters": {
- "type": "object",
- "properties": {
- "code": {
- "type": "string",
- "description": "Javascript code to execute, such as `console.log(\"hello world\")`"
- }
- },
- "required": [
- "code"
- ]
- }
- }
-}
-
-exports.execute = function execute(data) {
- eval(data.code)
+/**
+ * Runs the javascript code in node.js.
+ * @typedef {Object} Args
+ * @property {string} code - Javascript code to execute, such as `console.log("hello world")`
+ * @param {Args} args
+ */
+exports.main = function main({ code }) {
+ eval(code);
}
diff --git a/tools/may_execute_py_code.py b/tools/may_execute_py_code.py
index 01a59b9..8e55a8d 100644
--- a/tools/may_execute_py_code.py
+++ b/tools/may_execute_py_code.py
@@ -1,21 +1,6 @@
-def declarate():
- return {
- "name": "may_execute_py_code",
- "description": "Runs the python code.",
- "parameters": {
- "type": "object",
- "properties": {
- "code": {
- "type": "string",
- "description": "Python code to execute, such as `print(\"hello world\")`"
- }
- },
- "required": [
- "code"
- ]
- }
- }
-
-
-def execute(data):
- exec(data["code"])
+def main(code: str):
+ """Runs the python code.
+ Args:
+ code: Python code to execute, such as `print("hello world")`
+ """
+ exec(code)