mindroot 8.13.0__py3-none-any.whl → 8.14.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.
- mindroot/coreplugins/chat/router.py +2 -2
- mindroot/lib/chatcontext.py +28 -23
- mindroot/lib/chatlog.py +103 -43
- mindroot/lib/token_counter.py +33 -27
- {mindroot-8.13.0.dist-info → mindroot-8.14.0.dist-info}/METADATA +1 -1
- {mindroot-8.13.0.dist-info → mindroot-8.14.0.dist-info}/RECORD +10 -10
- {mindroot-8.13.0.dist-info → mindroot-8.14.0.dist-info}/WHEEL +0 -0
- {mindroot-8.13.0.dist-info → mindroot-8.14.0.dist-info}/entry_points.txt +0 -0
- {mindroot-8.13.0.dist-info → mindroot-8.14.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-8.13.0.dist-info → mindroot-8.14.0.dist-info}/top_level.txt +0 -0
|
@@ -237,7 +237,7 @@ async def get_token_count(request: Request, log_id: str):
|
|
|
237
237
|
Returns:
|
|
238
238
|
- JSON with token counts or error message if log not found
|
|
239
239
|
"""
|
|
240
|
-
token_counts = count_tokens_for_log_id(log_id)
|
|
240
|
+
token_counts = await count_tokens_for_log_id(log_id)
|
|
241
241
|
|
|
242
242
|
if token_counts is None:
|
|
243
243
|
return {"status": "error", "message": f"Chat log with ID {log_id} not found"}
|
|
@@ -301,7 +301,7 @@ async def get_token_count(request: Request, log_id: str):
|
|
|
301
301
|
"""
|
|
302
302
|
from lib.token_counter import count_tokens_for_log_id
|
|
303
303
|
|
|
304
|
-
token_counts = count_tokens_for_log_id(log_id)
|
|
304
|
+
token_counts = await count_tokens_for_log_id(log_id)
|
|
305
305
|
|
|
306
306
|
if token_counts is None:
|
|
307
307
|
return {"status": "error", "message": f"Chat log with ID {log_id} not found"}
|
mindroot/lib/chatcontext.py
CHANGED
|
@@ -2,6 +2,9 @@ from .providers.services import service_manager
|
|
|
2
2
|
from .providers.commands import command_manager
|
|
3
3
|
import os
|
|
4
4
|
import json
|
|
5
|
+
import asyncio
|
|
6
|
+
import aiofiles
|
|
7
|
+
import aiofiles.os
|
|
5
8
|
from .chatlog import ChatLog
|
|
6
9
|
from .chatlog import extract_delegate_task_log_ids, find_child_logs_by_parent_id, find_chatlog_file
|
|
7
10
|
from typing import TypeVar, Type, Protocol, runtime_checkable, Set
|
|
@@ -85,31 +88,32 @@ class ChatContext:
|
|
|
85
88
|
def cmds(self, command_set: Type[CommandSetT]) -> CommandSetT:
|
|
86
89
|
return self._commands[command_set]
|
|
87
90
|
|
|
88
|
-
def save_context_data(self):
|
|
91
|
+
async def save_context_data(self):
|
|
89
92
|
if not self.log_id:
|
|
90
93
|
raise ValueError('log_id is not set for the context.')
|
|
91
94
|
else:
|
|
92
95
|
pass
|
|
93
96
|
context_file = f'data/context/{self.username}/context_{self.log_id}.json'
|
|
94
|
-
os.makedirs(os.path.dirname(context_file), exist_ok=True)
|
|
97
|
+
await aiofiles.os.makedirs(os.path.dirname(context_file), exist_ok=True)
|
|
95
98
|
try:
|
|
96
|
-
with open(context_file, 'r') as f:
|
|
97
|
-
|
|
99
|
+
async with aiofiles.open(context_file, 'r') as f:
|
|
100
|
+
content = await f.read()
|
|
101
|
+
context_data = json.loads(content)
|
|
98
102
|
except FileNotFoundError:
|
|
99
103
|
context_data = {}
|
|
100
104
|
finally:
|
|
101
105
|
pass
|
|
102
106
|
context_data['data'] = self.data
|
|
103
|
-
with open(context_file, 'w') as f:
|
|
104
|
-
json.
|
|
107
|
+
async with aiofiles.open(context_file, 'w') as f:
|
|
108
|
+
await f.write(json.dumps(context_data, indent=2))
|
|
105
109
|
|
|
106
|
-
def save_context(self):
|
|
110
|
+
async def save_context(self):
|
|
107
111
|
if not self.log_id:
|
|
108
112
|
raise ValueError('log_id is not set for the context.')
|
|
109
113
|
else:
|
|
110
114
|
pass
|
|
111
115
|
context_file = f'data/context/{self.username}/context_{self.log_id}.json'
|
|
112
|
-
os.makedirs(os.path.dirname(context_file), exist_ok=True)
|
|
116
|
+
await aiofiles.os.makedirs(os.path.dirname(context_file), exist_ok=True)
|
|
113
117
|
self.data['log_id'] = self.log_id
|
|
114
118
|
context_data = {'data': self.data, 'chat_log': self.chat_log._get_log_data()}
|
|
115
119
|
if 'name' in self.agent:
|
|
@@ -124,15 +128,15 @@ class ChatContext:
|
|
|
124
128
|
raise ValueError('Tried to save chat context, but agent name not found in context')
|
|
125
129
|
else:
|
|
126
130
|
pass
|
|
127
|
-
with open(context_file, 'w') as f:
|
|
128
|
-
json.
|
|
129
|
-
|
|
131
|
+
async with aiofiles.open(context_file, 'w') as f:
|
|
132
|
+
await f.write(json.dumps(context_data, indent=2))
|
|
130
133
|
async def load_context(self, log_id):
|
|
131
134
|
self.log_id = log_id
|
|
132
135
|
context_file = f'data/context/{self.username}/context_{log_id}.json'
|
|
133
|
-
if os.path.exists(context_file):
|
|
134
|
-
with open(context_file, 'r') as f:
|
|
135
|
-
|
|
136
|
+
if await aiofiles.os.path.exists(context_file):
|
|
137
|
+
async with aiofiles.open(context_file, 'r') as f:
|
|
138
|
+
content = await f.read()
|
|
139
|
+
context_data = json.loads(content)
|
|
136
140
|
self.data = context_data.get('data', {})
|
|
137
141
|
if 'agent_name' in context_data and context_data.get('agent_name') is not None:
|
|
138
142
|
self.agent_name = context_data.get('agent_name')
|
|
@@ -186,22 +190,23 @@ class ChatContext:
|
|
|
186
190
|
chatlog_dir_base = os.environ.get('CHATLOG_DIR', 'data/chat')
|
|
187
191
|
chatlog_file_path_current = os.path.join(chatlog_dir_base, user, agent, f'chatlog_{log_id}.json')
|
|
188
192
|
|
|
189
|
-
if os.path.exists(chatlog_file_path_current):
|
|
193
|
+
if await aiofiles.os.path.exists(chatlog_file_path_current):
|
|
190
194
|
try:
|
|
191
|
-
with open(chatlog_file_path_current, 'r') as f:
|
|
192
|
-
|
|
195
|
+
async with aiofiles.open(chatlog_file_path_current, 'r') as f:
|
|
196
|
+
content = await f.read()
|
|
197
|
+
log_data = json.loads(content)
|
|
193
198
|
messages_for_child_finding = log_data.get('messages', [])
|
|
194
199
|
except Exception as e:
|
|
195
200
|
print(f"Error reading chatlog {chatlog_file_path_current} for child finding: {e}")
|
|
196
201
|
|
|
197
202
|
delegated_child_ids = extract_delegate_task_log_ids(messages_for_child_finding)
|
|
198
|
-
parented_child_ids = find_child_logs_by_parent_id(log_id)
|
|
203
|
+
parented_child_ids = await find_child_logs_by_parent_id(log_id)
|
|
199
204
|
all_child_log_ids = set(delegated_child_ids) | set(parented_child_ids)
|
|
200
205
|
|
|
201
206
|
for child_id in all_child_log_ids:
|
|
202
207
|
if child_id in visited_log_ids: # Check again before processing child
|
|
203
208
|
continue
|
|
204
|
-
child_log_path = find_chatlog_file(child_id) # This searches across all users/agents
|
|
209
|
+
child_log_path = await find_chatlog_file(child_id) # This searches across all users/agents
|
|
205
210
|
if child_log_path:
|
|
206
211
|
try:
|
|
207
212
|
relative_path = os.path.relpath(child_log_path, chatlog_dir_base)
|
|
@@ -222,9 +227,9 @@ class ChatContext:
|
|
|
222
227
|
# --- Delete Current Session's Files ---
|
|
223
228
|
# ChatLog File
|
|
224
229
|
chatlog_file_to_delete = os.path.join(os.environ.get('CHATLOG_DIR', 'data/chat'), user, agent, f'chatlog_{log_id}.json')
|
|
225
|
-
if os.path.exists(chatlog_file_to_delete):
|
|
230
|
+
if await aiofiles.os.path.exists(chatlog_file_to_delete):
|
|
226
231
|
try:
|
|
227
|
-
os.remove(chatlog_file_to_delete)
|
|
232
|
+
await aiofiles.os.remove(chatlog_file_to_delete)
|
|
228
233
|
print(f"Deleted chatlog file: {chatlog_file_to_delete}")
|
|
229
234
|
except Exception as e:
|
|
230
235
|
print(f"Error deleting chatlog file {chatlog_file_to_delete}: {e}")
|
|
@@ -233,9 +238,9 @@ class ChatContext:
|
|
|
233
238
|
|
|
234
239
|
# ChatContext File (Agent is not part of the context file path structure)
|
|
235
240
|
context_file_to_delete = os.path.join('data/context', user, f'context_{log_id}.json')
|
|
236
|
-
if os.path.exists(context_file_to_delete):
|
|
241
|
+
if await aiofiles.os.path.exists(context_file_to_delete):
|
|
237
242
|
try:
|
|
238
|
-
os.remove(context_file_to_delete)
|
|
243
|
+
await aiofiles.os.remove(context_file_to_delete)
|
|
239
244
|
print(f"Deleted context file: {context_file_to_delete}")
|
|
240
245
|
except Exception as e:
|
|
241
246
|
print(f"Error deleting context file {context_file_to_delete}: {e}")
|
mindroot/lib/chatlog.py
CHANGED
|
@@ -5,6 +5,9 @@ import sys
|
|
|
5
5
|
import traceback
|
|
6
6
|
import re
|
|
7
7
|
import time
|
|
8
|
+
import asyncio
|
|
9
|
+
import aiofiles
|
|
10
|
+
import aiofiles.os
|
|
8
11
|
from mindroot.lib.utils.debug import debug_box
|
|
9
12
|
|
|
10
13
|
class ChatLog:
|
|
@@ -32,7 +35,9 @@ class ChatLog:
|
|
|
32
35
|
self.log_dir = os.path.join(self.log_dir, self.agent)
|
|
33
36
|
if not os.path.exists(self.log_dir):
|
|
34
37
|
os.makedirs(self.log_dir)
|
|
35
|
-
|
|
38
|
+
# For backward compatibility, we'll load synchronously in constructor
|
|
39
|
+
# but provide async methods for new code
|
|
40
|
+
self._load_log_sync()
|
|
36
41
|
|
|
37
42
|
def _get_log_data(self) -> Dict[str, any]:
|
|
38
43
|
return {
|
|
@@ -45,7 +50,45 @@ class ChatLog:
|
|
|
45
50
|
def _calculate_message_length(self, message: Dict[str, str]) -> int:
|
|
46
51
|
return len(json.dumps(message)) // 3
|
|
47
52
|
|
|
53
|
+
def _load_log_sync(self, log_id=None) -> None:
|
|
54
|
+
"""Synchronous version for backward compatibility"""
|
|
55
|
+
if log_id is None:
|
|
56
|
+
log_id = self.log_id
|
|
57
|
+
self.log_id = log_id
|
|
58
|
+
log_file = os.path.join(self.log_dir, f'chatlog_{log_id}.json')
|
|
59
|
+
if os.path.exists(log_file):
|
|
60
|
+
with open(log_file, 'r') as f:
|
|
61
|
+
log_data = json.load(f)
|
|
62
|
+
self.agent = log_data.get('agent')
|
|
63
|
+
self.messages = log_data.get('messages', [])
|
|
64
|
+
self.parent_log_id = log_data.get('parent_log_id', None)
|
|
65
|
+
print("Loaded log file at ", log_file)
|
|
66
|
+
print("Message length: ", len(self.messages))
|
|
67
|
+
else:
|
|
68
|
+
print("Could not find log file at ", log_file)
|
|
69
|
+
self.messages = []
|
|
70
|
+
|
|
71
|
+
def _save_log_sync(self) -> None:
|
|
72
|
+
"""Synchronous version for backward compatibility"""
|
|
73
|
+
log_file = os.path.join(self.log_dir, f'chatlog_{self.log_id}.json')
|
|
74
|
+
with open(log_file, 'w') as f:
|
|
75
|
+
json.dump(self._get_log_data(), f, indent=2)
|
|
76
|
+
|
|
48
77
|
def add_message(self, message: Dict[str, str]) -> None:
|
|
78
|
+
"""Synchronous version for backward compatibility"""
|
|
79
|
+
should_save = self._add_message_impl(message)
|
|
80
|
+
if should_save:
|
|
81
|
+
self._save_log_sync()
|
|
82
|
+
else:
|
|
83
|
+
# Handle the image case that returned False - save synchronously
|
|
84
|
+
if (len(self.messages) > 0 and
|
|
85
|
+
isinstance(self.messages[-1].get('content'), list) and
|
|
86
|
+
len(self.messages[-1]['content']) > 0 and
|
|
87
|
+
self.messages[-1]['content'][0].get('type') == 'image'):
|
|
88
|
+
self._save_log_sync()
|
|
89
|
+
|
|
90
|
+
def _add_message_impl(self, message: Dict[str, str]) -> None:
|
|
91
|
+
"""Internal implementation shared by sync and async versions"""
|
|
49
92
|
if len(self.messages)>0 and self.messages[-1]['role'] == message['role']:
|
|
50
93
|
print("found repeat role")
|
|
51
94
|
# check if messasge is str
|
|
@@ -57,8 +100,7 @@ class ChatLog:
|
|
|
57
100
|
if part['type'] == 'image':
|
|
58
101
|
print("found image")
|
|
59
102
|
self.messages.append(message)
|
|
60
|
-
|
|
61
|
-
return
|
|
103
|
+
return False # Indicate caller should NOT save (we'll handle it)
|
|
62
104
|
|
|
63
105
|
try:
|
|
64
106
|
cmd_list = json.loads(self.messages[-1]['content'][0]['text'])
|
|
@@ -90,7 +132,19 @@ class ChatLog:
|
|
|
90
132
|
print('roles do not repeat, last message role is ', self.messages[-1]['role'], 'new message role is ', message['role'])
|
|
91
133
|
debug_box("5")
|
|
92
134
|
self.messages.append(message)
|
|
93
|
-
self.
|
|
135
|
+
self._save_log_sync()
|
|
136
|
+
async def add_message_async(self, message: Dict[str, str]) -> None:
|
|
137
|
+
"""Async version for new code that needs non-blocking operations"""
|
|
138
|
+
should_save = self._add_message_impl(message)
|
|
139
|
+
if should_save:
|
|
140
|
+
await self.save_log()
|
|
141
|
+
else:
|
|
142
|
+
# Handle the image case that returned False - save asynchronously
|
|
143
|
+
if (len(self.messages) > 0 and
|
|
144
|
+
isinstance(self.messages[-1].get('content'), list) and
|
|
145
|
+
len(self.messages[-1]['content']) > 0 and
|
|
146
|
+
self.messages[-1]['content'][0].get('type') == 'image'):
|
|
147
|
+
await self.save_log()
|
|
94
148
|
|
|
95
149
|
def get_history(self) -> List[Dict[str, str]]:
|
|
96
150
|
return self.messages
|
|
@@ -112,20 +166,21 @@ class ChatLog:
|
|
|
112
166
|
#
|
|
113
167
|
#return recent_messages
|
|
114
168
|
|
|
115
|
-
def save_log(self) -> None:
|
|
169
|
+
async def save_log(self) -> None:
|
|
116
170
|
log_file = os.path.join(self.log_dir, f'chatlog_{self.log_id}.json')
|
|
117
|
-
with open(log_file, 'w') as f:
|
|
118
|
-
json.
|
|
171
|
+
async with aiofiles.open(log_file, 'w') as f:
|
|
172
|
+
await f.write(json.dumps(self._get_log_data(), indent=2))
|
|
119
173
|
|
|
120
174
|
|
|
121
|
-
def load_log(self, log_id = None) -> None:
|
|
175
|
+
async def load_log(self, log_id = None) -> None:
|
|
122
176
|
if log_id is None:
|
|
123
177
|
log_id = self.log_id
|
|
124
178
|
self.log_id = log_id
|
|
125
179
|
log_file = os.path.join(self.log_dir, f'chatlog_{log_id}.json')
|
|
126
|
-
if os.path.exists(log_file):
|
|
127
|
-
with open(log_file, 'r') as f:
|
|
128
|
-
|
|
180
|
+
if await aiofiles.os.path.exists(log_file):
|
|
181
|
+
async with aiofiles.open(log_file, 'r') as f:
|
|
182
|
+
content = await f.read()
|
|
183
|
+
log_data = json.loads(content)
|
|
129
184
|
self.agent = log_data.get('agent')
|
|
130
185
|
self.messages = log_data.get('messages', [])
|
|
131
186
|
self.parent_log_id = log_data.get('parent_log_id', None)
|
|
@@ -175,7 +230,7 @@ class ChatLog:
|
|
|
175
230
|
'input_tokens_total': input_tokens_total
|
|
176
231
|
}
|
|
177
232
|
|
|
178
|
-
def find_chatlog_file(log_id: str) -> str:
|
|
233
|
+
async def find_chatlog_file(log_id: str) -> str:
|
|
179
234
|
"""
|
|
180
235
|
Find a chatlog file by its log_id.
|
|
181
236
|
|
|
@@ -188,14 +243,14 @@ def find_chatlog_file(log_id: str) -> str:
|
|
|
188
243
|
chat_dir = os.environ.get('CHATLOG_DIR', 'data/chat')
|
|
189
244
|
|
|
190
245
|
# Use os.walk to search through all subdirectories
|
|
191
|
-
for root, dirs, files in os.walk
|
|
246
|
+
for root, dirs, files in await asyncio.to_thread(os.walk, chat_dir):
|
|
192
247
|
for file in files:
|
|
193
248
|
if file == f"chatlog_{log_id}.json":
|
|
194
249
|
return os.path.join(root, file)
|
|
195
250
|
|
|
196
251
|
return None
|
|
197
252
|
|
|
198
|
-
def find_child_logs_by_parent_id(parent_log_id: str) -> List[str]:
|
|
253
|
+
async def find_child_logs_by_parent_id(parent_log_id: str) -> List[str]:
|
|
199
254
|
"""
|
|
200
255
|
Find all chat logs that have the given parent_log_id.
|
|
201
256
|
|
|
@@ -209,12 +264,13 @@ def find_child_logs_by_parent_id(parent_log_id: str) -> List[str]:
|
|
|
209
264
|
chat_dir = os.environ.get('CHATLOG_DIR', 'data/chat')
|
|
210
265
|
|
|
211
266
|
# Search through all chatlog files
|
|
212
|
-
for root, dirs, files in os.walk
|
|
267
|
+
for root, dirs, files in await asyncio.to_thread(os.walk, chat_dir):
|
|
213
268
|
for file in files:
|
|
214
269
|
if file.startswith("chatlog_") and file.endswith(".json"):
|
|
215
270
|
try:
|
|
216
|
-
with open(os.path.join(root, file), 'r') as f:
|
|
217
|
-
|
|
271
|
+
async with aiofiles.open(os.path.join(root, file), 'r') as f:
|
|
272
|
+
content = await f.read()
|
|
273
|
+
log_data = json.loads(content)
|
|
218
274
|
if log_data.get('parent_log_id') == parent_log_id:
|
|
219
275
|
# Extract log_id from the data
|
|
220
276
|
child_log_ids.append(log_data.get('log_id'))
|
|
@@ -263,24 +319,24 @@ def extract_delegate_task_log_ids(messages: List[Dict]) -> List[str]:
|
|
|
263
319
|
|
|
264
320
|
return log_ids
|
|
265
321
|
|
|
266
|
-
def get_cache_dir() -> str:
|
|
322
|
+
async def get_cache_dir() -> str:
|
|
267
323
|
"""
|
|
268
324
|
Get the directory for token count cache files.
|
|
269
325
|
Creates the directory if it doesn't exist.
|
|
270
326
|
"""
|
|
271
327
|
cache_dir = os.environ.get('TOKEN_CACHE_DIR', 'data/token_cache')
|
|
272
|
-
if not os.path.exists(cache_dir):
|
|
273
|
-
os.makedirs(cache_dir)
|
|
328
|
+
if not await aiofiles.os.path.exists(cache_dir):
|
|
329
|
+
await aiofiles.os.makedirs(cache_dir)
|
|
274
330
|
return cache_dir
|
|
275
331
|
|
|
276
|
-
def get_cache_path(log_id: str) -> str:
|
|
332
|
+
async def get_cache_path(log_id: str) -> str:
|
|
277
333
|
"""
|
|
278
334
|
Get the path to the cache file for a specific log_id.
|
|
279
335
|
"""
|
|
280
|
-
cache_dir = get_cache_dir()
|
|
336
|
+
cache_dir = await get_cache_dir()
|
|
281
337
|
return os.path.join(cache_dir, f"tokens_{log_id}.json")
|
|
282
338
|
|
|
283
|
-
def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
339
|
+
async def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
284
340
|
"""
|
|
285
341
|
Get cached token counts if available and valid.
|
|
286
342
|
|
|
@@ -291,16 +347,16 @@ def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
|
291
347
|
Returns:
|
|
292
348
|
Cached token counts if valid, None otherwise
|
|
293
349
|
"""
|
|
294
|
-
cache_path = get_cache_path(log_id)
|
|
350
|
+
cache_path = await get_cache_path(log_id)
|
|
295
351
|
|
|
296
352
|
# If cache doesn't exist, return None
|
|
297
|
-
if not os.path.exists(cache_path):
|
|
353
|
+
if not await aiofiles.os.path.exists(cache_path):
|
|
298
354
|
return None
|
|
299
355
|
|
|
300
356
|
try:
|
|
301
357
|
# Get modification times
|
|
302
|
-
log_mtime = os.path.getmtime(log_path)
|
|
303
|
-
cache_mtime = os.path.getmtime(cache_path)
|
|
358
|
+
log_mtime = await aiofiles.os.path.getmtime(log_path)
|
|
359
|
+
cache_mtime = await aiofiles.os.path.getmtime(cache_path)
|
|
304
360
|
current_time = time.time()
|
|
305
361
|
|
|
306
362
|
# If log was modified after cache was created, cache is invalid
|
|
@@ -309,28 +365,31 @@ def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
|
309
365
|
|
|
310
366
|
# Don't recalculate sooner than 3 minutes after last calculation
|
|
311
367
|
if current_time - cache_mtime < 180: # 3 minutes in seconds
|
|
312
|
-
with open(cache_path, 'r') as f:
|
|
313
|
-
|
|
368
|
+
async with aiofiles.open(cache_path, 'r') as f:
|
|
369
|
+
content = await f.read()
|
|
370
|
+
return json.loads(content)
|
|
314
371
|
|
|
315
372
|
# For logs that haven't been modified in over an hour, consider them "finished"
|
|
316
373
|
# and use the cache regardless of when it was last calculated
|
|
317
374
|
if current_time - log_mtime > 3600: # 1 hour in seconds
|
|
318
|
-
with open(cache_path, 'r') as f:
|
|
319
|
-
|
|
375
|
+
async with aiofiles.open(cache_path, 'r') as f:
|
|
376
|
+
content = await f.read()
|
|
377
|
+
return json.loads(content)
|
|
320
378
|
|
|
321
379
|
except (json.JSONDecodeError, IOError) as e:
|
|
322
380
|
print(f"Error reading token cache: {e}")
|
|
323
381
|
|
|
324
382
|
return None
|
|
325
383
|
|
|
326
|
-
def save_token_counts_to_cache(log_id: str, token_counts: Dict[str, int]) -> None:
|
|
384
|
+
async def save_token_counts_to_cache(log_id: str, token_counts: Dict[str, int]) -> None:
|
|
327
385
|
"""
|
|
328
386
|
Save token counts to cache.
|
|
329
387
|
"""
|
|
330
|
-
cache_path = get_cache_path(log_id)
|
|
331
|
-
with open(cache_path, 'w') as f:
|
|
332
|
-
json.
|
|
333
|
-
|
|
388
|
+
cache_path = await get_cache_path(log_id)
|
|
389
|
+
async with aiofiles.open(cache_path, 'w') as f:
|
|
390
|
+
await f.write(json.dumps(token_counts))
|
|
391
|
+
|
|
392
|
+
async def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
334
393
|
"""
|
|
335
394
|
Count tokens for a chat log identified by log_id, including any delegated tasks.
|
|
336
395
|
|
|
@@ -341,12 +400,12 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
341
400
|
Dictionary with token counts or None if log not found
|
|
342
401
|
"""
|
|
343
402
|
# Find the chatlog file
|
|
344
|
-
chatlog_path = find_chatlog_file(log_id)
|
|
403
|
+
chatlog_path = await find_chatlog_file(log_id)
|
|
345
404
|
if not chatlog_path:
|
|
346
405
|
return None
|
|
347
406
|
|
|
348
407
|
# Check cache first
|
|
349
|
-
cached_counts = get_cached_token_counts(log_id, chatlog_path)
|
|
408
|
+
cached_counts = await get_cached_token_counts(log_id, chatlog_path)
|
|
350
409
|
if cached_counts:
|
|
351
410
|
print(f"Using cached token counts for {log_id}")
|
|
352
411
|
return cached_counts
|
|
@@ -354,8 +413,9 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
354
413
|
print(f"Calculating token counts for {log_id}")
|
|
355
414
|
|
|
356
415
|
# Load the chat log
|
|
357
|
-
with open(chatlog_path, 'r') as f:
|
|
358
|
-
|
|
416
|
+
async with aiofiles.open(chatlog_path, 'r') as f:
|
|
417
|
+
content = await f.read()
|
|
418
|
+
log_data = json.loads(content)
|
|
359
419
|
|
|
360
420
|
# Get parent_log_id if it exists
|
|
361
421
|
parent_log_id = log_data.get('parent_log_id')
|
|
@@ -378,7 +438,7 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
378
438
|
delegated_log_ids = extract_delegate_task_log_ids(temp_log.messages)
|
|
379
439
|
|
|
380
440
|
# Also find child logs by parent_log_id
|
|
381
|
-
child_logs_by_parent = find_child_logs_by_parent_id(log_id)
|
|
441
|
+
child_logs_by_parent = await find_child_logs_by_parent_id(log_id)
|
|
382
442
|
|
|
383
443
|
# Combine all child log IDs (delegated tasks and parent_log_id children)
|
|
384
444
|
all_child_log_ids = set(delegated_log_ids) | set(child_logs_by_parent)
|
|
@@ -389,7 +449,7 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
389
449
|
|
|
390
450
|
# Recursively count tokens for all child tasks
|
|
391
451
|
for child_id in all_child_log_ids:
|
|
392
|
-
delegated_counts = count_tokens_for_log_id(child_id)
|
|
452
|
+
delegated_counts = await count_tokens_for_log_id(child_id)
|
|
393
453
|
if delegated_counts:
|
|
394
454
|
combined_counts['input_tokens_sequence'] += delegated_counts['input_tokens_sequence']
|
|
395
455
|
combined_counts['output_tokens_sequence'] += delegated_counts['output_tokens_sequence']
|
|
@@ -408,6 +468,6 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
408
468
|
}
|
|
409
469
|
|
|
410
470
|
# Save to cache
|
|
411
|
-
save_token_counts_to_cache(log_id, token_counts)
|
|
471
|
+
await save_token_counts_to_cache(log_id, token_counts)
|
|
412
472
|
|
|
413
473
|
return token_counts
|
mindroot/lib/token_counter.py
CHANGED
|
@@ -2,10 +2,13 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import re
|
|
4
4
|
import time
|
|
5
|
+
import asyncio
|
|
6
|
+
import aiofiles
|
|
7
|
+
import aiofiles.os
|
|
5
8
|
from typing import Dict, List
|
|
6
9
|
from mindroot.lib.chatlog import ChatLog
|
|
7
10
|
|
|
8
|
-
def find_chatlog_file(log_id: str) -> str:
|
|
11
|
+
async def find_chatlog_file(log_id: str) -> str:
|
|
9
12
|
"""
|
|
10
13
|
Find a chatlog file by its log_id.
|
|
11
14
|
|
|
@@ -18,7 +21,7 @@ def find_chatlog_file(log_id: str) -> str:
|
|
|
18
21
|
chat_dir = os.environ.get('CHATLOG_DIR', 'data/chat')
|
|
19
22
|
|
|
20
23
|
# Use os.walk to search through all subdirectories
|
|
21
|
-
for root, dirs, files in os.walk
|
|
24
|
+
for root, dirs, files in await asyncio.to_thread(os.walk, chat_dir):
|
|
22
25
|
for file in files:
|
|
23
26
|
if file == f"chatlog_{log_id}.json":
|
|
24
27
|
return os.path.join(root, file)
|
|
@@ -65,24 +68,24 @@ def extract_delegate_task_log_ids(messages: List[Dict]) -> List[str]:
|
|
|
65
68
|
|
|
66
69
|
return log_ids
|
|
67
70
|
|
|
68
|
-
def get_cache_dir() -> str:
|
|
71
|
+
async def get_cache_dir() -> str:
|
|
69
72
|
"""
|
|
70
73
|
Get the directory for token count cache files.
|
|
71
74
|
Creates the directory if it doesn't exist.
|
|
72
75
|
"""
|
|
73
76
|
cache_dir = os.environ.get('TOKEN_CACHE_DIR', 'data/token_cache')
|
|
74
|
-
if not os.path.exists(cache_dir):
|
|
75
|
-
os.makedirs(cache_dir)
|
|
77
|
+
if not await aiofiles.os.path.exists(cache_dir):
|
|
78
|
+
await aiofiles.os.makedirs(cache_dir)
|
|
76
79
|
return cache_dir
|
|
77
80
|
|
|
78
|
-
def get_cache_path(log_id: str) -> str:
|
|
81
|
+
async def get_cache_path(log_id: str) -> str:
|
|
79
82
|
"""
|
|
80
83
|
Get the path to the cache file for a specific log_id.
|
|
81
84
|
"""
|
|
82
|
-
cache_dir = get_cache_dir()
|
|
85
|
+
cache_dir = await get_cache_dir()
|
|
83
86
|
return os.path.join(cache_dir, f"tokens_{log_id}.json")
|
|
84
87
|
|
|
85
|
-
def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
88
|
+
async def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
86
89
|
"""
|
|
87
90
|
Get cached token counts if available and valid.
|
|
88
91
|
|
|
@@ -93,16 +96,16 @@ def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
|
93
96
|
Returns:
|
|
94
97
|
Cached token counts if valid, None otherwise
|
|
95
98
|
"""
|
|
96
|
-
cache_path = get_cache_path(log_id)
|
|
99
|
+
cache_path = await get_cache_path(log_id)
|
|
97
100
|
|
|
98
101
|
# If cache doesn't exist, return None
|
|
99
|
-
if not os.path.exists(cache_path):
|
|
102
|
+
if not await aiofiles.os.path.exists(cache_path):
|
|
100
103
|
return None
|
|
101
104
|
|
|
102
105
|
try:
|
|
103
106
|
# Get modification times
|
|
104
|
-
log_mtime = os.path.getmtime(log_path)
|
|
105
|
-
cache_mtime = os.path.getmtime(cache_path)
|
|
107
|
+
log_mtime = await aiofiles.os.path.getmtime(log_path)
|
|
108
|
+
cache_mtime = await aiofiles.os.path.getmtime(cache_path)
|
|
106
109
|
current_time = time.time()
|
|
107
110
|
|
|
108
111
|
# If log was modified after cache was created, cache is invalid
|
|
@@ -111,29 +114,31 @@ def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
|
|
|
111
114
|
|
|
112
115
|
# Don't recalculate sooner than 3 minutes after last calculation
|
|
113
116
|
if current_time - cache_mtime < 180: # 3 minutes in seconds
|
|
114
|
-
with open(cache_path, 'r') as f:
|
|
115
|
-
|
|
117
|
+
async with aiofiles.open(cache_path, 'r') as f:
|
|
118
|
+
content = await f.read()
|
|
119
|
+
return json.loads(content)
|
|
116
120
|
|
|
117
121
|
# For logs that haven't been modified in over an hour, consider them "finished"
|
|
118
122
|
# and use the cache regardless of when it was last calculated
|
|
119
123
|
if current_time - log_mtime > 3600: # 1 hour in seconds
|
|
120
|
-
with open(cache_path, 'r') as f:
|
|
121
|
-
|
|
124
|
+
async with aiofiles.open(cache_path, 'r') as f:
|
|
125
|
+
content = await f.read()
|
|
126
|
+
return json.loads(content)
|
|
122
127
|
|
|
123
128
|
except (json.JSONDecodeError, IOError) as e:
|
|
124
129
|
print(f"Error reading token cache: {e}")
|
|
125
130
|
|
|
126
131
|
return None
|
|
127
132
|
|
|
128
|
-
def save_token_counts_to_cache(log_id: str, token_counts: Dict[str, int]) -> None:
|
|
133
|
+
async def save_token_counts_to_cache(log_id: str, token_counts: Dict[str, int]) -> None:
|
|
129
134
|
"""
|
|
130
135
|
Save token counts to cache.
|
|
131
136
|
"""
|
|
132
|
-
cache_path = get_cache_path(log_id)
|
|
133
|
-
with open(cache_path, 'w') as f:
|
|
134
|
-
json.
|
|
137
|
+
cache_path = await get_cache_path(log_id)
|
|
138
|
+
async with aiofiles.open(cache_path, 'w') as f:
|
|
139
|
+
await f.write(json.dumps(token_counts))
|
|
135
140
|
|
|
136
|
-
def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
141
|
+
async def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
137
142
|
"""
|
|
138
143
|
Count tokens for a chat log identified by log_id, including any delegated tasks.
|
|
139
144
|
|
|
@@ -144,12 +149,12 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
144
149
|
Dictionary with token counts or None if log not found
|
|
145
150
|
"""
|
|
146
151
|
# Find the chatlog file
|
|
147
|
-
chatlog_path = find_chatlog_file(log_id)
|
|
152
|
+
chatlog_path = await find_chatlog_file(log_id)
|
|
148
153
|
if not chatlog_path:
|
|
149
154
|
return None
|
|
150
155
|
|
|
151
156
|
# Check cache first
|
|
152
|
-
cached_counts = get_cached_token_counts(log_id, chatlog_path)
|
|
157
|
+
cached_counts = await get_cached_token_counts(log_id, chatlog_path)
|
|
153
158
|
if cached_counts:
|
|
154
159
|
print(f"Using cached token counts for {log_id}")
|
|
155
160
|
return cached_counts
|
|
@@ -157,8 +162,9 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
157
162
|
print(f"Calculating token counts for {log_id}")
|
|
158
163
|
|
|
159
164
|
# Load the chat log
|
|
160
|
-
with open(chatlog_path, 'r') as f:
|
|
161
|
-
|
|
165
|
+
async with aiofiles.open(chatlog_path, 'r') as f:
|
|
166
|
+
content = await f.read()
|
|
167
|
+
log_data = json.loads(content)
|
|
162
168
|
|
|
163
169
|
# Create a temporary ChatLog instance to count tokens
|
|
164
170
|
temp_log = ChatLog(log_id=log_id, user="system", agent=log_data.get('agent', 'unknown'))
|
|
@@ -178,7 +184,7 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
178
184
|
|
|
179
185
|
# Recursively count tokens for delegated tasks
|
|
180
186
|
for delegated_id in delegated_log_ids:
|
|
181
|
-
delegated_counts = count_tokens_for_log_id(delegated_id)
|
|
187
|
+
delegated_counts = await count_tokens_for_log_id(delegated_id)
|
|
182
188
|
if delegated_counts:
|
|
183
189
|
combined_counts['input_tokens_sequence'] += delegated_counts['input_tokens_sequence']
|
|
184
190
|
combined_counts['output_tokens_sequence'] += delegated_counts['output_tokens_sequence']
|
|
@@ -196,6 +202,6 @@ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
|
|
|
196
202
|
token_counts['combined_input_tokens_total'] = combined_counts['input_tokens_total']
|
|
197
203
|
|
|
198
204
|
# Save to cache
|
|
199
|
-
save_token_counts_to_cache(log_id, token_counts)
|
|
205
|
+
await save_token_counts_to_cache(log_id, token_counts)
|
|
200
206
|
|
|
201
207
|
return token_counts
|
|
@@ -452,7 +452,7 @@ mindroot/coreplugins/chat/commands.py,sha256=vlgGOvwvjpCbSsW25x4HaeFzeRNWXoEKrdq
|
|
|
452
452
|
mindroot/coreplugins/chat/format_result_msgs.py,sha256=daEdpEyAJIa8b2VkCqSKcw8PaExcB6Qro80XNes_sHA,2
|
|
453
453
|
mindroot/coreplugins/chat/mod.py,sha256=Xydjv3feKJJRbwdiB7raqiQnWtaS_2GcdC9bXYQX3nE,425
|
|
454
454
|
mindroot/coreplugins/chat/models.py,sha256=GRcRuDUAJFpyWERPMxkxUaZ21igNlWeeamriruEKiEQ,692
|
|
455
|
-
mindroot/coreplugins/chat/router.py,sha256=
|
|
455
|
+
mindroot/coreplugins/chat/router.py,sha256=iZLohQknhwj-DOo4xAO4WrD6jCoZ5TBoe6d4cBgeMME,11272
|
|
456
456
|
mindroot/coreplugins/chat/services.py,sha256=Wc_PzC9yT53vWKhiuh9wBrvdAigD5OKfwPwQT-27bXA,17950
|
|
457
457
|
mindroot/coreplugins/chat/utils.py,sha256=BiE14PpsAcQSO5vbU88klHGm8cAXJDXxgVgva-EXybU,155
|
|
458
458
|
mindroot/coreplugins/chat/static/assistant.png,sha256=oAt1ctkFKLSPBoAZGNnSixooW9ANVIk1GwniauVWDXo,215190
|
|
@@ -1771,8 +1771,8 @@ mindroot/lib/__init__.py,sha256=388n_hMskU0TnZ4xT10US_kFkya-EPBjWcv7AZf_HOk,74
|
|
|
1771
1771
|
mindroot/lib/buchatlog.py,sha256=LJZc3ksKgJcStltmHrrwNLaON3EDzhOKVAWj0Wl22wk,5861
|
|
1772
1772
|
mindroot/lib/buchatlog2.py,sha256=Va9FteBWePEjWD9OZcw-OtQfEb-IoCVGTmJeMRaX9is,13729
|
|
1773
1773
|
mindroot/lib/butemplates.py,sha256=gfHGPTOjvoEenXsR7xokNuqMjOAPuC2DawheH1Ae4bU,12196
|
|
1774
|
-
mindroot/lib/chatcontext.py,sha256=
|
|
1775
|
-
mindroot/lib/chatlog.py,sha256=
|
|
1774
|
+
mindroot/lib/chatcontext.py,sha256=VMJ-_uooblik5InKb_r7iLn6YmgJb3_RMTU0nDg95DI,12148
|
|
1775
|
+
mindroot/lib/chatlog.py,sha256=QbUNPgCnr9KhreQt4A_kXNICy1-JsVjpLKYt6URYhaQ,19390
|
|
1776
1776
|
mindroot/lib/chatlog_optimized.py,sha256=rL7KBP-V4_cGgMLihxPm3HoKcjFEyA1uEtPtqvkOa3A,20011
|
|
1777
1777
|
mindroot/lib/json_escape.py,sha256=5cAmAdNbnYX2uyfQcnse2fFtNI0CdB-AfZ23RwaDm-k,884
|
|
1778
1778
|
mindroot/lib/model_selector.py,sha256=Wz-8NZoiclmnhLeCNnI3WCuKFmjsO5HE4bK5F8GpZzU,1397
|
|
@@ -1783,7 +1783,7 @@ mindroot/lib/route_decorators.py,sha256=L3E-bn48zhuxk6YPtyc2oP76-5WuV_SmjxtngJeY
|
|
|
1783
1783
|
mindroot/lib/session_files.py,sha256=yY8TKfJPGZoK4yAy3WbfrJ7I5xL6NfDJmIICH6qC7Bw,1167
|
|
1784
1784
|
mindroot/lib/streamcmd.py,sha256=f9n3OtryEkMbNNuFr5BAZn1EpSLUKuDZw-zpo97XxJk,4714
|
|
1785
1785
|
mindroot/lib/templates.py,sha256=5dODCS6UeC9Y_PdMWlUuQCCZUUt2ICR0S1YF6XrG3eM,15154
|
|
1786
|
-
mindroot/lib/token_counter.py,sha256=
|
|
1786
|
+
mindroot/lib/token_counter.py,sha256=6k6nq2oCjJ4XiWVp89vYEdMTmfRb4ejEcpjK-vXcI6w,7607
|
|
1787
1787
|
mindroot/lib/auth/__init__.py,sha256=5EZbCTcdlnTHYE0JNk8znWNSO7mOsokMOvRBjb5Mq-M,49
|
|
1788
1788
|
mindroot/lib/auth/auth.py,sha256=2vhF_LfZcTPt2N2VLWy1ZP7h2pKFv7XM3xW1iRVOTkU,3129
|
|
1789
1789
|
mindroot/lib/db/organize_models.py,sha256=kiadXfhGjCY16c36l1JmxXcKSH4ShWWEUnHi7WRRn9c,5028
|
|
@@ -1821,9 +1821,9 @@ mindroot/protocols/services/stream_chat.py,sha256=fMnPfwaB5fdNMBLTEg8BXKAGvrELKH
|
|
|
1821
1821
|
mindroot/registry/__init__.py,sha256=40Xy9bmPHsgdIrOzbtBGzf4XMqXVi9P8oZTJhn0r654,151
|
|
1822
1822
|
mindroot/registry/component_manager.py,sha256=WZFNPg4SNvpqsM5NFiC2DpgmrJQCyR9cNhrCBpp30Qk,995
|
|
1823
1823
|
mindroot/registry/data_access.py,sha256=NgNMamxIjaKeYxzxnVaQz1Y-Rm0AI51si3788_JHUTM,5316
|
|
1824
|
-
mindroot-8.
|
|
1825
|
-
mindroot-8.
|
|
1826
|
-
mindroot-8.
|
|
1827
|
-
mindroot-8.
|
|
1828
|
-
mindroot-8.
|
|
1829
|
-
mindroot-8.
|
|
1824
|
+
mindroot-8.14.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
|
|
1825
|
+
mindroot-8.14.0.dist-info/METADATA,sha256=mqrRl3JpXNNGWikD9tpXVCBVGX0ri7MgIvVto4UFcJ4,892
|
|
1826
|
+
mindroot-8.14.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
1827
|
+
mindroot-8.14.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
|
|
1828
|
+
mindroot-8.14.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
|
|
1829
|
+
mindroot-8.14.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|