diff options
| author | sigoden <sigoden@gmail.com> | 2024-06-08 20:46:27 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-08 20:46:27 +0800 |
| commit | 213e949fc8c2362046d197554fda98c87a7906df (patch) | |
| tree | 595a883c6b1fd1154a916569c3d0255634c6e193 | |
| parent | 82d7a7de8a76e56cff306b0da7f4f14fdb57cbf1 (diff) | |
| download | llm-functions-docker-213e949fc8c2362046d197554fda98c87a7906df.tar.gz | |
feat: support bots (#39)
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Argcfile.sh | 237 | ||||
| l--------- | bots/todo-js/index.yaml | 1 | ||||
| -rw-r--r-- | bots/todo-js/tools.js | 73 | ||||
| l--------- | bots/todo-py/index.yaml | 1 | ||||
| -rw-r--r-- | bots/todo-py/tools.py | 65 | ||||
| -rw-r--r-- | bots/todo-sh/index.yaml | 19 | ||||
| -rwxr-xr-x | bots/todo-sh/tools.sh | 72 | ||||
| -rwxr-xr-x | scripts/declarations-util.sh | 4 | ||||
| -rwxr-xr-x | scripts/run-bot.js | 107 | ||||
| -rwxr-xr-x | scripts/run-bot.py | 105 | ||||
| -rwxr-xr-x | scripts/run-bot.sh | 80 |
12 files changed, 757 insertions, 9 deletions
@@ -1,7 +1,7 @@ /tmp functions.txt -bots.txt tools.txt +bots.txt functions.json /bin /cache diff --git a/Argcfile.sh b/Argcfile.sh index 1283a55..b0f3c95 100644 --- a/Argcfile.sh +++ b/Argcfile.sh @@ -21,7 +21,7 @@ run-tool() { ext=".cmd" fi if [[ -z "$argc_json" ]]; then - declaration="$(jq --arg name "$argc_cmd" '.[] | select(.name == $name)' functions.json)" + declaration="$(cat functions.json | jq --arg name "$argc_cmd" '.[] | select(.name == $name)')" if [[ -n "$declaration" ]]; then _ask_json_data "$declaration" fi @@ -32,9 +32,33 @@ run-tool() { "$BIN_DIR/$argc_cmd$ext" "$argc_json" } +# @cmd Run the tool +# @arg cmd![`_choice_bot`] The bot command +# @arg action![`_choice_bot_action`] The bot action +# @arg json The json data +run-bot() { + if _is_win; then + ext=".cmd" + fi + if [[ -z "$argc_json" ]]; then + functions_path="bots/$argc_cmd/functions.json" + if [[ -f "$functions_path" ]]; then + declaration="$(jq --arg name "$argc_action" '.[] | select(.name == $name)' "$functions_path")" + if [[ -n "$declaration" ]]; then + _ask_json_data "$declaration" + fi + fi + fi + if [[ -z "$argc_json" ]]; then + _die "error: no JSON data" + fi + "$BIN_DIR/$argc_cmd$ext" "$argc_action" "$argc_json" +} + # @cmd Build the project build() { argc build-tools + argc build-bots } # @cmd Build tools @@ -81,7 +105,7 @@ build-tools-bin() { if [[ -f "$tool_path" ]]; then if _is_win; then bin_file="$BIN_DIR/$basename.cmd" - _build_win_shim_tool $lang > "$bin_file" + _build_win_shim tool $lang > "$bin_file" else bin_file="$BIN_DIR/$basename" ln -s -f "$PWD/scripts/run-tool.$lang" "$bin_file" @@ -143,16 +167,147 @@ build-tool-declaration() { "$cmd" "scripts/build-declarations.$lang" "tools/$1" | jq '.[0]' } -# @cmd List tools that can be put into tools.txt +# @cmd Build bots +# @option --names-file=bots.txt Path to a file containing bot filenames, one per line. +# Example: +# hackernews +# spotify +# @arg bots*[`_choice_bot`] The bot filenames +build-bots() { + if [[ "${#argc_bots[@]}" -gt 0 ]]; then + mkdir -p "$TMP_DIR" + argc_names_file="$TMP_DIR/bots.txt" + printf "%s\n" "${argc_bots[@]}" > "$argc_names_file" + else + argc clean-bots + fi + argc build-bots-json --names-file "${argc_names_file}" + argc build-bots-bin --names-file "${argc_names_file}" +} + +# @cmd Build tools to bin +# @option --names-file=bots.txt Path to a file containing bot filenames, one per line. +# @arg bots*[`_choice_bot`] The bot names +build-bots-bin() { + mkdir -p "$BIN_DIR" + if [[ "${#argc_bots[@]}" -gt 0 ]]; then + names=("${argc_bots[@]}" ) + elif [[ -f "$argc_names_file" ]]; then + names=($(cat "$argc_names_file")) + if [[ "${#names[@]}" -gt 0 ]]; then + (cd "$BIN_DIR" && rm -rf "${names[@]}") + fi + fi + if [[ -z "$names" ]]; then + _die "error: not input bots, not found '$argc_names_file', please create it add some tools." + fi + not_found_bots=() + for name in "${names[@]}"; do + bot_dir="bots/$name" + found=false + for item in "${LANG_CMDS[@]}"; do + lang="${item%:*}" + bot_tools_file="$bot_dir/tools.$lang" + if [[ -f "$bot_tools_file" ]]; then + found=true + if _is_win; then + bin_file="$BIN_DIR/$name.cmd" + _build_win_shim bot $lang > "$bin_file" + else + bin_file="$BIN_DIR/$name" + ln -s -f "$PWD/scripts/run-bot.$lang" "$bin_file" + fi + echo "Build bot $name" + fi + done + if [[ "$found" = "false" ]]; then + not_found_bots+=("$name") + fi + done + if [[ -n "$not_found_bots" ]]; then + _die "error: not found bots: ${not_found_bots[*]}" + fi +} + +# @cmd Build bots functions.json +# @option --names-file=bots.txt Path to a file containing bot filenames, one per line. +# @arg tools*[`_choice_tool`] The tool filenames +build-bots-json() { + if [[ "${#argc_bots[@]}" -gt 0 ]]; then + names=("${argc_bots[@]}" ) + elif [[ -f "$argc_names_file" ]]; then + names=($(cat "$argc_names_file")) + fi + if [[ -z "$names" ]]; then + _die "error: not input bots, not found '$argc_names_file', please create it add some tools." + fi + not_found_bots=() + build_failed_bots=() + for name in "${names[@]}"; do + bot_dir="bots/$name" + build_ok=false + found=false + for item in "${LANG_CMDS[@]}"; do + lang="${item%:*}" + bot_tools_file="$bot_dir/tools.$lang" + if [[ -f "$bot_tools_file" ]]; then + found=true + json_data="$(build-bot-declarations "$name")" || { + build_failed_bots+=("$name") + } + declarations_file="$bot_dir/functions.json" + echo "Build $declarations_file" + echo "$json_data" > "$declarations_file" + fi + done + if [[ "$found" == "false" ]]; then + not_found_bots+=("$name") + fi + done + if [[ -n "$not_found_bots" ]]; then + _die "error: not found bots: ${not_found_bots[*]}" + fi + if [[ -n "$build_failed_bots" ]]; then + _die "error: invalid bots: ${build_failed_bots[*]}" + fi +} + +# @cmd Build function declarations for an bot +# @flag --oneline Summary JSON in one line +# @arg bot![`_choice_bot`] The bot name +build-bot-declarations() { + tools_path="$(_get_bot_tools_path "$1")" + if [[ -z "$tools_path" ]]; then + _die "error: no found entry file at bots/$1/tools.<lang>" + fi + lang="${tools_path##*.}" + cmd="$(_lang_to_cmd "$lang")" + json="$("$cmd" "scripts/build-declarations.$lang" "$tools_path")" + if [[ -n "$argc_oneline" ]]; then + echo "$json" | jq -r '.[] | .name + ": " + (.description | split("\n"))[0]' + else + echo "$json" + fi +} + +# @cmd List tools that can be put into functions.txt # Examples: # argc list-tools > tools.txt list-tools() { _choice_tool } +# @cmd List bots that can be put into bots.txt +# Examples: +# argc list-bots > bots.txt +list-bots() { + _choice_bot +} + # @cmd Test the project test() { test-tools + test-bots } # @cmd Test tools @@ -218,10 +373,55 @@ test-tools-demo() { done } +# @cmd Test bots +test-bots() { + tmp_dir="cache/tmp" + mkdir -p "$tmp_dir" + names_file="$tmp_dir/bots.txt" + argc list-bots > "$names_file" + argc build-bots --names-file "$names_file" + test-bots-todo-lang +} + +# @cmd Test todo-* bots +test-bots-todo-lang() { + if _is_win; then + ext=".cmd" + fi + test_cases=( \ + 'add_todo#{"desc":"Add a todo item"}' \ + 'add_todo#{"desc":"Add another todo item"}' \ + 'del_todo#{"id":1}' \ + 'list_todos#{}' \ + 'clear_todos#{}' \ + ) + for item in "${LANG_CMDS[@]}"; do + cmd="${item#*:}" + if command -v "$cmd" &> /dev/null; then + lang="${item%:*}" + bot_name="todo-$lang" + rm -rf "cache/$bot_name/todos.json" + for test_case in "${test_cases[@]}"; do + IFS='#' read -r action data <<<"${test_case}" + cmd_path="$BIN_DIR/$bot_name$ext" + echo "Test $cmd_path: " + "$cmd_path" "$action" "$data" + done + fi + done + +} # @cmd Clean tools clean-tools() { _choice_tool | sed 's/\.\([a-z]\+\)$//' | xargs -I{} rm -rf "$BIN_DIR/{}" + rm -rf functions.json +} + +# @cmd Clean bots +clean-bots() { + _choice_bot | xargs -I{} rm -rf "$BIN_DIR/{}" + _choice_bot | xargs -I{} rm -rf bots/{}/functions.json } # @cmd Install this repo to aichat functions_dir @@ -269,8 +469,20 @@ _lang_to_cmd() { done } -_build_win_shim_tool() { - lang="$1" +_get_bot_tools_path() { + name="$1" + for item in "${LANG_CMDS[@]}"; do + lang="${item%:*}" + entry_file="bots/$name/tools.$lang" + if [[ -f "bots/$name/tools.$lang" ]]; then + echo "$entry_file" + fi + done +} + +_build_win_shim() { + kind="$1" + lang="$2" cmd="$(_lang_to_cmd "$lang")" if [[ "$lang" == "sh" ]]; then run="\"$(argc --argc-shell-path)\" --noprofile --norc" @@ -285,7 +497,7 @@ 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-tool.$lang" "%script_name%.$lang" %* +$run "%script_dir%scripts\run-$kind.$lang" "%script_name%" %* EOF } @@ -338,6 +550,19 @@ _choice_tool() { done } +_choice_bot() { + ls -1 bots +} + +_choice_bot_action() { + if [[ "$ARGC_COMPGEN" -eq 1 ]]; then + expr="s/: /\t/" + else + expr="s/:.*//" + fi + argc build-bot-declarations "$1" --oneline | sed "$expr" +} + _choice_cmd() { ls -1 "$BIN_DIR" | sed -e 's/\.cmd$//' } diff --git a/bots/todo-js/index.yaml b/bots/todo-js/index.yaml new file mode 120000 index 0000000..0d19c11 --- /dev/null +++ b/bots/todo-js/index.yaml @@ -0,0 +1 @@ +../todo-sh/index.yaml
\ No newline at end of file diff --git a/bots/todo-js/tools.js b/bots/todo-js/tools.js new file mode 100644 index 0000000..ed3ed39 --- /dev/null +++ b/bots/todo-js/tools.js @@ -0,0 +1,73 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Add a new todo item + * @typedef {Object} AddTodoArgs + * @property {string} desc - The task description + * @param {AddTodoArgs} args + */ +exports.add_todo = function addTodo(args) { + const todosFile = _getTodosFile(); + if (fs.existsSync(todosFile)) { + const num = JSON.parse(fs.readFileSync(todosFile)).reduce((max, item) => Math.max(max, item.id), 0) + 1; + const data = fs.readFileSync(todosFile); + fs.writeFileSync(todosFile, JSON.stringify([...JSON.parse(data), { id: num, desc: args.desc }])); + console.log(`Successfully added todo id=${num}`); + } else { + fs.writeFileSync(todosFile, JSON.stringify([{ id: 1, desc: args.desc }])); + console.log('Successfully added todo id=1'); + } +} + +/** + * Delete an existing todo item + * @typedef {Object} DelTodoArgs + * @property {number} id - The task id + * @param {DelTodoArgs} args + */ +exports.del_todo = function delTodo(args) { + const todosFile = _getTodosFile(); + if (fs.existsSync(todosFile)) { + const data = fs.readFileSync(todosFile); + const newData = JSON.parse(data).filter(item => item.id !== args.id); + fs.writeFileSync(todosFile, JSON.stringify(newData)); + console.log(`Successfully deleted todo id=${args.id}`); + } else { + _die('Empty todo list'); + } +} + +/** + * Display the current todo list in json format. + */ +exports.list_todos = function listTodos() { + const todosFile = _getTodosFile(); + if (fs.existsSync(todosFile)) { + console.log(fs.readFileSync(todosFile, "utf8")); + } else { + _die('Empty todo list'); + } +} + +/** + * Delete the entire todo list. + */ +exports.clear_todos = function clearTodos() { + const todosFile = _getTodosFile(); + fs.unlinkSync(todosFile) + console.log("Successfully deleted entry todo list"); +} + +function _getTodosFile() { + const cacheDir = process.env.LLM_BOT_CACHE_DIR || '/tmp'; + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); + } + return path.join(cacheDir, 'todos.json'); +} + +function _die(message) { + console.error(message); + process.exit(1); +}
\ No newline at end of file diff --git a/bots/todo-py/index.yaml b/bots/todo-py/index.yaml new file mode 120000 index 0000000..0d19c11 --- /dev/null +++ b/bots/todo-py/index.yaml @@ -0,0 +1 @@ +../todo-sh/index.yaml
\ No newline at end of file diff --git a/bots/todo-py/tools.py b/bots/todo-py/tools.py new file mode 100644 index 0000000..29e13ea --- /dev/null +++ b/bots/todo-py/tools.py @@ -0,0 +1,65 @@ +import json +import sys +import os +from json import JSONDecodeError + + +def add_todo(desc: str): + """Add a new todo item + Args: + desc: The task description + """ + todos_file = _get_todos_file() + try: + with open(todos_file, "r") as f: + data = json.load(f) + except (FileNotFoundError, JSONDecodeError): + data = [] + num = max([item["id"] for item in data] + [0]) + 1 + data.append({"id": num, "desc": desc}) + with open(todos_file, "w") as f: + json.dump(data, f) + print(f"Successfully added todo id={num}") + + +def del_todo(id: int): + """Delete an existing todo item + Args: + id: The task id + """ + todos_file = _get_todos_file() + try: + with open(todos_file, "r") as f: + data = json.load(f) + except (FileNotFoundError, JSONDecodeError): + _die("Empty todo list") + data = [item for item in data if item["id"] != id] + with open(todos_file, "w") as f: + json.dump(data, f) + print(f"Successfully deleted todo id={id}") + + +def list_todos(): + """Display the current todo list in json format.""" + todos_file = _get_todos_file() + try: + with open(todos_file, "r") as f: + print(f.read()) + except FileNotFoundError: + _die("Empty todo list") + + +def clear_todos(): + """Delete the entire todo list.""" + os.remove(_get_todos_file()) + + +def _get_todos_file() -> str: + cache_dir=os.environ.get("LLM_BOT_CACHE_DIR", "/tmp") + if not os.path.exists(cache_dir): + os.makedirs(cache_dir, exist_ok=True) + return os.path.join(cache_dir, "todos.json") + +def _die(msg: str): + print(msg, file=sys.stderr) + exit(1)
\ No newline at end of file diff --git a/bots/todo-sh/index.yaml b/bots/todo-sh/index.yaml new file mode 100644 index 0000000..0988e01 --- /dev/null +++ b/bots/todo-sh/index.yaml @@ -0,0 +1,19 @@ +name: Todo List +description: Your name is TodoBot and you are a helpful chatbot that manages a todo list. +instructions: | + You will be provided with a list of todos. + Users can interact with you using the following commands: + * add_todo: Add a todo to the list. + * rm_todo: Remove a todo from the list. + * list_todos: Display the current todo list. + * clear_todos: Delete the entire todo list. + Based on the interaction, ensure that you provide appropriate confirmations or errors for the requested operation. For example: + - Confirmations: "Todo item added successfully!", "Todo item removed successfully!", "All todo items deleted!" + - Errors: "Cannot add todo item, missing description.", "Todo item with id {id} not found.", "No todo items to delete." + Make sure you understand the user request properly before performing any action. If unsure, ask clarifying questions like "Do you want to remove all todos or just a specific one?" +conversation_starters: + - "Add a new todo item 'Finish report'." + - "Remove the todo item with id=2." + - "Delete all my todos." + - "What todos do I have pending?" + - "How can I remove a specific todo item?"
\ No newline at end of file diff --git a/bots/todo-sh/tools.sh b/bots/todo-sh/tools.sh new file mode 100755 index 0000000..1b1758a --- /dev/null +++ b/bots/todo-sh/tools.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -e + +# @cmd Add a new todo item +# @option --desc! The task description +add_todo() { + todos_file="$(_get_todos_file)" + if [[ -f "$todos_file" ]]; then + num="$(cat "$todos_file" | jq '[.[].id] | max + 1')" + data="$(cat "$todos_file")" + else + num=1 + data="[]" + fi + echo "$data" | \ + jq --arg new_id $num \ + --arg new_desc "$argc_desc" \ + '. += [{"id": $new_id | tonumber, "desc": $new_desc}]' \ + > "$todos_file" + echo "Successfully added todo id=$num" +} + +# @cmd Delete an existing todo item +# @option --id! <INT> The task id +del_todo() { + todos_file="$(_get_todos_file)" + if [[ -f "$todos_file" ]]; then + data="$(cat "$todos_file")" + echo "$data" | \ + jq --arg id $argc_id '[.[] | select(.id != ($id | tonumber))]' \ + > "$todos_file" + echo "Successfully deleted todo id=$argc_id" + else + _die "Empty todo list" + fi +} + +# @cmd Display the current todo list in json format. +list_todos() { + todos_file="$(_get_todos_file)" + if [[ -f "$todos_file" ]]; then + cat "$todos_file" + else + _die "Empty todo list" + fi +} + +# @cmd Delete the entire todo list. +clear_todos() { + todos_file="$(_get_todos_file)" + if [[ -f "$todos_file" ]]; then + rm -rf "$todos_file" + fi + echo "Successfully deleted entry todo list" +} + +_argc_before() { + todos_file="$(_get_todos_file)" + mkdir -p "$(dirname "$todos_file")" +} + +_get_todos_file() { + echo "${LLM_BOT_CACHE_DIR:-/tmp}/todos.json" +} + +_die() { + echo "$*" >&2 + exit 1 +} + +# See more details at https://github.com/sigoden/argc +eval "$(argc --argc-eval "$0" "$@")" diff --git a/scripts/declarations-util.sh b/scripts/declarations-util.sh index 2a0929d..7646500 100755 --- a/scripts/declarations-util.sh +++ b/scripts/declarations-util.sh @@ -15,8 +15,8 @@ pretty-print() { # @cmd Generate placeholder json according to declarations # Examples: -# ./scripts/declarations.sh generate-json-data functions.json -# cat functions.json | ./scripts/declarations.sh generate-json-data functions.json +# ./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 diff --git a/scripts/run-bot.js b/scripts/run-bot.js new file mode 100755 index 0000000..7a5b70c --- /dev/null +++ b/scripts/run-bot.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +const path = require("path"); +const fs = require("fs"); +const os = require("os"); + +async function main() { + const [botName, botFunc, rawData] = parseArgv("run-bot.js"); + const botData = parseRawData(rawData); + + const rootDir = path.resolve(__dirname, ".."); + setupEnv(rootDir, botName); + + const botToolsPath = path.resolve(rootDir, `bots/${botName}/tools.js`); + await run(botToolsPath, botFunc, botData); +} + +function parseArgv(thisFileName) { + let botName = process.argv[1]; + let botFunc = ""; + let botData = null; + + if (botName.endsWith(thisFileName)) { + botName = process.argv[2]; + botFunc = process.argv[3]; + botData = process.argv[4]; + } else { + botName = path.basename(botName); + botFunc = process.argv[2]; + botData = process.argv[3]; + } + + if (botName.endsWith(".js")) { + botName = botName.slice(0, -3); + } + + return [botName, botFunc, botData]; +} + +function parseRawData(data) { + if (!data) { + throw new Error("No JSON data"); + } + try { + return JSON.parse(data); + } catch { + throw new Error("Invalid JSON data"); + } +} + +function setupEnv(rootDir, botName) { + process.env["LLM_ROOT_DIR"] = rootDir; + loadEnv(path.resolve(rootDir, ".env")); + process.env["LLM_BOT_NAME"] = botName; + process.env["LLM_BOT_ROOT_DIR"] = path.resolve(rootDir, "bots", botName); + process.env["LLM_BOT_CACHE_DIR"] = path.resolve(rootDir, "cache", botName); +} + +function loadEnv(filePath) { + try { + const data = fs.readFileSync(filePath, "utf-8"); + const lines = data.split("\n"); + + lines.forEach((line) => { + if (line.trim().startsWith("#") || line.trim() === "") return; + + const [key, ...value] = line.split("="); + process.env[key.trim()] = value.join("=").trim(); + }); + } catch {} +} + +async function run(botPath, botFunc, botData) { + let mod; + if (os.platform() === "win32") { + botPath = `file://${botPath}`; + } + try { + mod = await import(botPath); + } catch { + throw new Error(`Unable to load bot tools at '${botPath}'`); + } + if (!mod || !mod[botFunc]) { + throw new Error(`Not module function '${botFunc}' at '${botPath}'`); + } + const value = await mod[botFunc](botData); + dumpValue(value); +} + +function dumpValue(value) { + if (value === null || value === undefined) { + return; + } + const type = typeof value; + if (type === "string" || type === "number" || type === "boolean") { + console.log(value); + } 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)); + console.log(valueStr); + } + } +} + +main(); diff --git a/scripts/run-bot.py b/scripts/run-bot.py new file mode 100755 index 0000000..d7c7ae1 --- /dev/null +++ b/scripts/run-bot.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +import os +import json +import sys +import importlib.util + + +def main(): + (bot_name, bot_func, raw_data) = parse_argv("run-bot.py") + bot_data = parse_raw_data(raw_data) + + root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + setup_env(root_dir, bot_name) + + bot_tools_path = os.path.join(root_dir, f"bots/{bot_name}/tools.py") + run(bot_tools_path, bot_func, bot_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)) + + bot_name = argv[0] + bot_func = "" + bot_data = None + + if bot_name.endswith(this_file_name): + bot_name = sys.argv[1] + bot_func = sys.argv[2] + bot_data = sys.argv[3] + else: + bot_name = os.path.basename(bot_name) + bot_func = sys.argv[1] + bot_data = sys.argv[2] + + if bot_name.endswith(".py"): + bot_name = bot_name[:-3] + + return bot_name, bot_func, bot_data + + +def setup_env(root_dir, bot_name): + os.environ["LLM_ROOT_DIR"] = root_dir + load_env(os.path.join(root_dir, ".env")) + os.environ["LLM_BOT_NAME"] = bot_name + os.environ["LLM_BOT_ROOT_DIR"] = os.path.join(root_dir, "bots", bot_name) + os.environ["LLM_BOT_CACHE_DIR"] = os.path.join(root_dir, "cache", bot_name) + + +def load_env(file_path): + try: + with open(file_path, "r") as f: + for line in f: + line = line.strip() + if line.startswith("#") or line == "": + continue + + key, *value = line.split("=") + os.environ[key.strip()] = "=".join(value).strip() + except FileNotFoundError: + pass + + +def run(bot_path, bot_func, bot_data): + try: + spec = importlib.util.spec_from_file_location( + os.path.basename(bot_path), bot_path + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + except: + raise Exception(f"Unable to load bot tools at '{bot_path}'") + + if not hasattr(mod, bot_func): + raise Exception(f"Not module function '{bot_func}' at '{bot_path}'") + + value = getattr(mod, bot_func)(**bot_data) + dump_value(value) + + +def dump_value(value): + if value is None: + return + + value_type = type(value).__name__ + if value_type in ("str", "int", "float", "bool"): + print(value) + elif value_type == "dict" or value_type == "list": + value_str = json.dumps(value, indent=2) + assert value == json.loads(value_str) + print(value_str) + + +if __name__ == "__main__": + main() diff --git a/scripts/run-bot.sh b/scripts/run-bot.sh new file mode 100755 index 0000000..a709453 --- /dev/null +++ b/scripts/run-bot.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -e + +main() { + this_file_name=run-bot.sh + parse_argv "$@" + root_dir="$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd)" + setup_env + bot_tools_path="$root_dir/bots/$bot_name/tools.sh" + run +} + +parse_argv() { + if [[ "$0" == *"$this_file_name" ]]; then + bot_name="$1" + bot_func="$2" + bot_data="$3" + else + bot_name="$(basename "$0")" + bot_func="$1" + bot_data="$2" + fi + if [[ "$bot_name" == *.sh ]]; then + bot_name="${bot_name:0:$((${#bot_name}-3))}" + fi +} + +setup_env() { + export LLM_ROOT_DIR="$root_dir" + if [[ -f "$LLM_ROOT_DIR/.env" ]]; then + source "$LLM_ROOT_DIR/.env" + fi + export LLM_BOT_NAME="$bot_name" + export LLM_BOT_ROOT_DIR="$LLM_ROOT_DIR/bots/$bot_name" + export LLM_BOT_CACHE_DIR="$LLM_ROOT_DIR/cache/$bot_name" +} + +run() { + if [[ -z "$bot_data" ]]; then + die "No JSON data" + fi + + _jq=jq + if [[ "$OS" == "Windows_NT" ]]; then + _jq="jq -b" + bot_tools_path="$(cygpath -w "$bot_tools_path")" + fi + + data="$( + echo "$bot_data" | \ + $_jq -r ' + to_entries | .[] | + (.key | split("_") | join("-")) as $key | + if .value | type == "array" then + .value | .[] | "--\($key)\n\(. | @json)" + elif .value | type == "boolean" then + if .value then "--\($key)" else "" end + else + "--\($key)\n\(.value | @json)" + end' + )" || { + die "Invalid JSON data" + } + while IFS= read -r line; do + if [[ "$line" == '--'* ]]; then + args+=("$line") + else + args+=("$(echo "$line" | $_jq -r '.')") + fi + done <<< "$data" + "$bot_tools_path" "$bot_func" "${args[@]}" +} + +die() { + echo "$*" >&2 + exit 1 +} + +main "$@" + |
