convoviz 0.2.7__tar.gz → 0.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {convoviz-0.2.7 → convoviz-0.2.9}/PKG-INFO +2 -3
  2. {convoviz-0.2.7 → convoviz-0.2.9}/README.md +1 -2
  3. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/analysis/wordcloud.py +5 -2
  4. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/config.py +2 -2
  5. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/io/assets.py +16 -0
  6. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/io/writers.py +69 -9
  7. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/models/message.py +70 -9
  8. {convoviz-0.2.7 → convoviz-0.2.9}/pyproject.toml +1 -1
  9. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/__init__.py +0 -0
  10. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/__main__.py +0 -0
  11. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/analysis/__init__.py +0 -0
  12. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/analysis/graphs.py +0 -0
  13. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/colormaps.txt +0 -0
  14. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/AmaticSC-Regular.ttf +0 -0
  15. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/ArchitectsDaughter-Regular.ttf +0 -0
  16. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/BebasNeue-Regular.ttf +0 -0
  17. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Borel-Regular.ttf +0 -0
  18. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Courgette-Regular.ttf +0 -0
  19. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/CroissantOne-Regular.ttf +0 -0
  20. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Handjet-Regular.ttf +0 -0
  21. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/IndieFlower-Regular.ttf +0 -0
  22. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Kalam-Regular.ttf +0 -0
  23. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Lobster-Regular.ttf +0 -0
  24. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/MartianMono-Regular.ttf +0 -0
  25. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/MartianMono-Thin.ttf +0 -0
  26. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Montserrat-Regular.ttf +0 -0
  27. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Mooli-Regular.ttf +0 -0
  28. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Pacifico-Regular.ttf +0 -0
  29. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/PlayfairDisplay-Regular.ttf +0 -0
  30. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Raleway-Regular.ttf +0 -0
  31. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/RobotoMono-Regular.ttf +0 -0
  32. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/RobotoMono-Thin.ttf +0 -0
  33. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/RobotoSlab-Regular.ttf +0 -0
  34. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/RobotoSlab-Thin.ttf +0 -0
  35. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Ruwudu-Regular.ttf +0 -0
  36. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Sacramento-Regular.ttf +0 -0
  37. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/SedgwickAveDisplay-Regular.ttf +0 -0
  38. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/ShadowsIntoLight-Regular.ttf +0 -0
  39. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/TitilliumWeb-Regular.ttf +0 -0
  40. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Yellowtail-Regular.ttf +0 -0
  41. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/YsabeauOffice-Regular.ttf +0 -0
  42. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/YsabeauSC-Regular.ttf +0 -0
  43. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/YsabeauSC-Thin.ttf +0 -0
  44. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/fonts/Zeyada-Regular.ttf +0 -0
  45. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/assets/stopwords.txt +0 -0
  46. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/cli.py +0 -0
  47. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/exceptions.py +0 -0
  48. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/interactive.py +0 -0
  49. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/io/__init__.py +0 -0
  50. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/io/loaders.py +0 -0
  51. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/models/__init__.py +0 -0
  52. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/models/collection.py +0 -0
  53. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/models/conversation.py +0 -0
  54. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/models/node.py +0 -0
  55. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/pipeline.py +0 -0
  56. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/py.typed +0 -0
  57. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/renderers/__init__.py +0 -0
  58. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/renderers/markdown.py +0 -0
  59. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/renderers/yaml.py +0 -0
  60. {convoviz-0.2.7 → convoviz-0.2.9}/convoviz/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: convoviz
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Get analytics and visualizations on your ChatGPT data!
5
5
  Keywords: markdown,chatgpt,openai,visualization,analytics,json,export,data-analysis,obsidian
6
6
  Author: Mohamed Cheikh Sidiya
@@ -34,8 +34,7 @@ Convert your ChatGPT history into well-formatted Markdown files. Additionally, v
34
34
  ## Features
35
35
 
36
36
  - **YAML Headers**: Optional and included by default.
37
- - **Track message versions**: prompt/response edits included.
38
- - **Code Interpreter**: Environment code blocks and execution results.
37
+ - **Inline Images**: Media attachments rendered directly in Markdown.
39
38
  - **Data Visualizations**: Word clouds, graphs, and more.
40
39
  - **Custom Instructions**: All your custom instructions in one JSON file.
41
40
 
@@ -8,8 +8,7 @@ Convert your ChatGPT history into well-formatted Markdown files. Additionally, v
8
8
  ## Features
