aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsigoden <sigoden@gmail.com>2024-06-08 20:46:27 +0800
committerGitHub <noreply@github.com>2024-06-08 20:46:27 +0800
commit213e949fc8c2362046d197554fda98c87a7906df (patch)
tree595a883c6b1fd1154a916569c3d0255634c6e193
parent82d7a7de8a76e56cff306b0da7f4f14fdb57cbf1 (diff)
downloadllm-functions-docker-213e949fc8c2362046d197554fda98c87a7906df.tar.gz
feat: support bots (#39)
-rw-r--r--.gitignore2
-rw-r--r--Argcfile.sh237
l---------bots/todo-js/index.yaml1
-rw-r--r--bots/todo-js/tools.js73
l---------bots/todo-py/index.yaml1
-rw-r--r--bots/todo-py/tools.py65
-rw-r--r--bots/todo-sh/index.yaml19
-rwxr-xr-xbots/todo-sh/tools.sh72
-rwxr-xr-xscripts/declarations-util.sh4
-rwxr-xr-xscripts/run-bot.js107
-rwxr-xr-xscripts/run-bot.py105
-rwxr-xr-xscripts/run-bot.sh80
12 files changed, 757 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore
index a6e40bc..29f8666 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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 "$@"
+