Readwren
An adaptive multi-agent system that extracts your literary DNA through conversation and generates actionable reading profiles.
Install / Use
/learn @muratcankoylan/ReadwrenREADME
WREN: AI Literary Interview Agent
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.
Why WREN?
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.
Architecture: Multi-Agent System
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 │
└───────────────────────┘
Agent 1: InterviewAgent
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
Agent 2: ProfileGeneratorAgent
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.
Agent 3: ReasoningExtractor
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
LangGraph State Machine
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 metrics
Graph Flow
User 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
Redis Integration
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 TTL
Why 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
Prompt Engineering
Adaptive System Prompt
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
Profile Generation Prompt
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
Tools & Analysis
ProfileAnalyzerTool
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 {
