# kokoro-chat

[![PyPI version](https://img.shields.io/pypi/v/kokoro-chat)](https://pypi.org/project/kokoro-chat/)
[![CI](https://github.com/motosan-dev/kokoro-chat/actions/workflows/ci.yml/badge.svg)](https://github.com/motosan-dev/kokoro-chat/actions/workflows/ci.yml)
[![Python](https://img.shields.io/pypi/pyversions/kokoro-chat)](https://pypi.org/project/kokoro-chat/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

Character chat engine core — extracted from [roleplay-chat](https://github.com/daiwanwei/roleplay-chat).

Protocol-based storage interfaces + dependency-injectable ChatEngine. No database or web framework dependencies.

## Features

- **ChatEngine** — full LLM conversation loop with tool calling (attitude, memory, memo, search)
- **Protocol-based storage** — 9 `@runtime_checkable` Protocol interfaces; inject any DB backend
- **Character system** — YAML V2 character cards with lorebook, attitude levels, few-shot examples
- **Attitude system** — affection/trust tracking (0-100) with time decay, event-triggered modifiers, 5 relationship levels
- **Memory** — semantic search + rolling summary + structured memo with room/private visibility
- **Lorebook** — keyword-triggered context injection with token budget and priority sorting
- **Prompt assembly** — static (slim) and full paths with progressive degradation on budget overflow
- **Streaming** — async generator yielding `text_delta` / `message_complete` events
- **Multi-provider LLM** — Anthropic, OpenAI, MiniMax via [motosan-ai](https://github.com/motosan-dev/motosan-ai)

## Architecture

```
kokoro_chat/
├── character/      ← Character model + YAML V2 loader
├── lorebook/       ← Lorebook engine (keyword trigger → context injection)
├── attitude/       ← AttitudeState + 5 behavior levels (stranger → married)
├── memory/         ← Memo stale-filtering logic
├── prompt/
│   ├── assembler.py  ← System prompt assembly (static + full paths)
│   ├── sections.py   ← Shared section builders (character, traits, speech...)
│   ├── budget.py     ← Token budget constants + estimator
│   └── formatter.py  ← Conversation history formatting
├── providers/      ← Context providers (attitude, persona, recent_chat)
├── tools/          ← LLM tools (update_attitude, save_memory, upsert_memo, search_memories)
├── storage/        ← 9 Protocol interfaces (DB-agnostic)
├── engine/
│   ├── chat.py              ← ChatEngine (orchestration)
│   ├── llm.py               ← LLM client factory + bridges
│   ├── attitude_manager.py  ← AttitudeManager
│   ├── message_manager.py   ← MessageManager
│   ├── summarizer.py        ← Summarizer
│   └── background.py        ← Background task prompts + parsers
└── config.py       ← ChatConfig dataclass
```

## Installation

```bash
pip install kokoro-chat
```

Or from source:
```bash
pip install -e ".[dev]"
```

## Quick Start

```python
from kokoro_chat import Character, ChatConfig, ChatEngine

# 1. Build a Character (from DB model or manually)
character = Character(
    id="xing_lang",
    name="星浪",
    description="一個活潑開朗的女孩，喜歡音樂和冒險。",
    personality_summary="外向、好奇心強、有點衝動",
    likes=["音樂", "冒險", "甜食"],
    default_mood="happy",
)

# 2. Configure
config = ChatConfig(
    chat_provider="anthropic",
    chat_model="claude-sonnet-4-5-20250929",
    anthropic_api_key="sk-ant-...",
)

# 3. Inject your storage implementations (must implement the Protocols)
engine = ChatEngine(
    config=config,
    attitude_store=your_attitude_store,         # AttitudeStore
    cache=your_cache_store,                     # CacheStore
    chat_history=your_history_store,            # ChatHistoryStore
    chat_writer=your_message_writer,            # ChatMessageWriter
    memory_store=your_memory_store,             # MemoryVectorStore
    memo_store=your_memo_store,                 # MemoStore
    rolling_summary_store=your_summary_store,   # RollingSummaryStore
    event_store=your_event_store,               # EventStore
)

# 4. Stream a conversation
async for chunk in engine.chat_stream(
    user_message="你好啊",
    character=character,
    user_id="user-123",
    chat_mode="short",
):
    if chunk["type"] == "text_delta":
        print(chunk["text"], end="", flush=True)
    elif chunk["type"] == "message_complete":
        print()  # done
```

## Storage Protocols

All storage interfaces are `typing.Protocol` with `@runtime_checkable`.
Implement any concrete backend (PostgreSQL, SQLite, in-memory dict) — as long as it matches the protocol signature.

```python
from kokoro_chat.storage.protocol import (
    AttitudeStore,        # get / get_or_create / save — affection, trust, mood
    ChatHistoryStore,     # get_recent — recent messages
    MemoryVectorStore,    # search / add — semantic memory with visibility
    MemoStore,            # get / upsert — key-value user info
    RollingSummaryStore,  # get_or_create / update — compressed history
    EventStore,           # get_by_character — character/user events
    UsageLogStore,        # log — token count + cost tracking
    ChatMessageWriter,    # add_message — persist new messages
    CacheStore,           # get/set attitude, messages, rolling summary
)
```

See [`kokoro_chat/storage/protocol.py`](kokoro_chat/storage/protocol.py) for full protocol definitions and value objects (`ChatMessage`, `AttitudeRecord`, `MemorySearchResult`, `RollingSummaryRecord`, `EventRecord`).

## Attitude System

Characters have dynamic attitude toward each user:

```
affection: 0-100    (好感度)
trust:     0-100    (信任度)
mood:      string   (當前心情)
```

**5 relationship levels** based on affection score:

| Level | Range | Behavior |
|-------|-------|----------|
| stranger | 0-20 | Polite, distant, formal |
| friend | 21-40 | Casual, joking, caring |
| crush | 41-60 | Shy, blushing, finding excuses to get closer |
| lover | 61-80 | Intimate, clingy, using pet names |
| married | 81-100 | Maximum intimacy, deep trust |

- **Time decay** — affection/trust decrease after 3+ days of inactivity (floor: affection 30, trust 20)
- **Event-triggered modifiers** — temporary attitude shifts with turn-based countdown
- **Custom overrides** — characters can define their own behavior text per level

## Room Memory

Characters in the same room share non-private memories:

```python
# save_memory tool — LLM decides visibility
save_memory(text="User is learning guitar", visibility="room")    # shared in room
save_memory(text="User has a crush on NPC", visibility="private") # only this character

# search_memories — returns own + same-room memories
results = await memory_store.search(
    query="music",
    character_id="xing_lang",
    user_id="user-123",
    room_id="cafe",  # includes other characters' room-visible memories
)
```

## Character Card V2

Supports standard Character Card V2 format with Kokoro extensions:

| Extension | Purpose |
|-----------|---------|
| `speech_patterns_v2` | Mood-based speech patterns (`{mood: pattern}`) |
| `affection_overrides` | Custom behavior text per relationship level |
| `post_history_by_level` | Level-specific post-history instructions |
| `anti_patterns` | Prevent character drift (`{bad, correct, why}`) |

Load from YAML V2:
```python
from kokoro_chat.character.loader import map_v2_yaml_to_db

db_kwargs = map_v2_yaml_to_db(yaml_data)  # → dict for DB model creation
```

Load from DB model:
```python
character = Character.from_db_model(db_char)  # duck-typed, reads attributes
```

## Dependencies

| Package | Purpose |
|---------|---------|
| [`motosan-ai`](https://github.com/motosan-dev/motosan-ai) | LLM client (Anthropic, OpenAI, MiniMax) |
| [`motosan-chat`](https://github.com/motosan-dev/motosan-chat) | AgentLoop, ContextProvider, Tool framework |
| `pyyaml` | YAML V2 character card parsing |

## License

MIT
