refactor: restructure repo layout
This commit is contained in:
222
llm-functions/scripts/build-declarations.js
Executable file
222
llm-functions/scripts/build-declarations.js
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const TOOL_ENTRY_FUNC = "run";
|
||||
|
||||
function main() {
|
||||
const scriptfile = process.argv[2];
|
||||
const isTool = path.dirname(scriptfile) == "tools";
|
||||
const contents = fs.readFileSync(process.argv[2], "utf8");
|
||||
const functions = extractFunctions(contents, isTool);
|
||||
let declarations = [];
|
||||
for (const { funcName, jsdoc } of functions) {
|
||||
const { description, params } = parseJsDoc(jsdoc, funcName);
|
||||
if (!description) continue;
|
||||
const declaration = buildDeclaration(funcName, description, params);
|
||||
declarations.push(declaration);
|
||||
}
|
||||
if (isTool) {
|
||||
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) {
|
||||
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 (new RegExp(`^export (async )?function ${TOOL_ENTRY_FUNC}|^exports\.${TOOL_ENTRY_FUNC}`).test(line)) {
|
||||
output.push({
|
||||
funcName: TOOL_ENTRY_FUNC,
|
||||
jsdoc,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let match = /^export (async )?function ([A-Za-z0-9_]+)/.exec(line);
|
||||
let funcName = null;
|
||||
if (match) {
|
||||
funcName = match[2];
|
||||
}
|
||||
if (!funcName) {
|
||||
match = /^exports\.([A-Za-z0-9_]+) = (async )?function /.exec(line);
|
||||
if (match) {
|
||||
funcName = match[1];
|
||||
}
|
||||
}
|
||||
if (funcName && !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} name
|
||||
* @param {string} description
|
||||
* @param {Param[]} params
|
||||
*/
|
||||
function buildDeclaration(name, description, params) {
|
||||
const declaration = {
|
||||
name,
|
||||
description,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
};
|
||||
const schema = declaration.parameters;
|
||||
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 declaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
main();
|
||||
190
llm-functions/scripts/build-declarations.py
Executable file
190
llm-functions/scripts/build-declarations.py
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import ast
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
TOOL_ENTRY_FUNC = "run"
|
||||
|
||||
|
||||
def main(is_tool=True):
|
||||
scriptfile = sys.argv[1]
|
||||
is_tool = os.path.dirname(scriptfile) == "tools"
|
||||
|
||||
with open(scriptfile, "r", encoding="utf-8") as f:
|
||||
contents = f.read()
|
||||
|
||||
functions = extract_functions(contents, is_tool)
|
||||
declarations = []
|
||||
for function in functions:
|
||||
func_name, docstring, func_args = function
|
||||
description, params = parse_docstring(docstring)
|
||||
if not description:
|
||||
continue
|
||||
declarations.append(
|
||||
build_declaration(func_name, description, params, func_args)
|
||||
)
|
||||
|
||||
if is_tool:
|
||||
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, is_tool: bool):
|
||||
tree = ast.parse(contents)
|
||||
output = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.FunctionDef):
|
||||
continue
|
||||
func_name = node.name
|
||||
if is_tool and func_name != TOOL_ENTRY_FUNC:
|
||||
continue
|
||||
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_, param_description = parse_param(rawParam)
|
||||
params[name] = (type_, param_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]:
|
||||
declaration = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
}
|
||||
schema = declaration["parameters"]
|
||||
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 declaration
|
||||
|
||||
|
||||
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()
|
||||
73
llm-functions/scripts/build-declarations.sh
Executable file
73
llm-functions/scripts/build-declarations.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
main() {
|
||||
scriptfile="$1"
|
||||
is_tool=false
|
||||
if [[ "$(dirname "$scriptfile")" == tools ]]; then
|
||||
is_tool=true
|
||||
fi
|
||||
if [[ "$is_tool" == "true" ]]; then
|
||||
expr='[.]'
|
||||
else
|
||||
expr='.subcommands'
|
||||
fi
|
||||
argc --argc-export "$scriptfile" | \
|
||||
jq "$expr" | \
|
||||
build_declarations
|
||||
}
|
||||
|
||||
build_declarations() {
|
||||
jq --arg is_tool "$is_tool" -r '
|
||||
def filter_declaration:
|
||||
(if $is_tool == "true" then
|
||||
.
|
||||
else
|
||||
select(.name | startswith("_") | not)
|
||||
end) | select(.description != "");
|
||||
|
||||
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")],
|
||||
};
|
||||
|
||||
def parse_declaration:
|
||||
{
|
||||
name: (.name | sub("-"; "_"; "g")),
|
||||
description: .describe,
|
||||
parameters: parse_parameter([.flag_options[] | select(.id != "help" and .id != "version")])
|
||||
};
|
||||
[
|
||||
.[] | parse_declaration | filter_declaration
|
||||
]'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
78
llm-functions/scripts/check-deps.sh
Executable file
78
llm-functions/scripts/check-deps.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# @describe Check dependencies
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/check-deps.sh tools/execute_sql_code.sh
|
||||
# ./scripts/check-deps.sh agents/json-viewer/tools.js
|
||||
#
|
||||
# @arg script-path! The script file path
|
||||
|
||||
main() {
|
||||
script_path="$argc_script_path"
|
||||
if [[ ! -f "$script_path" ]]; then
|
||||
echo "✗ not found $script_path"
|
||||
exit 0
|
||||
fi
|
||||
ext="${script_path##*.}"
|
||||
if [[ "$script_path" == tools/* ]]; then
|
||||
if [[ "$ext" == "sh" ]]; then
|
||||
check_sh_dependencies
|
||||
fi
|
||||
elif [[ "$script_path" == agents/* ]]; then
|
||||
if [[ "$ext" == "sh" ]]; then
|
||||
check_sh_dependencies
|
||||
elif [[ "$ext" == "js" ]]; then
|
||||
check_agent_js_dependencies
|
||||
elif [[ "$ext" == "py" ]]; then
|
||||
check_agent_py_dependencies
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_sh_dependencies() {
|
||||
deps=( $(sed -E -n 's/.*@meta require-tools //p' "$script_path") )
|
||||
missing_deps=()
|
||||
for dep in "${deps[@]}"; do
|
||||
if ! command -v "$dep" &> /dev/null; then
|
||||
missing_deps+=("$dep")
|
||||
fi
|
||||
done
|
||||
if [[ -n "${missing_deps}" ]]; then
|
||||
echo "✗ missing tools: ${missing_deps[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_agent_js_dependencies() {
|
||||
agent_dir="$(dirname "$script_path")"
|
||||
if [[ -f "$agent_dir/package.json" ]]; then
|
||||
npm ls --prefix="$agent_dir" --depth=0 --silent >/dev/null 2>&1 || \
|
||||
{
|
||||
cmd="cd $agent_dir && npm install"
|
||||
echo "✗ missing node modules"
|
||||
read -p "? run \`$cmd\` to fix [Y/n] " choice
|
||||
if [[ "$choice" == "Y" || "$choice" == "y" || -z "$choice" ]]; then
|
||||
(eval "$cmd")
|
||||
fi
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
check_agent_py_dependencies() {
|
||||
agent_dir="$(dirname "$script_path")"
|
||||
if [[ -f "$agent_dir/requirements.txt" ]]; then
|
||||
python <(cat "$agent_dir/requirements.txt" | sed -E -n 's/^([A-Za-z_]+).*/import \1/p') >/dev/null 2>&1 || \
|
||||
{
|
||||
cmd="cd $agent_dir && pip install -r requirements.txt"
|
||||
echo "✗ missing python modules"
|
||||
read -p "? run \`$cmd\` to fix [Y/n] " choice
|
||||
if [[ "$choice" == "Y" || "$choice" == "y" || -z "$choice" ]]; then
|
||||
(eval "$cmd")
|
||||
fi
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# See more details at https://github.com/sigoden/argc
|
||||
eval "$(argc --argc-eval "$0" "$@")"
|
||||
195
llm-functions/scripts/create-tool.sh
Executable file
195
llm-functions/scripts/create-tool.sh
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# @describe Create a boilplate tool script
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/create-tool.sh _test.py foo bar! baz+ qux*
|
||||
#
|
||||
# @option --description <text> The tool description
|
||||
# @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" ]] && [[ -z "$argc_force" ]]; then
|
||||
_die "$output already exists"
|
||||
fi
|
||||
ext="${argc_name##*.}"
|
||||
description="${argc_description:-"The description for the tool"}"
|
||||
support_exts=('.sh' '.js' '.py')
|
||||
if [[ "$ext" == "$argc_name" ]]; then
|
||||
_die "error: no extension name, pelease add one of ${support_exts[*]}"
|
||||
fi
|
||||
case $ext in
|
||||
sh) create_sh ;;
|
||||
js) create_js ;;
|
||||
py) create_py ;;
|
||||
*) _die "error: invalid extension name: $ext, must be one of ${support_exts[*]}" ;;
|
||||
esac
|
||||
echo "$output generated"
|
||||
}
|
||||
|
||||
create_sh() {
|
||||
cat <<-'EOF' > "$output"
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
EOF
|
||||
echo "# @describe $description" >> "$output"
|
||||
for param in "${argc_params[@]}"; do
|
||||
echo "# @option --$(echo $param | sed 's/-/_/g')" >> "$output"
|
||||
done
|
||||
cat <<-'EOF' >> "$output"
|
||||
|
||||
main() {
|
||||
( set -o posix ; set ) | grep ^argc_
|
||||
}
|
||||
|
||||
eval "$(argc --argc-eval "$0" "$@")"
|
||||
EOF
|
||||
chmod +x "$output"
|
||||
}
|
||||
|
||||
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"
|
||||
/**
|
||||
* ${description}
|
||||
* @typedef {Object} Args${properties}
|
||||
* @param {Args} args
|
||||
*/
|
||||
exports.run = function (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"
|
||||
${imports}
|
||||
def run(${required_arguments}${optional_arguments}):
|
||||
"""${description}
|
||||
Args:${required_properties}${optional_properties}
|
||||
"""
|
||||
pass
|
||||
EOF
|
||||
}
|
||||
|
||||
build_schema() {
|
||||
echo '{
|
||||
"name": "'"${argc_name%%.*}"'",
|
||||
"description": "",
|
||||
"parameters": '"$(build_properties)"'
|
||||
}' | jq '.' | sed '2,$s/^/ /g'
|
||||
}
|
||||
|
||||
build_properties() {
|
||||
required_params=()
|
||||
properties=''
|
||||
for param in "${argc_params[@]}"; do
|
||||
if [[ "$param" == *'!' ]]; then
|
||||
param="${param:0:$((${#param}-1))}"
|
||||
required_params+=("$param")
|
||||
property='{"'"$param"'":{"type":"string","description":""}}'
|
||||
elif [[ "$param" == *'+' ]]; then
|
||||
param="${param:0:$((${#param}-1))}"
|
||||
required_params+=("$param")
|
||||
property='{"'"$param"'":{"type":"array","description":"","items": {"type":"string"}}}'
|
||||
elif [[ "$param" == *'*' ]]; then
|
||||
param="${param:0:$((${#param}-1))}"
|
||||
property='{"'"$param"'":{"type":"array","description":"","items": {"type":"string"}}}'
|
||||
else
|
||||
property='{"'"$param"'":{"type":"string","description":""}}'
|
||||
fi
|
||||
properties+="$property"
|
||||
done
|
||||
required=''
|
||||
for param in "${required_params[@]}"; do
|
||||
if [[ -z "$required" ]]; then
|
||||
required=',"required":['
|
||||
fi
|
||||
required+="\"$param\","
|
||||
done
|
||||
if [[ -n "$required" ]]; then
|
||||
required="${required:0:$((${#required}-1))}"
|
||||
required+="]"
|
||||
fi
|
||||
echo '{
|
||||
"type": "object",
|
||||
"properties": '"$(echo "$properties" | jq -s 'add')$required"'
|
||||
}' | jq '.'
|
||||
}
|
||||
|
||||
_die() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# See more details at https://github.com/sigoden/argc
|
||||
eval "$(argc --argc-eval "$0" "$@")"
|
||||
135
llm-functions/scripts/declarations-util.sh
Executable file
135
llm-functions/scripts/declarations-util.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# @describe Utils for declarations json file
|
||||
|
||||
# @cmd Pretty print declarations
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/declarations.sh pretty-print functions.json
|
||||
# cat functions.json | ./scripts/declarations.sh pretty-print functions.json
|
||||
# @flag --no-type Do not to display param type info
|
||||
# @arg json-file The json file, Read stdin if omitted
|
||||
pretty-print() {
|
||||
_run _pretty_print
|
||||
}
|
||||
|
||||
# @cmd Generate placeholder json according to declarations
|
||||
# Examples:
|
||||
# ./scripts/declarations.sh generate-json functions.json
|
||||
# cat functions.json | ./scripts/declarations.sh generate-json functions.json
|
||||
# @arg json-file The json file, Read stdin if omitted
|
||||
generate-json() {
|
||||
_run _generate_json
|
||||
}
|
||||
|
||||
_run() {
|
||||
func="$1"
|
||||
_get_declarations_data
|
||||
if [[ "$json_type" == "object" ]]; then
|
||||
echo "$json_data" | $func
|
||||
elif [[ "$json_type" == "array" ]]; then
|
||||
for i in $(seq 1 $json_array_len); do
|
||||
echo "$json_data" | jq '.['$((i-1))']' | $func
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
_get_declarations_data() {
|
||||
if [[ -f "$argc_json_file" ]]; then
|
||||
json_data="$(cat "$argc_json_file")"
|
||||
else
|
||||
json_data="$(cat)"
|
||||
fi
|
||||
json_type="$(echo "$json_data" | jq -r '
|
||||
if type == "array" then
|
||||
(. | length) as $len | "array;\($len)"
|
||||
else
|
||||
if type == "object" then
|
||||
type
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
' 2>/dev/null || true)"
|
||||
if [[ "$json_type" == *object* ]]; then
|
||||
:;
|
||||
elif [[ "$json_type" == *array* ]]; then
|
||||
json_array_len="${json_type#*;}"
|
||||
json_type="${json_type%%;*}"
|
||||
if [[ ! "$json_array_len" -gt 0 ]]; then
|
||||
json_type=""
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$json_type" ]]; then
|
||||
echo "error: invalid JSON data" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
_pretty_print() {
|
||||
jq --arg no_type "$argc_no_type" -r '
|
||||
def get_type:
|
||||
.value.type as $type |
|
||||
(if .required then "" else "?" end) as $symbol |
|
||||
(.value.enum // []) as $enum |
|
||||
([
|
||||
{ condition: ($type == "array"), result: "string[]" },
|
||||
{ condition: ($type == "string" and ($enum | length > 0)), result: ($enum | join("|")) },
|
||||
{ condition: ($type == "string"), result: "" },
|
||||
{ condition: true, result: $type }
|
||||
] | map(select(.condition) | .result) | first) as $kind |
|
||||
if $kind != "" then "(\($kind))\($symbol)" else $symbol end;
|
||||
|
||||
def oneline_description: split("\n")[0];
|
||||
|
||||
def parse_property:
|
||||
.key as $key |
|
||||
(.value.description | oneline_description) as $description |
|
||||
(if $no_type != "1" then (. | get_type) else "" end) as $type |
|
||||
" \($key)\($type): \($description)";
|
||||
|
||||
def print_params:
|
||||
.parameters |
|
||||
.required as $requiredProperties |
|
||||
.properties | to_entries[] |
|
||||
.key as $key | .+ { "required": ($requiredProperties | index($key) != null) } |
|
||||
parse_property;
|
||||
|
||||
def print_title:
|
||||
(.description | oneline_description) as $description |
|
||||
"\(.name): \($description)";
|
||||
|
||||
print_title, print_params
|
||||
'
|
||||
}
|
||||
|
||||
_generate_json() {
|
||||
jq -r -c '
|
||||
def convert_string:
|
||||
if has("enum") then .enum[0] else "foo" end;
|
||||
|
||||
def parse_property:
|
||||
.key as $key |
|
||||
.value.type as $type |
|
||||
[
|
||||
{ condition: ($type == "string"), result: { $key: (.value | convert_string) }},
|
||||
{ condition: ($type == "boolean"), result: { $key: false }},
|
||||
{ condition: ($type == "integer"), result: { $key: 42 }},
|
||||
{ condition: ($type == "number"), result: { $key: 3.14 }},
|
||||
{ condition: ($type == "array"), result: { $key: [ "v1" ] } }
|
||||
] | map(select(.condition) | .result) | first;
|
||||
|
||||
.name,
|
||||
(
|
||||
.parameters |
|
||||
[
|
||||
.properties | to_entries[] | parse_property
|
||||
] | add // {}
|
||||
)
|
||||
'
|
||||
}
|
||||
|
||||
# See more details at https://github.com/sigoden/argc
|
||||
eval "$(argc --argc-eval "$0" "$@")"
|
||||
211
llm-functions/scripts/mcp.sh
Executable file
211
llm-functions/scripts/mcp.sh
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
ROOT_DIR="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||
BIN_DIR="$ROOT_DIR/bin"
|
||||
MCP_DIR="$ROOT_DIR/cache/__mcp__"
|
||||
MCP_LOG_FILE="$MCP_DIR/mcp-bridge.log"
|
||||
MCP_JSON_PATH="$ROOT_DIR/mcp.json"
|
||||
FUNCTIONS_JSON_PATH="$ROOT_DIR/functions.json"
|
||||
MCP_BRIDGE_PORT="${MCP_BRIDGE_PORT:-8808}"
|
||||
|
||||
# @cmd Start/restart the mcp bridge server
|
||||
# @alias restart
|
||||
start() {
|
||||
if [[ ! -f "$MCP_JSON_PATH" ]]; then
|
||||
_die "error: not found mcp.json"
|
||||
fi
|
||||
stop
|
||||
mkdir -p "$MCP_DIR"
|
||||
index_js="$ROOT_DIR/mcp/bridge/index.js"
|
||||
llm_functions_dir="$ROOT_DIR"
|
||||
if _is_win; then
|
||||
index_js="$(cygpath -w "$index_js")"
|
||||
llm_functions_dir="$(cygpath -w "$llm_functions_dir")"
|
||||
fi
|
||||
echo "Start MCP Bridge server..."
|
||||
echo "Install node dependencies..." > "$MCP_LOG_FILE"
|
||||
(cd "$ROOT_DIR/mcp/bridge" && npm install 1>/dev/null 2>> "$MCP_LOG_FILE")
|
||||
nohup node "$index_js" "$llm_functions_dir" >> "$MCP_LOG_FILE" 2>&1 &
|
||||
wait-for-server
|
||||
echo "Merge MCP tools into functions.json"
|
||||
"$0" merge-functions -S
|
||||
build-bin
|
||||
}
|
||||
|
||||
# @cmd Stop the mcp bridge server
|
||||
stop() {
|
||||
pid="$(get-server-pid)"
|
||||
if [[ -n "$pid" ]]; then
|
||||
if _is_win; then
|
||||
taskkill /PID "$pid" /F > /dev/null 2>&1 || true
|
||||
else
|
||||
kill -9 "$pid" > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
"$0" recovery-functions -S
|
||||
}
|
||||
|
||||
# @cmd Check the mcp bridge server is running
|
||||
check() {
|
||||
if [[ -f "$MCP_JSON_PATH" ]]; then
|
||||
echo "Check mcp/bridge"
|
||||
pid="$(get-server-pid)"
|
||||
if [[ -z "$pid" ]]; then
|
||||
stop
|
||||
echo "✗ server is not running"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Run the mcp tool
|
||||
# @arg tool![`_choice_tool`] The tool name
|
||||
# @arg json The json data
|
||||
run@tool() {
|
||||
if [[ -z "$argc_json" ]]; then
|
||||
declaration="$(generate-declarations | jq --arg tool "$argc_tool" -r '.[] | select(.name == $tool)')"
|
||||
if [[ -n "$declaration" ]]; then
|
||||
_ask_json_data "$declaration"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$argc_json" ]]; then
|
||||
_die "error: no JSON data"
|
||||
fi
|
||||
bash "$ROOT_DIR/scripts/run-mcp-tool.sh" "$argc_tool" "$argc_json"
|
||||
}
|
||||
|
||||
# @cmd Show the logs
|
||||
# @flag -f --follow Follow mode
|
||||
logs() {
|
||||
if [[ ! -f "$MCP_LOG_FILE" ]]; then
|
||||
_die "error: not found log file at '$MCP_LOG_FILE'"
|
||||
fi
|
||||
if [[ -n "$argc_follow" ]]; then
|
||||
tail -f "$MCP_LOG_FILE"
|
||||
else
|
||||
cat "$MCP_LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Build tools to bin
|
||||
build-bin() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
tools=( $(generate-declarations | jq -r '.[].name') )
|
||||
for tool in "${tools[@]}"; do
|
||||
if _is_win; then
|
||||
bin_file="$BIN_DIR/$tool.cmd"
|
||||
_build_win_shim > "$bin_file"
|
||||
else
|
||||
bin_file="$BIN_DIR/$tool"
|
||||
ln -s -f "$ROOT_DIR/scripts/run-mcp-tool.sh" "$bin_file"
|
||||
fi
|
||||
echo "Build bin/$tool"
|
||||
done
|
||||
}
|
||||
|
||||
# @cmd Merge mcp tools into functions.json
|
||||
# @flag -S --save Save to functions.json
|
||||
merge-functions() {
|
||||
local tmpdir="$(mktemp -d)"
|
||||
"$0" recovery-functions > "$tmpdir/1.json"
|
||||
generate-declarations > "$tmpdir/2.json"
|
||||
result="$(jq -s '.[0] + .[1]' "$tmpdir/1.json" "$tmpdir/2.json")"
|
||||
if [[ -n "$argc_save" ]]; then
|
||||
printf "%s" "$result" > "$FUNCTIONS_JSON_PATH"
|
||||
else
|
||||
printf "%s" "$result"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Unmerge mcp tools from functions.json
|
||||
# @flag -S --save Save to functions.json
|
||||
recovery-functions() {
|
||||
functions="[]"
|
||||
if [[ -f "$FUNCTIONS_JSON_PATH" ]]; then
|
||||
functions="$(cat "$FUNCTIONS_JSON_PATH")"
|
||||
fi
|
||||
result="$(printf "%s" "$functions" | jq 'map(select(has("mcp") | not))')"
|
||||
if [[ -n "$argc_save" ]]; then
|
||||
printf "%s" "$result" > "$FUNCTIONS_JSON_PATH"
|
||||
else
|
||||
printf "%s" "$result"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Generate function declarations for the mcp tools
|
||||
generate-declarations() {
|
||||
pid="$(get-server-pid)"
|
||||
if [[ -n "$pid" ]]; then
|
||||
curl -sS http://localhost:$MCP_BRIDGE_PORT/tools
|
||||
else
|
||||
echo "[]"
|
||||
fi
|
||||
}
|
||||
|
||||
# @cmd Wait for the mcp bridge server to ready
|
||||
wait-for-server() {
|
||||
while true; do
|
||||
if [[ "$(curl -fsS --max-time 5 http://localhost:$MCP_BRIDGE_PORT/health 2>&1)" == "OK" ]]; then
|
||||
break;
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# @cmd Get the server pid
|
||||
get-server-pid() {
|
||||
curl -fsS --max-time 5 http://localhost:$MCP_BRIDGE_PORT/pid 2>/dev/null || true
|
||||
}
|
||||
|
||||
_ask_json_data() {
|
||||
declaration="$1"
|
||||
echo 'Missing the JSON data but here are its properties:'
|
||||
echo "$declaration" | ./scripts/declarations-util.sh pretty-print | sed -n '2,$s/^/>/p'
|
||||
echo 'Generate placeholder data:'
|
||||
data="$(echo "$declaration" | _declarations_json_data)"
|
||||
echo "> $data"
|
||||
read -e -r -p 'JSON data (Press ENTER to use placeholder): ' res
|
||||
if [[ -z "$res" ]]; then
|
||||
argc_json="$data"
|
||||
else
|
||||
argc_json="$res"
|
||||
fi
|
||||
}
|
||||
|
||||
_declarations_json_data() {
|
||||
./scripts/declarations-util.sh generate-json | tail -n +2
|
||||
}
|
||||
|
||||
_build_win_shim() {
|
||||
run="\"$(argc --argc-shell-path)\" --noprofile --norc"
|
||||
cat <<-EOF
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "bin_dir=%~dp0"
|
||||
for %%i in ("%bin_dir:~0,-1%") do set "script_dir=%%~dpi"
|
||||
set "script_name=%~n0"
|
||||
|
||||
$run "%script_dir%scripts\run-mcp-tool.sh" "%script_name%" %*
|
||||
EOF
|
||||
}
|
||||
|
||||
_is_win() {
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_choice_tool() {
|
||||
generate-declarations | jq -r '.[].name'
|
||||
}
|
||||
|
||||
_die() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# See more details at https://github.com/sigoden/argc
|
||||
eval "$(argc --argc-eval "$0" "$@")"
|
||||
172
llm-functions/scripts/run-agent.js
Executable file
172
llm-functions/scripts/run-agent.js
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Usage: ./run-agent.js <agent-name> <agent-func> <agent-data>
|
||||
|
||||
const path = require("path");
|
||||
const { readFile, writeFile } = require("fs/promises");
|
||||
const os = require("os");
|
||||
|
||||
async function main() {
|
||||
const [agentName, agentFunc, rawData] = parseArgv("run-agent.js");
|
||||
const agentData = parseRawData(rawData);
|
||||
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
await setupEnv(rootDir, agentName, agentFunc);
|
||||
|
||||
const agentToolsPath = path.resolve(rootDir, `agents/${agentName}/tools.js`);
|
||||
await run(agentName, agentToolsPath, agentFunc, agentData);
|
||||
}
|
||||
|
||||
function parseArgv(thisFileName) {
|
||||
let agentName = process.argv[1];
|
||||
let agentFunc = "";
|
||||
let agentData = null;
|
||||
|
||||
if (agentName.endsWith(thisFileName)) {
|
||||
agentName = process.argv[2];
|
||||
agentFunc = process.argv[3];
|
||||
agentData = process.argv[4];
|
||||
} else {
|
||||
agentName = path.basename(agentName);
|
||||
agentFunc = process.argv[2];
|
||||
agentData = process.argv[3];
|
||||
}
|
||||
|
||||
if (agentName && agentName.endsWith(".js")) {
|
||||
agentName = agentName.slice(0, -3);
|
||||
}
|
||||
|
||||
if (!agentData || !agentFunc || !agentName) {
|
||||
console.log(`Usage: ./run-agent.js <agent-name> <agent-func> <agent-data>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return [agentName, agentFunc, agentData];
|
||||
}
|
||||
|
||||
function parseRawData(data) {
|
||||
if (!data) {
|
||||
throw new Error("No JSON data");
|
||||
}
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
throw new Error("Invalid JSON data");
|
||||
}
|
||||
}
|
||||
|
||||
async function setupEnv(rootDir, agentName, agentFunc) {
|
||||
await loadEnv(path.resolve(rootDir, ".env"));
|
||||
process.env["LLM_ROOT_DIR"] = rootDir;
|
||||
process.env["LLM_AGENT_NAME"] = agentName;
|
||||
process.env["LLM_AGENT_FUNC"] = agentFunc;
|
||||
process.env["LLM_AGENT_ROOT_DIR"] = path.resolve(
|
||||
rootDir,
|
||||
"agents",
|
||||
agentName,
|
||||
);
|
||||
process.env["LLM_AGENT_CACHE_DIR"] = path.resolve(
|
||||
rootDir,
|
||||
"cache",
|
||||
agentName,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadEnv(filePath) {
|
||||
let lines = [];
|
||||
try {
|
||||
const data = await readFile(filePath, "utf-8");
|
||||
lines = data.split("\n");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const envVars = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith("#") || line.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [key, ...valueParts] = line.split("=");
|
||||
const envName = key.trim();
|
||||
|
||||
if (!process.env[envName]) {
|
||||
let envValue = valueParts.join("=").trim();
|
||||
if ((envValue.startsWith('"') && envValue.endsWith('"')) || (envValue.startsWith("'") && envValue.endsWith("'"))) {
|
||||
envValue = envValue.slice(1, -1);
|
||||
}
|
||||
envVars.set(envName, envValue);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [envName, envValue] of envVars.entries()) {
|
||||
process.env[envName] = envValue;
|
||||
}
|
||||
}
|
||||
|
||||
async function run(agentName, agentPath, agentFunc, agentData) {
|
||||
if (os.platform() === "win32") {
|
||||
agentPath = `file://${agentPath}`;
|
||||
}
|
||||
const mod = await import(agentPath);
|
||||
if (!mod || !mod[agentFunc]) {
|
||||
throw new Error(`Not module function '${agentFunc}' at '${agentPath}'`);
|
||||
}
|
||||
const value = await mod[agentFunc](agentData);
|
||||
await returnToLLM(value);
|
||||
await dumpResult(`${agentName}:${agentFunc}`);
|
||||
}
|
||||
|
||||
async function returnToLLM(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
const write = async (value) => {
|
||||
if (process.env["LLM_OUTPUT"]) {
|
||||
await writeFile(process.env["LLM_OUTPUT"], value);
|
||||
} else {
|
||||
process.stdout.write(value);
|
||||
}
|
||||
}
|
||||
const type = typeof value;
|
||||
if (type === "string" || type === "number" || type === "boolean") {
|
||||
await write(value.toString());
|
||||
} else if (type === "object") {
|
||||
const proto = Object.prototype.toString.call(value);
|
||||
if (proto === "[object Object]" || proto === "[object Array]") {
|
||||
const valueStr = JSON.stringify(value, null, 2);
|
||||
require("assert").deepStrictEqual(value, JSON.parse(valueStr));
|
||||
await write(valueStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function dumpResult(name) {
|
||||
if (!process.env["LLM_DUMP_RESULTS"] || !process.env["LLM_OUTPUT"] || !process.stdout.isTTY) {
|
||||
return;
|
||||
}
|
||||
let showResult = false;
|
||||
try {
|
||||
if (new RegExp(`\\b(${process.env["LLM_DUMP_RESULTS"]})\\b`).test(name)) {
|
||||
showResult = true;
|
||||
}
|
||||
} catch { }
|
||||
|
||||
if (!showResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await readFile(process.env["LLM_OUTPUT"], "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
153
llm-functions/scripts/run-agent.py
Executable file
153
llm-functions/scripts/run-agent.py
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Usage: ./run-agent.py <agent-name> <agent-func> <agent-data>
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import importlib.util
|
||||
|
||||
|
||||
def main():
|
||||
(agent_name, agent_func, raw_data) = parse_argv("run-agent.py")
|
||||
agent_data = parse_raw_data(raw_data)
|
||||
|
||||
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
setup_env(root_dir, agent_name, agent_func)
|
||||
|
||||
agent_tools_path = os.path.join(root_dir, f"agents/{agent_name}/tools.py")
|
||||
run(agent_name, agent_tools_path, agent_func, agent_data)
|
||||
|
||||
|
||||
def parse_raw_data(data):
|
||||
if not data:
|
||||
raise ValueError("No JSON data")
|
||||
|
||||
try:
|
||||
return json.loads(data)
|
||||
except Exception:
|
||||
raise ValueError("Invalid JSON data")
|
||||
|
||||
|
||||
def parse_argv(this_file_name):
|
||||
argv = sys.argv[:] + [None] * max(0, 4 - len(sys.argv))
|
||||
|
||||
agent_name = argv[0]
|
||||
agent_func = ""
|
||||
agent_data = ""
|
||||
|
||||
if agent_name.endswith(this_file_name):
|
||||
if len(sys.argv) > 3:
|
||||
agent_name = sys.argv[1]
|
||||
agent_func = sys.argv[2]
|
||||
agent_data = sys.argv[3]
|
||||
else:
|
||||
if len(sys.argv) > 2:
|
||||
agent_name = os.path.basename(agent_name)
|
||||
agent_func = sys.argv[1]
|
||||
agent_data = sys.argv[2]
|
||||
|
||||
if agent_name and agent_name.endswith(".py"):
|
||||
agent_name = agent_name[:-3]
|
||||
|
||||
if (not agent_data) or (not agent_func) or (not agent_name):
|
||||
print("Usage: ./run-agent.py <agent-name> <agent-func> <agent-data>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return agent_name, agent_func, agent_data
|
||||
|
||||
|
||||
def setup_env(root_dir, agent_name, agent_func):
|
||||
load_env(os.path.join(root_dir, ".env"))
|
||||
os.environ["LLM_ROOT_DIR"] = root_dir
|
||||
os.environ["LLM_AGENT_NAME"] = agent_name
|
||||
os.environ["LLM_AGENT_FUNC"] = agent_func
|
||||
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", agent_name)
|
||||
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", agent_name)
|
||||
|
||||
|
||||
def load_env(file_path):
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
except:
|
||||
return
|
||||
|
||||
env_vars = {}
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
continue
|
||||
|
||||
key, *value_parts = line.split("=")
|
||||
env_name = key.strip()
|
||||
|
||||
if env_name not in os.environ:
|
||||
env_value = "=".join(value_parts).strip()
|
||||
if (env_value.startswith('"') and env_value.endswith('"')) or (env_value.startswith("'") and env_value.endswith("'")):
|
||||
env_value = env_value[1:-1]
|
||||
env_vars[env_name] = env_value
|
||||
|
||||
os.environ.update(env_vars)
|
||||
|
||||
|
||||
def run(agent_name, agent_path, agent_func, agent_data):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
os.path.basename(agent_path), agent_path
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
if not hasattr(mod, agent_func):
|
||||
raise Exception(f"Not module function '{agent_func}' at '{agent_path}'")
|
||||
|
||||
value = getattr(mod, agent_func)(**agent_data)
|
||||
return_to_llm(value)
|
||||
dump_result(rf'{agent_name}:{agent_func}')
|
||||
|
||||
|
||||
def return_to_llm(value):
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if "LLM_OUTPUT" in os.environ:
|
||||
writer = open(os.environ["LLM_OUTPUT"], "w")
|
||||
else:
|
||||
writer = sys.stdout
|
||||
|
||||
value_type = type(value).__name__
|
||||
if value_type in ("str", "int", "float", "bool"):
|
||||
writer.write(str(value))
|
||||
elif value_type == "dict" or value_type == "list":
|
||||
value_str = json.dumps(value, indent=2)
|
||||
assert value == json.loads(value_str)
|
||||
writer.write(value_str)
|
||||
|
||||
|
||||
def dump_result(name):
|
||||
if (not os.getenv("LLM_DUMP_RESULTS")) or (not os.getenv("LLM_OUTPUT")) or (not os.isatty(1)):
|
||||
return
|
||||
|
||||
show_result = False
|
||||
try:
|
||||
if re.search(rf'\b({os.environ["LLM_DUMP_RESULTS"]})\b', name):
|
||||
show_result = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if not show_result:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(os.environ["LLM_OUTPUT"], "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
except:
|
||||
return
|
||||
|
||||
print(f"\x1b[2m----------------------\n{data}\n----------------------\x1b[0m")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
125
llm-functions/scripts/run-agent.sh
Executable file
125
llm-functions/scripts/run-agent.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: ./run-agent.sh <agent-name> <agent-func> <agent-data>
|
||||
|
||||
set -e
|
||||
|
||||
main() {
|
||||
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||
self_name=run-agent.sh
|
||||
parse_argv "$@"
|
||||
setup_env
|
||||
tools_path="$root_dir/agents/$agent_name/tools.sh"
|
||||
run
|
||||
}
|
||||
|
||||
parse_argv() {
|
||||
if [[ "$0" == *"$self_name" ]]; then
|
||||
agent_name="$1"
|
||||
agent_func="$2"
|
||||
agent_data="$3"
|
||||
else
|
||||
agent_name="$(basename "$0")"
|
||||
agent_func="$1"
|
||||
agent_data="$2"
|
||||
fi
|
||||
if [[ "$agent_name" == *.sh ]]; then
|
||||
agent_name="${agent_name:0:$((${#agent_name}-3))}"
|
||||
fi
|
||||
if [[ -z "$agent_data" ]] || [[ -z "$agent_func" ]] || [[ -z "$agent_name" ]]; then
|
||||
die "usage: ./run-agent.sh <agent-name> <agent-func> <agent-data>"
|
||||
fi
|
||||
}
|
||||
|
||||
setup_env() {
|
||||
load_env "$root_dir/.env"
|
||||
export LLM_ROOT_DIR="$root_dir"
|
||||
export LLM_AGENT_NAME="$agent_name"
|
||||
export LLM_AGENT_FUNC="$agent_func"
|
||||
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/$agent_name"
|
||||
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/$agent_name"
|
||||
}
|
||||
|
||||
load_env() {
|
||||
local env_file="$1" env_vars
|
||||
if [[ -f "$env_file" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
if [[ "$key" == $'#'* ]] || [[ -z "$key" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -z "${!key+x}" ]]; then
|
||||
env_vars="$env_vars $key=$value"
|
||||
fi
|
||||
done < <(cat "$env_file"; echo "")
|
||||
if [[ -n "$env_vars" ]]; then
|
||||
eval "export $env_vars"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run() {
|
||||
if [[ -z "$agent_data" ]]; then
|
||||
die "error: no JSON data"
|
||||
fi
|
||||
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tools_path="$(cygpath -w "$tools_path")"
|
||||
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||
fi
|
||||
|
||||
jq_script="$(cat <<-'EOF'
|
||||
def escape_shell_word:
|
||||
tostring
|
||||
| gsub("'"; "'\"'\"'")
|
||||
| gsub("\n"; "'$'\\n''")
|
||||
| "'\(.)'";
|
||||
def to_args:
|
||||
to_entries | .[] |
|
||||
(.key | split("_") | join("-")) as $key |
|
||||
if .value | type == "array" then
|
||||
.value | .[] | "--\($key) \(. | escape_shell_word)"
|
||||
elif .value | type == "boolean" then
|
||||
if .value then "--\($key)" else "" end
|
||||
else
|
||||
"--\($key) \(.value | escape_shell_word)"
|
||||
end;
|
||||
[ to_args ] | join(" ")
|
||||
EOF
|
||||
)"
|
||||
args="$(echo "$agent_data" | jq -r "$jq_script" 2>/dev/null)" || {
|
||||
die "error: invalid JSON data"
|
||||
}
|
||||
|
||||
if [[ -z "$LLM_OUTPUT" ]]; then
|
||||
is_temp_llm_output=1
|
||||
export LLM_OUTPUT="$(mktemp)"
|
||||
fi
|
||||
eval "'$tools_path' '$agent_func' $args"
|
||||
if [[ "$is_temp_llm_output" -eq 1 ]]; then
|
||||
cat "$LLM_OUTPUT"
|
||||
else
|
||||
dump_result "${LLM_AGENT_NAME}:${LLM_AGENT_FUNC}"
|
||||
fi
|
||||
}
|
||||
|
||||
dump_result() {
|
||||
if [[ "$LLM_OUTPUT" == "/dev/stdout" ]] || [[ -z "$LLM_DUMP_RESULTS" ]] || [[ ! -t 1 ]]; then
|
||||
return;
|
||||
fi
|
||||
if grep -q -w -E "$LLM_DUMP_RESULTS" <<<"$1"; then
|
||||
cat <<EOF
|
||||
$(echo -e "\e[2m")----------------------
|
||||
$(cat "$LLM_OUTPUT")
|
||||
----------------------$(echo -e "\e[0m")
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
107
llm-functions/scripts/run-mcp-tool.sh
Executable file
107
llm-functions/scripts/run-mcp-tool.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: ./run-mcp-tool.sh <tool-name> <tool-data>
|
||||
|
||||
set -e
|
||||
|
||||
main() {
|
||||
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||
self_name=run-mcp-tool.sh
|
||||
parse_argv "$@"
|
||||
load_env "$root_dir/.env"
|
||||
run
|
||||
}
|
||||
|
||||
parse_argv() {
|
||||
if [[ "$0" == *"$self_name" ]]; then
|
||||
tool_name="$1"
|
||||
tool_data="$2"
|
||||
else
|
||||
tool_name="$(basename "$0")"
|
||||
tool_data="$1"
|
||||
fi
|
||||
if [[ "$tool_name" == *.sh ]]; then
|
||||
tool_name="${tool_name:0:$((${#tool_name}-3))}"
|
||||
fi
|
||||
if [[ -z "$tool_data" ]] || [[ -z "$tool_name" ]]; then
|
||||
die "usage: ./run-tool.sh <tool-name> <tool-data>"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
load_env() {
|
||||
local env_file="$1" env_vars
|
||||
if [[ -f "$env_file" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
if [[ "$key" == $'#'* ]] || [[ -z "$key" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -z "${!key+x}" ]]; then
|
||||
env_vars="$env_vars $key=$value"
|
||||
fi
|
||||
done < <(cat "$env_file"; echo "")
|
||||
if [[ -n "$env_vars" ]]; then
|
||||
eval "export $env_vars"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run() {
|
||||
if [[ -z "$tool_data" ]]; then
|
||||
die "error: no JSON data"
|
||||
fi
|
||||
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||
fi
|
||||
|
||||
if [[ -z "$LLM_OUTPUT" ]]; then
|
||||
is_temp_llm_output=1
|
||||
export LLM_OUTPUT="$(mktemp)"
|
||||
fi
|
||||
|
||||
if [[ -n "$LLM_MCP_SKIP_CONFIRM" ]]; then
|
||||
if grep -q -w -E "$LLM_MCP_SKIP_CONFIRM" <<<"$tool_name"; then
|
||||
skip_confirm=1
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$LLM_MCP_NEED_CONFIRM" ]]; then
|
||||
if grep -q -w -E "$LLM_MCP_NEED_CONFIRM" <<<"$tool_name"; then
|
||||
skip_confirm=0
|
||||
fi
|
||||
fi
|
||||
if [[ -t 1 ]] && [[ "$skip_confirm" -ne 1 ]]; then
|
||||
read -r -p "Are you sure you want to continue? [Y/n] " ans
|
||||
if [[ "$ans" == "N" || "$ans" == "n" ]]; then
|
||||
echo "error: canceled!" 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
curl -sS "http://localhost:${MCP_BRIDGE_PORT:-8808}/tools/$tool_name" \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-d "$tool_data" > "$LLM_OUTPUT"
|
||||
|
||||
if [[ "$is_temp_llm_output" -eq 1 ]]; then
|
||||
cat "$LLM_OUTPUT"
|
||||
else
|
||||
dump_result "$tool_name"
|
||||
fi
|
||||
}
|
||||
|
||||
dump_result() {
|
||||
if [[ "$LLM_OUTPUT" == "/dev/stdout" ]] || [[ -z "$LLM_DUMP_RESULTS" ]] || [[ ! -t 1 ]]; then
|
||||
return;
|
||||
fi
|
||||
if grep -q -w -E "$LLM_DUMP_RESULTS" <<<"$1"; then
|
||||
cat <<EOF
|
||||
$(echo -e "\e[2m")----------------------
|
||||
$(cat "$LLM_OUTPUT")
|
||||
----------------------$(echo -e "\e[0m")
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
159
llm-functions/scripts/run-tool.js
Executable file
159
llm-functions/scripts/run-tool.js
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Usage: ./run-tool.js <tool-name> <tool-data>
|
||||
|
||||
const path = require("path");
|
||||
const { readFile, writeFile } = require("fs/promises");
|
||||
const os = require("os");
|
||||
|
||||
async function main() {
|
||||
const [toolName, rawData] = parseArgv("run-tool.js");
|
||||
const toolData = parseRawData(rawData);
|
||||
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
await setupEnv(rootDir, toolName);
|
||||
|
||||
const toolPath = path.resolve(rootDir, `tools/${toolName}.js`);
|
||||
await run(toolName, toolPath, "run", toolData);
|
||||
}
|
||||
|
||||
function parseArgv(thisFileName) {
|
||||
let toolName = process.argv[1];
|
||||
let toolData = null;
|
||||
|
||||
if (toolName.endsWith(thisFileName)) {
|
||||
toolName = process.argv[2];
|
||||
toolData = process.argv[3];
|
||||
} else {
|
||||
toolName = path.basename(toolName);
|
||||
toolData = process.argv[2];
|
||||
}
|
||||
|
||||
if (toolName && toolName.endsWith(".js")) {
|
||||
toolName = toolName.slice(0, -3);
|
||||
}
|
||||
|
||||
if (!toolData || !toolName) {
|
||||
console.log(`Usage: ./run-tools.js <tool-name> <tool-data>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return [toolName, toolData];
|
||||
}
|
||||
|
||||
function parseRawData(data) {
|
||||
if (!data) {
|
||||
throw new Error("No JSON data");
|
||||
}
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
throw new Error("Invalid JSON data");
|
||||
}
|
||||
}
|
||||
|
||||
async function setupEnv(rootDir, toolName) {
|
||||
await loadEnv(path.resolve(rootDir, ".env"));
|
||||
process.env["LLM_ROOT_DIR"] = rootDir;
|
||||
process.env["LLM_TOOL_NAME"] = toolName;
|
||||
process.env["LLM_TOOL_CACHE_DIR"] = path.resolve(rootDir, "cache", toolName);
|
||||
}
|
||||
|
||||
async function loadEnv(filePath) {
|
||||
let lines = [];
|
||||
try {
|
||||
const data = await readFile(filePath, "utf-8");
|
||||
lines = data.split("\n");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const envVars = new Map();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith("#") || line.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [key, ...valueParts] = line.split("=");
|
||||
const envName = key.trim();
|
||||
|
||||
if (!process.env[envName]) {
|
||||
let envValue = valueParts.join("=").trim();
|
||||
if ((envValue.startsWith('"') && envValue.endsWith('"')) || (envValue.startsWith("'") && envValue.endsWith("'"))) {
|
||||
envValue = envValue.slice(1, -1);
|
||||
}
|
||||
envVars.set(envName, envValue);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [envName, envValue] of envVars.entries()) {
|
||||
process.env[envName] = envValue;
|
||||
}
|
||||
}
|
||||
|
||||
async function run(toolName, toolPath, toolFunc, toolData) {
|
||||
if (os.platform() === "win32") {
|
||||
toolPath = `file://${toolPath}`;
|
||||
}
|
||||
const mod = await import(toolPath);
|
||||
if (!mod || !mod[toolFunc]) {
|
||||
throw new Error(`Not module function '${toolFunc}' at '${toolPath}'`);
|
||||
}
|
||||
const value = await mod[toolFunc](toolData);
|
||||
await returnToLLM(value);
|
||||
await dumpResult(toolName);
|
||||
}
|
||||
|
||||
async function returnToLLM(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
const write = async (value) => {
|
||||
if (process.env["LLM_OUTPUT"]) {
|
||||
await writeFile(process.env["LLM_OUTPUT"], value);
|
||||
} else {
|
||||
process.stdout.write(value);
|
||||
}
|
||||
}
|
||||
const type = typeof value;
|
||||
if (type === "string" || type === "number" || type === "boolean") {
|
||||
await write(value.toString());
|
||||
} else if (type === "object") {
|
||||
const proto = Object.prototype.toString.call(value);
|
||||
if (proto === "[object Object]" || proto === "[object Array]") {
|
||||
const valueStr = JSON.stringify(value, null, 2);
|
||||
require("assert").deepStrictEqual(value, JSON.parse(valueStr));
|
||||
await write(valueStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function dumpResult(name) {
|
||||
if (!process.env["LLM_DUMP_RESULTS"] || !process.env["LLM_OUTPUT"] || !process.stdout.isTTY) {
|
||||
return;
|
||||
}
|
||||
let showResult = false;
|
||||
try {
|
||||
if (new RegExp(`\\b(${process.env["LLM_DUMP_RESULTS"]})\\b`).test(name)) {
|
||||
showResult = true;
|
||||
}
|
||||
} catch { }
|
||||
|
||||
if (!showResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = "";
|
||||
try {
|
||||
data = await readFile(process.env["LLM_OUTPUT"], "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`\x1b[2m----------------------\n${data}\n----------------------\x1b[0m\n`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
148
llm-functions/scripts/run-tool.py
Executable file
148
llm-functions/scripts/run-tool.py
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Usage: ./run-tool.py <tool-name> <tool-data>
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import importlib.util
|
||||
|
||||
|
||||
def main():
|
||||
(tool_name, raw_data) = parse_argv("run-tool.py")
|
||||
tool_data = parse_raw_data(raw_data)
|
||||
|
||||
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
setup_env(root_dir, tool_name)
|
||||
|
||||
tool_path = os.path.join(root_dir, f"tools/{tool_name}.py")
|
||||
run(tool_name, tool_path, "run", tool_data)
|
||||
|
||||
|
||||
def parse_raw_data(data):
|
||||
if not data:
|
||||
raise ValueError("No JSON data")
|
||||
|
||||
try:
|
||||
return json.loads(data)
|
||||
except Exception:
|
||||
raise ValueError("Invalid JSON data")
|
||||
|
||||
|
||||
def parse_argv(this_file_name):
|
||||
argv = sys.argv[:] + [None] * max(0, 3 - len(sys.argv))
|
||||
|
||||
tool_name = argv[0]
|
||||
tool_data = ""
|
||||
|
||||
if tool_name.endswith(this_file_name):
|
||||
if len(sys.argv) > 2:
|
||||
tool_name = argv[1]
|
||||
tool_data = argv[2]
|
||||
else:
|
||||
if len(sys.argv) > 1:
|
||||
tool_name = os.path.basename(tool_name)
|
||||
tool_data = sys.argv[1]
|
||||
|
||||
if tool_name and tool_name.endswith(".py"):
|
||||
tool_name = tool_name[:-3]
|
||||
|
||||
if (not tool_data) or (not tool_name):
|
||||
print("Usage: ./run-tool.py <tool-name> <tool-data>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return tool_name, tool_data
|
||||
|
||||
|
||||
def setup_env(root_dir, tool_name):
|
||||
load_env(os.path.join(root_dir, ".env"))
|
||||
os.environ["LLM_ROOT_DIR"] = root_dir
|
||||
os.environ["LLM_TOOL_NAME"] = tool_name
|
||||
os.environ["LLM_TOOL_CACHE_DIR"] = os.path.join(root_dir, "cache", tool_name)
|
||||
|
||||
|
||||
def load_env(file_path):
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
except:
|
||||
return
|
||||
|
||||
env_vars = {}
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
continue
|
||||
|
||||
key, *value_parts = line.split("=")
|
||||
env_name = key.strip()
|
||||
|
||||
if env_name not in os.environ:
|
||||
env_value = "=".join(value_parts).strip()
|
||||
if (env_value.startswith('"') and env_value.endswith('"')) or (env_value.startswith("'") and env_value.endswith("'")):
|
||||
env_value = env_value[1:-1]
|
||||
env_vars[env_name] = env_value
|
||||
|
||||
os.environ.update(env_vars)
|
||||
|
||||
|
||||
def run(tool_name, tool_path, tool_func, tool_data):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
os.path.basename(tool_path), tool_path
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
if not hasattr(mod, tool_func):
|
||||
raise Exception(f"Not module function '{tool_func}' at '{tool_path}'")
|
||||
|
||||
value = getattr(mod, tool_func)(**tool_data)
|
||||
return_to_llm(value)
|
||||
dump_result(tool_name)
|
||||
|
||||
|
||||
def return_to_llm(value):
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if "LLM_OUTPUT" in os.environ:
|
||||
writer = open(os.environ["LLM_OUTPUT"], "w")
|
||||
else:
|
||||
writer = sys.stdout
|
||||
|
||||
value_type = type(value).__name__
|
||||
if value_type in ("str", "int", "float", "bool"):
|
||||
writer.write(str(value))
|
||||
elif value_type == "dict" or value_type == "list":
|
||||
value_str = json.dumps(value, indent=2)
|
||||
assert value == json.loads(value_str)
|
||||
writer.write(value_str)
|
||||
|
||||
|
||||
def dump_result(name):
|
||||
if (not os.getenv("LLM_DUMP_RESULTS")) or (not os.getenv("LLM_OUTPUT")) or (not os.isatty(1)):
|
||||
return
|
||||
|
||||
show_result = False
|
||||
try:
|
||||
if re.search(rf'\b({os.environ["LLM_DUMP_RESULTS"]})\b', name):
|
||||
show_result = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if not show_result:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(os.environ["LLM_OUTPUT"], "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
except:
|
||||
return
|
||||
|
||||
print(f"\x1b[2m----------------------\n{data}\n----------------------\x1b[0m")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
119
llm-functions/scripts/run-tool.sh
Executable file
119
llm-functions/scripts/run-tool.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Usage: ./run-tool.sh <tool-name> <tool-data>
|
||||
|
||||
set -e
|
||||
|
||||
main() {
|
||||
root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)"
|
||||
self_name=run-tool.sh
|
||||
parse_argv "$@"
|
||||
setup_env
|
||||
tool_path="$root_dir/tools/$tool_name.sh"
|
||||
run
|
||||
}
|
||||
|
||||
parse_argv() {
|
||||
if [[ "$0" == *"$self_name" ]]; then
|
||||
tool_name="$1"
|
||||
tool_data="$2"
|
||||
else
|
||||
tool_name="$(basename "$0")"
|
||||
tool_data="$1"
|
||||
fi
|
||||
if [[ "$tool_name" == *.sh ]]; then
|
||||
tool_name="${tool_name:0:$((${#tool_name}-3))}"
|
||||
fi
|
||||
if [[ -z "$tool_data" ]] || [[ -z "$tool_name" ]]; then
|
||||
die "usage: ./run-tool.sh <tool-name> <tool-data>"
|
||||
fi
|
||||
}
|
||||
|
||||
setup_env() {
|
||||
load_env "$root_dir/.env"
|
||||
export LLM_ROOT_DIR="$root_dir"
|
||||
export LLM_TOOL_NAME="$tool_name"
|
||||
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/$tool_name"
|
||||
}
|
||||
|
||||
load_env() {
|
||||
local env_file="$1" env_vars
|
||||
if [[ -f "$env_file" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
if [[ "$key" == $'#'* ]] || [[ -z "$key" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -z "${!key+x}" ]]; then
|
||||
env_vars="$env_vars $key=$value"
|
||||
fi
|
||||
done < <(cat "$env_file"; echo "")
|
||||
if [[ -n "$env_vars" ]]; then
|
||||
eval "export $env_vars"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
run() {
|
||||
if [[ -z "$tool_data" ]]; then
|
||||
die "error: no JSON data"
|
||||
fi
|
||||
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tool_path="$(cygpath -w "$tool_path")"
|
||||
tool_data="$(echo "$tool_data" | sed 's/\\/\\\\/g')"
|
||||
fi
|
||||
|
||||
jq_script="$(cat <<-'EOF'
|
||||
def escape_shell_word:
|
||||
tostring
|
||||
| gsub("'"; "'\"'\"'")
|
||||
| gsub("\n"; "'$'\\n''")
|
||||
| "'\(.)'";
|
||||
def to_args:
|
||||
to_entries | .[] |
|
||||
(.key | split("_") | join("-")) as $key |
|
||||
if .value | type == "array" then
|
||||
.value | .[] | "--\($key) \(. | escape_shell_word)"
|
||||
elif .value | type == "boolean" then
|
||||
if .value then "--\($key)" else "" end
|
||||
else
|
||||
"--\($key) \(.value | escape_shell_word)"
|
||||
end;
|
||||
[ to_args ] | join(" ")
|
||||
EOF
|
||||
)"
|
||||
args="$(echo "$tool_data" | jq -r "$jq_script" 2>/dev/null)" || {
|
||||
die "error: invalid JSON data"
|
||||
}
|
||||
if [[ -z "$LLM_OUTPUT" ]]; then
|
||||
is_temp_llm_output=1
|
||||
export LLM_OUTPUT="$(mktemp)"
|
||||
fi
|
||||
eval "'$tool_path' $args"
|
||||
if [[ "$is_temp_llm_output" -eq 1 ]]; then
|
||||
cat "$LLM_OUTPUT"
|
||||
else
|
||||
dump_result "$tool_name"
|
||||
fi
|
||||
}
|
||||
|
||||
dump_result() {
|
||||
if [[ "$LLM_OUTPUT" == "/dev/stdout" ]] || [[ -z "$LLM_DUMP_RESULTS" ]] || [[ ! -t 1 ]]; then
|
||||
return;
|
||||
fi
|
||||
if grep -q -w -E "$LLM_DUMP_RESULTS" <<<"$1"; then
|
||||
cat <<EOF
|
||||
$(echo -e "\e[2m")----------------------
|
||||
$(cat "$LLM_OUTPUT")
|
||||
----------------------$(echo -e "\e[0m")
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user