feat(kei-buddy): scaffold runtime crate — 11-state onboarding FSM enum
First atom of the kei-buddy phase-1 plan. Pure scaffold — no business
logic; that comes in follow-up commits.
Crate location: _primitives/_rust/kei-buddy/
LOC: 262 across 7 files (largest src/state.rs 85 LOC; all <200).
Contents:
* src/state.rs — OnboardState enum with 11 variants matching the
TS state-machine in keisei-marketplace/src/lib/keibuddy/chat-onboard.ts:
Intro, AskName, AskTone, AskInterests, AskHobbies, TopicSpecifics,
TopicNowLater, TopicResearch, TopicSources, AskSchedule, Ready.
serde(rename_all = "snake_case") matches TS naming.
`next()` is a stub (returns self.clone(); real transitions TBD).
* src/transition.rs — TransitionInput struct (user_text +
extracted_fields json::Value). Struct only, no extraction yet.
* src/error.rs — BuddyError enum via thiserror (StateMachine /
Memory / Transport). No From impls yet.
* src/lib.rs — module declarations + re-exports.
* src/bin/kei-buddy.rs — minimal `kei-buddy serve` clap subcommand,
currently prints "not yet implemented".
* Cargo.toml — workspace member, maturity = "concept".
* README.md — crate-level README, roadmap of 4 follow-up bullets.
Workspace registration: _primitives/_rust/Cargo.toml members list
gains "kei-buddy". Lockfile updated accordingly.
Verify-before-commit (RULE 0.13 §):
* cargo check --offline -p kei-buddy: PASS
* cargo test --offline -p kei-buddy --lib: 1 passed / 0 failed
(state::tests::all_variants_serde_roundtrip)
* cargo check --workspace --offline: PASS
* STATUS-TRUTH MARKER from agent: shipped=scaffolding, stubs=1
(state.rs:50 next() returns self.clone(), expected for scaffold)
Follow-up tasks (tracked in TaskList):
* Port handleStep transition logic from chat-onboard.ts
* LLM extract via kei-cortex
* Memory binding via kei-memory-sqlite
* Telegram webhook driver (new crate kei-telegram-webhook)
* kei-tts trait + 4 backends (ElevenLabs / OpenAI / Google / Piper)
* kei-stt trait + 3 backends (Whisper local / Deepgram / OpenAI API)
This commit is contained in:
parent
94e975c92b
commit
a2d4bc9206
9 changed files with 304 additions and 29 deletions
69
_primitives/_rust/Cargo.lock
generated
69
_primitives/_rust/Cargo.lock
generated
|
|
@ -713,7 +713,7 @@ dependencies = [
|
|||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower",
|
||||
"tower 0.5.3",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -889,7 +889,7 @@ dependencies = [
|
|||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.24.0",
|
||||
"tower",
|
||||
"tower 0.5.3",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
|
@ -1574,12 +1574,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
|
|
@ -3186,6 +3185,18 @@ dependencies = [
|
|||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kei-buddy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kei-cache"
|
||||
version = "0.1.0"
|
||||
|
|
@ -3366,7 +3377,7 @@ dependencies = [
|
|||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.5.2",
|
||||
"url",
|
||||
"uuid",
|
||||
|
|
@ -3530,7 +3541,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
|
@ -3683,22 +3694,6 @@ dependencies = [
|
|||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kei-graph-stream"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"clap",
|
||||
"futures",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.29.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kei-hibernate"
|
||||
version = "0.1.0"
|
||||
|
|
@ -4500,11 +4495,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.11.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -5894,7 +5889,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower 0.5.3",
|
||||
"tower-http 0.6.8",
|
||||
"tower-service",
|
||||
"url",
|
||||
|
|
@ -7546,6 +7541,23 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
|
|
@ -7557,7 +7569,6 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
|
@ -7593,7 +7604,7 @@ dependencies = [
|
|||
"http-body 1.0.1",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower 0.5.3",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@ members = [
|
|||
"kei-db-contract",
|
||||
# Live runtime-graph exporter (registry + ledger → D3 space fragment)
|
||||
"kei-graph-export",
|
||||
# KeiBuddy personal-assistant Telegram bot — onboarding FSM scaffold
|
||||
"kei-buddy",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
33
_primitives/_rust/kei-buddy/Cargo.toml
Normal file
33
_primitives/_rust/kei-buddy/Cargo.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "kei-buddy"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "KeiBuddy personal-assistant Telegram bot — onboarding state-machine + skeleton driver. Concept-level scaffold."
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "kei-buddy"
|
||||
path = "src/bin/kei-buddy.rs"
|
||||
|
||||
[lib]
|
||||
name = "kei_buddy"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
tracing = "0.1"
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
|
||||
[features]
|
||||
# future: pulls in kei-notify-telegram for real Telegram transport
|
||||
telegram = []
|
||||
|
||||
[package.metadata.keisei]
|
||||
maturity = "concept"
|
||||
description = "KeiBuddy personal-assistant: onboarding FSM + bot driver scaffold"
|
||||
authors = ["Denis Parfionovich <parfionovich@keilab.io>"]
|
||||
37
_primitives/_rust/kei-buddy/README.md
Normal file
37
_primitives/_rust/kei-buddy/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# 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.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- **State-machine transition logic** — port `handleStep` from
|
||||
`chat-onboard.ts`; wire per-state LLM-extract calls through kei-cortex.
|
||||
- **Memory binding** — persist scratchpad and finalised persona via
|
||||
kei-memory-sqlite; implement `getChatState` / `setChatStep` equivalents.
|
||||
- **Persona binding** — read persona manifest via `kei-pet`; apply tone
|
||||
overlay to outgoing replies.
|
||||
- **Transport binding** — wire kei-notify-telegram for outbound messages;
|
||||
add a real Telegram webhook server (or kei-gateway adapter) for inbound.
|
||||
34
_primitives/_rust/kei-buddy/src/bin/kei-buddy.rs
Normal file
34
_primitives/_rust/kei-buddy/src/bin/kei-buddy.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//! kei-buddy binary entry point.
|
||||
//!
|
||||
//! Scaffold — the `serve` subcommand is a no-op stub until the
|
||||
//! Telegram webhook driver and memory layer are wired in.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "kei-buddy",
|
||||
about = "KeiBuddy personal-assistant bot (scaffold)",
|
||||
version
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Start the Telegram webhook listener (not yet implemented).
|
||||
Serve,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Serve => {
|
||||
println!("kei-buddy serve: not yet implemented, scaffold only");
|
||||
}
|
||||
}
|
||||
}
|
||||
26
_primitives/_rust/kei-buddy/src/error.rs
Normal file
26
_primitives/_rust/kei-buddy/src/error.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//! Error type for kei-buddy operations.
|
||||
//!
|
||||
//! Three categories cover the three integration layers that will be
|
||||
//! wired in follow-up tasks: state-machine, memory store, transport.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Top-level error type for the KeiBuddy crate.
|
||||
///
|
||||
/// Variants will gain `#[from]` impls once the concrete dependencies
|
||||
/// (kei-memory-sqlite, kei-notify-telegram, kei-cortex) are wired.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BuddyError {
|
||||
/// Error originating in the onboarding state machine.
|
||||
#[error("state machine error: {0}")]
|
||||
StateMachine(String),
|
||||
|
||||
/// Error originating in the memory persistence layer.
|
||||
#[error("memory error: {0}")]
|
||||
Memory(String),
|
||||
|
||||
/// Error originating in the Telegram (or other) transport layer.
|
||||
#[error("transport error: {0}")]
|
||||
Transport(String),
|
||||
}
|
||||
23
_primitives/_rust/kei-buddy/src/lib.rs
Normal file
23
_primitives/_rust/kei-buddy/src/lib.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//! kei-buddy — KeiBuddy personal-assistant Telegram bot scaffold.
|
||||
//!
|
||||
//! Concept-level crate. This file declares the public module surface.
|
||||
//! No business logic lives here; see individual modules.
|
||||
//!
|
||||
//! Module layout (Constructor Pattern — one file, one responsibility):
|
||||
//! * `state` — `OnboardState` enum + `next()` stub
|
||||
//! * `transition` — `TransitionInput` input struct
|
||||
//! * `error` — `BuddyError` error type
|
||||
//!
|
||||
//! Follow-up tasks will add:
|
||||
//! * LLM extraction via kei-cortex
|
||||
//! * Memory persistence via kei-memory-sqlite
|
||||
//! * Telegram webhook driver (kei-notify-telegram)
|
||||
|
||||
pub mod error;
|
||||
pub mod state;
|
||||
pub mod transition;
|
||||
|
||||
pub use error::BuddyError;
|
||||
pub use state::OnboardState;
|
||||
pub use transition::TransitionInput;
|
||||
85
_primitives/_rust/kei-buddy/src/state.rs
Normal file
85
_primitives/_rust/kei-buddy/src/state.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//! Onboarding state-machine enum.
|
||||
//!
|
||||
//! Ported from `keisei-marketplace/src/lib/keibuddy/chat-onboard.ts`.
|
||||
//! Each variant corresponds to one `Step` in the TypeScript source.
|
||||
//! Transitions are stubs; real logic arrives in a follow-up task.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::transition::TransitionInput;
|
||||
|
||||
/// 11-state onboarding finite-state machine.
|
||||
///
|
||||
/// Mirrors the TypeScript `Step` union type exactly:
|
||||
/// `intro | ask_name | ask_tone | ask_interests | ask_hobbies |
|
||||
/// topic_specifics | topic_now_later | topic_research |
|
||||
/// topic_sources | ask_schedule | ready`
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OnboardState {
|
||||
/// Initial greeting — bot explains itself.
|
||||
Intro,
|
||||
/// Collecting user's display name.
|
||||
AskName,
|
||||
/// Collecting preferred communication tone.
|
||||
AskTone,
|
||||
/// Collecting list of interests.
|
||||
AskInterests,
|
||||
/// Collecting list of hobbies.
|
||||
AskHobbies,
|
||||
/// Per-topic: "what specifically interests you here?"
|
||||
TopicSpecifics,
|
||||
/// Per-topic: "discuss now or save for later?"
|
||||
TopicNowLater,
|
||||
/// Per-topic: "want ongoing source monitoring?"
|
||||
TopicResearch,
|
||||
/// Per-topic: "here are proposed sources, which to add?"
|
||||
TopicSources,
|
||||
/// Collecting digest schedule (morning/evening hours + timezone).
|
||||
AskSchedule,
|
||||
/// Onboarding complete; regular conversation mode.
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl OnboardState {
|
||||
/// Advance to the next state given user input.
|
||||
///
|
||||
/// **Stub** — returns `self.clone()` until transition logic is ported.
|
||||
/// Real implementation will extract fields via kei-cortex and follow
|
||||
/// the per-topic queue logic from `chat-onboard.ts::handleStep`.
|
||||
pub fn next(&self, _input: &TransitionInput) -> Self {
|
||||
// TODO: port transition logic from chat-onboard.ts::handleStep
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Smoke test: every variant round-trips through JSON serialisation.
|
||||
#[test]
|
||||
fn all_variants_serde_roundtrip() {
|
||||
let variants = [
|
||||
OnboardState::Intro,
|
||||
OnboardState::AskName,
|
||||
OnboardState::AskTone,
|
||||
OnboardState::AskInterests,
|
||||
OnboardState::AskHobbies,
|
||||
OnboardState::TopicSpecifics,
|
||||
OnboardState::TopicNowLater,
|
||||
OnboardState::TopicResearch,
|
||||
OnboardState::TopicSources,
|
||||
OnboardState::AskSchedule,
|
||||
OnboardState::Ready,
|
||||
];
|
||||
for variant in &variants {
|
||||
let json = serde_json::to_string(variant)
|
||||
.unwrap_or_else(|e| panic!("serialize {:?}: {e}", variant));
|
||||
let back: OnboardState = serde_json::from_str(&json)
|
||||
.unwrap_or_else(|e| panic!("deserialize {:?} from {json:?}: {e}", variant));
|
||||
assert_eq!(variant, &back, "round-trip failed for {:?}", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
_primitives/_rust/kei-buddy/src/transition.rs
Normal file
24
_primitives/_rust/kei-buddy/src/transition.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//! Input struct for state-machine transitions.
|
||||
//!
|
||||
//! `TransitionInput` carries the raw user message and any structured
|
||||
//! fields that an LLM extractor has already parsed from it.
|
||||
//! In the scaffold phase the `extracted_fields` value is always `null`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input to `OnboardState::next()`.
|
||||
///
|
||||
/// `user_text` is the verbatim Telegram message body.
|
||||
/// `extracted_fields` will hold the result of an LLM-extract call
|
||||
/// (e.g. `extractName`, `extractTone`, `extractList` from
|
||||
/// `chat-onboard-extract.ts`) once kei-cortex integration is wired.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TransitionInput {
|
||||
/// Raw message text from the user, UTF-8, already trimmed.
|
||||
pub user_text: String,
|
||||
|
||||
/// Structured fields extracted by an LLM call.
|
||||
/// `serde_json::Value::Null` until kei-cortex integration is added.
|
||||
pub extracted_fields: serde_json::Value,
|
||||
}
|
||||
Loading…
Reference in a new issue