Commit graph

6 commits

Author SHA1 Message Date
Parfii-bot
450156a476 feat(kei-buddy fleet): 5 atomics — google/apple contacts + classifier + tick + slash-commands
Parallel agent batch. All five tasks delivered functional + tested.
NOT deployed — user is in live conversation with the bot.

## Crates added (2 new)

### kei-contacts-google (466 LOC, 5 tests)
Thin Google People API client. Takes pre-acquired access_token from
kei-auth-google's OAuth flow; calls /v1/people/me/connections?personFields=...,
parses 200-entry first page (TODO: pagination via nextPageToken), maps
to kei_social_store::Person. Errors: Http / Auth(401) / Parse.

### kei-contacts-apple (593 LOC, 7 tests + 1 doc-test)
CardDAV client for iCloud Contacts using Basic Auth (Apple ID +
app-specific password). Sends REPORT with addressbook-query XML body,
parses multistatus → embedded vCards → AppleContact. Tiny vCard
parser (~150 LOC) handles FN/N/EMAIL/TEL/ORG/NOTE/UID, single-line
only (no line-folding for MVP). Discovery (PROPFIND .well-known/carddav
→ principal → addressbook-home-set) deferred — user supplies
addressbook URL via with_addressbook_url().

Both crates registered in workspace members.

## kei-buddy crate additions

### src/topic_classify.rs (116 LOC, 3 tests)
Free fn classify_and_store_topic(extractor, topics, chat_id, text)
called from process_text when state == OnboardState::Ready. Builds
classifier prompt → LLM → parses {slug, title} → validates slug
shape (kebab-case, ascii) → Topics::add_topic + add_digest. All
failure paths log + return; conversation never blocks.

### src/tick.rs (188 LOC, 3 integration tests) + src/bin/kei-buddy-tick.rs (67 LOC)
Second binary. Oneshot CLI for systemd timer: walks all known
chat_ids in BuddyStore → lists topics → searches recent chat
messages per topic (configurable window/limit) → LLM digest →
Topics::add_digest. Outputs JSON TickReport to stdout. Env-driven
config. NoOpExtractor fallback when no LLM creds (graceful degradation).

### src/commands.rs (146 LOC) + src/command_exec.rs (111 LOC, 7 tests)
Slash-commands intercepted BEFORE handle_step in process_text:
  /whois <name>   contacts.search_contacts + common_connections for hits
  /find <q>       chat_log.search scoped to chat_id
  /topics         topics.list_topics
  /contacts       contacts.search_contacts("", 10)
  /help           static usage text (Russian)
If command parsed, response built from stores, sent, logged to
chat_log — FSM skipped for that turn.

### src/serve_runner.rs (69 LOC) — refactor
run_serve + start_listener + init_tracing extracted out of serve.rs
to bring serve.rs back to 189 LOC (was 248 after previous wave).

### Wiring
BuddyContext gains `contacts: Arc<Contacts>` and `topics: Arc<Topics>`.
ServeConfig gains contacts_db_path + topics_db_path. Binary reads
KEI_BUDDY_CONTACTS_DB_PATH + KEI_BUDDY_TOPICS_DB_PATH env (defaults
./kei-buddy-contacts.db, ./kei-buddy-topics.db). cmd_migrate applies
schema for all three side-stores (chat_log + contacts + topics).

## Verify-before-commit (RULE 0.13 §)
  * cargo check -p kei-buddy (default + extractor-openai): PASS
  * cargo test -p kei-buddy --lib: 41 passed / 0 failed (was 31)
  * cargo test -p kei-buddy --tests: 3 passed (tick integration)
  * cargo build -p kei-buddy --features extractor-openai: PASS
    (builds both kei-buddy + kei-buddy-tick binaries)
  * cargo check -p kei-contacts-google: PASS (5 tests)
  * cargo check -p kei-contacts-apple: PASS (7 + 1 doc)
  * cargo check --workspace: PASS

## STATUS-TRUTH from all 5 agents: shipped=functional, behaviour-verified=yes

## Follow-up (deferred, non-blocking)
  * Google People API pagination (nextPageToken loop) — first 200 only
  * CardDAV auto-discovery (PROPFIND .well-known/carddav)
  * vCard line-folding (RFC 6350 §3.2)
  * Wire kei-contacts-google + kei-contacts-apple → Contacts.add_contact
    sync command (no glue yet)
  * systemd timer file for kei-buddy-tick (not shipped here — config only)
2026-05-12 16:33:58 +08:00
Parfii-bot
fa7a20d572 feat(kei-buddy): wire kei-social-store + kei-sage — contacts + topics + FTS5
Two parallel atoms in one commit. Both reuse existing KeiSeiKit
primitives (zero new crates) per RULE feedback_inventory_before_decompose.

## src/contacts.rs (200 LOC, +4 tests)

Adapter over kei-social-store. Address book + interaction log + relationship
graph for shared connections.

API:
  * Contacts::from_path / from_memory
  * add_contact / get_contact / search_contacts
  * log_meet(person_id, target_id, channel, note) / interactions_for
  * relationship_graph — returns Vec<Pair>, the kei-social-store output
  * common_connections(a, b) — post-filters relationship_graph to find
    target_ids that appear in pairs with BOTH a and b. This is the
    "у нас с Денисом общий друг X" feature.

