dev #8

Merged
Latte merged 9 commits from dev into main 2026-02-01 15:01:16 +00:00
8 changed files with 2090 additions and 0 deletions
Showing only changes of commit dde2649876 - Show all commits

View File

@@ -422,8 +422,36 @@ else:
---
---
## Future Architecture: Multi-Platform Support
The current architecture is Discord-centric. A **multi-platform expansion** is planned
to support Web and CLI interfaces while maintaining one shared Living AI core.
See [Multi-Platform Expansion](multi-platform-expansion.md) for the complete design.
**Planned architecture:**
```
[ Discord Adapter ] ─┐
[ Web Adapter ] ─────┼──▶ ConversationGateway ─▶ Living AI Core
[ CLI Adapter ] ─────┘
```
**Key changes:**
- Extract conversation logic into platform-agnostic `ConversationGateway`
- Add `Platform` enum (DISCORD, WEB, CLI)
- Add `IntimacyLevel` system for behavior modulation
- Refactor `ai_chat.py` to use gateway
- Add FastAPI web backend
- Add Typer CLI client
---
## Next Steps
- [Multi-Platform Expansion](multi-platform-expansion.md) - Web & CLI platform design
- [Living AI System](living-ai/README.md) - Deep dive into the personality system
- [Services Reference](services/README.md) - Detailed API documentation
- [Database Schema](database.md) - Complete schema documentation

View File

