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>
This commit is contained in:
commit
44ed9936b7
48 changed files with 9707 additions and 0 deletions
118
app/models/conversation.py
Normal file
118
app/models/conversation.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
"""
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue