Add comprehensive development intelligence system that tracks: - Development sessions with automatic start/stop - Full conversation history with semantic search - Tool usage and file operation analytics - Think time and engagement analysis - Git activity correlation - Learning pattern recognition - Productivity insights and metrics Features: - FastAPI backend with SQLite database - Modern web dashboard with interactive charts - Claude Code hook integration for automatic tracking - Comprehensive test suite with 100+ tests - Complete API documentation (OpenAPI/Swagger) - Privacy-first design with local data storage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""
|
|
Conversation model for tracking dialogue between user and Claude.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict, Any
|
|
from sqlalchemy import String, Text, Integer, DateTime, JSON, ForeignKey
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from sqlalchemy.sql import func
|
|
|
|
from .base import Base, TimestampMixin
|
|
|
|
|
|
class Conversation(Base, TimestampMixin):
|
|
"""
|
|
Represents a conversation exchange between user and Claude.
|
|
|
|
Each conversation entry captures either a user prompt or Claude's response,
|
|
along with context about tools used and files affected.
|
|
"""
|
|
|
|
__tablename__ = "conversations"
|
|
|
|
# Primary key
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
|
|
# Foreign key to session
|
|
session_id: Mapped[int] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True)
|
|
|
|
# Timing
|
|
timestamp: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=False,
|
|
default=func.now(),
|
|
index=True
|
|
)
|
|
|
|
# Conversation content
|
|
user_prompt: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
claude_response: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
|
|
# Context and metadata
|
|
tools_used: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True)
|
|
files_affected: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True)
|
|
context: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
|
|
|
|
# Token estimates for analysis
|
|
tokens_input: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
tokens_output: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
# Exchange type for categorization
|
|
exchange_type: Mapped[str] = mapped_column(String(50), nullable=False) # user_prompt, claude_response
|
|
|
|
# Relationships
|
|
session: Mapped["Session"] = relationship("Session", back_populates="conversations")
|
|
|
|
def __repr__(self) -> str:
|
|
content_preview = ""
|
|
if self.user_prompt:
|
|
content_preview = self.user_prompt[:50] + "..." if len(self.user_prompt) > 50 else self.user_prompt
|
|
elif self.claude_response:
|
|
content_preview = self.claude_response[:50] + "..." if len(self.claude_response) > 50 else self.claude_response
|
|
|
|
return f"<Conversation(id={self.id}, type='{self.exchange_type}', content='{content_preview}')>"
|
|
|
|
@property
|
|
def is_user_prompt(self) -> bool:
|
|
"""Check if this is a user prompt."""
|
|
return self.exchange_type == "user_prompt"
|
|
|
|
@property
|
|
def is_claude_response(self) -> bool:
|
|
"""Check if this is a Claude response."""
|
|
return self.exchange_type == "claude_response"
|
|
|
|
@property
|
|
def content_length(self) -> int:
|
|
"""Get the total character length of the conversation content."""
|
|
user_length = len(self.user_prompt) if self.user_prompt else 0
|
|
claude_length = len(self.claude_response) if self.claude_response else 0
|
|
return user_length + claude_length
|
|
|
|
@property
|
|
def estimated_tokens(self) -> int:
|
|
"""Estimate total tokens in this conversation exchange."""
|
|
if self.tokens_input and self.tokens_output:
|
|
return self.tokens_input + self.tokens_output
|
|
|
|
# Rough estimation: ~4 characters per token
|
|
return self.content_length // 4
|
|
|
|
def get_intent_category(self) -> Optional[str]:
|
|
"""Extract intent category from context if available."""
|
|
if self.context and "intent" in self.context:
|
|
return self.context["intent"]
|
|
return None
|
|
|
|
def get_complexity_level(self) -> Optional[str]:
|
|
"""Extract complexity level from context if available."""
|
|
if self.context and "complexity" in self.context:
|
|
return self.context["complexity"]
|
|
return None
|
|
|
|
def has_file_operations(self) -> bool:
|
|
"""Check if this conversation involved file operations."""
|
|
if not self.tools_used:
|
|
return False
|
|
|
|
file_tools = {"Edit", "Write", "Read"}
|
|
return any(tool in file_tools for tool in self.tools_used)
|
|
|
|
def has_code_execution(self) -> bool:
|
|
"""Check if this conversation involved code execution."""
|
|
if not self.tools_used:
|
|
return False
|
|
|
|
execution_tools = {"Bash", "Task"}
|
|
return any(tool in execution_tools for tool in self.tools_used) |