9
9
 
10
10
  - **YAML Headers**: Optional and included by default.
11
- - **Track message versions**: prompt/response edits included.
12
- - **Code Interpreter**: Environment code blocks and execution results.
11
+ - **Inline Images**: Media attachments rendered directly in Markdown.
13
12
  - **Data Visualizations**: Word clouds, graphs, and more.
14
13
  - **Custom Instructions**: All your custom instructions in one JSON file.
15
14
 
@@ -139,7 +139,8 @@ def generate_wordclouds(
139
139
  text = group.plaintext("user", "assistant")
140
140
  if text.strip():
141
141
  img = generate_wordcloud(text, config)
142
- img.save(output_dir / f"{week.strftime('%Y week %W')}.png", optimize=True)
142
+ # Format: 2024-W15.png (ISO week format)
143
+ img.save(output_dir / f"{week.strftime('%Y-W%W')}.png", optimize=True)
143
144
 
144
145
  for month, group in tqdm(
145
146
  month_groups.items(),
@@ -149,7 +150,8 @@ def generate_wordclouds(
149
150
  text = group.plaintext("user", "assistant")
150
151
  if text.strip():
151
152
  img = generate_wordcloud(text, config)
152
- img.save(output_dir / f"{month.strftime('%Y %B')}.png", optimize=True)
153
+ # Format: 2024-03-March.png (consistent with folder naming)
154
+ img.save(output_dir / f"{month.strftime('%Y-%m-%B')}.png", optimize=True)
153
155
 
154
156
  for year, group in tqdm(
155
157
  year_groups.items(),
@@ -159,4 +161,5 @@ def generate_wordclouds(
159
161
  text = group.plaintext("user", "assistant")
160
162
  if text.strip():
161
163
  img = generate_wordcloud(text, config)
164
+ # Format: 2024.png
162
165
  img.save(output_dir / f"{year.strftime('%Y')}.png", optimize=True)
@@ -10,8 +10,8 @@ from pydantic import BaseModel, Field
10
10
  class FolderOrganization(str, Enum):
11
11
  """How to organize markdown output files in folders."""
12
12
 
13
- FLAT = "flat" # All files in one directory (default)
14
- DATE = "date" # Nested by year/month/week
13
+ FLAT = "flat" # All files in one directory
14
+ DATE = "date" # Nested by year/month (default)
15
15
 
16
16
 
17
17
  class AuthorHeaders(BaseModel):
@@ -57,6 +57,22 @@ def resolve_asset_path(source_dir: Path, asset_id: str) -> Path | None:
57
57
  except Exception:
58
58
  pass
59
59
 
60
+ # 4. Try prefix match in user-* directories (new 2025 format)
61
+ try:
62
+ for user_dir in source_dir.glob("user-*"):
63
+ if user_dir.is_dir():
64
+ user_dir = user_dir.resolve()
65
+ candidates = list(user_dir.glob(f"{asset_id}*"))
66
+ files = [
67
+ p.resolve()
68
+ for p in candidates
69
+ if p.is_file() and p.resolve().is_relative_to(user_dir)
70
+ ]
71
+ if files:
72
+ return files[0]
73
+ except Exception:
74
+ pass
75
+
60
76
  return None
61
77
 
62
78
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from os import utime as os_utime
4
4
  from pathlib import Path
5
+ from urllib.parse import quote
5
6
 
6
7
  from orjson import OPT_INDENT_2, dumps
7
8
  from tqdm import tqdm
@@ -32,8 +33,8 @@ _MONTH_NAMES = [
32
33
  def get_date_folder_path(conversation: Conversation) -> Path:
33
34
  """Get the date-based folder path for a conversation.
34
35
 
35
- Creates a nested structure: year/month/week
36
- Example: 2024/03-March/Week-02/
36
+ Creates a nested structure: year/month
37
+ Example: 2024/03-March/
37
38
 
38
39
  Args:
39
40
  conversation: The conversation to get the path for
@@ -51,13 +52,7 @@ def get_date_folder_path(conversation: Conversation) -> Path:
51
52
  month_name = _MONTH_NAMES[month_num - 1]
52
53
  month = f"{month_num:02d}-{month_name}"
53
54
 
54
- # Week folder: "Week-01" through "Week-05" (week of the month)
55
- # Calculate which week of the month this date falls into
56
- day = create_time.day
57
- week_of_month = (day - 1) // 7 + 1
58
- week = f"Week-{week_of_month:02d}"
59
-
60
- return Path(year) / month / week
55
+ return Path(year) / month
61
56
 
62
57
 
63
58
  def save_conversation(
@@ -115,6 +110,62 @@ def save_conversation(
115
110
  return final_path
116
111
 
117
112
 
113
+ def _generate_year_index(year_dir: Path, year: str) -> None:
114
+ """Generate a _index.md file for a year folder.
115
+
116
+ Args:
117
+ year_dir: Path to the year directory
118
+ year: The year string (e.g., "2024")
119
+ """
120
+ months = sorted(
121
+ [d.name for d in year_dir.iterdir() if d.is_dir()],
122
+ key=lambda m: int(m.split("-")[0]),
123
+ )
124
+
125
+ lines = [
126
+ f"# {year}",
127
+ "",
128
+ "## Months",
129
+ "",
130
+ ]
131
+
132
+ for month in months:
133
+ month_name = month.split("-", 1)[1] if "-" in month else month
134
+ lines.append(f"- [{month_name}]({month}/_index.md)")
135
+
136
+ index_path = year_dir / "_index.md"
137
+ index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
138
+
139
+
140
+ def _generate_month_index(month_dir: Path, year: str, month: str) -> None:
141
+ """Generate a _index.md file for a month folder.
142
+
143
+ Args:
144
+ month_dir: Path to the month directory
145
+ year: The year string (e.g., "2024")
146
+ month: The month folder name (e.g., "03-March")
147
+ """
148
+ month_name = month.split("-", 1)[1] if "-" in month else month
149
+ files = sorted(
150
+ [f.name for f in month_dir.glob("*.md") if f.name != "_index.md"]
151
+ )
152
+
153
+ lines = [
154
+ f"# {month_name} {year}",
155
+ "",
156
+ "## Conversations",
157
+ "",
158
+ ]
159
+
160
+ for file in files:
161
+ title = file[:-3] # Remove .md extension
162
+ encoded_file = quote(file)
163
+ lines.append(f"- [{title}]({encoded_file})")
164
+
165
+ index_path = month_dir / "_index.md"
166
+ index_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
167
+
168
+
118
169
  def save_collection(
119
170
  collection: ConversationCollection,
120
171
  directory: Path,
@@ -151,6 +202,15 @@ def save_collection(
151
202
  filepath = target_dir / f"{sanitize(conv.title)}.md"
152
203
  save_conversation(conv, filepath, config, headers, source_path=collection.source_path)
153
204
 
205
+ # Generate index files for date organization
206
+ if folder_organization == FolderOrganization.DATE:
207
+ for year_dir in directory.iterdir():
208
+ if year_dir.is_dir() and year_dir.name.isdigit():
209
+ for month_dir in year_dir.iterdir():
210
+ if month_dir.is_dir():
211
+ _generate_month_index(month_dir, year_dir.name, month_dir.name)
212
+ _generate_year_index(year_dir, year_dir.name)
213
+
154
214
 
155
215
  def save_custom_instructions(
156
216
  collection: ConversationCollection,
@@ -28,6 +28,14 @@ class MessageContent(BaseModel):
28
28
  parts: list[Any] | None = None
29
29
  text: str | None = None
30
30
  result: str | None = None
31
+ # reasoning_recap content type
32
+ content: str | None = None
33
+ # thoughts content type (list of thought objects with summary/content/finished)
34
+ thoughts: list[Any] | None = None
35
+ # tether_quote content type
36
+ url: str | None = None
37
+ domain: str | None = None
38
+ title: str | None = None
31
39
 
32
40
 
33
41
  class MessageMetadata(BaseModel):
@@ -36,6 +44,7 @@ class MessageMetadata(BaseModel):
36
44
  model_slug: str | None = None
37
45
  invoked_plugin: dict[str, Any] | None = None
38
46
  is_user_system_message: bool | None = None
47
+ is_visually_hidden_from_conversation: bool | None = None
39
48
  user_context_message_data: dict[str, Any] | None = None
40
49
 
41
50
  model_config = ConfigDict(protected_namespaces=())
@@ -105,12 +114,48 @@ class Message(BaseModel):
105
114
  if self.content.parts:
106
115
  return ""
107
116
 
117
+ # tether_quote: render as a blockquote with attribution (check before .text)
118
+ if self.content.content_type == "tether_quote":
119
+ return self._render_tether_quote()
108
120
  if self.content.text is not None:
109
121
  return self.content.text
110
122
  if self.content.result is not None:
111
123
  return self.content.result
124
+ # reasoning_recap content type uses 'content' field
125
+ if self.content.content is not None:
126
+ return self.content.content
127
+ # thoughts content type uses 'thoughts' field (list of thought objects)
128
+ if self.content.thoughts is not None:
129
+ return self._render_thoughts()
112
130
  raise MessageContentError(self.id)
113
131
 
132
+ def _render_thoughts(self) -> str:
133
+ """Render thoughts content (list of thought objects with summary/content)."""
134
+ if not self.content.thoughts:
135
+ return ""
136
+ summaries = []
137
+ for thought in self.content.thoughts:
138
+ if isinstance(thought, dict) and (summary := thought.get("summary")):
139
+ summaries.append(summary)
140
+ return "\n".join(summaries) if summaries else ""
141
+
142
+ def _render_tether_quote(self) -> str:
143
+ """Render tether_quote content as a blockquote."""
144
+ quote_text = self.content.text or ""
145
+ if not quote_text.strip():
146
+ return ""
147
+ # Format as blockquote with source
148
+ lines = [f"> {line}" for line in quote_text.strip().split("\n")]
149
+ blockquote = "\n".join(lines)
150
+ # Add attribution if we have title/domain/url
151
+ if self.content.title and self.content.url:
152
+ blockquote += f"\n> — [{self.content.title}]({self.content.url})"
153
+ elif self.content.domain and self.content.url:
154
+ blockquote += f"\n> — [{self.content.domain}]({self.content.url})"
155
+ elif self.content.url:
156
+ blockquote += f"\n> — <{self.content.url}>"
157
+ return blockquote
158
+
114
159
  @property
115
160
  def has_content(self) -> bool:
116
161
  """Check if the message has extractable content."""
@@ -132,26 +177,42 @@ class Message(BaseModel):
132
177
 
133
178
  Hidden if:
134
179
  1. It is empty (no text, no images).
135
- 2. It is an internal system message (not custom instructions).
136
- 3. It is a browser tool output (intermediate search steps).
180
+ 2. Explicitly marked as visually hidden.
181
+ 3. It is an internal system message (not custom instructions).
182
+ 4. It is a browser tool output (intermediate search steps).
183
+ 5. It is an assistant message targeting a tool (internal call).
184
+ 6. It is code interpreter input (content_type="code").
185
+ 7. It is browsing status (tether_browsing_display).
186
+ 8. It is internal reasoning (thoughts, reasoning_recap from o1/o3).
137
187
  """
138
188
  if self.is_empty:
139
189
  return True
140
190
 
191
+ # Explicitly marked as hidden by OpenAI
192
+ if self.metadata.is_visually_hidden_from_conversation:
193
+ return True
194
+
141
195
  # Hide internal system messages
142
196
  if self.author.role == "system":
143
197
  # Only show if explicitly marked as user system message (Custom Instructions)
144
198
  return not self.metadata.is_user_system_message
145
199
 
146
- # Hide browser tool outputs (usually intermediate search steps)
200
+ # Hide browser tool outputs (intermediate search steps)
147
201
  if self.author.role == "tool" and self.author.name == "browser":
148
202
  return True
149
203
 
150
- # Hide assistant calls to browser tool (e.g. "search(...)") or code interpreter
151
- if self.author.role == "assistant" and (
152
- self.recipient == "browser" or self.content.content_type == "code"
153
- ):
204
+ # Hide assistant messages targeting tools (e.g., search(...), code input)
205
+ # recipient="all" or None means it's for the user; anything else is internal
206
+ if self.author.role == "assistant" and self.recipient not in ("all", None):
154
207
  return True
155
208
 
156
- # Hide browsing status messages
157
- return self.content.content_type == "tether_browsing_display"
209
+ # Hide code interpreter input (content_type="code")
210
+ if self.author.role == "assistant" and self.content.content_type == "code":
211
+ return True
212
+
213
+ # Hide browsing status and internal reasoning steps (o1/o3 models)
214
+ return self.content.content_type in (
215
+ "tether_browsing_display",
216
+ "thoughts",
217
+ "reasoning_recap",
218
+ )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "convoviz"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "Get analytics and visualizations on your ChatGPT data!"
5
5
  license = "MIT"
6
6
  keywords = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes