t-261002193628-1d1e5f: установка одной командой + визард первого запуска
- scripts/install.sh: clone → venv → deps → обёртка ~/.local/bin/simbiot (чистый env, как hermes) → тихий init/визард → проверка simbiot status; флаги --dir/--repo/--wizard/--no-wizard/--systemd/-q - cli.py wizard: интерактивный конфиг .env (TG-токен, чаты, ключ 9router, модель SIMBIOT_MODEL) + init стора; --noninteractive и флаги - harness/levels.py: SIMBIOT_MODEL переопределяет модель уровней (env/.env) - requirements.txt: pinned-зависимости для установщика - scripts/simbiot-harness.service: systemd-юнит (опция --systemd) - README.md: секция «Установка одной командой»
This commit is contained in:
parent
e435289e55
commit
4c4ac34560
6 changed files with 360 additions and 0 deletions
24
README.md
24
README.md
|
|
@ -3,6 +3,30 @@
|
|||
Ритмичный агент: живёт тактами, как человек. CQRS + Event Sourcing на спине,
|
||||
Strands Harness SDK как исполнительное ядро, модели через 9router.
|
||||
|
||||
## Установка одной командой (t-261002193628-1d1e5f, как hermes)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.cil.by/simbiot/install.sh | bash
|
||||
```
|
||||
|
||||
Что делает `scripts/install.sh`: клонирует репо в `~/simbiot-harness`,
|
||||
создаёт venv (Python 3.11+), ставит зависимости, кладёт обёртку
|
||||
`~/.local/bin/simbiot` (чистый env: unset PYTHONPATH/PYTHONHOME →
|
||||
`exec venv/bin/python cli.py`), тихо инициализирует стор и проверяет
|
||||
`simbiot status`. Флаги: `--dir DIR`, `--repo URL`, `--wizard` /
|
||||
`--no-wizard`, `--systemd` (юнит, только root), `-q` (тихо).
|
||||
|
||||
Первый запуск — визард настройки (токен TG-бота, ключ 9router, модель):
|
||||
|
||||
```bash
|
||||
simbiot wizard # интерактивно
|
||||
simbiot wizard --noninteractive --router-key KEY --model auto
|
||||
```
|
||||
|
||||
`.env` в корне репо читается стартапом (SIMBIOT_TG_TOKEN,
|
||||
SIMBIOT_TG_ALLOWED, ROUTER_KEY, SIMBIOT_MODEL и др.); в systemd-режиме
|
||||
`scripts/simbiot-harness.service` подхватывает его сам.
|
||||
|
||||
## Главная цель (фиксация Alex, 2026-09-30)
|
||||
|
||||
Метрика харнеса — **максимизация долгосрочной AUM** (доход/ресурс всех
|
||||
|
|
|
|||
128
cli.py
128
cli.py
|
|
@ -121,6 +121,21 @@ def main():
|
|||
p_init = sub.add_parser("init", help="инициализация стора")
|
||||
p_init.add_argument("--identity", default="Simbiot Harness — ритмичный агент кооператива")
|
||||
|
||||
# Визард первого запуска (t-261002193628-1d1e5f, аналогично hermes):
|
||||
# интерактивный конфиг .env (токен TG, ключ 9router, модель) + init стора.
|
||||
p_wiz = sub.add_parser("wizard", help="визард первого запуска: настройка .env и стора")
|
||||
p_wiz.add_argument("--noninteractive", action="store_true",
|
||||
help="без вопросов: значения из ENV/--флагов, init без TG-настройки")
|
||||
p_wiz.add_argument("--tg-token", default=None, help="токен TG-бота (SIMBIOT_TG_TOKEN)")
|
||||
p_wiz.add_argument("--tg-allowed", default=None,
|
||||
help="разрешённые чат-ids через запятую (SIMBIOT_TG_ALLOWED)")
|
||||
p_wiz.add_argument("--router-key", default=None, help="ключ 9router (ROUTER_KEY)")
|
||||
p_wiz.add_argument("--router-base-url", default=None,
|
||||
help="база 9router (ROUTER_BASE_URL, по умолчанию http://127.0.0.1:20128/v1)")
|
||||
p_wiz.add_argument("--model", default=None,
|
||||
help="модель уровней (по умолчанию auto — как в harness/levels.py)")
|
||||
p_wiz.add_argument("--no-init", action="store_true", help="не инициализировать стор")
|
||||
|
||||
p_rag = sub.add_parser("rag-query", help="семантический поиск по базе кооператива (вендор /root/rag)")
|
||||
p_rag.add_argument("question")
|
||||
p_rag.add_argument("-k", type=int, default=5, help="сколько чанков (<=5)")
|
||||
|
|
@ -133,6 +148,9 @@ def main():
|
|||
es.init_db()
|
||||
commands.set_policy(identity=args.identity)
|
||||
print("OK: стор инициализирован, политика записана")
|
||||
|
||||
elif args.cmd == "wizard":
|
||||
_run_wizard(args)
|
||||
elif args.cmd == "task":
|
||||
tid, size = commands.submit_task(args.title, args.details, args.minutes)
|
||||
if args.await_delivery:
|
||||
|
|
@ -416,5 +434,115 @@ def main():
|
|||
print(f"{ev['id']:4} {ev['timestamp'][11:19]} {ev['aggregate']:8} {ev['event_type']:16} {d}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Визард первого запуска (t-261002193628-1d1e5f, аналогично hermes): конфиг
|
||||
# .env атомарно (0600) + init стора. Дополнен шагом «модель» — она реально
|
||||
# переключает уровни через SIMBIOT_MODEL (env-оверрайд в harness/levels.py).
|
||||
# ============================================================================
|
||||
def _mask(v: str) -> str:
|
||||
v = (v or "").strip()
|
||||
return "(не задано)" if not v else (v[:4] + "…" + v[-4:] if len(v) > 12 else "***")
|
||||
|
||||
|
||||
def _dotenv_get(env_path: str, key: str) -> str:
|
||||
"""Значение key из .env (env процесса сильнее, как в runtime._env_or_file)."""
|
||||
val = os.environ.get(key, "")
|
||||
if val:
|
||||
return val
|
||||
try:
|
||||
with open(env_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith(key + "="):
|
||||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _dotenv_set(env_path: str, key: str, value: str) -> None:
|
||||
"""Атомарная запись key=value в .env: обновить/добавить, остальное — как было."""
|
||||
lines = []
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
out, replaced = [], False
|
||||
for ln in lines:
|
||||
if ln.startswith(key + "="):
|
||||
if not replaced:
|
||||
out.append(f"{key}={value}")
|
||||
replaced = True # повторные вхождения key выкидываем
|
||||
else:
|
||||
out.append(ln)
|
||||
if not replaced:
|
||||
out.append(f"{key}={value}")
|
||||
tmp = env_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(out).rstrip("\n") + "\n")
|
||||
os.replace(tmp, env_path)
|
||||
os.chmod(env_path, 0o600)
|
||||
|
||||
|
||||
def _ask(prompt: str, current: str, secret: bool = False) -> str:
|
||||
"""Ответ пользователя; Enter = оставить текущее; EOF (не-tty) = ничего."""
|
||||
hint = _mask(current) if secret else (current if current else "пусто")
|
||||
try:
|
||||
raw = input(f"{prompt} [{hint}]: ").strip()
|
||||
except EOFError:
|
||||
raw = ""
|
||||
return raw if raw else current
|
||||
|
||||
|
||||
def _tg_selftest(token: str) -> str:
|
||||
"""getMe-самотест (только проверка токена, ничего не отправляет)."""
|
||||
if not token:
|
||||
return ""
|
||||
import urllib.request
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"https://api.telegram.org/bot{token}/getMe", timeout=5) as r:
|
||||
who = json.loads(r.read().decode()).get("result", {}).get("username")
|
||||
return f"bot @{who}" if who else "ok"
|
||||
except Exception as e:
|
||||
return f"ОШИБКА ({e.__class__.__name__}) — токен не принят, но запись в .env сделал"
|
||||
|
||||
|
||||
def _run_wizard(args) -> None:
|
||||
env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
||||
vals = {
|
||||
"SIMBIOT_TG_TOKEN": args.tg_token if args.tg_token is not None else None,
|
||||
"SIMBIOT_TG_ALLOWED": args.tg_allowed,
|
||||
"ROUTER_KEY": args.router_key,
|
||||
"ROUTER_BASE_URL": args.router_base_url,
|
||||
"SIMBIOT_MODEL": args.model,
|
||||
}
|
||||
if not getattr(args, "noninteractive", False):
|
||||
print("⬢ Simbiot Harness — визард первого запуска.")
|
||||
print(" Enter = оставить текущее значение. Всё пишется в .env (0600).")
|
||||
tok = _ask("Токен TG-бота", _dotenv_get(env_path, "SIMBIOT_TG_TOKEN"), secret=True)
|
||||
allowed = _ask("Разрешённые chat-id через запятую",
|
||||
_dotenv_get(env_path, "SIMBIOT_TG_ALLOWED"))
|
||||
key = _ask("Ключ 9router", _dotenv_get(env_path, "ROUTER_KEY"), secret=True)
|
||||
base = _ask("База 9router (URL)",
|
||||
_dotenv_get(env_path, "ROUTER_BASE_URL")
|
||||
or "http://127.0.0.1:20128/v1")
|
||||
model = _ask("Модель уровней (или auto)", _dotenv_get(env_path, "SIMBIOT_MODEL") or "auto")
|
||||
vals = {"SIMBIOT_TG_TOKEN": tok, "SIMBIOT_TG_ALLOWED": allowed,
|
||||
"ROUTER_KEY": key, "ROUTER_BASE_URL": base, "SIMBIOT_MODEL": model}
|
||||
if tok:
|
||||
print(f" TG-токен: {_tg_selftest(tok)}")
|
||||
for key, val in vals.items():
|
||||
if val not in (None, "",):
|
||||
if key == "ROUTER_BASE_URL" and val == "http://127.0.0.1:20128/v1" \
|
||||
and _dotenv_get(env_path, "ROUTER_BASE_URL") == val:
|
||||
continue # дефолт уже такой — .env не засоряем
|
||||
_dotenv_set(env_path, key, str(val))
|
||||
print(f" .env ← {key}={_mask(str(val)) if 'KEY' in key or 'TOKEN' in key else val}")
|
||||
if not args.no_init:
|
||||
es.init_db()
|
||||
print("OK: стор инициализирован.")
|
||||
print("Дальше: simbiot status — проверить; запустить харнес: scripts/install.sh --systemd "
|
||||
"или ./venv/bin/python -m harness.scheduler (см. README).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
import os
|
||||
|
||||
SEC = 1
|
||||
MIN = 60
|
||||
|
|
@ -14,6 +15,25 @@ QUARTER = 91 * DAY
|
|||
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания
|
||||
|
||||
|
||||
def _cfg_model(default: str = "auto") -> str:
|
||||
"""SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f):
|
||||
одна модель для всех LLM-уровней вместо auto — env сильнее .env
|
||||
(как runtime._env_or_file, без circular import — свой мини-ридер).
|
||||
Пустое/absent значение = дефолт конфига (как было)."""
|
||||
val = os.environ.get("SIMBIOT_MODEL", "").strip()
|
||||
if val:
|
||||
return val
|
||||
try:
|
||||
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
|
||||
with open(env_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("SIMBIOT_MODEL="):
|
||||
return line.split("=", 1)[1].strip().strip('"').strip("'").strip() or default
|
||||
except OSError:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
@dataclass
|
||||
class CycleLevel:
|
||||
name: str
|
||||
|
|
|
|||
23
requirements.txt
Normal file
23
requirements.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Simbiot Harness — runtime-зависимости (strands-ядро + телеметрия).
|
||||
# Версии = эталонный venv кооператива (2026-10-02); установка: scripts/install.sh
|
||||
strands-agents==1.57.1
|
||||
strands-harness==0.1.2
|
||||
openai==3.20.0
|
||||
pydantic==2.13.5
|
||||
PyJWT==2.15.1
|
||||
PyYAML==6.0.3
|
||||
# event store / CLI — stdlib (sqlite3, argparse); audit-цепочка: cryptography
|
||||
cryptography==50.0.1
|
||||
# 9router-клиент (OpenAI-совместимый) + телеметрия Strands
|
||||
botocore==1.43.104
|
||||
boto3==1.43.104
|
||||
opentelemetry-sdk==1.45.0
|
||||
opentelemetry-exporter-otlp-proto-http==1.45.0
|
||||
opentelemetry-semantic-conventions==0.66b0
|
||||
# дашборд и HTTP-эндпоинты
|
||||
starlette==1.7.0
|
||||
uvicorn==0.54.0
|
||||
sse-starlette==3.5.0
|
||||
python-multipart==0.0.32
|
||||
py-spy==0.4.2
|
||||
watchdog==6.0.0
|
||||
150
scripts/install.sh
Normal file
150
scripts/install.sh
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# Simbiot Harness — установка одной командой
|
||||
# ============================================================================
|
||||
# curl -fsSL https://raw.cil.by/simbiot/install.sh | bash
|
||||
# # или из клона репо:
|
||||
# bash scripts/install.sh
|
||||
#
|
||||
# Что делает:
|
||||
# 1. Клонирует (или обновляет) репо в ~/simbiot-harness
|
||||
# 2. Создаёт Python 3.11+ venv, ставит зависимости (requirements.txt)
|
||||
# 3. Кладёт обёртку `simbiot` в ~/.local/bin (аналог hermes: чистый env,
|
||||
# exec venv/bin/python cli.py — вне репо работает отовсюду)
|
||||
# 4. Флаги: --dir DIR (куда ставить, по умолчанию ~/simbiot-harness),
|
||||
# --repo URL (по умолчанию https://git.cil.by/symbiot/simbiot-harness.git),
|
||||
# --wizard (визард первого запуска даже в интерактиве не нужен),
|
||||
# --no-wizard (тихий режим: без вопросов), -q (минимум вывода).
|
||||
# Опционально ставит systemd-юниты (если root и systemd), иначе — как
|
||||
# запустить руками, печатает подсказку симулятивно.
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://git.cil.by/symbiot/simbiot-harness.git"
|
||||
INSTALL_DIR="$(eval echo '~')/simbiot-harness"
|
||||
WIZARD_AUTO="" # "", "yes", "no"
|
||||
QUIET=0
|
||||
FORCE_SYSTEMD=""
|
||||
INTERACTIVE=0
|
||||
[ -t 0 ] && [ -t 1 ] && INTERACTIVE=1
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dir) INSTALL_DIR="$2"; shift 2 ;;
|
||||
--repo) REPO_URL="$2"; shift 2 ;;
|
||||
--wizard) WIZARD_AUTO="yes"; shift ;;
|
||||
--no-wizard) WIZARD_AUTO="no"; shift ;;
|
||||
--systemd) FORCE_SYSTEMD="yes"; shift ;;
|
||||
-q|--quiet) QUIET=1; shift ;;
|
||||
-h|--help) grep '^#' "$0" | sed -n '2,20p' | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "Неизвестный аргумент: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[0;33m'; NC='\033[0m'
|
||||
say() { [ "$QUIET" = 1 ] || echo -e "$1"; }
|
||||
|
||||
say "${CYAN}⬢ Simbiot Harness — установка${NC}"
|
||||
say ""
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 1. Репозиторий
|
||||
# ----------------------------------------------------------------------------
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
say "${CYAN}→${NC} Обновляю существующий клон: $INSTALL_DIR"
|
||||
git -C "$INSTALL_DIR" fetch --depth=1 origin >/dev/null 2>&1 || true
|
||||
git -C "$INSTALL_DIR" reset --hard origin/HEAD >/dev/null 2>&1 || \
|
||||
git -C "$INSTALL_DIR" reset --hard origin/main >/dev/null 2>&1 || true
|
||||
else
|
||||
say "${CYAN}→${NC} Клонирую репозиторий → $INSTALL_DIR"
|
||||
mkdir -p "$(dirname "$INSTALL_DIR")"
|
||||
for i in 1 2 3; do
|
||||
git clone --depth=1 "$REPO_URL" "$INSTALL_DIR" && break
|
||||
[ "$i" = 3 ] && { echo "Ошибка: клонирование не удалось ($REPO_URL)" >&2; exit 1; }
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 2. Python venv + зависимости
|
||||
# ----------------------------------------------------------------------------
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3 не найден. Установи Python 3.11+: apt install -y python3 python3-venv python3-pip" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYV=$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')
|
||||
PYMAJOR=${PYV%%.*}; PYMINOR=${PYV#*.}
|
||||
if [ "$PYMAJOR" -lt 3 ] || { [ "$PYMAJOR" = 3 ] && [ "$PYMINOR" -lt 11 ]; }; then
|
||||
echo "Нужен Python ≥ 3.11, найден $PYV" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x venv/bin/python ]; then
|
||||
say "${CYAN}→${NC} Создаю venv (python $PYV)…"
|
||||
python3 -m venv venv
|
||||
fi
|
||||
say "${CYAN}→${NC} Ставлю зависимости…"
|
||||
./venv/bin/pip install --upgrade pip >/dev/null
|
||||
./venv/bin/pip install -r requirements.txt >/dev/null 2>&1 || \
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 3. Обёртка `simbiot` (аналог /usr/local/bin/hermes)
|
||||
# ----------------------------------------------------------------------------
|
||||
BIN_DIR="$(bash -c 'echo -n "${XDG_BIN_HOME:-$HOME/.local/bin}"')"
|
||||
mkdir -p "$BIN_DIR"
|
||||
cat > "$BIN_DIR/simbiot" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
# Simbiot Harness CLI (установлен scripts/install.sh в $INSTALL_DIR)
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
cd "$INSTALL_DIR"
|
||||
exec ./venv/bin/python cli.py "\$@"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/simbiot"
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) ;;
|
||||
*) export PATH="$BIN_DIR:$PATH" ;;
|
||||
esac
|
||||
say "${GREEN}→${NC} Команда simbiot готова: $BIN_DIR/simbiot"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 4. Первый запуск: init стора (тихо) + визард (если просили/интерактив)
|
||||
# ----------------------------------------------------------------------------
|
||||
if [ ! -f harness.db ]; then
|
||||
if [ "$WIZARD_AUTO" = "no" ] || { [ -z "$WIZARD_AUTO" ] && [ "$INTERACTIVE" = 0 ]; }; then
|
||||
say "${CYAN}→${NC} Тихий режим: ./venv/bin/python cli.py init"
|
||||
./venv/bin/python cli.py init
|
||||
say "${YELLOW}→${NC} Настройки (токен TG, ключ 9router): ${BIN_DIR}/simbiot wizard — когда будешь готов."
|
||||
else
|
||||
./venv/bin/python cli.py wizard || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 5. systemd (только если root и просили явно)
|
||||
# ----------------------------------------------------------------------------
|
||||
if [ "${FORCE_SYSTEMD:-}" = "yes" ]; then
|
||||
if [ "$(id -u)" = "0" ] && command -v systemctl >/dev/null 2>&1 && systemctl is-system-running >/dev/null 2>&1; then
|
||||
cp "$INSTALL_DIR/scripts/simbiot-harness.service" /etc/systemd/system/ 2>/dev/null || sed -e "s|/root/simbiot-harness|$INSTALL_DIR|g" -e "s|root|$USER|g" "$INSTALL_DIR/scripts/simbiot-harness.service" > /etc/systemd/system/simbiot-harness.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now simbiot-harness
|
||||
say "${GREEN}→${NC} systemd: simbiot-harness запущен (systemctl status simbiot-harness)"
|
||||
else
|
||||
say "${YELLOW}→${NC} systemd-юнит пропущен (нужны root и systemd)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 6. Проверка одной проверяемой командой
|
||||
# ----------------------------------------------------------------------------
|
||||
say ""
|
||||
if simbiot status >/dev/null 2>&1; then
|
||||
say "${GREEN}✅ Установка завершена: simbiot status работает.${NC}"
|
||||
else
|
||||
echo "⚠ simbiot status вернул ошибку — см. вывод выше." >&2
|
||||
exit 1
|
||||
fi
|
||||
say "Дальше: simbiot wizard (настройка), simbiot --help (команды)."
|
||||
15
scripts/simbiot-harness.service
Normal file
15
scripts/simbiot-harness.service
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=Simbiot Harness - tact ladder (Strands + CQRS/ES)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=__INSTALL_DIR__
|
||||
ExecStart=__INSTALL_DIR__/venv/bin/python -m harness.scheduler
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Loading…
Reference in a new issue