diff options
| author | sigoden <sigoden@gmail.com> | 2024-06-07 15:16:31 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-07 15:16:31 +0800 |
| commit | 739a832d87c00e3b5977a24bba5654fa5ea7a702 (patch) | |
| tree | 2bb4c102a3e04b9c8c1ecd61bdb6c92f84ca27cb | |
| parent | 2b07fc2c7e4e6311d35ae72c17b25e47680d61f6 (diff) | |
| download | llm-functions-docker-739a832d87c00e3b5977a24bba5654fa5ea7a702.tar.gz | |
feat: js/py generate declarations from comments (#30)
| -rw-r--r-- | Argcfile.sh | 102 | ||||
| -rw-r--r-- | README.md | 76 | ||||
| -rw-r--r-- | scripts/build-declarations.js | 203 | ||||
| -rw-r--r-- | scripts/build-declarations.py | 177 | ||||
| -rwxr-xr-x | scripts/build-declarations.sh | 43 | ||||
| -rwxr-xr-x | scripts/create-tool.sh | 98 | ||||
| -rwxr-xr-x | scripts/run-tool.js | 31 | ||||
| -rwxr-xr-x | scripts/run-tool.py | 42 | ||||
| -rwxr-xr-x | scripts/run-tool.sh | 98 | ||||
| -rw-r--r-- | tools/demo_tool.js | 16 | ||||
| -rw-r--r-- | tools/demo_tool.py | 32 | ||||
| -rwxr-xr-x | tools/demo_tool.sh | 15 | ||||
| -rw-r--r-- | tools/may_execute_js_code.js | 29 | ||||
| -rw-r--r-- | tools/may_execute_py_code.py | 27 |
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#*:}" @@ -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) |