Pattern: Arc<Mutex<kei_social_store::Store>> + tokio::spawn_blocking,
mirroring chat_log.rs. Errors map to BuddyError::Memory.

Tests: add_and_get_contact_roundtrip / search_contacts_finds_by_name /
log_meet_and_list_interactions / common_connections_finds_shared_target.

## src/topics.rs (200 LOC, +4 tests)

Adapter over kei-sage. Topics + digest notes + FTS5 search. Each topic
is a sage Unit{unit_type="buddy_topic", category="kei-buddy",
source_path="kei-buddy/chat-{chat_id}/topic/{slug}"}. Digests are
Unit{unit_type="buddy_digest"} linked via add_edge(topic→digest,
edge_type="digest_for").

API:
  * Topics::from_path / from_memory
  * add_topic(chat_id, slug, title, content) — idempotent via path lookup
  * add_digest(chat_id, topic_slug, timestamp, content) — creates Unit +
    edge
  * search(query, limit) — fts_search over all kei-buddy units
  * digests_for(chat_id, topic_slug) — follows outgoing edges
  * list_topics(chat_id) — raw SELECT scoped by source_path LIKE prefix

Tests: add_topic_then_search_finds_it / add_topic_is_idempotent /
add_digest_creates_edge_and_dest / list_topics_scopes_per_chat.

## Dependencies added

kei-social-store + kei-sage as local path deps. Both already in workspace,
no new external crates.

## Verify-before-commit

  * cargo check -p kei-buddy: PASS
  * cargo test -p kei-buddy --lib: 31/0 (was 23, +4 contacts +4 topics)

Net change: 4 files touched, ~400 LOC added across the two adapters.

NOT deployed. User still in active bot conversation.
2026-05-12 16:05:32 +08:00
Parfii-bot
fb7c1bf859 feat(kei-buddy): wire kei-chat-store — log every user/bot message with FTS5
After-Ready conversation was going to /dev/null. With this change every
inbound Telegram text + every bot response is persisted to a SQLite +
FTS5 archive via the existing kei-chat-store primitive (no new crate).

Each Telegram chat_id maps 1:1 to a kei-chat-store session
(project="kei-buddy", title="tg-<chat_id>", model="telegram"). Cache
prevents per-message session lookups.

New file:
  * src/chat_log.rs (198 LOC) — ChatLog adapter wrapping
    kei_chat_store::Store + a chat_id→session_id Mutex cache.
    API: from_path / from_memory / ensure_session / log_user /
    log_bot / search(query, chat_id?, limit). Errors map to
    BuddyError::Memory and never propagate from on_event — chat-log
    failure is logged but does not block the conversation.

Modified:
  * Cargo.toml — kei-chat-store path dep added.
  * src/lib.rs — pub mod chat_log + re-export ChatLog.
  * src/serve.rs — BuddyContext gains Arc<ChatLog>;
    process_text calls log_user before handle_step + log_bot after
    send_message; ServeConfig gains chat_log_db_path.
  * src/bin/kei-buddy.rs — KEI_BUDDY_CHAT_LOG_PATH env
    (default ./kei-buddy-chat.db); migrate subcommand applies the
    chat-store schema alongside buddy_state schema.

Tests (3 new in src/chat_log.rs, all pass):
  * log_user_creates_session_and_message
  * log_bot_uses_same_session_as_log_user
  * different_chats_get_different_sessions

Verify-before-commit:
  * cargo check -p kei-buddy (default): PASS
  * cargo check -p kei-buddy --features extractor-openai: PASS
  * cargo test -p kei-buddy --lib: 23 passed / 0 failed
    (was 20 before this commit; 3 new ChatLog tests)

NOT deployed — user is in active conversation with the live bot.
Will roll forward when user signals readiness.
2026-05-12 15:51:24 +08:00
Parfii-bot
0045b6ac77 feat(kei-buddy): wire OpenAiExtractor + chat_id whitelist + env-configurable LLM
Two additions on top of the MVP serve binary:

1. Whitelist by chat_id (KEI_BUDDY_ALLOWED_CHAT_IDS env, CSV).
   * BuddyContext gains Arc<Option<Vec<i64>>> allowed_chat_ids
   * chat_allowed() check fires before process_text
   * Non-whitelisted chats: warn-log + ignore (no response sent)
   * None or empty list = accept all (back-compat with prior behaviour)

2. Real LLM wiring (KEI_BUDDY_LLM_PROXY / _LLM_KEY / _LLM_MODEL).
   * When extractor-openai feature compiled in AND both proxy+key set,
     run_serve instantiates OpenAiExtractor instead of MockExtractor
   * Defaults: proxy=https://api.openai.com, key=OPENAI_API_KEY env,
     model=gpt-4o-mini
   * Fallback: warns + MockExtractor (state machine still walks, but
     LLM-extracted fields are empty)
   * extractor::OpenAiExtractor gains new_with_model(proxy, key, model);
     model is now per-instance instead of compile-time DEFAULT_MODEL

3. start_listener extracted as helper — keeps run_serve readable across
   the two feature-gated branches.

Verify-before-commit:
  * cargo check -p kei-buddy (default): PASS
  * cargo check -p kei-buddy --features extractor-openai: PASS
  * cargo test -p kei-buddy --lib: 20/0 unchanged
2026-05-12 14:49:43 +08:00
Parfii-bot
7414d14cc7 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
Parfii-bot
a2d4bc9206 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)
2026-05-12 13:14:00 +08:00