feat: js/py generate declarations from comments (#30)

This commit is contained in:
sigoden
2024-06-07 15:16:31 +08:00
committed by GitHub
parent 2b07fc2c7e
commit 739a832d87
14 changed files with 717 additions and 272 deletions
+53 -49
View File
@@ -22,26 +22,32 @@ call() {
} }
# @cmd Build the project # @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. # This file specifies which function files to build.
# Example: # Example:
# get_current_weather.sh # get_current_weather.sh
# may_execute_js_code.js # may_execute_js_code.js
build() { 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}" argc build-bin --names-file "${argc_names_file}"
} }
# @cmd Build bin dir # @cmd Build tool binaries
# @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.
# @arg tools*[`_choice_tool`] The tool filenames
build-bin() { build-bin() {
if [[ ! -f "$argc_names_file" ]]; then if [[ "${#argc_tools[@]}" -gt 0 ]]; then
_die "no found "$argc_names_file"" 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 fi
mkdir -p "$BIN_DIR" mkdir -p "$BIN_DIR"
rm -rf "$BIN_DIR"/* rm -rf "$BIN_DIR"/*
names=($(cat "$argc_names_file")) not_found_tools=()
not_found_funcs=()
for name in "${names[@]}"; do for name in "${names[@]}"; do
basename="${name%.*}" basename="${name%.*}"
lang="${name##*.}" lang="${name##*.}"
@@ -52,14 +58,14 @@ build-bin() {
_build_win_shim $lang > "$bin_file" _build_win_shim $lang > "$bin_file"
else else
bin_file="$BIN_DIR/$basename" 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 fi
else else
not_found_funcs+=("$name") not_found_tools+=("$name")
fi fi
done done
if [[ -n "$not_found_funcs" ]]; then if [[ -n "$not_found_tools" ]]; then
_die "error: not founds functions: ${not_found_funcs[*]}" _die "error: not found tools: ${not_found_tools[*]}"
fi fi
for name in "$BIN_DIR"/*; do for name in "$BIN_DIR"/*; do
echo "Build $name" echo "Build $name"
@@ -67,73 +73,71 @@ build-bin() {
} }
# @cmd Build declarations.json # @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 tool filenames, one per line.
# @option --names-file=functions.txt Path to a file containing function filenames, one per line. # @option --declarations-file=functions.json <FILE> Path to a json file to save function declarations
# @arg funcs*[`_choice_func`] The function filenames # @arg tools*[`_choice_tool`] The tool filenames
build-declarations-json() { build-declarations-json() {
if [[ "${#argc_funcs[@]}" -gt 0 ]]; then if [[ "${#argc_tools[@]}" -gt 0 ]]; then
names=("${argc_funcs[@]}" ) names=("${argc_tools[@]}" )
elif [[ -f "$argc_names_file" ]]; then elif [[ -f "$argc_names_file" ]]; then
names=($(cat "$argc_names_file")) names=($(cat "$argc_names_file"))
fi fi
if [[ -z "$names" ]]; then if [[ -z "$names" ]]; then
_die "error: no function for building declarations.json" _die "error: no tools selected"
fi fi
json_list=() json_list=()
not_found_funcs=() not_found_tools=()
build_failed_funcs=() build_failed_tools=()
for name in "${names[@]}"; do for name in "${names[@]}"; do
lang="${name##*.}" lang="${name##*.}"
func_file="tools/$name" func_file="tools/$name"
if [[ ! -f "$func_file" ]]; then if [[ ! -f "$func_file" ]]; then
not_found_funcs+=("$name") not_found_tools+=("$name")
continue; continue;
fi fi
json_data="$("build-single-declaration" "$name")" || { json_data="$(build-tool-declaration "$name")" || {
build_failed_funcs+=("$name") build_failed_tools+=("$name")
} }
json_list+=("$json_data") json_list+=("$json_data")
done done
if [[ -n "$not_found_funcs" ]]; then if [[ -n "$not_found_tools" ]]; then
_die "error: not found functions: ${not_found_funcs[*]}" _die "error: not found tools: ${not_found_tools[*]}"
fi fi
if [[ -n "$build_failed_funcs" ]]; then if [[ -n "$build_failed_tools" ]]; then
_die "error: invalid functions: ${build_failed_funcs[*]}" _die "error: invalid tools: ${build_failed_tools[*]}"
fi fi
echo "Build $argc_output" echo "Build $argc_declarations_file"
echo "["$(IFS=,; echo "${json_list[*]}")"]" | jq '.' > "$argc_output" echo "["$(IFS=,; echo "${json_list[*]}")"]" | jq '.' > "$argc_declarations_file"
} }
# @cmd Build single declaration # @cmd Build function declaration for a tool
# @arg func![`_choice_func`] The function name # @arg tool![`_choice_tool`] The function name
build-single-declaration() { build-tool-declaration() {
func="$1" lang="${1##*.}"
lang="${func##*.}"
cmd="$(_lang_to_cmd "$lang")" 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: # Examples:
# argc --list-functions > functions.txt # argc list-tools > functions.txt
# argc --list-functions search_duckduckgo.sh >> functions.txt list-tools() {
# @arg funcs*[`_choice_func`] The function filenames, list all available functions if not provided _choice_tool
list-functions() {
_choice_func
} }
# @cmd Test the project # @cmd Test the project
test() { test() {
func_names_file=functions.txt.test mkdir -p tmp/tests
argc list-functions > "$func_names_file" names_file=tmp/tests/functions.txt
argc build --names-file "$func_names_file" declarations_file=tmp/tests/functions.json
argc test-functions argc list-tools > "$names_file"
rm -rf "$func_names_file" argc build --names-file "$names_file" --declarations-file "$declarations_file"
argc test-tools
} }
# @cmd Test call functions # @cmd Test call functions
test-functions() { test-tools() {
if _is_win; then if _is_win; then
ext=".cmd" ext=".cmd"
fi fi
@@ -172,7 +176,7 @@ install() {
fi fi
} }
# @cmd Create a boilplate tool script file. # @cmd Create a boilplate tool scriptfile.
# @arg args~ # @arg args~
create() { create() {
./scripts/create-tool.sh "$@" ./scripts/create-tool.sh "$@"
@@ -239,7 +243,7 @@ _is_win() {
fi fi
} }
_choice_func() { _choice_tool() {
for item in "${LANG_CMDS[@]}"; do for item in "${LANG_CMDS[@]}"; do
lang="${item%:*}" lang="${item%:*}"
cmd="${item#*:}" cmd="${item#*:}"
+17 -57
View File
@@ -72,7 +72,9 @@ AIChat will ask permission before running the function.
## Writing Your Own Functions ## 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 ### Bash
@@ -92,47 +94,19 @@ main() {
eval "$(argc --argc-eval "$0" "$@")" 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 ### Javascript
Create a new javascript in the [./tools/](./tools/) directory (.e.g. `may_execute_js_code.js`). Create a new javascript in the [./tools/](./tools/) directory (.e.g. `may_execute_js_code.js`).
```js ```js
exports.declarate = function declarate() { /**
return { * Runs the javascript code in node.js.
"name": "may_execute_js_code", * @typedef {Object} Args
"description": "Runs the javascript code in node.js.", * @property {string} code - Javascript code to execute, such as `console.log("hello world")`
"parameters": { * @param {Args} args
"type": "object", */
"properties": { exports.main = function main({ code }) {
"code": { eval(code);
"type": "string",
"description": "Javascript code to execute, such as `console.log(\"hello world\")`"
}
},
"required": [
"code"
]
}
}
}
exports.execute = function execute(data) {
eval(data.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`). Create a new python script in the [./tools/](./tools/) directory (e.g., `may_execute_py_code.py`).
```py ```py
def declarate(): def main(code: str):
return { """Runs the python code.
"name": "may_execute_py_code", Args:
"description": "Runs the python code.", code: Python code to execute, such as `print("hello world")`
"parameters": { """
"type": "object", exec(code)
"properties": {
"code": {
"type": "string",
"description": "python code to execute, such as `print(\"hello world\")`"
}
},
"required": [
"code"
]
}
}
def execute(data):
exec(data["code"])
``` ```
## License ## License
+203
View File
@@ -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();
+177
View File
@@ -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()
+43
View File
@@ -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")])
}]'
+84 -14
View File
@@ -1,17 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e 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`. # It automatically generate declaration json for `*.py` and `*.js` and generate `@option` tags for `.sh`.
# Examples: # Examples:
# argc create abc.sh foo bar! baz+ qux* # argc create abc.sh foo bar! baz+ qux*
# ./scripts/create-tool.sh test.py 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 name! The script file name.
# @arg params* The script parameters # @arg params* The script parameters
main() { main() {
output="tools/$argc_name" output="tools/$argc_name"
if [[ -f "$output" ]]; then if [[ -f "$output" ]] && [[ -z "$argc_force" ]]; then
_die "$output already exists" _die "$output already exists"
fi fi
ext="${argc_name##*.}" ext="${argc_name##*.}"
@@ -25,6 +28,7 @@ main() {
py) create_py ;; py) create_py ;;
*) _die "Invalid extension name: $ext, must be one of ${support_exts[*]}" ;; *) _die "Invalid extension name: $ext, must be one of ${support_exts[*]}" ;;
esac esac
_die "$output generated"
} }
create_sh() { create_sh() {
@@ -49,25 +53,91 @@ EOF
} }
create_js() { 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" cat <<EOF > "$output"
exports.declarate = function declarate() { /**
return $(build_schema) *
} * @typedef {Object} Args${properties}
* @param {Args} args
exports.execute = function execute(data) { */
console.log(data) exports.main = function main(args) {
console.log(args);
} }
EOF EOF
} }
create_py() { 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" cat <<EOF > "$output"
def declarate(): ${imports}
return $(build_schema) def main(${required_arguments}${optional_arguments}):
"""
Args:${required_properties}${optional_properties}
def execute(data): """
print(data) pass
EOF EOF
} }
+8 -13
View File
@@ -58,23 +58,18 @@ const [funcName, funcData] = parseArgv();
process.env["LLM_FUNCTION_NAME"] = funcName; process.env["LLM_FUNCTION_NAME"] = funcName;
if (process.env["LLM_FUNCTION_ACTION"] == "declarate") { if (!funcData) {
const { declarate } = loadFunc(funcName);
console.log(JSON.stringify(declarate(), null, 2));
} else {
if (!funcData) {
console.log("No json data"); console.log("No json data");
process.exit(1); process.exit(1);
} }
let args; let args;
try { try {
args = JSON.parse(funcData); args = JSON.parse(funcData);
} catch { } catch {
console.log("Invalid json data"); console.log("Invalid json data");
process.exit(1); process.exit(1);
}
const { execute } = loadFunc(funcName);
execute(args);
} }
const { main } = loadFunc(funcName);
main(args);
+17 -15
View File
@@ -5,6 +5,7 @@ import json
import sys import sys
import importlib.util import importlib.util
def parse_argv(): def parse_argv():
func_name = sys.argv[0] func_name = sys.argv[0]
func_data = None func_data = None
@@ -21,6 +22,7 @@ def parse_argv():
return func_name, func_data return func_name, func_data
def load_func(func_name): def load_func(func_name):
func_file_name = f"{func_name}.py" func_file_name = f"{func_name}.py"
func_path = os.path.join(os.environ["LLM_FUNCTIONS_DIR"], f"tools/{func_file_name}") 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}") print(f"Invalid function: {func_file_name}")
sys.exit(1) sys.exit(1)
def load_env(file_path): def load_env(file_path):
try: try:
with open(file_path, 'r') as f: with open(file_path, "r") as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line.startswith('#') or line == '': if line.startswith("#") or line == "":
continue continue
key, *value = line.split('=') key, *value = line.split("=")
os.environ[key.strip()] = '='.join(value).strip() os.environ[key.strip()] = "=".join(value).strip()
except FileNotFoundError: except FileNotFoundError:
pass 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")) 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 os.environ["LLM_FUNCTION_NAME"] = func_name
if os.getenv("LLM_FUNCTION_ACTION") == "declarate": if not func_data:
module = load_func(func_name)
print(json.dumps(module.declarate(), indent=2))
else:
if not func_data:
print("No json data") print("No json data")
sys.exit(1) sys.exit(1)
args = None args = None
try: try:
args = json.loads(func_data) args = json.loads(func_data)
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
print("Invalid json data") print("Invalid json data")
sys.exit(1) sys.exit(1)
module = load_func(func_name) module = load_func(func_name)
module.execute(args) module.main(**args)
+8 -52
View File
@@ -27,55 +27,12 @@ if [[ "$OS" == "Windows_NT" ]]; then
func_file="$(cygpath -w "$func_file")" func_file="$(cygpath -w "$func_file")"
fi fi
if [[ "$LLM_FUNCTION_ACTION" == "declarate" ]]; then if [[ -z "$func_data" ]]; 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],
};
{
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" echo "No json data"
exit 1 exit 1
fi fi
data="$( data="$(
echo "$func_data" | \ echo "$func_data" | \
$JQ -r ' $JQ -r '
to_entries | .[] | to_entries | .[] |
@@ -87,16 +44,15 @@ else
else else
"--\($key)\n\(.value | @json)" "--\($key)\n\(.value | @json)"
end' end'
)" || { )" || {
echo "Invalid json data" echo "Invalid json data"
exit 1 exit 1
} }
while IFS= read -r line; do while IFS= read -r line; do
if [[ "$line" == '--'* ]]; then if [[ "$line" == '--'* ]]; then
args+=("$line") args+=("$line")
else else
args+=("$(echo "$line" | $JQ -r '.')") args+=("$(echo "$line" | $JQ -r '.')")
fi fi
done <<< "$data" done <<< "$data"
"$func_file" "${args[@]}" "$func_file" "${args[@]}"
fi
+16
View File
@@ -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);
}
+32
View File
@@ -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}")
+15
View File
@@ -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" "$@")"
+8 -21
View File
@@ -1,22 +1,9 @@
exports.declarate = function declarate() { /**
return { * Runs the javascript code in node.js.
"name": "may_execute_js_code", * @typedef {Object} Args
"description": "Runs the javascript code in node.js.", * @property {string} code - Javascript code to execute, such as `console.log("hello world")`
"parameters": { * @param {Args} args
"type": "object", */
"properties": { exports.main = function main({ code }) {
"code": { eval(code);
"type": "string",
"description": "Javascript code to execute, such as `console.log(\"hello world\")`"
}
},
"required": [
"code"
]
}
}
}
exports.execute = function execute(data) {
eval(data.code)
} }
+6 -21
View File
@@ -1,21 +1,6 @@
def declarate(): def main(code: str):
return { """Runs the python code.
"name": "may_execute_py_code", Args:
"description": "Runs the python code.", code: Python code to execute, such as `print("hello world")`
"parameters": { """
"type": "object", exec(code)
"properties": {
"code": {
"type": "string",
"description": "Python code to execute, such as `print(\"hello world\")`"
}
},
"required": [
"code"
]
}
}
def execute(data):
exec(data["code"])