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.
@@ -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"}
@@ -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
- context_data = json.load(f)
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.dump(context_data, f, indent=2)
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.dump(context_data, f, indent=2)
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
- context_data = json.load(f)
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
- log_data = json.load(f)
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
- self.load_log()
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
- self.save_log()
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.save_log()
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.dump(self._get_log_data(), f, indent=2)
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
- log_data = json.load(f)
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(chat_dir):
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(chat_dir):
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
- log_data = json.load(f)
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
- return json.load(f)
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
- return json.load(f)
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.dump(token_counts, f)
333
- def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
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
- log_data = json.load(f)
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
@@ -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(chat_dir):
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
- return json.load(f)
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
- return json.load(f)
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.dump(token_counts, f)
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
- log_data = json.load(f)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindroot
3
- Version: 8.13.0
3
+ Version: 8.14.0
4
4
  Summary: MindRoot AI Agent Framework
5
5
  Requires-Python: >=3.9
6
6
  License-File: LICENSE
@@ -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=u7zSFwRenVzdyZxSE81mBzqeOHgNgubdvHGNycC8zoI,11260
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=yJQOC0lhS-M7sk0oHet8W3B8urxUZBRwEkvQlDUpsws,11702
1775
- mindroot/lib/chatlog.py,sha256=F5rKxiDotLvJnZVjHlbUrChRLdFkbS4V2gNcAQ-XE-s,16176
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=U3tyw2CG1uK1FmupOHzTkyBwx5UKI30hRrRTqu_-ALQ,7170
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.13.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
1825
- mindroot-8.13.0.dist-info/METADATA,sha256=_zCH38FtgPU8Na2nBDSHZTs5HWkxLk2QvH4RUfhhWUg,892
1826
- mindroot-8.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1827
- mindroot-8.13.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
1828
- mindroot-8.13.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
1829
- mindroot-8.13.0.dist-info/RECORD,,
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,,