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.
- convoviz/__init__.py +25 -5
- convoviz/__main__.py +6 -5
- convoviz/analysis/__init__.py +9 -0
- convoviz/analysis/graphs.py +98 -0
- convoviz/analysis/wordcloud.py +142 -0
- convoviz/assets/colormaps.txt +15 -16
- convoviz/cli.py +101 -94
- convoviz/config.py +88 -0
- convoviz/exceptions.py +47 -0
- convoviz/interactive.py +178 -0
- convoviz/io/__init__.py +21 -0
- convoviz/io/loaders.py +135 -0
- convoviz/io/writers.py +96 -0
- convoviz/models/__init__.py +26 -6
- convoviz/models/collection.py +107 -0
- convoviz/models/conversation.py +149 -0
- convoviz/models/message.py +77 -0
- convoviz/models/node.py +66 -0
- convoviz/pipeline.py +120 -0
- convoviz/renderers/__init__.py +10 -0
- convoviz/renderers/markdown.py +182 -0
- convoviz/renderers/yaml.py +42 -0
- convoviz/utils.py +68 -237
- {convoviz-0.1.6.dist-info → convoviz-0.2.0.dist-info}/METADATA +61 -42
- {convoviz-0.1.6.dist-info → convoviz-0.2.0.dist-info}/RECORD +27 -17
- convoviz-0.2.0.dist-info/WHEEL +4 -0
- convoviz-0.2.0.dist-info/entry_points.txt +3 -0
- convoviz/configuration.py +0 -125
- convoviz/data_analysis.py +0 -118
- convoviz/long_runs.py +0 -91
- convoviz/models/_conversation.py +0 -288
- convoviz/models/_conversation_set.py +0 -190
- convoviz/models/_message.py +0 -89
- convoviz/models/_node.py +0 -74
- convoviz-0.1.6.dist-info/LICENSE +0 -21
- convoviz-0.1.6.dist-info/WHEEL +0 -4
|
@@ -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
|
convoviz/models/_message.py
DELETED
|
@@ -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"
|
convoviz-0.1.6.dist-info/LICENSE
DELETED
|
@@ -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.
|
convoviz-0.1.6.dist-info/WHEEL
DELETED