diff --git a/_primitives/_rust/Cargo.lock b/_primitives/_rust/Cargo.lock index 897a6ea..d469483 100644 --- a/_primitives/_rust/Cargo.lock +++ b/_primitives/_rust/Cargo.lock @@ -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", ] diff --git a/_primitives/_rust/Cargo.toml b/_primitives/_rust/Cargo.toml index 4cabd3e..d7b6572 100644 --- a/_primitives/_rust/Cargo.toml +++ b/_primitives/_rust/Cargo.toml @@ -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] diff --git a/_primitives/_rust/kei-buddy/Cargo.toml b/_primitives/_rust/kei-buddy/Cargo.toml new file mode 100644 index 0000000..1f77f50 --- /dev/null +++ b/_primitives/_rust/kei-buddy/Cargo.toml @@ -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 "] diff --git a/_primitives/_rust/kei-buddy/README.md b/_primitives/_rust/kei-buddy/README.md new file mode 100644 index 0000000..48b6cde --- /dev/null +++ b/_primitives/_rust/kei-buddy/README.md @@ -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. diff --git a/_primitives/_rust/kei-buddy/src/bin/kei-buddy.rs b/_primitives/_rust/kei-buddy/src/bin/kei-buddy.rs new file mode 100644 index 0000000..7511b45 --- /dev/null +++ b/_primitives/_rust/kei-buddy/src/bin/kei-buddy.rs @@ -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"); + } + } +} diff --git a/_primitives/_rust/kei-buddy/src/error.rs b/_primitives/_rust/kei-buddy/src/error.rs new file mode 100644 index 0000000..72bf828 --- /dev/null +++ b/_primitives/_rust/kei-buddy/src/error.rs @@ -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), +} diff --git a/_primitives/_rust/kei-buddy/src/lib.rs b/_primitives/_rust/kei-buddy/src/lib.rs new file mode 100644 index 0000000..4f07e74 --- /dev/null +++ b/_primitives/_rust/kei-buddy/src/lib.rs @@ -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; diff --git a/_primitives/_rust/kei-buddy/src/state.rs b/_primitives/_rust/kei-buddy/src/state.rs new file mode 100644 index 0000000..0949a99 --- /dev/null +++ b/_primitives/_rust/kei-buddy/src/state.rs @@ -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); + } + } +} diff --git a/_primitives/_rust/kei-buddy/src/transition.rs b/_primitives/_rust/kei-buddy/src/transition.rs new file mode 100644 index 0000000..2f2c540 --- /dev/null +++ b/_primitives/_rust/kei-buddy/src/transition.rs @@ -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, +}