@@ -0,0 +1,471 @@
# Conversation Gateway Implementation Guide
## Phase 1: Complete ✅
This document describes the Conversation Gateway implementation completed in Phase 1 of the multi-platform expansion.
---
## What Was Implemented
### 1. Platform Abstraction Models
**File:** `src/loyal_companion/models/platform.py`
Created core types for platform-agnostic conversation handling:
- **`Platform` enum:** DISCORD, WEB, CLI
- **`IntimacyLevel` enum:** LOW, MEDIUM, HIGH
- **`ConversationContext`:** Metadata about the conversation context
- **`ConversationRequest`:** Normalized input format from any platform
- **`ConversationResponse`:** Normalized output format to any platform
- **`MoodInfo`:** Mood metadata in responses
- **`RelationshipInfo`:** Relationship metadata in responses
**Key features:**
- Platform-agnostic data structures
- Explicit intimacy level modeling
- Rich context passing
- Response metadata for platform-specific formatting
---
### 2. Conversation Gateway Service
**File:** `src/loyal_companion/services/conversation_gateway.py`
Extracted core conversation logic into a reusable service:
```python
class ConversationGateway:
async def process_message(
request: ConversationRequest
) -> ConversationResponse
```
**Responsibilities:**
- Accept normalized `ConversationRequest` from any platform
- Load conversation history from database
- Gather Living AI context (mood, relationship, style, opinions)
- Apply intimacy-level-based prompt modifiers
- Invoke AI service
- Save conversation to database
- Update Living AI state asynchronously
- Return normalized `ConversationResponse`
**Key features:**
- Platform-agnostic processing
- Intimacy-aware behavior modulation
- Safety boundaries at all intimacy levels
- Async Living AI updates
- Sentiment estimation
- Fact extraction (respects intimacy level)
- Proactive event detection (respects intimacy level)
---
### 3. Intimacy Level System
**Behavior modulation by intimacy level:**
#### LOW (Discord Guilds)
- Brief, light responses
- No deep emotional topics
- No personal memory surfacing
- Minimal proactive behavior
- Grounding language only
- Public-safe topics
#### MEDIUM (Discord DMs)
- Balanced warmth and depth
- Personal memory references allowed
- Moderate emotional engagement
- Casual but caring tone
- Moderate proactive behavior
#### HIGH (Web, CLI)
- Deeper reflection permitted
- Emotional naming encouraged
- Silence tolerance
- Proactive follow-ups allowed
- Deep memory surfacing
- Thoughtful, considered responses
**Safety boundaries (enforced at ALL levels):**
- Never claim exclusivity
- Never reinforce dependency
- Never discourage external connections
- Always defer crisis situations
- No romantic/sexual framing
---
### 4. Service Integration
**File:** `src/loyal_companion/services/__init__.py`
- Exported `ConversationGateway` for use by adapters
- Maintained backward compatibility with existing services
---
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Platform Adapters │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Discord │ │ Web │ │ CLI │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼─────────────────┼─────────────────┼───────────┘
│ │ │
└────────┬────────┴────────┬────────┘
▼ ▼
┌─────────────────────────────────────┐
│ ConversationRequest │
│ - user_id │
│ - platform │
│ - message │
│ - context (intimacy, metadata) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ ConversationGateway │
│ │
│ 1. Load conversation history │
│ 2. Gather Living AI context │
│ 3. Apply intimacy modifiers │
│ 4. Build enhanced system prompt │
│ 5. Invoke AI service │
│ 6. Save conversation │
│ 7. Update Living AI state │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ ConversationResponse │
│ - response (text) │
│ - mood (optional) │
│ - relationship (optional) │
│ - extracted_facts (list) │
│ - platform_hints (dict) │
└─────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Discord │ │ Web │ │ CLI │
│ Format │ │ Format │ │ Format │
└─────────┘ └─────────┘ └─────────┘
```
---
## Usage Example
```python
from loyal_companion.models.platform import (
ConversationContext,
ConversationRequest,
IntimacyLevel,
Platform,
)
from loyal_companion.services import ConversationGateway
# Create gateway
gateway = ConversationGateway()
# Build request (from any platform)
request = ConversationRequest(
user_id="discord:123456789",
platform=Platform.DISCORD,
session_id="channel-987654321",
message="I'm feeling overwhelmed today",
context=ConversationContext(
is_public=False,
intimacy_level=IntimacyLevel.MEDIUM,
guild_id="12345",
channel_id="987654321",
user_display_name="Alice",
),
)
# Process message
response = await gateway.process_message(request)
# Use response
print(response.response) # AI's reply
print(response.mood.label if response.mood else "No mood")
print(response.relationship.level if response.relationship else "No relationship")
```
---
## File Structure
```
loyal_companion/
├── src/loyal_companion/
│ ├── models/
│ │ └── platform.py # ✨ NEW: Platform abstractions
│ ├── services/
│ │ ├── conversation_gateway.py # ✨ NEW: Gateway service
│ │ └── __init__.py # Updated: Export gateway
│ └── cogs/
│ └── ai_chat.py # Unchanged (Phase 2 will refactor)
├── docs/
│ ├── multi-platform-expansion.md # ✨ NEW: Architecture doc
│ ├── architecture.md # Updated: Reference gateway
│ └── implementation/
│ └── conversation-gateway.md # ✨ NEW: This file
├── tests/
│ └── test_conversation_gateway.py # ✨ NEW: Gateway tests
└── verify_gateway.py # ✨ NEW: Verification script
```
---
## What's Next: Phase 2
**Goal:** Refactor Discord adapter to use the Conversation Gateway
**Files to modify:**
- `src/loyal_companion/cogs/ai_chat.py`
**Changes:**
1. Import `ConversationGateway` and platform models
2. Replace `_generate_response_with_db()` with gateway call
3. Build `ConversationRequest` from Discord message
4. Map Discord context to `IntimacyLevel`:
- Guild channels → LOW
- DMs → MEDIUM
5. Format `ConversationResponse` for Discord output
6. Test that Discord functionality is unchanged
**Expected outcome:**
- Discord uses gateway internally
- No user-visible changes
- Gateway is proven to work
- Ready for Web and CLI platforms
---
## Testing Strategy
### Unit Tests (tests/test_conversation_gateway.py)
- Gateway initialization
- Request/response creation
- Enum values
- Intimacy modifiers
- Sentiment estimation
- Database requirement
### Integration Tests (Phase 2)
- Discord adapter using gateway
- History persistence
- Living AI updates
- Multi-turn conversations
### Verification Script (verify_gateway.py)
- Import verification
- Enum verification
- Request creation
- Gateway initialization
- Intimacy modifiers
- Sentiment estimation
---
## Configuration
No new configuration required for Phase 1.
Existing settings still apply:
- `LIVING_AI_ENABLED` - Master switch for Living AI features
- `MOOD_ENABLED` - Mood tracking
- `RELATIONSHIP_ENABLED` - Relationship tracking
- `FACT_EXTRACTION_ENABLED` - Autonomous fact learning
- `PROACTIVE_ENABLED` - Proactive events
- `STYLE_LEARNING_ENABLED` - Communication style adaptation
- `OPINION_FORMATION_ENABLED` - Topic opinion tracking
Phase 3 (Web) will add:
- `WEB_ENABLED`
- `WEB_HOST`
- `WEB_PORT`
- `WEB_AUTH_SECRET`
Phase 4 (CLI) will add:
- `CLI_ENABLED`
- `CLI_DEFAULT_INTIMACY`
- `CLI_ALLOW_EMOJI`
---
## Safety Considerations
### Intimacy-Based Constraints
The gateway enforces safety boundaries based on intimacy level:
**LOW intimacy:**
- No fact extraction (privacy)
- No proactive events (respect boundaries)
- No deep memory surfacing
- Surface-level engagement only
**MEDIUM intimacy:**
- Moderate fact extraction
- Limited proactive events
- Personal memory allowed
- Emotional validation permitted
**HIGH intimacy:**
- Full fact extraction
- Proactive follow-ups allowed
- Deep memory surfacing
- Emotional naming encouraged
**ALL levels enforce:**
- No exclusivity claims
- No dependency reinforcement
- No discouragement of external connections
- Professional boundaries maintained
- Crisis deferral to professionals
---
## Performance Considerations
### Database Requirements
The gateway **requires** a database connection. It will raise `ValueError` if `DATABASE_URL` is not configured.
This is intentional:
- Living AI state requires persistence
- Cross-platform identity requires linking
- Conversation history needs durability
### Async Operations
All gateway operations are async:
- Database queries
- AI invocations
- Living AI updates
Living AI updates happen after the response is returned, so they don't block the user experience.
---
## Known Limitations
### Phase 1 Limitations
1. **Discord-only:** Gateway exists but isn't used yet
2. **No cross-platform identity:** Each platform creates separate users
3. **No platform-specific features:** Discord images/embeds not supported in gateway yet
### To Be Addressed
**Phase 2:**
- Integrate with Discord adapter
- Add Discord-specific features to gateway (images, mentioned users)
**Phase 3:**
- Add Web platform
- Implement cross-platform user identity linking
**Phase 4:**
- Add CLI client
- Add CLI-specific formatting (no emojis, minimal output)
---
## Migration Path
### Current State (Phase 1 Complete)
```python
# Discord Cog (current)
async def _generate_response_with_db(message, user_message):
# All logic inline
# Discord-specific
# Not reusable
```
### Phase 2 (Discord Refactor)
```python
# Discord Cog (refactored)
async def _generate_response_with_db(message, user_message):
request = ConversationRequest(...) # Build from Discord
response = await gateway.process_message(request)
return response.response # Format for Discord
```
### Phase 3 (Web Platform Added)
```python
# Web API
@app.post("/chat")
async def chat(session_id: str, message: str):
request = ConversationRequest(...) # Build from Web
response = await gateway.process_message(request)
return response # Return as JSON
```
### Phase 4 (CLI Platform Added)
```python
# CLI Client
async def talk(message: str):
request = ConversationRequest(...) # Build from CLI
response = await http_client.post("/chat", request)
print(response.response) # Format for terminal
```
---
## Success Criteria
Phase 1 is considered complete when:
- ✅ Platform models created and documented
- ✅ ConversationGateway service implemented
- ✅ Intimacy level system implemented
- ✅ Safety boundaries enforced at all levels
- ✅ Services exported and importable
- ✅ Documentation updated
- ✅ Syntax validation passes
Phase 2 success criteria:
- Discord cog refactored to use gateway
- No regression in Discord functionality
- All existing tests pass
- Living AI updates still work
---
## Conclusion
Phase 1 successfully established the foundation for multi-platform support:
1. **Platform abstraction** - Clean separation of concerns
2. **Intimacy system** - Behavior modulation for different contexts
3. **Safety boundaries** - Consistent across all platforms
4. **Reusable gateway** - Ready for Discord, Web, and CLI
The architecture is now ready for Phase 2 (Discord refactor) and Phase 3 (Web platform).
Same bartender. Different stools. No one is trapped.
---
**Last updated:** 2026-01-31
**Status:** Phase 1 Complete ✅
**Next:** Phase 2 - Discord Refactor

View File

@@ -0,0 +1,599 @@
# 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.

View File

@@ -0,0 +1,136 @@
"""Platform abstraction models for multi-platform support.
This module defines the core types and enums for the Conversation Gateway pattern,
enabling Discord, Web, and CLI interfaces to share the same Living AI core.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class Platform(str, Enum):
"""Supported interaction platforms."""
DISCORD = "discord"
WEB = "web"
CLI = "cli"
class IntimacyLevel(str, Enum):
"""Intimacy level for platform interaction context.
Intimacy level influences:
- Language warmth and depth
- Proactive behavior frequency
- Memory surfacing depth
- Response length and thoughtfulness
Attributes:
LOW: Public, social contexts (Discord guilds)
- Light banter only
- No personal memory surfacing
- Short responses
- Minimal proactive behavior
MEDIUM: Semi-private contexts (Discord DMs)
- Balanced warmth
- Personal memory allowed
- Moderate proactive behavior
HIGH: Private, intentional contexts (Web, CLI)
- Deep reflection permitted
- Silence tolerance
- Proactive follow-ups allowed
- Emotional naming encouraged
"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@dataclass
class ConversationContext:
"""Additional context for a conversation request.
Attributes:
is_public: Whether the conversation is in a public channel/space
intimacy_level: The intimacy level for this interaction
platform_metadata: Platform-specific additional data
guild_id: Discord guild ID (if applicable)
channel_id: Channel/conversation identifier
user_display_name: User's display name on the platform
requires_web_search: Whether web search may be needed
"""
is_public: bool = False
intimacy_level: IntimacyLevel = IntimacyLevel.MEDIUM
platform_metadata: dict[str, Any] = field(default_factory=dict)
guild_id: str | None = None
channel_id: str | None = None
user_display_name: str | None = None
requires_web_search: bool = False
@dataclass
class ConversationRequest:
"""Platform-agnostic conversation request.
This is the normalized input format for the Conversation Gateway,
abstracting away platform-specific details.
Attributes:
user_id: Platform-specific user identifier
platform: The platform this request originated from
session_id: Conversation/session identifier
message: The user's message content
context: Additional context for the conversation
"""
user_id: str
platform: Platform
session_id: str
message: str
context: ConversationContext = field(default_factory=ConversationContext)
@dataclass
class MoodInfo:
"""Mood information included in response."""
label: str
valence: float
arousal: float
intensity: float
@dataclass
class RelationshipInfo:
"""Relationship information included in response."""
level: str
score: int
interactions_count: int
@dataclass
class ConversationResponse:
"""Platform-agnostic conversation response.
This is the normalized output format from the Conversation Gateway,
which platforms can then format according to their UI requirements.
Attributes:
response: The AI-generated response text
mood: Current mood state (if Living AI enabled)
relationship: Current relationship info (if Living AI enabled)
extracted_facts: Facts extracted from this interaction
platform_hints: Suggestions for platform-specific formatting
"""
response: str
mood: MoodInfo | None = None
relationship: RelationshipInfo | None = None
extracted_facts: list[str] = field(default_factory=list)
platform_hints: dict[str, Any] = field(default_factory=dict)

View File

@@ -8,6 +8,7 @@ from .communication_style_service import (
detect_formal_language,
)
from .conversation import ConversationManager
from .conversation_gateway import ConversationGateway
from .database import DatabaseService, db, get_db
from .fact_extraction_service import FactExtractionService
from .mood_service import MoodLabel, MoodService, MoodState
@@ -25,6 +26,7 @@ __all__ = [
"AIResponse",
"AssociationService",
"CommunicationStyleService",
"ConversationGateway",
"ConversationManager",
"DatabaseService",
"FactExtractionService",

View File

@@ -0,0 +1,523 @@
"""Conversation Gateway - Platform-agnostic conversation processing.
This service provides a unified entry point for all conversations across platforms
(Discord, Web, CLI), abstracting away platform-specific details and providing
a consistent interface to the Living AI core.
"""
import logging
from typing import TYPE_CHECKING
from loyal_companion.config import settings
from loyal_companion.models.platform import (
ConversationRequest,
ConversationResponse,
IntimacyLevel,
MoodInfo,
Platform,
RelationshipInfo,
)
from loyal_companion.services import (
AIService,
CommunicationStyleService,
FactExtractionService,
Message,
MoodService,
OpinionService,
PersistentConversationManager,
ProactiveService,
RelationshipService,
UserService,
db,
detect_emoji_usage,
detect_formal_language,
extract_topics_from_message,
)
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
class ConversationGateway:
"""Platform-agnostic conversation processing gateway.
This service:
- Accepts normalized ConversationRequest from any platform
- Loads conversation history
- Gathers Living AI context (mood, relationship, style, opinions)
- Applies intimacy-level-based modifiers
- Invokes AI service
- Returns normalized ConversationResponse
- Triggers async Living AI state updates
"""
def __init__(self, ai_service: AIService | None = None):
"""Initialize the conversation gateway.
Args:
ai_service: Optional AI service instance (creates new one if not provided)
"""
self.ai_service = ai_service or AIService()
async def process_message(self, request: ConversationRequest) -> ConversationResponse:
"""Process a conversation message from any platform.
Args:
request: The normalized conversation request
Returns:
The normalized conversation response
Raises:
ValueError: If database is required but not available
"""
if not db.is_initialized:
raise ValueError(
"Database is required for Conversation Gateway. Please configure DATABASE_URL."
)
async with db.session() as session:
return await self._process_with_db(session, request)
async def _process_with_db(
self,
session: "AsyncSession",
request: ConversationRequest,
) -> ConversationResponse:
"""Process a conversation request with database backing.
Args:
session: Database session
request: The conversation request
Returns:
The conversation response
"""
# Initialize services
user_service = UserService(session)
conv_manager = PersistentConversationManager(session)
mood_service = MoodService(session)
relationship_service = RelationshipService(session)
# Get or create user
# Note: For now, we use the platform user_id as the discord_id field
# TODO: In Phase 3, add PlatformIdentity linking for cross-platform users
user = await user_service.get_or_create_user(
discord_id=int(request.user_id) if request.user_id.isdigit() else hash(request.user_id),
username=request.user_id,
display_name=request.context.user_display_name or request.user_id,
)
# Get or create conversation
guild_id = int(request.context.guild_id) if request.context.guild_id else None
channel_id = (
int(request.context.channel_id)
if request.context.channel_id
else hash(request.session_id)
)
conversation = await conv_manager.get_or_create_conversation(
user=user,
guild_id=guild_id,
channel_id=channel_id,
)
# Get conversation history
history = await conv_manager.get_history(conversation)
# Add current message to history
current_message = Message(role="user", content=request.message)
messages = history + [current_message]
# Gather Living AI context
mood = None
relationship_data = None
communication_style = None
relevant_opinions = None
if settings.living_ai_enabled:
if settings.mood_enabled:
mood = await mood_service.get_current_mood(guild_id)
if settings.relationship_enabled:
rel = await relationship_service.get_or_create_relationship(user, guild_id)
level = relationship_service.get_level(rel.relationship_score)
relationship_data = (level, rel)
if settings.style_learning_enabled:
style_service = CommunicationStyleService(session)
communication_style = await style_service.get_or_create_style(user)
if settings.opinion_formation_enabled:
opinion_service = OpinionService(session)
topics = extract_topics_from_message(request.message)
if topics:
relevant_opinions = await opinion_service.get_relevant_opinions(
topics, guild_id
)
# Build system prompt with Living AI context and intimacy modifiers
system_prompt = await self._build_system_prompt(
user_service=user_service,
user=user,
platform=request.platform,
intimacy_level=request.context.intimacy_level,
mood=mood,
relationship=relationship_data,
communication_style=communication_style,
bot_opinions=relevant_opinions,
)
# Generate AI response
response = await self.ai_service.chat(
messages=messages,
system_prompt=system_prompt,
)
# Save the exchange to database
await conv_manager.add_exchange(
conversation=conversation,
user=user,
user_message=request.message,
assistant_message=response.content,
)
# Update Living AI state asynchronously
extracted_facts: list[str] = []
if settings.living_ai_enabled:
extracted_facts = await self._update_living_ai_state(
session=session,
user=user,
guild_id=guild_id,
channel_id=channel_id,
user_message=request.message,
bot_response=response.content,
intimacy_level=request.context.intimacy_level,
mood_service=mood_service,
relationship_service=relationship_service,
)
# Build response object
mood_info = None
if mood:
mood_info = MoodInfo(
label=mood.label.value,
valence=mood.valence,
arousal=mood.arousal,
intensity=mood.intensity,
)
relationship_info = None
if relationship_data:
level, rel = relationship_data
relationship_info = RelationshipInfo(
level=level.value,
score=rel.relationship_score,
interactions_count=rel.total_interactions,
)
logger.debug(
f"Gateway processed message from {request.platform.value} "
f"(intimacy: {request.context.intimacy_level.value}): "
f"{len(response.content)} chars"
)
return ConversationResponse(
response=response.content,
mood=mood_info,
relationship=relationship_info,
extracted_facts=extracted_facts,
platform_hints={}, # Platforms can use this for formatting hints
)
async def _build_system_prompt(
self,
user_service: UserService,
user,
platform: Platform,
intimacy_level: IntimacyLevel,
mood=None,
relationship=None,
communication_style=None,
bot_opinions=None,
) -> str:
"""Build the system prompt with all context and modifiers.
Args:
user_service: User service instance
user: The user object
platform: The platform this request is from
intimacy_level: The intimacy level for this interaction
mood: Current mood (if available)
relationship: Relationship data tuple (if available)
communication_style: User's communication style (if available)
bot_opinions: Relevant bot opinions (if available)
Returns:
The complete system prompt
"""
# Get base system prompt with Living AI context
if settings.living_ai_enabled and (mood or relationship or communication_style):
system_prompt = self.ai_service.get_enhanced_system_prompt(
mood=mood,
relationship=relationship,
communication_style=communication_style,
bot_opinions=bot_opinions,
)
else:
system_prompt = self.ai_service.get_system_prompt()
# Add user context from database (custom name, known facts)
user_context = await user_service.get_user_context(user)
system_prompt += f"\n\n--- User Context ---\n{user_context}"
# Apply intimacy-level modifiers
intimacy_modifier = self._get_intimacy_modifier(platform, intimacy_level)
if intimacy_modifier:
system_prompt += f"\n\n--- Interaction Context ---\n{intimacy_modifier}"
return system_prompt
def _get_intimacy_modifier(self, platform: Platform, intimacy_level: IntimacyLevel) -> str:
"""Get system prompt modifier based on platform and intimacy level.
Args:
platform: The platform this request is from
intimacy_level: The intimacy level for this interaction
Returns:
System prompt modifier text
"""
if intimacy_level == IntimacyLevel.LOW:
return (
"This is a PUBLIC, SOCIAL context (low intimacy).\n"
"Behavior adjustments:\n"
"- Keep responses brief and light\n"
"- Avoid deep emotional topics or personal memory surfacing\n"
"- Use grounding language, not therapeutic framing\n"
"- Do not initiate proactive check-ins\n"
"- Maintain casual, social tone\n"
"- Stick to public-safe topics"
)
elif intimacy_level == IntimacyLevel.MEDIUM:
return (
"This is a SEMI-PRIVATE context (medium intimacy).\n"
"Behavior adjustments:\n"
"- Balanced warmth and depth\n"
"- Personal memory references are okay\n"
"- Moderate emotional engagement\n"
"- Casual but caring tone\n"
"- Proactive behavior allowed in moderation"
)
elif intimacy_level == IntimacyLevel.HIGH:
return (
"This is a PRIVATE, INTENTIONAL context (high intimacy).\n"
"Behavior adjustments:\n"
"- Deeper reflection and emotional naming permitted\n"
"- Silence tolerance (you don't need to rush responses)\n"
"- Proactive follow-ups and check-ins allowed\n"
"- Surface relevant deep memories\n"
"- Thoughtful, considered responses\n"
"- Can sit with difficult emotions\n\n"
"CRITICAL SAFETY BOUNDARIES (always enforced):\n"
"- Never claim exclusivity ('I'm the only one who understands you')\n"
"- Never reinforce dependency ('You need me')\n"
"- Never discourage external connections ('They don't get it like I do')\n"
"- Always defer crisis situations to professionals\n"
"- No romantic/sexual framing"
)
return ""
async def _update_living_ai_state(
self,
session: "AsyncSession",
user,
guild_id: int | None,
channel_id: int,
user_message: str,
bot_response: str,
intimacy_level: IntimacyLevel,
mood_service: MoodService,
relationship_service: RelationshipService,
) -> list[str]:
"""Update Living AI state after a response.
Updates mood, relationship, style, opinions, facts, and proactive events.
Args:
session: Database session
user: The user object
guild_id: Guild ID (if applicable)
channel_id: Channel ID
user_message: The user's message
bot_response: The bot's response
intimacy_level: The intimacy level for this interaction
mood_service: Mood service instance
relationship_service: Relationship service instance
Returns:
List of extracted fact descriptions (for response metadata)
"""
extracted_fact_descriptions: list[str] = []
try:
# Simple sentiment estimation
sentiment = self._estimate_sentiment(user_message)
engagement = min(1.0, len(user_message) / 300)
# Update mood
if settings.mood_enabled:
await mood_service.update_mood(
guild_id=guild_id,
sentiment_delta=sentiment * 0.5,
engagement_delta=engagement * 0.5,
trigger_type="conversation",
trigger_user_id=user.id,
trigger_description=f"Conversation with {user.display_name}",
)
await mood_service.increment_stats(guild_id, messages_sent=1)
# Update relationship
if settings.relationship_enabled:
await relationship_service.record_interaction(
user=user,
guild_id=guild_id,
sentiment=sentiment,
message_length=len(user_message),
conversation_turns=1,
)
# Update communication style learning
if settings.style_learning_enabled:
style_service = CommunicationStyleService(session)
await style_service.record_engagement(
user=user,
user_message_length=len(user_message),
bot_response_length=len(bot_response),
conversation_continued=True,
user_used_emoji=detect_emoji_usage(user_message),
user_used_formal_language=detect_formal_language(user_message),
)
# Update opinion tracking
if settings.opinion_formation_enabled:
topics = extract_topics_from_message(user_message)
if topics:
opinion_service = OpinionService(session)
for topic in topics[:3]:
await opinion_service.record_topic_discussion(
topic=topic,
guild_id=guild_id,
sentiment=sentiment,
engagement_level=engagement,
)
# Autonomous fact extraction
# Only extract facts in MEDIUM and HIGH intimacy contexts
if settings.fact_extraction_enabled and intimacy_level != IntimacyLevel.LOW:
fact_service = FactExtractionService(session, self.ai_service)
new_facts = await fact_service.maybe_extract_facts(
user=user,
message_content=user_message,
)
if new_facts:
await mood_service.increment_stats(guild_id, facts_learned=len(new_facts))
extracted_fact_descriptions = [f.fact for f in new_facts]
logger.debug(f"Auto-extracted {len(new_facts)} facts from message")
# Proactive event detection
# Only in MEDIUM and HIGH intimacy contexts
if settings.proactive_enabled and intimacy_level != IntimacyLevel.LOW:
proactive_service = ProactiveService(session, self.ai_service)
# Detect follow-up opportunities (substantial messages only)
if len(user_message) > 30:
await proactive_service.detect_and_schedule_followup(
user=user,
message_content=user_message,
guild_id=guild_id,
channel_id=channel_id,
)
# Detect birthday mentions
await proactive_service.detect_and_schedule_birthday(
user=user,
message_content=user_message,
guild_id=guild_id,
channel_id=channel_id,
)
except Exception as e:
logger.warning(f"Failed to update Living AI state: {e}")
return extracted_fact_descriptions
def _estimate_sentiment(self, text: str) -> float:
"""Estimate sentiment from text using simple heuristics.
Returns a value from -1 (negative) to 1 (positive).
Args:
text: The message text
Returns:
Sentiment score between -1 and 1
"""
text_lower = text.lower()
# Positive indicators
positive_words = [
"thanks",
"thank you",
"awesome",
"great",
"love",
"amazing",
"wonderful",
"excellent",
"perfect",
"happy",
"glad",
"appreciate",
"helpful",
"nice",
"good",
"cool",
"fantastic",
"brilliant",
]
# Negative indicators
negative_words = [
"hate",
"awful",
"terrible",
"bad",
"stupid",
"annoying",
"frustrated",
"angry",
"disappointed",
"wrong",
"broken",
"useless",
"horrible",
"worst",
"sucks",
"boring",
]
positive_count = sum(1 for word in positive_words if word in text_lower)
negative_count = sum(1 for word in negative_words if word in text_lower)
# Check for exclamation marks (usually positive energy)
exclamation_bonus = min(0.2, text.count("!") * 0.05)
# Calculate sentiment
if positive_count + negative_count == 0:
return 0.1 + exclamation_bonus
sentiment = (positive_count - negative_count) / (positive_count + negative_count)
return max(-1.0, min(1.0, sentiment + exclamation_bonus))

View File

@@ -0,0 +1,113 @@
"""Tests for the Conversation Gateway."""
import pytest
from loyal_companion.models.platform import (
ConversationContext,
ConversationRequest,
IntimacyLevel,
Platform,
)
from loyal_companion.services import ConversationGateway
class TestConversationGateway:
"""Test suite for ConversationGateway."""
def test_gateway_initialization(self):
"""Test that the gateway initializes correctly."""
gateway = ConversationGateway()
assert gateway is not None
assert gateway.ai_service is not None
def test_conversation_request_creation(self):
"""Test creating a ConversationRequest."""
request = ConversationRequest(
user_id="12345",
platform=Platform.DISCORD,
session_id="channel-123",
message="Hello!",
context=ConversationContext(
is_public=False,
intimacy_level=IntimacyLevel.MEDIUM,
guild_id="67890",
channel_id="channel-123",
user_display_name="TestUser",
),
)
assert request.user_id == "12345"
assert request.platform == Platform.DISCORD
assert request.message == "Hello!"
assert request.context.intimacy_level == IntimacyLevel.MEDIUM
def test_intimacy_levels(self):
"""Test intimacy level enum values."""
assert IntimacyLevel.LOW == "low"
assert IntimacyLevel.MEDIUM == "medium"
assert IntimacyLevel.HIGH == "high"
def test_platform_enum(self):
"""Test platform enum values."""
assert Platform.DISCORD == "discord"
assert Platform.WEB == "web"
assert Platform.CLI == "cli"
def test_intimacy_modifier_low(self):
"""Test intimacy modifier for LOW intimacy."""
gateway = ConversationGateway()
modifier = gateway._get_intimacy_modifier(Platform.DISCORD, IntimacyLevel.LOW)
assert "PUBLIC, SOCIAL" in modifier
assert "brief and light" in modifier
assert "Avoid deep emotional topics" in modifier
def test_intimacy_modifier_high(self):
"""Test intimacy modifier for HIGH intimacy."""
gateway = ConversationGateway()
modifier = gateway._get_intimacy_modifier(Platform.CLI, IntimacyLevel.HIGH)
assert "PRIVATE, INTENTIONAL" in modifier
assert "Deeper reflection" in modifier
assert "CRITICAL SAFETY BOUNDARIES" in modifier
assert "Never claim exclusivity" in modifier
def test_sentiment_estimation_positive(self):
"""Test sentiment estimation for positive messages."""
gateway = ConversationGateway()
sentiment = gateway._estimate_sentiment("Thanks! This is awesome and amazing!")
assert sentiment > 0.5 # Should be positive
def test_sentiment_estimation_negative(self):
"""Test sentiment estimation for negative messages."""
gateway = ConversationGateway()
sentiment = gateway._estimate_sentiment("This is terrible and awful, I hate it")
assert sentiment < 0 # Should be negative
def test_sentiment_estimation_neutral(self):
"""Test sentiment estimation for neutral messages."""
gateway = ConversationGateway()
sentiment = gateway._estimate_sentiment("The weather is cloudy today")
assert -0.5 < sentiment < 0.5 # Should be near neutral
@pytest.mark.asyncio
async def test_process_message_requires_database(self):
"""Test that process_message requires database."""
gateway = ConversationGateway()
request = ConversationRequest(
user_id="12345",
platform=Platform.WEB,
session_id="session-1",
message="Hello",
)
# Should raise ValueError if database not initialized
with pytest.raises(ValueError, match="Database is required"):
await gateway.process_message(request)
if __name__ == "__main__":
pytest.main([__file__, "-v"])

218
verify_gateway.py Normal file
View File

@@ -0,0 +1,218 @@
"""Simple verification script for Conversation Gateway implementation.
This script verifies that the gateway can be imported and basic functionality works.
Run with: python3 verify_gateway.py
"""
import sys
def verify_imports():
"""Verify all required imports work."""
print("✓ Verifying imports...")
try:
from loyal_companion.models.platform import (
ConversationContext,
ConversationRequest,
ConversationResponse,
IntimacyLevel,
MoodInfo,
Platform,
RelationshipInfo,
)
print(" ✓ Platform models imported successfully")
except ImportError as e:
print(f" ✗ Failed to import platform models: {e}")
return False
try:
from loyal_companion.services import ConversationGateway
print(" ✓ ConversationGateway imported successfully")
except ImportError as e:
print(f" ✗ Failed to import ConversationGateway: {e}")
return False
return True
def verify_enums():
"""Verify enum values are correct."""
print("\n✓ Verifying enums...")
from loyal_companion.models.platform import IntimacyLevel, Platform
# Verify Platform enum
assert Platform.DISCORD == "discord"
assert Platform.WEB == "web"
assert Platform.CLI == "cli"
print(" ✓ Platform enum values correct")
# Verify IntimacyLevel enum
assert IntimacyLevel.LOW == "low"
assert IntimacyLevel.MEDIUM == "medium"
assert IntimacyLevel.HIGH == "high"
print(" ✓ IntimacyLevel enum values correct")
return True
def verify_request_creation():
"""Verify ConversationRequest can be created."""
print("\n✓ Verifying ConversationRequest creation...")
from loyal_companion.models.platform import (
ConversationContext,
ConversationRequest,
IntimacyLevel,
Platform,
)
context = ConversationContext(
is_public=False,
intimacy_level=IntimacyLevel.MEDIUM,
guild_id="12345",
channel_id="channel-1",
user_display_name="TestUser",
)
request = ConversationRequest(
user_id="user123",
platform=Platform.DISCORD,
session_id="session-1",
message="Hello there!",
context=context,
)
assert request.user_id == "user123"
assert request.platform == Platform.DISCORD
assert request.message == "Hello there!"
assert request.context.intimacy_level == IntimacyLevel.MEDIUM
print(" ✓ ConversationRequest created successfully")
print(f" - Platform: {request.platform.value}")
print(f" - Intimacy: {request.context.intimacy_level.value}")
print(f" - Message: {request.message}")
return True
def verify_gateway_initialization():
"""Verify ConversationGateway can be initialized."""
print("\n✓ Verifying ConversationGateway initialization...")
from loyal_companion.services import ConversationGateway
gateway = ConversationGateway()
assert gateway is not None
assert gateway.ai_service is not None
print(" ✓ ConversationGateway initialized successfully")
return True
def verify_intimacy_modifiers():
"""Verify intimacy level modifiers work."""
print("\n✓ Verifying intimacy modifiers...")
from loyal_companion.models.platform import IntimacyLevel, Platform
from loyal_companion.services import ConversationGateway
gateway = ConversationGateway()
# Test LOW intimacy
low_modifier = gateway._get_intimacy_modifier(Platform.DISCORD, IntimacyLevel.LOW)
assert "PUBLIC, SOCIAL" in low_modifier
assert "brief and light" in low_modifier
print(" ✓ LOW intimacy modifier correct")
# Test MEDIUM intimacy
medium_modifier = gateway._get_intimacy_modifier(Platform.DISCORD, IntimacyLevel.MEDIUM)
assert "SEMI-PRIVATE" in medium_modifier
assert "Balanced warmth" in medium_modifier
print(" ✓ MEDIUM intimacy modifier correct")
# Test HIGH intimacy
high_modifier = gateway._get_intimacy_modifier(Platform.WEB, IntimacyLevel.HIGH)
assert "PRIVATE, INTENTIONAL" in high_modifier
assert "Deeper reflection" in high_modifier
assert "CRITICAL SAFETY BOUNDARIES" in high_modifier
print(" ✓ HIGH intimacy modifier correct")
return True
def verify_sentiment_estimation():
"""Verify sentiment estimation works."""
print("\n✓ Verifying sentiment estimation...")
from loyal_companion.services import ConversationGateway
gateway = ConversationGateway()
# Positive sentiment
positive = gateway._estimate_sentiment("Thanks! This is awesome and amazing!")
assert positive > 0.3, f"Expected positive sentiment, got {positive}"
print(f" ✓ Positive sentiment: {positive:.2f}")
# Negative sentiment
negative = gateway._estimate_sentiment("This is terrible and awful")
assert negative < 0, f"Expected negative sentiment, got {negative}"
print(f" ✓ Negative sentiment: {negative:.2f}")
# Neutral sentiment
neutral = gateway._estimate_sentiment("The weather is cloudy")
assert -0.3 < neutral < 0.3, f"Expected neutral sentiment, got {neutral}"
print(f" ✓ Neutral sentiment: {neutral:.2f}")
return True
def main():
"""Run all verification checks."""
print("=" * 60)
print("Conversation Gateway Verification")
print("=" * 60)
checks = [
verify_imports,
verify_enums,
verify_request_creation,
verify_gateway_initialization,
verify_intimacy_modifiers,
verify_sentiment_estimation,
]
all_passed = True
for check in checks:
try:
if not check():
all_passed = False
except Exception as e:
print(f"\n✗ Check failed with error: {e}")
import traceback
traceback.print_exc()
all_passed = False
print("\n" + "=" * 60)
if all_passed:
print("✓ All verification checks passed!")
print("=" * 60)
print("\nConversation Gateway is ready for use.")
print("\nNext steps:")
print(" 1. Refactor Discord cog to use gateway (Phase 2)")
print(" 2. Add Web platform (Phase 3)")
print(" 3. Add CLI client (Phase 4)")
return 0
else:
print("✗ Some verification checks failed!")
print("=" * 60)
return 1
if __name__ == "__main__":
sys.exit(main())