convoviz 0.1.6__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
@@ -1,190 +0,0 @@
1
- """ConversationSet class to model a set of conversations.
2
-
3
- Groups conversations by week, month, and year, etc.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, Unpack
10
-
11
- from orjson import OPT_INDENT_2, dumps, loads
12
- from pydantic import BaseModel
13
- from tqdm import tqdm
14
-
15
- from convoviz.data_analysis import generate_week_barplot, generate_wordcloud
16
- from convoviz.utils import get_archive, sanitize
17
-
18
- from ._conversation import Conversation # noqa: TCH001
19
-
20
- if TYPE_CHECKING:
21
- from datetime import datetime
22
-
23
- from matplotlib.figure import Figure
24
- from PIL.Image import Image
25
-
26
- from convoviz.utils import GraphKwargs, WordCloudKwargs
27
-
28
- from ._message import AuthorRole
29
-
30
-
31
- class ConversationSet(BaseModel):
32
- """Stores a set of conversations."""
33
-
34
- array: list[Conversation]
35
-
36
- @property
37
- def index(self) -> dict[str, Conversation]:
38
- """Get the index of conversations."""
39
- return {convo.conversation_id: convo for convo in self.array}
40
-
41
- @classmethod
42
- def from_json(cls, filepath: Path | str) -> ConversationSet:
43
- """Load from a JSON file, containing an array of conversations."""
44
- filepath = Path(filepath)
45
- with filepath.open(encoding="utf-8") as file:
46
- return cls(array=loads(file.read()))
47
-
48
- @classmethod
49
- def from_zip(cls, filepath: Path | str) -> ConversationSet:
50
- """Load from a ZIP file, containing a JSON file."""
51
- filepath = Path(filepath)
52
- convos_path = get_archive(filepath) / "conversations.json"
53
-
54
- return cls.from_json(convos_path)
55
-
56
- @property
57
- def last_updated(self) -> datetime:
58
- """Return the timestamp of the last updated conversation in the list."""
59
- return max(conversation.update_time for conversation in self.array)
60
-
61
- def update(self, conv_set: ConversationSet) -> None:
62
- """Update the conversation set with the new one."""
63
- if conv_set.last_updated <= self.last_updated:
64
- return
65
- self.index.update(conv_set.index)
66
- self.array = list(self.index.values())
67
-
68
- def save(self, dir_path: Path | str, *, progress_bar: bool = False) -> None:
69
- """Save the conversation set to the directory."""
70
- dir_path = Path(dir_path)
71
- dir_path.mkdir(parents=True, exist_ok=True)
72
-
73
- for conversation in tqdm(
74
- self.array,
75
- "Writing Markdown 📄 files",
76
- disable=not progress_bar,
77
- ):
78
- filepath = dir_path / sanitize(f"{conversation.title}.md")
79
- conversation.save(filepath)
80
-
81
- @property
82
- def custom_instructions(self) -> list[dict[str, Any]]:
83
- """Get a list of all custom instructions, in all conversations in the set."""
84
- custom_instructions: list[dict[str, Any]] = []
85
-
86
- for conversation in self.array:
87
- if not conversation.custom_instructions:
88
- continue
89
-
90
- instructions_info = {
91
- "chat_title": conversation.title,
92
- "chat_link": conversation.url,
93
- "time": conversation.create_time,
94
- "custom_instructions": conversation.custom_instructions,
95
- }
96
-
97
- custom_instructions.append(instructions_info)
98
-
99
- return custom_instructions
100
-
101
- def save_custom_instructions(self, filepath: Path | str) -> None:
102
- """Save the custom instructions to the file."""
103
- filepath = Path(filepath)
104
- with filepath.open("w", encoding="utf-8") as file:
105
- file.write(dumps(self.custom_instructions, option=OPT_INDENT_2).decode())
106
-
107
- def timestamps(
108
- self,
109
- *authors: AuthorRole,
110
- ) -> list[float]:
111
- """Get a list of all message timestamps, in all conversations in the list."""
112
- timestamps: list[float] = []
113
-
114
- for conversation in self.array:
115
- timestamps.extend(conversation.timestamps(*authors))
116
-
117
- return timestamps
118
-
119
- def week_barplot(
120
- self,
121
- title: str,
122
- *authors: AuthorRole,
123
- **kwargs: Unpack[GraphKwargs],
124
- ) -> Figure:
125
- """Create a bar graph from the given conversation set."""
126
- if len(authors) == 0:
127
- authors = ("user",)
128
- timestamps = self.timestamps(*authors)
129
- return generate_week_barplot(timestamps, title, **kwargs)
130
-
131
- def plaintext(
132
- self,
133
- *authors: AuthorRole,
134
- ) -> str:
135
- """Get a string of all text, in all conversations in the list."""
136
- return "\n".join(
137
- conversation.plaintext(*authors) for conversation in self.array
138
- )
139
-
140
- def wordcloud(
141
- self,
142
- *authors: AuthorRole,
143
- **kwargs: Unpack[WordCloudKwargs],
144
- ) -> Image:
145
- """Create a wordcloud from the given conversation set."""
146
- if len(authors) == 0:
147
- authors = ("user", "assistant")
148
- text = self.plaintext(*authors)
149
- return generate_wordcloud(text, **kwargs)
150
-
151
- def add(self, conv: Conversation) -> None:
152
- """Add a conversation to the dictionary and list."""
153
- self.index[conv.conversation_id] = conv
154
- self.array.append(conv)
155
-
156
- def group_by_week(self) -> dict[datetime, ConversationSet]:
157
- """Get a dictionary of conversations grouped by the start of the week."""
158
- grouped: dict[datetime, ConversationSet] = {}
159
-
160
- for conversation in self.array:
161
- week_start = conversation.week_start
162
- if week_start not in grouped:
163
- grouped[week_start] = ConversationSet(array=[])
164
- grouped[week_start].add(conversation)
165
-
166
- return grouped
167
-
168
- def group_by_month(self) -> dict[datetime, ConversationSet]:
169
- """Get a dictionary of conversations grouped by the start of the month."""
170
- grouped: dict[datetime, ConversationSet] = {}
171
-
172
- for conversation in self.array:
173
- month_start = conversation.month_start
174
- if month_start not in grouped:
175
- grouped[month_start] = ConversationSet(array=[])
176
- grouped[month_start].add(conversation)
177
-
178
- return grouped
179
-
180
- def group_by_year(self) -> dict[datetime, ConversationSet]:
181
- """Get a dictionary of conversations grouped by the start of the year."""
182
- grouped: dict[datetime, ConversationSet] = {}
183
-
184
- for conversation in self.array:
185
- year_start = conversation.year_start
186
- if year_start not in grouped:
187
- grouped[year_start] = ConversationSet(array=[])
188
- grouped[year_start].add(conversation)
189
-
190
- return grouped
@@ -1,89 +0,0 @@
1
- """Represents a single message in a conversation. It's contained in a Node object.
2
-
3
- object path : conversations.json -> conversation -> mapping -> mapping node -> message
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from typing import TYPE_CHECKING, Any, ClassVar, Literal
9
-
10
- from pydantic import BaseModel, ConfigDict
11
-
12
- from convoviz.utils import DEFAULT_MESSAGE_CONFIGS, MessageConfigs, code_block
13
-
14
- if TYPE_CHECKING:
15
- from datetime import datetime
16
-
17
- AuthorRole = Literal["user", "assistant", "system", "tool"]
18
-
19
-
20
- class MessageAuthor(BaseModel):
21
- """Type of the `author` field in a `message`."""
22
-
23
- role: AuthorRole
24
- name: str | None = None
25
- metadata: dict[str, Any]
26
-
27
-
28
- class MessageContent(BaseModel):
29
- """Type of the `content` field in a `message`."""
30
-
31
- content_type: str
32
- parts: list[str] | None = None
33
- text: str | None = None
34
- result: str | None = None
35
-
36
-
37
- class MessageMetadata(BaseModel):
38
- """Type of the `metadata` field in a `message`."""
39
-
40
- model_slug: str | None = None
41
- invoked_plugin: dict[str, Any] | None = None
42
- is_user_system_message: bool | None = None
43
- user_context_message_data: dict[str, Any] | None = None
44
-
45
- model_config = ConfigDict(protected_namespaces=())
46
-
47
-
48
- class Message(BaseModel):
49
- """Wrapper class for the `message` field in a `node`.
50
-
51
- see `MessageJSON` and `models.Node` for more details
52
- """
53
-
54
- __configs: ClassVar[MessageConfigs] = DEFAULT_MESSAGE_CONFIGS
55
-
56
- id: str # noqa: A003
57
- author: MessageAuthor
58
- create_time: datetime | None = None
59
- update_time: datetime | None = None
60
- content: MessageContent
61
- status: str
62
- end_turn: bool | None = None
63
- weight: float
64
- metadata: MessageMetadata
65
- recipient: str
66
-
67
- @classmethod
68
- def update_configs(cls, configs: MessageConfigs) -> None:
69
- """Set the configuration for all messages."""
70
- cls.__configs.update(configs)
71
-
72
- @property
73
- def header(self) -> str:
74
- """Get the title header of the message based on the configs."""
75
- return self.__configs["author_headers"][self.author.role]
76
-
77
- @property
78
- def text(self) -> str:
79
- """Get the text content of the message."""
80
- if self.content.parts is not None:
81
- return str(self.content.parts[0])
82
- if self.content.text is not None:
83
- return code_block(self.content.text)
84
- if self.content.result is not None:
85
- return self.content.result
86
-
87
- # this error caught some hidden bugs in the data. need more of these
88
- err_msg = f"No valid content found in message: {self.id}"
89
- raise ValueError(err_msg)
convoviz/models/_node.py DELETED
@@ -1,74 +0,0 @@
1
- """Node class and methods for the node object in a conversation.
2
-
3
- object path : conversations.json -> conversation -> mapping -> mapping node
4
-
5
- will implement methods to handle conversation branches, like
6
- counting the number of branches,
7
- get the branch of a given node,
8
- and some other version control stuff
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- from pydantic import BaseModel
14
-
15
- from ._message import Message # noqa: TCH001
16
-
17
-
18
- class Node(BaseModel):
19
- """Wrapper class for a `node` in the `mapping` field of a `conversation`."""
20
-
21
- id: str # noqa: A003
22
- message: Message | None = None
23
- parent: str | None = None
24
- children: list[str]
25
- parent_node: Node | None = None
26
- children_nodes: list[Node] = []
27
-
28
- def add_child(self, node: Node) -> None:
29
- """Add a child to the node."""
30
- self.children_nodes.append(node)
31
- node.parent_node = self
32
-
33
- @classmethod
34
- def mapping(cls, mapping: dict[str, Node]) -> dict[str, Node]:
35
- """Return a dictionary of connected Node objects, based on the mapping."""
36
- # Initialize connections
37
- for node in mapping.values():
38
- node.children_nodes = [] # Ensure list is empty to avoid duplicates
39
- node.parent_node = None # Ensure parent_node is None
40
-
41
- # Connect nodes
42
- for node in mapping.values():
43
- for child_id in node.children:
44
- child_node = mapping[child_id]
45
- node.add_child(child_node)
46
-
47
- return mapping
48
-
49
- @property
50
- def header(self) -> str:
51
- """Get the header of the node message, containing a link to its parent."""
52
- if self.message is None:
53
- return ""
54
-
55
- parent_link = (
56
- f"[parent ⬆️](#{self.parent_node.id})\n"
57
- if self.parent_node and self.parent_node.message
58
- else ""
59
- )
60
- return f"###### {self.id}\n{parent_link}{self.message.header}\n"
61
-
62
- @property
63
- def footer(self) -> str:
64
- """Get the footer of the node message, containing links to its children."""
65
- if len(self.children_nodes) == 0:
66
- return ""
67
- if len(self.children_nodes) == 1:
68
- return f"\n[child ⬇️](#{self.children_nodes[0].id})\n"
69
-
70
- footer = "\n" + " | ".join(
71
- f"[child {i+1} ⬇️](#{child.id})"
72
- for i, child in enumerate(self.children_nodes)
73
- )
74
- return footer + "\n"
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 Mohamed Cheikh Sidiya
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any