Ryan Malloy 44ed9936b7 Initial commit: Claude Code Project Tracker
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>
2025-08-11 02:59:21 -06:00

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)