All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 35s
600 lines
14 KiB
Markdown
600 lines
14 KiB
Markdown
# Multi-Platform Expansion
|
|
## Adding Web & CLI Interfaces
|
|
|
|
This document extends the Loyal Companion architecture beyond Discord.
|
|
The goal is to support **Web** and **CLI** interaction channels while preserving:
|
|
|
|
- one shared Living AI core
|
|
- consistent personality & memory
|
|
- attachment-safe A+C hybrid behavior
|
|
- clear separation between platform and cognition
|
|
|
|
---
|
|
|
|
## 1. Core Principle
|
|
|
|
**Platforms are adapters, not identities.**
|
|
|
|
Discord, Web, and CLI are merely different rooms
|
|
through which the same companion is accessed.
|
|
|
|
The companion:
|
|
- remains one continuous entity
|
|
- may adjust tone by platform
|
|
- never fragments into separate personalities
|
|
|
|
---
|
|
|
|
## 2. New Architectural Layer: Conversation Gateway
|
|
|
|
### Purpose
|
|
|
|
Introduce a single entry point for **all conversations**, regardless of platform.
|
|
|
|
```text
|
|
[ Discord Adapter ] ─┐
|
|
[ Web Adapter ] ─────┼──▶ ConversationGateway ─▶ Living AI Core
|
|
[ CLI Adapter ] ─────┘
|
|
```
|
|
|
|
### Responsibilities
|
|
|
|
The Conversation Gateway:
|
|
|
|
* normalizes incoming messages
|
|
* assigns platform metadata
|
|
* invokes the existing AI + Living AI pipeline
|
|
* returns responses in a platform-agnostic format
|
|
|
|
### Required Data Structure
|
|
|
|
```python
|
|
@dataclass
|
|
class ConversationRequest:
|
|
user_id: str # Platform-specific user ID
|
|
platform: Platform # Enum: DISCORD | WEB | CLI
|
|
session_id: str # Conversation/channel identifier
|
|
message: str # User's message content
|
|
context: ConversationContext # Additional metadata
|
|
|
|
@dataclass
|
|
class ConversationContext:
|
|
is_public: bool # Public channel vs private
|
|
intimacy_level: IntimacyLevel # LOW | MEDIUM | HIGH
|
|
platform_metadata: dict # Platform-specific extras
|
|
guild_id: str | None = None # Discord guild (if applicable)
|
|
channel_id: str | None = None # Discord/Web channel
|
|
```
|
|
|
|
### Current Implementation Location
|
|
|
|
**Existing message handling:** `src/loyal_companion/cogs/ai_chat.py`
|
|
|
|
The current `_generate_response_with_db()` method contains all the logic
|
|
that will be extracted into the Conversation Gateway:
|
|
|
|
- History loading
|
|
- Living AI context gathering (mood, relationship, style, opinions)
|
|
- System prompt enhancement
|
|
- AI invocation
|
|
- Post-response Living AI updates
|
|
|
|
**Goal:** Extract this into a platform-agnostic service layer.
|
|
|
|
---
|
|
|
|
## 3. Platform Metadata & Intimacy Levels
|
|
|
|
### Intimacy Levels (Important for A+C Safety)
|
|
|
|
Intimacy level influences:
|
|
|
|
* language warmth
|
|
* depth of reflection
|
|
* frequency of proactive behavior
|
|
* memory surfacing
|
|
|
|
| Platform | Default Intimacy | Notes |
|
|
| --------------- | ---------------- | ------------------------ |
|
|
| Discord (guild) | LOW | Social, public, shared |
|
|
| Discord (DM) | MEDIUM | Private but casual |
|
|
| Web | HIGH | Intentional, reflective |
|
|
| CLI | HIGH | Quiet, personal, focused |
|
|
|
|
### Intimacy Level Behavior Modifiers
|
|
|
|
**LOW (Discord Guild):**
|
|
- Less emotional intensity
|
|
- More grounding language
|
|
- Minimal proactive behavior
|
|
- Surface-level memory recall only
|
|
- Shorter responses
|
|
- Public-safe topics only
|
|
|
|
**MEDIUM (Discord DM):**
|
|
- Balanced warmth
|
|
- Casual tone
|
|
- Moderate proactive behavior
|
|
- Personal memory recall allowed
|
|
- Normal response length
|
|
|
|
**HIGH (Web/CLI):**
|
|
- Deeper reflection permitted
|
|
- Silence tolerance (not rushing to respond)
|
|
- Proactive check-ins allowed
|
|
- Deep memory surfacing
|
|
- Longer, more thoughtful responses
|
|
- Emotional naming encouraged
|
|
|
|
---
|
|
|
|
## 4. Web Platform
|
|
|
|
### Goal
|
|
|
|
Provide a **private, 1-on-1 chat interface**
|
|
for deeper, quieter conversations than Discord allows.
|
|
|
|
### Architecture
|
|
|
|
* Backend: FastAPI (async Python web framework)
|
|
* Transport: HTTP REST + optional WebSocket
|
|
* Auth: Magic link / JWT token / local account
|
|
* No guilds, no other users visible
|
|
* Session persistence via database
|
|
|
|
### Backend Components
|
|
|
|
#### New API Module Structure
|
|
|
|
```
|
|
src/loyal_companion/web/
|
|
├── __init__.py
|
|
├── app.py # FastAPI application factory
|
|
├── dependencies.py # Dependency injection (DB sessions, auth)
|
|
├── middleware.py # CORS, rate limiting, error handling
|
|
├── routes/
|
|
│ ├── __init__.py
|
|
│ ├── chat.py # POST /chat, WebSocket /ws
|
|
│ ├── session.py # GET/POST /sessions
|
|
│ ├── history.py # GET /sessions/{id}/history
|
|
│ └── auth.py # POST /auth/login, /auth/verify
|
|
├── models.py # Pydantic request/response models
|
|
└── adapter.py # Web → ConversationGateway adapter
|
|
```
|
|
|
|
#### Chat Flow
|
|
|
|
1. User sends message via web UI
|
|
2. Web adapter creates `ConversationRequest`
|
|
3. `ConversationGateway.process_message()` invoked
|
|
4. Living AI generates response
|
|
5. Response returned as JSON
|
|
|
|
#### Example API Request
|
|
|
|
**POST /chat**
|
|
|
|
```json
|
|
{
|
|
"session_id": "abc123",
|
|
"message": "I'm having a hard evening."
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"response": "That sounds heavy. Want to sit with it for a bit?",
|
|
"mood": {
|
|
"label": "calm",
|
|
"valence": 0.2,
|
|
"arousal": -0.3
|
|
},
|
|
"relationship_level": "close_friend"
|
|
}
|
|
```
|
|
|
|
#### Authentication
|
|
|
|
**Phase 1:** Simple token-based auth
|
|
- User registers with email
|
|
- Server sends magic link
|
|
- Token stored in HTTP-only cookie
|
|
|
|
**Phase 2:** Optional OAuth integration
|
|
|
|
### UI Considerations (Out of Scope for Core)
|
|
|
|
The web UI should:
|
|
- Use minimal chat bubbles (user left, bot right)
|
|
- Avoid typing indicators from others (no other users)
|
|
- Optional timestamps
|
|
- No engagement metrics (likes, seen, read receipts)
|
|
- No "X is typing..." unless real-time WebSocket
|
|
- Dark mode default
|
|
|
|
**Recommended stack:**
|
|
- Frontend: SvelteKit / React / Vue
|
|
- Styling: TailwindCSS
|
|
- Real-time: WebSocket for live chat
|
|
|
|
---
|
|
|
|
## 5. CLI Platform
|
|
|
|
### Goal
|
|
|
|
A **local, quiet, terminal-based interface**
|
|
for people who want presence without noise.
|
|
|
|
### Invocation
|
|
|
|
```bash
|
|
loyal-companion talk
|
|
```
|
|
|
|
or (short alias):
|
|
|
|
```bash
|
|
lc talk
|
|
```
|
|
|
|
### CLI Behavior
|
|
|
|
* Single ongoing session by default
|
|
* Optional named sessions (`lc talk --session work`)
|
|
* No emojis unless explicitly enabled
|
|
* Text-first, reflective tone
|
|
* Minimal output (no spinners, no progress bars)
|
|
* Supports piping and scripting
|
|
|
|
### Architecture
|
|
|
|
CLI is a **thin client**, not the AI itself.
|
|
It communicates with the web backend via HTTP.
|
|
|
|
```
|
|
cli/
|
|
├── __init__.py
|
|
├── main.py # Typer CLI app entry point
|
|
├── client.py # HTTP client for web backend
|
|
├── session.py # Local session persistence (.lc/sessions.json)
|
|
├── config.py # CLI-specific config (~/.lc/config.toml)
|
|
└── formatters.py # Response formatting for terminal
|
|
```
|
|
|
|
### Session Management
|
|
|
|
Sessions are stored locally:
|
|
|
|
```
|
|
~/.lc/
|
|
├── config.toml # API endpoint, auth token, preferences
|
|
└── sessions.json # Session ID → metadata mapping
|
|
```
|
|
|
|
**Session lifecycle:**
|
|
|
|
1. First `lc talk` → creates default session, stores ID locally
|
|
2. Subsequent calls → reuses session ID
|
|
3. `lc talk --new` → starts fresh session
|
|
4. `lc talk --session work` → named session
|
|
|
|
### Example Interaction
|
|
|
|
```text
|
|
$ lc talk
|
|
Bartender is here.
|
|
|
|
You: I miss someone tonight.
|
|
|
|
Bartender: That kind of missing doesn't ask to be solved.
|
|
Do you want to talk about what it feels like in your body,
|
|
or just let it be here for a moment?
|
|
|
|
You: Just let it be.
|
|
|
|
Bartender: Alright. I'm here.
|
|
|
|
You: ^D
|
|
|
|
Session saved.
|
|
```
|
|
|
|
### CLI Commands
|
|
|
|
| Command | Purpose |
|
|
|---------|---------|
|
|
| `lc talk` | Start/resume conversation |
|
|
| `lc talk --session <name>` | Named session |
|
|
| `lc talk --new` | Start fresh session |
|
|
| `lc history` | Show recent exchanges |
|
|
| `lc sessions` | List all sessions |
|
|
| `lc config` | Show/edit configuration |
|
|
| `lc auth` | Authenticate with server |
|
|
|
|
---
|
|
|
|
## 6. Shared Identity & Memory
|
|
|
|
### Relationship Model
|
|
|
|
All platforms share:
|
|
|
|
* the same `User` record (keyed by platform-specific ID)
|
|
* the same `UserRelationship`
|
|
* the same long-term memory (`UserFact`)
|
|
* the same mood history
|
|
|
|
But:
|
|
|
|
* **contextual behavior varies** by intimacy level
|
|
* **expression adapts** to platform norms
|
|
* **intensity is capped** per platform
|
|
|
|
### Cross-Platform User Identity
|
|
|
|
**Challenge:** A user on Discord and CLI are the same person.
|
|
|
|
**Solution:**
|
|
|
|
1. Each platform creates a `User` record with platform-specific ID
|
|
2. Introduce `PlatformIdentity` linking model
|
|
|
|
```python
|
|
class PlatformIdentity(Base):
|
|
__tablename__ = "platform_identities"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
|
platform: Mapped[Platform] = mapped_column(Enum(Platform))
|
|
platform_user_id: Mapped[str] = mapped_column(String, unique=True)
|
|
|
|
user: Mapped["User"] = relationship(back_populates="identities")
|
|
```
|
|
|
|
**Later enhancement:** Account linking UI for users to connect platforms.
|
|
|
|
### Example Cross-Platform Memory Surfacing
|
|
|
|
A memory learned via CLI:
|
|
|
|
> "User tends to feel lonelier at night."
|
|
|
|
May surface on Web (HIGH intimacy):
|
|
|
|
> "You've mentioned nights can feel heavier for you."
|
|
|
|
But **not** in Discord guild chat (LOW intimacy).
|
|
|
|
---
|
|
|
|
## 7. Safety Rules per Platform
|
|
|
|
### Web & CLI (HIGH Intimacy)
|
|
|
|
**Allowed:**
|
|
- Deeper reflection
|
|
- Naming emotions ("That sounds like grief")
|
|
- Silence tolerance (not rushing responses)
|
|
- Proactive follow-ups ("You mentioned feeling stuck yesterday—how's that today?")
|
|
|
|
**Still forbidden:**
|
|
- Exclusivity claims ("I'm the only one who truly gets you")
|
|
- Dependency reinforcement ("You need me")
|
|
- Discouraging external connection ("They don't understand like I do")
|
|
- Romantic/sexual framing
|
|
- Crisis intervention (always defer to professionals)
|
|
|
|
### Discord DM (MEDIUM Intimacy)
|
|
|
|
**Allowed:**
|
|
- Personal memory references
|
|
- Emotional validation
|
|
- Moderate warmth
|
|
|
|
**Constraints:**
|
|
- Less proactive behavior than Web/CLI
|
|
- Lighter tone
|
|
- Shorter responses
|
|
|
|
### Discord Guild (LOW Intimacy)
|
|
|
|
**Allowed:**
|
|
- Light banter
|
|
- Topic-based conversation
|
|
- Public-safe responses
|
|
|
|
**Additional constraints:**
|
|
- No personal memory surfacing
|
|
- No emotional intensity
|
|
- No proactive check-ins
|
|
- Grounding language only
|
|
- Short responses
|
|
|
|
---
|
|
|
|
## 8. Configuration Additions
|
|
|
|
### New Settings (config.py)
|
|
|
|
```python
|
|
# Platform Toggles
|
|
web_enabled: bool = True
|
|
cli_enabled: bool = True
|
|
|
|
# Web Server
|
|
web_host: str = "127.0.0.1"
|
|
web_port: int = 8080
|
|
web_cors_origins: list[str] = ["http://localhost:3000"]
|
|
web_auth_secret: str = Field(..., env="WEB_AUTH_SECRET")
|
|
|
|
# CLI
|
|
cli_default_intimacy: IntimacyLevel = IntimacyLevel.HIGH
|
|
cli_allow_emoji: bool = False
|
|
|
|
# Intimacy Scaling
|
|
intimacy_enabled: bool = True
|
|
intimacy_discord_guild: IntimacyLevel = IntimacyLevel.LOW
|
|
intimacy_discord_dm: IntimacyLevel = IntimacyLevel.MEDIUM
|
|
intimacy_web: IntimacyLevel = IntimacyLevel.HIGH
|
|
intimacy_cli: IntimacyLevel = IntimacyLevel.HIGH
|
|
```
|
|
|
|
### Environment Variables
|
|
|
|
```env
|
|
# Platform Toggles
|
|
WEB_ENABLED=true
|
|
CLI_ENABLED=true
|
|
|
|
# Web
|
|
WEB_HOST=127.0.0.1
|
|
WEB_PORT=8080
|
|
WEB_AUTH_SECRET=<random-secret>
|
|
|
|
# CLI
|
|
CLI_DEFAULT_INTIMACY=high
|
|
CLI_ALLOW_EMOJI=false
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Implementation Order
|
|
|
|
### Phase 1: Extract Conversation Gateway ✅
|
|
|
|
**Goal:** Create platform-agnostic conversation processor
|
|
|
|
**Files to create:**
|
|
- `src/loyal_companion/services/conversation_gateway.py`
|
|
- `src/loyal_companion/models/platform.py` (enums, request/response types)
|
|
|
|
**Tasks:**
|
|
1. Define `Platform` enum (DISCORD, WEB, CLI)
|
|
2. Define `IntimacyLevel` enum (LOW, MEDIUM, HIGH)
|
|
3. Define `ConversationRequest` and `ConversationResponse` dataclasses
|
|
4. Extract logic from `cogs/ai_chat.py` into gateway
|
|
5. Add intimacy-level-based prompt modifiers
|
|
|
|
### Phase 2: Refactor Discord to Use Gateway ✅
|
|
|
|
**Files to modify:**
|
|
- `src/loyal_companion/cogs/ai_chat.py`
|
|
|
|
**Tasks:**
|
|
1. Import `ConversationGateway`
|
|
2. Replace `_generate_response_with_db()` with gateway call
|
|
3. Build `ConversationRequest` from Discord message
|
|
4. Format `ConversationResponse` for Discord output
|
|
5. Test that Discord functionality unchanged
|
|
|
|
### Phase 3: Add Web Platform 🌐
|
|
|
|
**Files to create:**
|
|
- `src/loyal_companion/web/` (entire module)
|
|
- `src/loyal_companion/web/app.py`
|
|
- `src/loyal_companion/web/routes/chat.py`
|
|
|
|
**Tasks:**
|
|
1. Set up FastAPI application
|
|
2. Add authentication middleware
|
|
3. Create `/chat` endpoint
|
|
4. Create WebSocket endpoint (optional)
|
|
5. Add session management
|
|
6. Test with Postman/curl
|
|
|
|
### Phase 4: Add CLI Client 💻
|
|
|
|
**Files to create:**
|
|
- `cli/` (new top-level directory)
|
|
- `cli/main.py`
|
|
- `cli/client.py`
|
|
|
|
**Tasks:**
|
|
1. Create Typer CLI app
|
|
2. Add `talk` command
|
|
3. Add local session persistence
|
|
4. Add authentication flow
|
|
5. Test end-to-end with web backend
|
|
|
|
### Phase 5: Intimacy Scaling 🔒
|
|
|
|
**Files to create:**
|
|
- `src/loyal_companion/services/intimacy_service.py`
|
|
|
|
**Tasks:**
|
|
1. Define intimacy level behavior modifiers
|
|
2. Modify system prompt based on intimacy
|
|
3. Filter proactive behavior by intimacy
|
|
4. Add memory surfacing rules
|
|
5. Add safety constraint enforcement
|
|
|
|
### Phase 6: Safety Regression Tests 🛡️
|
|
|
|
**Files to create:**
|
|
- `tests/test_safety_constraints.py`
|
|
- `tests/test_intimacy_boundaries.py`
|
|
|
|
**Tasks:**
|
|
1. Test no exclusivity claims at any intimacy level
|
|
2. Test no dependency reinforcement
|
|
3. Test intimacy boundaries respected
|
|
4. Test proactive behavior filtered by platform
|
|
5. Test memory surfacing respects intimacy
|
|
|
|
---
|
|
|
|
## 10. Non-Goals
|
|
|
|
This expansion does NOT aim to:
|
|
|
|
* Duplicate Discord features (guilds, threads, reactions)
|
|
* Introduce social feeds or timelines
|
|
* Add notifications or engagement streaks
|
|
* Increase engagement artificially
|
|
* Create a "social network"
|
|
* Add gamification mechanics
|
|
|
|
The goal is **availability**, not addiction.
|
|
|
|
---
|
|
|
|
## 11. Outcome
|
|
|
|
When complete:
|
|
|
|
* **Discord is the social bar** — casual, public, low-commitment
|
|
* **Web is the quiet back room** — intentional, private, reflective
|
|
* **CLI is the empty table at closing time** — minimal, focused, silent presence
|
|
|
|
Same bartender.
|
|
Different stools.
|
|
No one is trapped.
|
|
|
|
---
|
|
|
|
## 12. Current Implementation Status
|
|
|
|
### Completed
|
|
- ❌ None yet
|
|
|
|
### In Progress
|
|
- 🔄 Documentation update
|
|
- 🔄 Phase 1: Conversation Gateway extraction
|
|
|
|
### Planned
|
|
- ⏳ Phase 2: Discord refactor
|
|
- ⏳ Phase 3: Web platform
|
|
- ⏳ Phase 4: CLI client
|
|
- ⏳ Phase 5: Intimacy scaling
|
|
- ⏳ Phase 6: Safety tests
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
See [Implementation Guide](implementation/conversation-gateway.md) for detailed Phase 1 instructions.
|