feat: pos ai gemini — CLI + Telegram AI chat (ask, chat, models)
- bin/pos-ai-gemini: one-shot ask (stdout-only, pipe-friendly), interactive multi-turn chat REPL (q/Ctrl+C, /reset), models list; --model override, default gemini-2.5-flash; key via x-goog-api-key header, never printed. - Config scope 'ai' (AI_GEMINI_API_KEY secret, AI_GEMINI_MODEL) in ~/.config/linux_post_install/ai.env via 'pos config ai'; config/ai.env template installed no-clobber by postinstall. - Telegram listener: non-command text starting with 'ai ' (case-insensitive) is answered by Gemini via 'pos ai gemini ask'; owner-chat only, errors reply with the pos config ai hint. Future intents slot in as case arms. - ai-gemini added to INTERACTIVE_CMDS (chat reads stdin). - Docs: POS.md ai section + listener bridge, howto/ai.md, HOWTO/README index rows, bin/pos usage example; make gen refreshed context/completions.
This commit is contained in:
@@ -173,6 +173,11 @@ EXAMPLES
|
||||
pos entertainment enable weather 5m Auto-send weather to Telegram every 5 min
|
||||
pos entertainment status Show enabled plugins + next run
|
||||
|
||||
pos ai gemini ask "Explain DNS in one line"
|
||||
Ask Google Gemini a one-shot question
|
||||
pos ai gemini chat Interactive multi-turn chat
|
||||
pos ai gemini models List available Gemini models
|
||||
|
||||
pos config telegram Edit Telegram config interactively
|
||||
pos config Pick a config scope to edit
|
||||
|
||||
@@ -236,7 +241,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
|
||||
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||
|
||||
# Commands that read from stdin interactively — only log invocation
|
||||
INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server communication-telegram-listener config"
|
||||
INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server communication-telegram-listener ai-gemini config"
|
||||
|
||||
for ((i=n-1; i>=0; i--)); do
|
||||
cmd="pos"
|
||||
|
||||
Executable
+197
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# POS: ai gemini — Chat with Google Gemini (ask, chat, models)
|
||||
# POS_SUBCMDS: ask chat models
|
||||
# POS_FLAGS: --model
|
||||
# POS_CONFIG: ai | ai.env | AI_GEMINI_API_KEY=secret:API key from aistudio.google.com | AI_GEMINI_MODEL=:Model id (default gemini-2.5-flash)
|
||||
|
||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||
|
||||
CONFIG_FILE="$HOME/.config/linux_post_install/ai.env"
|
||||
API="https://generativelanguage.googleapis.com/v1beta"
|
||||
DEFAULT_MODEL="gemini-2.5-flash"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: pos ai gemini <subcommand> [--model <id>]
|
||||
|
||||
Chat with Google Gemini via the REST API (generativelanguage.googleapis.com).
|
||||
|
||||
Subcommands:
|
||||
ask "<prompt>" One-shot answer; prints ONLY the answer text to stdout
|
||||
(pipe/script/Telegram-friendly). The prompt may also be
|
||||
piped in via stdin when no argument is given.
|
||||
chat Interactive multi-turn conversation.
|
||||
models List models that support generateContent.
|
||||
|
||||
Options:
|
||||
--model <id> Override the model for this invocation.
|
||||
-h|--help This help.
|
||||
|
||||
Config: $CONFIG_FILE (edit with 'pos config ai')
|
||||
AI_GEMINI_API_KEY API key from aistudio.google.com (required)
|
||||
AI_GEMINI_MODEL Model id (default $DEFAULT_MODEL)
|
||||
|
||||
Examples:
|
||||
pos ai gemini ask "Explain DNS in one line"
|
||||
echo "summarize this log" | pos ai gemini ask
|
||||
pos ai gemini chat
|
||||
pos ai gemini models
|
||||
pos ai gemini ask --model gemini-2.5-flash "hi"
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── ai.env loader (same pattern as telegram.env) ────────────────
|
||||
load_config() {
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||
}
|
||||
|
||||
require_key() {
|
||||
load_config
|
||||
[ -n "${AI_GEMINI_API_KEY:-}" ] || err "No Gemini API key — run 'pos config ai'"
|
||||
}
|
||||
|
||||
resolve_model() {
|
||||
if [ -n "${MODEL_OVERRIDE:-}" ]; then
|
||||
printf '%s' "$MODEL_OVERRIDE"
|
||||
elif [ -n "${AI_GEMINI_MODEL:-}" ]; then
|
||||
printf '%s' "$AI_GEMINI_MODEL"
|
||||
else
|
||||
printf '%s' "$DEFAULT_MODEL"
|
||||
fi
|
||||
}
|
||||
|
||||
# One generateContent call. $1 = model, $2 = contents JSON array.
|
||||
# stdout = the answer text on success; an error message on failure (exit 1).
|
||||
gemini_generate() {
|
||||
local model="$1" contents="$2"
|
||||
local resp code body errmsg
|
||||
resp="$(curl -sS -m 60 -X POST "${API}/models/${model}:generateContent" \
|
||||
-H "x-goog-api-key: ${AI_GEMINI_API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--write-out $'\n%{http_code}' \
|
||||
--data "$contents")" || { echo "request failed (curl exit $?)" >&2; return 1; }
|
||||
code="${resp##*$'\n'}"
|
||||
body="${resp%$'\n'*}"
|
||||
if [ "$code" != "200" ]; then
|
||||
errmsg="$(printf '%s' "$body" | jq -r '.error.message // empty' 2>/dev/null || true)"
|
||||
echo "API error $code${errmsg:+: $errmsg}" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$body" | jq -r '[.candidates[0].content.parts[]?.text] | join("")'
|
||||
}
|
||||
|
||||
cmd_ask() {
|
||||
local prompt="" contents out
|
||||
if [ $# -gt 0 ]; then
|
||||
prompt="$*"
|
||||
elif [ ! -t 0 ]; then
|
||||
prompt="$(cat)"
|
||||
fi
|
||||
[ -n "$prompt" ] || err "No prompt given — usage: pos ai gemini ask \"<prompt>\""
|
||||
require_key
|
||||
contents="$(jq -nc --arg t "$prompt" '{contents:[{role:"user",parts:[{text:$t}]}]}')"
|
||||
if ! out="$(gemini_generate "$(resolve_model)" "$contents" 2>&1)"; then
|
||||
err "$out"
|
||||
fi
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
|
||||
cmd_chat() {
|
||||
[ $# -eq 0 ] || err "Unexpected argument for chat: $*"
|
||||
local model contents text answer
|
||||
model="$(resolve_model)"
|
||||
require_key
|
||||
contents='{"contents":[]}'
|
||||
trap 'echo; echo "bye"; exit 0' INT
|
||||
echo "Gemini · ${model} — type a message; q=quit, /reset=clear history"
|
||||
while true; do
|
||||
printf '> '
|
||||
IFS= read -r text || break
|
||||
case "$text" in
|
||||
"" ) continue ;;
|
||||
q|Q|quit|exit) echo; echo "bye"; return 0 ;;
|
||||
/reset) contents='{"contents":[]}'; echo "[history cleared]"; continue ;;
|
||||
esac
|
||||
contents="$(printf '%s' "$contents" | jq -c --arg t "$text" '.contents += [{role:"user",parts:[{text:$t}]}]')"
|
||||
if ! answer="$(gemini_generate "$model" "$contents" 2>&1)"; then
|
||||
warn "AI error: $answer"
|
||||
continue
|
||||
fi
|
||||
contents="$(printf '%s' "$contents" | jq -c --arg t "$answer" '.contents += [{role:"model",parts:[{text:$t}]}]')"
|
||||
printf '\n%s\n\n' "$answer"
|
||||
done
|
||||
echo
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_models() {
|
||||
[ $# -eq 0 ] || err "Unexpected argument for models: $*"
|
||||
local model resp code body m
|
||||
model="$(resolve_model)"
|
||||
require_key
|
||||
resp="$(curl -sS -m 30 -G "${API}/models" \
|
||||
-H "x-goog-api-key: ${AI_GEMINI_API_KEY}" \
|
||||
--data-urlencode "pageSize=1000" \
|
||||
--write-out $'\n%{http_code}')" || err "request failed (curl exit $?)"
|
||||
code="${resp##*$'\n'}"
|
||||
body="${resp%$'\n'*}"
|
||||
if [ "$code" != "200" ]; then
|
||||
err "API error $code: $(printf '%s' "$body" | jq -r '.error.message // empty')"
|
||||
fi
|
||||
local list
|
||||
list="$(printf '%s' "$body" | jq -r '.models[]? | select((.supportedGenerationMethods // []) | index("generateContent")) | .name' | sed 's#^models/##' | sort)"
|
||||
echo "Gemini models (generateContent-capable):"
|
||||
while IFS= read -r m; do
|
||||
[ -n "$m" ] || continue
|
||||
if [ "$m" = "$model" ]; then
|
||||
printf ' %-32s <- default\n' "$m"
|
||||
else
|
||||
printf ' %-32s\n' "$m"
|
||||
fi
|
||||
done <<< "$list"
|
||||
if ! grep -qxF "$model" <<< "$list"; then
|
||||
warn "configured default '$model' is not in the list — set AI_GEMINI_MODEL"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Parse flags + subcommand ────────────────────────────────────
|
||||
MODEL_OVERRIDE=""
|
||||
cmd=""
|
||||
args=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help) usage ;;
|
||||
--model)
|
||||
[ $# -ge 2 ] || err "--model needs a value"
|
||||
MODEL_OVERRIDE="$2"; shift 2 ;;
|
||||
-*) err "Unknown option '$1' (see --help)" ;;
|
||||
*)
|
||||
if [ -z "$cmd" ]; then
|
||||
cmd="$1"
|
||||
else
|
||||
args+=("$1")
|
||||
fi
|
||||
shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "${cmd:-}" in
|
||||
"") usage ;;
|
||||
ask) cmd_ask "${args[@]}" ;;
|
||||
chat) cmd_chat "${args[@]}" ;;
|
||||
models) cmd_models "${args[@]}" ;;
|
||||
*) err "Unknown ai gemini subcommand '$cmd' (see --help)" ;;
|
||||
esac
|
||||
@@ -436,6 +436,21 @@ handle_message() {
|
||||
reply "Mapped commands: $(map_cmds_list)" "$msg_id"
|
||||
return ;;
|
||||
esac
|
||||
# AI bridge: non-command text starting with "ai " (case-insensitive) is
|
||||
# forwarded to Gemini; the model's answer is replied verbatim. Future
|
||||
# non-command intents (e.g. reminders) slot in as more case arms here.
|
||||
if [[ "$text" != /* && "$text" =~ ^[Aa][Ii][[:space:]](.*)$ ]]; then
|
||||
local prompt="${BASH_REMATCH[1]}" answer
|
||||
[ -n "$prompt" ] || { reply "Usage: ai <prompt> — e.g. 'ai what is Nvidia'" "$msg_id"; return; }
|
||||
log "ai: $prompt"
|
||||
if answer="$(timeout 120 pos ai gemini ask "$prompt" 2>&1)"; then
|
||||
reply "$answer" "$msg_id"
|
||||
else
|
||||
[ -n "$answer" ] || answer="timed out after 120s"
|
||||
reply "AI error: $answer" "$msg_id"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
value="$(map_get "$text")"
|
||||
if [ -z "$value" ]; then
|
||||
reply "Unknown command: $text (send /help)" "$msg_id"
|
||||
|
||||
Reference in New Issue
Block a user