An adaptive multi-agent system that extracts your literary DNA through conversation and generates actionable reading profiles.
WREN solves a critical problem for LLM users: you know what you like, but explaining your literary taste to an AI is hard. WREN's interview agent asks the right questions, listens deeply, and builds a structured profile that any LLM can use to generate precisely targeted content.
Built with LangGraph, LangChain, and Kimi K2 Thinking models.
The Problem: Kimi K2 is a great writer, but users struggle to articulate their literary preferences in a way LLMs can act on. Vague prompts like "write me something good" produce generic results.
The Solution: WREN conducts a 12-turn adaptive interview that:
- Extracts taste anchors (what you love/hate and why)
- Maps your style signature (prose density, pacing, tone preferences)
- Identifies narrative desires (story types you wish existed)
- Captures implicit signals (vocabulary richness, engagement patterns)
- Generates a structured, machine-readable profile
The Result: A profile you can hand to any LLM to get content that matches your exact taste.
WREN uses a specialized multi-agent architecture with distinct roles:
┌────────────────────────────────────────────────────────────────────┐
│ CLI Interface │
│ (cli_interview.py) │
└────────────────────┬───────────────────────────────────────────────┘
│
┌─────────────┴──────────────┐
│ │
┌──────▼─────────────────┐ ┌──────▼──────────────────────┐
│ InterviewAgent │ │ ProfileGeneratorAgent │
│ (kimi-k2-thinking- │ │ (kimi-k2-thinking) │
│ turbo) │ │ │
│ │ │ Tools: │
│ Tools: │ │ ┌─────────────────────────┐ │
│ ┌────────────────────┐ │ │ │ ReasoningExtractor │ │
│ │ ProfileAnalyzer │ │ │ │ - Extract thinking │ │
│ │ - Vocab richness │ │ │ │ - Format reasoning │ │
│ │ - Response brevity │ │ │ └─────────────────────────┘ │
│ │ - Engagement level │ │ │ │
│ └────────────────────┘ │ │ ┌─────────────────────────┐ │
│ │ │ │ ProfileFormatter │ │
│ ┌────────────────────┐ │ │ │ - JSON → Markdown │ │
│ │ConversationAnalyzer│ │ │ │ - Shareable text │ │
│ │ - Turn tracking │ │ │ │ - Human-readable │ │
│ │ - Coverage check │ │ │ └─────────────────────────┘ │
│ │ - Readiness score │ │ │ │
│ └────────────────────┘ │ │ ┌─────────────────────────┐ │
└────────┬───────────────┘ │ │ ProfileSaver │ │
│ │ │ - Create user folders │ │
│ │ │ - Save logs + profiles │ │
│ │ │ - Multiple formats │ │
│ │ └─────────────────────────┘ │
│ └──────┬──────────────────────┘
│ │
┌────────▼──────────────────────────▼─────────────────────────┐
│ LangGraph StateGraph │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [analyze_node] │ │
│ │ ↓ │ │
│ │ Run ProfileAnalyzer + ConversationAnalyzer │ │
│ │ ↓ │ │
│ │ [_should_continue] │ │
│ │ ↓ ↓ │ │
│ │ turn < 12 turn >= 12 │ │
│ │ ↓ ↓ │ │
│ │ [generate_question] [generate_profile] │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ State: {messages, turn_count, analysis, profile_data} │
└────────┬─────────────────────────────────────────────────────┘
│
┌────────▼──────────────┐
│ RedisCheckpointSaver │
│ (State Persistence) │
│ │
│ - Pickle serialization│
│ - 24h TTL │
│ - Resume sessions │
└───────────────────────┘
Role: Conversational interviewer that adapts to user responses
Model: kimi-k2-thinking-turbo (fast, conversational)
Capabilities:
- Conducts 12-turn structured interview
- References previous answers (shows it's listening)
- Adjusts question depth based on response style
- Tracks coverage across 5 dimensions
- Uses LangGraph state machine for conversation flow
Key Innovation: Uses real-time analysis tools to adapt questioning:
ProfileAnalyzerTool: Measures vocabulary richness, brevity, engagementConversationAnalyzerTool: Tracks coverage and determines readiness
Role: Deep analyst that transforms conversation into structured profile
Model: kimi-k2-thinking (extended reasoning for analysis)
Capabilities:
- Parses full conversation transcript
- Generates JSON profile with 40+ data points
- Scores style preferences on 0-100 scales
- Provides human-readable explanations
- Extracts its own reasoning process
Key Innovation: Single-purpose agent runs once, uses expensive model only when needed, includes explanations in second-person for easy sharing.
Role: Extracts and formats Kimi K2's internal thinking
Capabilities:
- Pulls
reasoning_contentfrom model responses - Formats for human readability
- Saves separately for transparency
- Enables debugging and insight
WREN uses LangGraph for stateful conversation management:
class InterviewState(TypedDict):
messages: Annotated[List[BaseMessage], add] # Conversation history
turn_count: int # Current turn
profile_data: Dict[str, Any] # Generated profile
is_complete: bool # Completion flag
current_analysis: Dict[str, Any] # Real-time metricsUser Input
↓
[analyze_node]
├─> ProfileAnalyzerTool: Analyze response style
├─> ConversationAnalyzerTool: Check coverage
└─> Update state with analysis
↓
[_should_continue]
├─> turn_count >= 12? → generate_profile
└─> turn_count < 12? → generate_question
↓
[generate_question_node]
├─> Build prompt with turn context + analysis
├─> Invoke Kimi K2 Thinking Turbo
├─> Extract reasoning from response
└─> Return AIMessage
↓
State persisted to Redis → Ready for next turn
Why LangGraph?
- Built-in state persistence (Redis or in-memory)
- Clean separation of analysis → decision → generation
- Resumable sessions (pick up where you left off)
- Type-safe state transitions
WREN implements a custom Redis checkpointer for LangGraph:
class RedisCheckpointSaver(BaseCheckpointSaver):
def put(self, config, checkpoint, metadata, new_versions):
# Serializes full state with pickle (handles Python objects)
serialized = pickle.dumps({
"checkpoint": checkpoint,
"metadata": metadata,
"config": config
})
self.redis.setex(key, self.ttl, serialized) # 24h TTLWhy Custom?
- LangGraph doesn't include Redis checkpointer out of the box
- Standard JSON serialization fails on Python objects
- Pickle handles complex state including functions/lambdas
Benefits:
- Sessions persist across restarts
- Resume interrupted interviews
- Inspect state at any point
- Auto-expiration after 24 hours
SYSTEM_PROMPT = """You are a world-class literary profiler conducting
an adaptive interview.
CORE PRINCIPLES:
- Ask ONE question at a time
- Always reference their specific previous answers
- Adapt follow-ups based on response depth and style
- Continue asking questions until turn 12
STRICT RULES:
- CURRENT TURN: {turn_count} of 12
- If turn < 12: Ask another question (do NOT mention completion)
- If turn = 12: Only then offer to generate their profile
"""Key Features:
- Dynamic turn injection prevents premature completion
- Explicit rules override model's tendency to end early
- References previous answers (shows listening)
- Adapts energy level to user responses
The system dynamically loads scoring guidelines from PROFILE_RUBRIC.md:
@staticmethod
def get_summary_prompt(conversation: str, include_rubric: bool = True):
# Load rubric scales from file
rubric_section = _load_rubric_section()
return f"""Generate JSON profile with:
JSON SCHEMA: [detailed structure]
SCORING GUIDELINES:
{rubric_section} ← Dynamically loaded from PROFILE_RUBRIC.md
Conversation:
{conversation}
"""Why Dynamic Loading?
- Single source of truth (update rubric → prompts update automatically)
- LLM sees detailed scoring guidance
- Consistent scoring across all profiles
def _run(self, response_text: str, conversation_history: List) -> Dict:
"""Analyzes individual responses for implicit signals."""
# Calculate metrics
vocabulary_richness = unique_words / total_words
response_brevity = 1 / (word_count / 100) # Normalized
engagement_level = heuristic(examples, emotion_words, depth)
return {
"vocabulary_richness": 0.0-1.0,
"response_brevity": 0.0-1.0,
"engagement_level": 0.0-1.0
}def _run(self, conversation_history: List[Dict]) -> Dict:
"""Tracks coverage across 5 dimensions."""
coverage = {
"taste_anchors": check_keywords(["book", "author", "story"]),
"style_preference": check_keywords(["prose", "writing", "style"]),
"narrative_desire": check_keywords(["wish", "want", "story"]),
"consumption_habit": check_keywords(["read", "time", "daily"])
}
return {
"turn_count": len(user_messages),
"coverage": coverage,
"coverage_score": sum(coverage.values()) / len(coverage),
"ready_for_summary": turn >= 8 and coverage_score >= 0.75
}These tools feed into the agent's decision-making, enabling adaptive questioning based on what's been covered.
WREN generates 4 output formats:
{
"taste_anchors": {
"loves": ["The Remains of the Day", "Beloved"],
"hates": ["Finnegans Wake"],
"inferred_genres": ["modernist fiction", "trauma narratives"]
},
"style_signature": {
"prose_density": 70, // 0-100 scale
"pacing": 60,
"tone": 10,
"worldbuilding": 20,
"character_focus": 90
},
"narrative_desires": {
"wish": "Language fracturing to contain catastrophe",
"preferred_ending": "transcendent",
"themes": ["language limits", "trauma", "alienation"]
},
"consumption": {
"daily_time_minutes": 75,
"delivery_frequency": "every_few_days",
"pages_per_delivery": 30
},
"implicit": {
"vocabulary_richness": 0.95,
"response_brevity_score": 0.2,
"engagement_index": 0.95
},
"explanations": {
"prose_density": "You appreciate complex prose when it reveals emotion...",
"reading_philosophy": "You read as an act of emotional archaeology..."
},
"reader_archetype": "Fracture Dweller"
}Human-readable with sections and formatting.
Optimized for social media / documentation sharing.
Full transcript with reasoning content from Kimi K2.
# Clone repository
git clone https://github.com/muratcankoylan/readwren.git
cd readwren
# Install dependencies
pip install -r requirements.txt
# Set up environment
cp env.example .env
# Edit .env with your Moonshot API key# .env file
MOONSHOT_API_KEY=sk-your-api-key-here
MOONSHOT_BASE_URL=https://api.moonshot.ai/v1
# Optional: Redis for persistent sessions
REDIS_HOST=your-redis-host.com
REDIS_PORT=17887
REDIS_PASSWORD=your-passwordGet your Moonshot API key: https://platform.moonshot.ai/
./run_interview.shOr directly:
python cli_interview.pyLet's start simple. Name 3 books you've loved, and 1 you couldn't finish.
Your response: I love The Remains of the Day for its devastating restraint,
Beloved for how it makes the unspeakable tangible, and The Metamorphosis
for crystallizing alienation. I couldn't finish Finnegans Wake.
Agent: Your taste gravitates toward emotional precision over linguistic
spectacle. When you say "devastating restraint," what specific moment
in Ishiguro's novel best exemplifies this for you?
Progress: ●○○○○○○○○○○○ (1/12)
After 12 turns:
✓ Profile saved to: user_profiles/cli_20251108_145739/
- conversation_20251108_150303.json (full transcript)
- profile_20251108_150303.json (structured data)
- profile_20251108_150303.md (human-readable)
- profile_20251108_150303_SHAREABLE.txt (social sharing)
Once generated, give your profile to any LLM:
Claude/GPT/Grok/Kimi, write a short story for me using these preferences:
- Prose density: 70/100 (between Morrison and Ishiguro)
- Tone: 10/100 (dark, serious, restrained)
- Character focus: 90/100 (psychological depth over plot)
- Theme: Language failing to contain private catastrophe
- Ending: Transcendent (through remaining broken)
- Avoid: Linguistic performance for its own sake
Result: Precisely targeted content matching your exact taste.
readwren/
├── src/ # Core application
│ ├── agents/ # AI agents
│ │ ├── interview_agent.py # Main interviewer (LangGraph)
│ │ ├── profile_generator.py # Profile analyst
│ │ ├── redis_checkpointer.py # Custom Redis persistence
│ │ └── reasoning_extractor.py # Kimi K2 reasoning handler
│ ├── tools/ # Analysis tools
│ │ ├── profile_tools.py # Response analyzers
│ │ ├── profile_saver.py # File management
│ │ └── profile_formatter.py # Output formatting
│ ├── prompts/ # Prompt engineering
│ │ └── interview_prompts.py # System prompts + rubric loader
│ └── config/ # Configuration
│ └── settings.py
├── docs/ # Documentation
│ ├── TECHNICAL_DOCUMENTATION.md # Complete technical reference
│ ├── PROFILE_RUBRIC.md # Scoring system (loaded dynamically)
│ ├── RUBRIC_INTEGRATION.md # Rubric usage guide
│ └── REDIS_GUIDE.md # Redis setup and usage
├── scripts/ # Utility scripts
│ ├── view_redis_sessions.py # List active sessions
│ ├── view_session_conversation.py # Decode Redis checkpoints
│ ├── view_conversation_log.py # Display conversation logs
│ └── retrieve_profile.py # Retrieve and edit profiles
├── examples/ # Example outputs
│ └── example_session/ # Complete mock interview
│ ├── logs/ # Conversation transcript
│ └── profiles/ # Generated profiles
├── cli_interview.py # Interactive CLI entry point
├── run_interview.sh # Startup script
├── requirements.txt # Python dependencies
├── env.example # Environment template
├── LICENSE # MIT License
├── README.md # This file
└── user_profiles/ # Your generated outputs (gitignored)
- LangGraph: State machine for conversation flow
- LangChain: LLM abstraction layer
- Moonshot AI: Kimi K2 models (turbo + thinking)
- Redis: Session persistence (optional)
- Python 3.11+
| Aspect | InterviewAgent | ProfileGenerator |
|---|---|---|
| Model | kimi-k2-thinking-turbo | kimi-k2-thinking |
| Tokens | 800 | 4000 |
| Temperature | 0.8 | 0.7 |
| Speed | ~2-3s | ~10-15s |
| Reasoning | Basic thinking | Extended reasoning |
| Total calls | 12+ | 1 |
Strategy: Use thinking-turbo for fast conversational turns, reserve full thinking model for deep analysis at the end.
# LangGraph state with Redis persistence
state = {
"messages": [HumanMessage, AIMessage, ...],
"turn_count": 8,
"current_analysis": {
"coverage_score": 0.85,
"vocabulary_richness": 0.92
},
"is_complete": False
}
# Automatically saved to Redis after each turn
# TTL: 24 hours (configurable)# View all active Redis sessions
python scripts/view_redis_sessions.py
# View specific conversation
python scripts/view_conversation_log.py user_profiles/cli_20251108_145739/logs/conversation.json
# Retrieve session from Redis
python scripts/retrieve_profile.pyWREN extracts and displays Kimi K2's internal thinking:
💭 REASONING:
Let me analyze their preference for restraint. They mentioned Ishiguro
and Morrison, both use controlled prose. I should probe if they prefer
brevity or just emotional control. The Finnegans Wake rejection suggests
they dislike when complexity becomes performative...
Enable in CLI:
Show Kimi K2 reasoning? (y/N): yEdit PROFILE_RUBRIC.md to adjust scoring scales. Changes automatically propagate to profile generation prompts.
- TECHNICAL_DOCUMENTATION.md: Complete architecture, agents, tools, prompts, data flow
- PROFILE_RUBRIC.md: Scoring system and metric definitions
- RUBRIC_INTEGRATION.md: How rubric is used in code
- REDIS_GUIDE.md: Redis setup and session management
scripts/view_redis_sessions.py: List all active sessions with metadatascripts/view_session_conversation.py: Decode Redis checkpoint for a sessionscripts/view_conversation_log.py: Display saved conversation logsscripts/retrieve_profile.py: Retrieve and manually edit profiles
- Content Creators: Generate stories/essays matching your style
- Readers: Get precise book recommendations
- Writers: Articulate your voice for AI writing assistants
- Product Teams: Build user profiles for personalization
- Researchers: Study literary preferences and taste formation
Single-agent approach (naive):
- One LLM does everything: interview + analyze + generate profile
- Expensive model runs 12+ times
- Can't optimize for different tasks
- Mixed concerns (conversation vs analysis)
Multi-agent approach (WREN):
- InterviewAgent: Fast, conversational model (turbo)
- ProfileGenerator: Deep analysis model (thinking)
- Each agent optimized for its role
- Thinking model runs once (cost-efficient)
- Clean separation of concerns
- Tools feed into agent decision-making
This architecture is reusable: same pattern works for medical history, design preferences, dietary taste, etc.
WREN is open source. Contributions welcome:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
Areas for contribution:
- Additional analysis tools
- New output formats
- Web UI
- Multi-language support
- Alternative LLM support
Open Source - MIT License
Built by: Muratcan Koylan (@koylanai)
Powered by:
- Moonshot AI (Kimi K2 models)
- LangChain
- LangGraph
- Twitter: @koylanai
- GitHub Issues: readwren/issues
- Documentation: See TECHNICAL_DOCUMENTATION.md
WREN: Because explaining your taste shouldn't be harder than having it.