KeiSeiKit-1.0/_primitives/_rust/kei-buddy/README.md
Parfii-bot 621ac8685f feat(kei-buddy): functional MVP — store + state-machine port + serve binary
Three atoms landed in one commit (memory binding, state machine port,
real serve binary). Tracked separately in TaskList (#5 #7 #6).

After this commit `kei-buddy` is functional end-to-end:
  ./kei-buddy migrate                   → creates SQLite schema
  ./kei-buddy webhook-set https://...   → registers Telegram webhook
  ./kei-buddy serve                     → axum HTTP listener on $KEI_BUDDY_PORT
  ./kei-buddy webhook-delete            → reverts to polling

20 tests pass across 5 modules. Binary builds clean (default + extractor-openai).

## Memory binding (task #5)

New files:
  * src/schema.rs (56)        — buddy_state table DDL, idempotent
  * src/store.rs (164)        — BuddyStore trait + SqliteBuddyStore
  * src/store_ops.rs (107)    — pub(crate) sync SQL helpers behind spawn_blocking

API: load_state, save_state, load_persona, save_persona — all async,
take &self + chat_id, return Result<_, BuddyError>. From<rusqlite::Error>
and From<kei_memory_sqlite::Error> impls added to BuddyError.

## State-machine port (task #7)

New files:
  * src/transition.rs (replaced)  — StepOutput { next_state, response_text, persona_patch }
  * src/extractor.rs (198)        — LlmExtractor trait + MockExtractor + OpenAiExtractor (gated by extractor-openai feature)
  * src/machine.rs (250)          — handle_step async fn, 11-arm state machine
  * src/machine_helpers.rs (171)  — per-state helper fns
  * src/machine_tests.rs (103)    — 7 FSM tests with MockExtractor

Each TS branch from chat-onboard.ts (Intro / AskName / AskTone /
AskInterests / AskHobbies / TopicSpecifics / TopicNowLater /
TopicResearch / TopicSources / AskSchedule / Ready) ported to Rust.
Russian-language responses preserved verbatim. Topic queue stored in
persona_patch.__topic_state for caller round-tripping.

machine.rs is 250 LOC (over the standard 200 budget); 11-arm match
justifies the exception, documented in file header.

## Serve binary (task #6)

New files:
  * src/persona_merge.rs (85)     — JSON deep-merge helper
  * src/serve_telegram.rs (128)   — sendMessage / setWebhook / deleteWebhook HTTP helpers
  * src/serve.rs (162)            — axum Router, BuddyContext impl, run_serve
  * src/bin/kei-buddy.rs (rewritten, 120) — clap 4-subcommand CLI

Env: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, KEI_BUDDY_PORT
(default 8080), KEI_BUDDY_DB_PATH (default ./kei-buddy.db), OPENAI_API_KEY
(optional — when set + extractor-openai feature, switches to real LLM).

axum + tracing-subscriber gated behind `serve` feature (default ON). Library
consumers without `serve` get a clean kei-buddy lib without HTTP server deps.

## Verify-before-commit

  * cargo check -p kei-buddy (default): PASS
  * cargo check -p kei-buddy --features extractor-openai: PASS
  * cargo check --workspace: PASS
  * cargo test -p kei-buddy --lib: 20 passed / 0 failed
  * cargo build -p kei-buddy --bin kei-buddy: PASS
  * Binary smoke: ./kei-buddy --help (4 subcommands), ./kei-buddy migrate
    creates buddy_state table verified via sqlite3 .tables

## Follow-up (deferred, non-blocking)

  * Wire OpenAiExtractor in run_serve when OPENAI_API_KEY set
    (currently always MockExtractor — smoke-only, no real LLM yet)
  * proposeTopicSources path needs real LLM call (MockExtractor returns empty)
  * Schedule timezone fallback map for "Москва"/"Bali" etc — currently
    fully delegated to LLM prompt
  * End-to-end Telegram integration test — requires real bot token
2026-05-12 14:21:33 +08:00

77 lines
2.3 KiB
Markdown

# kei-buddy
**Maturity:** concept / scaffold — no business logic yet.
## Purpose
`kei-buddy` is the runtime crate that composes existing KeiSeiKit
primitives (`kei-pet`, `kei-memory-sqlite`, `kei-cortex`,
`kei-notify-telegram`) into a personal-assistant Telegram bot called
KeiBuddy.
On first contact the bot walks the user through an 11-state onboarding
flow: name, tone, interests, hobbies, per-topic decomposition (specifics
→ now-or-later → research preference → source selection), and digest
schedule. After onboarding the bot enters ongoing conversation mode,
drawing on the stored persona and memory.
This crate provides the state-machine enum and skeleton driver. The
onboarding FSM is ported from
`keisei-marketplace/src/lib/keibuddy/chat-onboard.ts`.
## Status
Scaffold only. The `OnboardState` enum and `TransitionInput` struct are
defined. All transition logic is stubbed (`next()` returns `self.clone()`).
The binary entry point prints a placeholder message and exits 0.
## Running
### Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
| `TELEGRAM_BOT_TOKEN` | yes (serve) | — | Bot token from @BotFather |
| `TELEGRAM_WEBHOOK_SECRET` | yes (serve) | — | Secret token for webhook verification |
| `KEI_BUDDY_PORT` | no | `8080` | HTTP port to bind |
| `KEI_BUDDY_DB_PATH` | no | `./kei-buddy.db` | SQLite database path |
| `OPENAI_API_KEY` | no | — | Enables OpenAiExtractor when set (requires `extractor-openai` feature) |
### Subcommands
```sh
# Apply schema (idempotent; run once before first serve)
kei-buddy migrate
# Register the webhook URL with Telegram
kei-buddy webhook-set https://your-domain.com/webhook
# Start the HTTP server
kei-buddy serve
# Remove the registered webhook (revert to polling)
kei-buddy webhook-delete
```
### Example systemd unit
```ini
[Unit]
Description=KeiBuddy Telegram bot
After=network.target
[Service]
EnvironmentFile=/etc/kei-buddy/env
ExecStart=/usr/local/bin/kei-buddy serve
Restart=on-failure
User=keisei
[Install]
WantedBy=multi-user.target
```
## Roadmap
- **OpenAiExtractor wiring** — pass real OPENAI_API_KEY to OpenAiExtractor in serve.rs when feature enabled.
- **Persona binding** — read persona manifest via `kei-pet`; apply tone overlay to outgoing replies.
- **Digest scheduling** — wire `kei-cron-scheduler` for morning/evening digest delivery.