-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconversation.py
More file actions
108 lines (87 loc) · 3.49 KB
/
conversation.py
File metadata and controls
108 lines (87 loc) · 3.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# Represents a Claude conversation with metadata and messages. Provides
# filename generation, date-based folder organization, and title
# sanitization for the HTML export pipeline.
from dataclasses import dataclass, field
from datetime import datetime
from zoneinfo import ZoneInfo
from chat_message import ChatMessage
UNSAFE_CHARS = frozenset("#'&?!@$%^*()+=[]{}<>|\\/:;\"~`")
@dataclass
class Conversation:
"""A complete Claude conversation with metadata and messages.
Attributes:
uuid: Unique conversation identifier.
name: Conversation title.
summary: Optional conversation summary.
created_at: ISO 8601 creation timestamp.
updated_at: ISO 8601 last-updated timestamp.
account_uuid: UUID of the owning account.
chat_messages: Ordered list of messages in this conversation.
"""
uuid: str
name: str
summary: str
created_at: str
updated_at: str
account_uuid: str
chat_messages: list[ChatMessage] = field(default_factory=list)
@classmethod
def from_dict(cls, data: dict) -> "Conversation":
"""Parse a Conversation from a raw JSON dict.
Args:
data: Dict from the top-level conversations JSON array.
Returns:
A populated Conversation instance with parsed messages.
"""
messages = [ChatMessage.from_dict(m) for m in data.get("chat_messages", [])]
return cls(
uuid=data["uuid"],
name=data.get("name", "") or "Untitled",
summary=data.get("summary", ""),
created_at=data["created_at"],
updated_at=data["updated_at"],
account_uuid=data.get("account", {}).get("uuid", ""),
chat_messages=messages,
)
def created_datetime(self, tz: ZoneInfo | None = None) -> datetime:
"""Return created_at as a timezone-aware datetime.
Args:
tz: Target timezone. If None, returns UTC.
Returns:
The creation datetime in the specified timezone.
"""
dt = datetime.fromisoformat(self.created_at)
if tz:
dt = dt.astimezone(tz)
return dt
def date_prefix(self, tz: ZoneInfo | None = None) -> str:
"""Return the creation date as a YYYYmmdd string.
Args:
tz: Target timezone for date calculation.
"""
return self.created_datetime(tz).strftime("%Y%m%d")
def sanitized_title(self) -> str:
"""Return the conversation title with unsafe characters removed.
Replaces spaces with underscores, collapses runs of underscores,
and truncates to 80 characters.
"""
title = self.name or "Untitled"
title = "".join(c for c in title if c not in UNSAFE_CHARS)
title = title.replace(" ", "_")
while "__" in title:
title = title.replace("__", "_")
title = title.strip("_")
return title[:80] if title else "Untitled"
def filename_stem(self, tz: ZoneInfo | None = None) -> str:
"""Return the base filename without extension (e.g. '20250310--My_Chat').
Args:
tz: Target timezone for the date prefix.
"""
return f"{self.date_prefix(tz)}--{self.sanitized_title()}"
def folder_path(self, tz: ZoneInfo | None = None) -> str:
"""Return the date-based folder path (e.g. '2025/03').
Args:
tz: Target timezone for folder calculation.
"""
dt = self.created_datetime(tz)
return f"{dt.year}/{dt.month:02d}"