mindroot 8.10.0__py3-none-any.whl → 8.11.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.

Potentially problematic release.


This version of mindroot might be problematic. Click here for more details.

@@ -199,6 +199,19 @@ async def scan_directory(request: DirectoryRequest):
199
199
  discovered_plugins = discover_plugins(directory)
200
200
  manifest = load_plugin_manifest()
201
201
  print("discoverd_plugins", discovered_plugins)
202
+
203
+ # Analyze plugins for index compatibility
204
+ addable_count = 0
205
+ for plugin_name, plugin_info in discovered_plugins.items():
206
+ # Check if plugin has GitHub info
207
+ has_github = (
208
+ plugin_info.get('github_url') or
209
+ plugin_info.get('remote_source') or
210
+ (plugin_info.get('metadata', {}).get('github_url'))
211
+ )
212
+ if has_github:
213
+ addable_count += 1
214
+
202
215
  # Update installed plugins from discovered ones
203
216
  for plugin_name, plugin_info in discovered_plugins.items():
204
217
  plugin_info['source'] = 'local'
@@ -218,9 +231,14 @@ async def scan_directory(request: DirectoryRequest):
218
231
  } for name, info in discovered_plugins.items()]
219
232
 
220
233
  save_plugin_manifest(manifest)
221
- return {"success": True,
222
- "message": f"Scanned {len(discovered_plugins)} plugins in {directory}",
223
- "plugins": plugins_list}
234
+
235
+ response = {"success": True,
236
+ "message": f"Scanned {len(discovered_plugins)} plugins in {directory}",
237
+ "plugins": plugins_list,
238
+ "addable_to_index": addable_count}
239
+ if addable_count < len(discovered_plugins):
240
+ response["warning"] = f"{len(discovered_plugins) - addable_count} plugins missing GitHub info and cannot be added to indices"
241
+ return response
224
242
  except Exception as e:
225
243
  trace = traceback.format_exc()
226
244
  return {"success": False, "message": f"Error during scan: {str(e)}\n\n{trace}"}
@@ -187,6 +187,14 @@ export class PluginAdvancedInstall extends PluginBase {
187
187
  if (response.success) {
188
188
  this.installDialog.addOutput(response.message, 'success');
189
189
 
190
+ // Show index compatibility info
191
+ if (response.addable_to_index !== undefined) {
192
+ this.installDialog.addOutput(`Index-compatible plugins: ${response.addable_to_index}/${response.plugins.length}`, 'info');
193
+ }
194
+ if (response.warning) {
195
+ this.installDialog.addOutput(response.warning, 'warning');
196
+ }
197
+
190
198
  // If plugins were found, list them
191
199
  if (response.plugins && response.plugins.length > 0) {
192
200
  this.installDialog.addOutput('Found plugins:', 'info');
@@ -11,44 +11,50 @@
11
11
  "version": "1.0.0",
12
12
  "description": "Simple Google search functionality",
13
13
  "source": "github",
14
- "github_url": "runvnc/ah_google",
14
+ "github_url": "https://github.com/runvnc/ah_google",
15
15
  "commands": ["google", "fetch_webpage"],
16
16
  "services": [],
17
17
  "dependencies": []
18
18
  },
19
19
  {
20
- "name": "Think (Chain-of-Thought)",
20
+ "name": "ah_think",
21
21
  "version": "0.0.1",
22
22
  "description": "Chain of thought reasoning capability",
23
- "source": "local",
24
- "source_path": "/xfiles/plugins_ah/ah_think",
23
+ "source": "github",
24
+ "github_url": "https://github.com/runvnc/ah_think",
25
25
  "commands": ["think"],
26
26
  "services": [],
27
27
  "dependencies": []
28
28
  },
29
29
  {
30
- "name": "Look at PDF/Image",
30
+ "name": "ah_look_at",
31
31
  "version": "0.0.1",
32
32
  "description": "PDF and image examination tools",
33
- "source": "local",
34
- "source_path": "/xfiles/plugins_ah/ah_look_at",
33
+ "source": "github",
34
+ "github_url": "https://github.com/runvnc/ah_look_at",
35
35
  "commands": ["examine_pdf", "examine_image"],
36
36
  "services": [],
37
37
  "dependencies": []
38
38
  },
39
39
  {
40
- "enabled": true,
41
- "source": "github",
42
- "source_path": "runvnc/ah_anthropic",
40
+ "name": "ah_anthropic",
43
41
  "version": "0.0.1",
44
- "name": "Anthropic (Claude)"
42
+ "description": "Anthropic Claude AI integration",
43
+ "source": "github",
44
+ "github_url": "https://github.com/runvnc/ah_anthropic",
45
+ "commands": [],
46
+ "services": [],
47
+ "dependencies": []
45
48
  },
46
49
  {
47
- "enabled": true,
48
- "source": "github",
49
- "source_path": "runvnc/ah_shell",
50
+ "name": "ah_shell",
50
51
  "version": "0.0.1",
51
- "name": "Shell"
52
+ "description": "Shell command execution",
53
+ "source": "github",
54
+ "github_url": "https://github.com/runvnc/ah_shell",
55
+ "commands": ["execute_command"],
56
+ "services": [],
57
+ "dependencies": []
52
58
  }
53
59
  ],
54
60
  "agents": [
@@ -82,6 +82,19 @@ async def add_plugin(INDEX_DIR: Path, index_name: str, plugin: PluginEntry):
82
82
  if not index_file.exists():
83
83
  return JSONResponse({'success': False, 'message': 'Index not found'})
84
84
 
85
+ # Early validation - check for required GitHub info
86
+ has_github_info = (
87
+ plugin.remote_source or
88
+ plugin.github_url or
89
+ getattr(plugin, 'metadata', {}).get('github_url')
90
+ )
91
+
92
+ if not has_github_info:
93
+ return JSONResponse({
94
+ 'success': False,
95
+ 'message': 'Plugin missing GitHub repository information. Cannot add to index.'
96
+ })
97
+
85
98
  # Check if plugin is local
86
99
  if plugin.source == 'local':
87
100
  return JSONResponse({
@@ -346,8 +346,9 @@
346
346
  "flags": [],
347
347
  "uncensored": false,
348
348
  "commands": [
349
- "say",
350
- "json_encoded_md",
349
+ "wait_for_user_reply",
350
+ "tell_and_continue",
351
+ "markdown_await_user",
351
352
  "read",
352
353
  "write",
353
354
  "dir",
@@ -369,8 +370,9 @@
369
370
  "persona": "Dr_Alex_Morgan",
370
371
  "instructions": "### Clinical Approach\\n\\nDr. Alex Morgan uses a client-centered approach, focusing on creating a safe and non-judgmental space. The primary techniques include cognitive restructuring, behavioral activation, exposure therapy, mindfulness, and goal setting.\\n\\n### Key CBT Skills and Techniques\\n\\n#### Cognitive Restructuring\\n- **Identify Negative Thoughts:** Use thought diaries to track negative thoughts.\\n- **Examine Evidence:** Evaluate evidence for and against negative thoughts.\\n- **Challenge Thoughts:** Use Socratic questioning (e.g., \\\"What is the evidence for this thought?\\\").\\n- **Replace Thoughts:** Develop balanced and realistic thoughts.\\n- **Practice:** Encourage regular practice of cognitive restructuring.\\n\\n#### Behavioral Activation\\n- **Activity Monitoring:** Track daily activities and mood.\\n- **Activity Scheduling:** Schedule enjoyable and meaningful activities.\\n- **Gradual Task Assignment:** Start with small tasks and gradually increase difficulty.\\n- **Review and Adjust:** Regularly review progress and adjust the plan.\\n- **Reinforce Positive Behavior:** Celebrate achievements to reinforce positive behavior.\\n\\n#### Goal Setting\\n- **SMART Goals:** Ensure goals are Specific, Measurable, Achievable, Relevant, and Time-bound.\\n- **Break Down Goals:** Divide larger goals into smaller steps.\\n- **Action Plan:** Create a detailed action plan with tasks and deadlines.\\n- **Monitor Progress:** Regularly review progress and provide feedback.\\n- **Celebrate Successes:** Acknowledge and celebrate achievements.\\n\\n### Overall Process\\n\\n1. **Initial Consultation:** Assess client's history, issues, and goals.\\n2. **Treatment Planning:** Develop a personalized treatment plan.\\n3. **Therapy Sessions:** Conduct regular sessions using CBT techniques.\\n4. **Progress Monitoring:** Review progress and adjust the plan.\\n5. **Termination and Follow-Up:** Gradually reduce sessions and develop a relapse prevention plan.\\n\\n### Per-Session Process\\n\\n1. **Check-In:** Discuss client's current mood and experiences.\\n2. **Review Homework:** Discuss homework assignments.\\n3. **Agenda Setting:** Set the session's agenda.\\n4. **CBT Interventions:** Implement CBT techniques.\\n5. **Skill Building:** Teach and practice new CBT skills.\\n6. **Homework Assignment:** Assign tasks for practice.\\n7. **Session Summary:** Summarize key points and provide feedback.\\n8. **Planning for Next Session:** Discuss focus and goals for the next session.",
371
372
  "commands": [
372
- "say",
373
- "json_encoded_md"
373
+ "wait_for_user_reply",
374
+ "tell_and_continue",
375
+ "markdown_await_user"
374
376
  ],
375
377
  "flags": [],
376
378
  "added_at": "2024-12-08T17:08:22.219537",
@@ -381,8 +383,9 @@
381
383
  "name": "Mindroot Engineering Expert",
382
384
  "persona": "Assistant",
383
385
  "commands": [
384
- "say",
385
- "json_encoded_md",
386
+ "wait_for_user_reply",
387
+ "tell_and_continue",
388
+ "markdown_await_user",
386
389
  "think",
387
390
  "memory_add",
388
391
  "memory_update",
@@ -96,6 +96,18 @@ class PluginSection extends BaseEl {
96
96
  opacity: 0.5;
97
97
  cursor: not-allowed;
98
98
  }
99
+
100
+ .plugin-warning {
101
+ font-size: 0.8rem;
102
+ color: #f39c12;
103
+ margin-top: 4px;
104
+ }
105
+
106
+ .disabled {
107
+ background: #555 !important;
108
+ color: #999 !important;
109
+ cursor: not-allowed !important;
110
+ }
99
111
  `;
100
112
 
101
113
  constructor() {
@@ -103,6 +115,22 @@ class PluginSection extends BaseEl {
103
115
  this.searchTerm = '';
104
116
  }
105
117
 
118
+ isPluginAddable(plugin) {
119
+ // Must have GitHub source info
120
+ const hasGitHubInfo = plugin.remote_source ||
121
+ plugin.github_url ||
122
+ (plugin.metadata && plugin.metadata.github_url);
123
+
124
+ // Must not be local source
125
+ const isNotLocal = plugin.source !== 'local';
126
+
127
+ // Must have basic metadata
128
+ const hasMetadata = plugin.metadata &&
129
+ (plugin.metadata.commands || plugin.metadata.services);
130
+
131
+ return hasGitHubInfo && isNotLocal && hasMetadata;
132
+ }
133
+
106
134
  filterPlugins(plugins) {
107
135
  if (!plugins) return [];
108
136
  const searchLower = this.searchTerm?.toLowerCase() || '';
@@ -165,6 +193,9 @@ class PluginSection extends BaseEl {
165
193
  <div class="plugin-description">${plugin.description}</div>
166
194
  ` : ''}
167
195
  <div class="plugin-source">Source: ${plugin.source}</div>
196
+ ${!this.isPluginAddable(plugin) ? html`
197
+ <div class="plugin-warning">⚠️ Missing GitHub info - cannot add to index</div>
198
+ ` : ''}
168
199
  </div>
169
200
  ${isIndexPlugins ? html`
170
201
  <button class="action-button remove-button"
@@ -172,11 +203,17 @@ class PluginSection extends BaseEl {
172
203
  Remove
173
204
  </button>
174
205
  ` : html`
175
- <button class="action-button add-button"
176
- ?disabled=${this.selectedIndex?.plugins?.some(p => p.name === plugin.name)}
177
- @click=${() => this.handleAddPlugin(plugin)}>
178
- Add
179
- </button>
206
+ ${this.isPluginAddable(plugin) ? html`
207
+ <button class="action-button add-button"
208
+ ?disabled=${this.selectedIndex?.plugins?.some(p => p.name === plugin.name)}
209
+ @click=${() => this.handleAddPlugin(plugin)}>
210
+ Add
211
+ </button>
212
+ ` : html`
213
+ <button class="action-button disabled" disabled title="Missing GitHub repository information">
214
+ Cannot Add
215
+ </button>
216
+ `}
180
217
  `}
181
218
  </div>
182
219
  `)}
@@ -50,6 +50,7 @@
50
50
  background-color: #45a049;
51
51
  }
52
52
  </style>
53
+ {% block head_extra %}{% endblock %}
53
54
  <script>
54
55
  async function handleLogin(event) {
55
56
  event.preventDefault();
@@ -78,8 +79,10 @@
78
79
  <input type="password" name="password" placeholder="Password" required>
79
80
  <button type="submit">Log In</button>
80
81
  </form>
82
+ {% block content %}{% endblock %}
81
83
  <div>
82
84
  <p>Don't have an account? <a href="/signup">Sign Up</a></p>
85
+ </div>
83
86
  </div>
84
87
  </body>
85
- </html>
88
+ </html>
@@ -0,0 +1,509 @@
1
+ import os
2
+ import json
3
+ from typing import List, Dict, Set, Optional, Tuple
4
+ import sys
5
+ import traceback
6
+ import re
7
+ import time
8
+ from mindroot.lib.utils.debug import debug_box
9
+ from collections import defaultdict
10
+ import threading
11
+
12
+ # Global cache for directory structure and parent-child relationships
13
+ _log_index_cache = {
14
+ 'log_paths': {}, # log_id -> full_path
15
+ 'parent_children': defaultdict(set), # parent_log_id -> set of child_log_ids
16
+ 'log_metadata': {}, # log_id -> {parent_log_id, agent, user, mtime}
17
+ 'last_scan': 0,
18
+ 'scan_lock': threading.Lock()
19
+ }
20
+
21
+ class ChatLog:
22
+ def __init__(self, log_id=0, agent=None, parent_log_id=None, context_length: int = 4096, user: str = None):
23
+ self.log_id = log_id
24
+ self.messages = []
25
+ self.parent_log_id = parent_log_id
26
+ self.agent = agent
27
+ if user is None or user == '' or user == 'None':
28
+ raise ValueError('User must be provided')
29
+ # make sure user is string
30
+ if not isinstance(user, str):
31
+ # does it have a username?
32
+ if hasattr(user, 'username'):
33
+ user = user.username
34
+ else:
35
+ # throw an error
36
+ raise ValueError('ChatLog(): user must be a string or have username field')
37
+ self.user = user
38
+ if agent is None or agent == '':
39
+ raise ValueError('Agent must be provided')
40
+ self.context_length = context_length
41
+ self.log_dir = os.environ.get('CHATLOG_DIR', 'data/chat')
42
+ self.log_dir = os.path.join(self.log_dir, self.user)
43
+ self.log_dir = os.path.join(self.log_dir, self.agent)
44
+ if not os.path.exists(self.log_dir):
45
+ os.makedirs(self.log_dir)
46
+ self.load_log()
47
+
48
+ def _get_log_data(self) -> Dict[str, any]:
49
+ return {
50
+ 'agent': self.agent,
51
+ 'log_id': self.log_id,
52
+ 'messages': self.messages,
53
+ 'parent_log_id': self.parent_log_id
54
+ }
55
+
56
+ def _calculate_message_length(self, message: Dict[str, str]) -> int:
57
+ return len(json.dumps(message)) // 3
58
+
59
+ def add_message(self, message: Dict[str, str]) -> None:
60
+ if len(self.messages)>0 and self.messages[-1]['role'] == message['role']:
61
+ print("found repeat role")
62
+ # check if messasge is str
63
+ # if so, convert to dict with type 'text':
64
+ if type(message['content']) == str:
65
+ message['content'] = [{'type':'text', 'text': message['content']}]
66
+ elif type(message['content']) == list:
67
+ for part in message['content']:
68
+ if part['type'] == 'image':
69
+ print("found image")
70
+ self.messages.append(message)
71
+ self.save_log()
72
+ return
73
+
74
+ try:
75
+ cmd_list = json.loads(self.messages[-1]['content'][0]['text'])
76
+ if type(cmd_list) != list:
77
+ debug_box("1")
78
+ cmd_list = [cmd_list]
79
+ new_json = json.loads(message['content'][0]['text'])
80
+ if type(new_json) != list:
81
+ debug_box("2")
82
+ new_json = [new_json]
83
+ new_cmd_list = cmd_list + new_json
84
+ debug_box("3")
85
+ self.messages[-1]['content'] = [{ 'type': 'text', 'text': json.dumps(new_cmd_list) }]
86
+ except Exception as e:
87
+ # assume previous mesage was not a command, was a string
88
+ debug_box("4")
89
+ print("Could not combine commands, probably normal if user message and previous system output, assuming string", e)
90
+ if type(self.messages[-1]['content']) == str:
91
+ new_msg_text = self.messages[-1]['content'] + message['content'][0]['text']
92
+ else:
93
+ new_msg_text = self.messages[-1]['content'][0]['text'] + message['content'][0]['text']
94
+ self.messages.append({'role': message['role'], 'content': [{'type': 'text', 'text': new_msg_text}]})
95
+ else:
96
+ if len(self.messages)>0:
97
+ print('roles do not repeat, last message role is ', self.messages[-1]['role'], 'new message role is ', message['role'])
98
+ debug_box("5")
99
+ self.messages.append(message)
100
+ self.save_log()
101
+
102
+ def get_history(self) -> List[Dict[str, str]]:
103
+ return self.messages
104
+
105
+ def get_recent(self, max_tokens: int = 4096) -> List[Dict[str, str]]:
106
+ recent_messages = []
107
+ total_length = 0
108
+ json_messages = json.dumps(self.messages)
109
+ return json.loads(json_messages)
110
+
111
+ def save_log(self) -> None:
112
+ log_file = os.path.join(self.log_dir, f'chatlog_{self.log_id}.json')
113
+ with open(log_file, 'w') as f:
114
+ json.dump(self._get_log_data(), f, indent=2)
115
+ # Invalidate cache when log is saved
116
+ _invalidate_log_cache()
117
+
118
+
119
+ def load_log(self, log_id = None) -> None:
120
+ if log_id is None:
121
+ log_id = self.log_id
122
+ self.log_id = log_id
123
+ log_file = os.path.join(self.log_dir, f'chatlog_{log_id}.json')
124
+ if os.path.exists(log_file):
125
+ with open(log_file, 'r') as f:
126
+ log_data = json.load(f)
127
+ self.agent = log_data.get('agent')
128
+ self.messages = log_data.get('messages', [])
129
+ self.parent_log_id = log_data.get('parent_log_id', None)
130
+ print("Loaded log file at ", log_file)
131
+ print("Message length: ", len(self.messages))
132
+ else:
133
+ print("Could not find log file at ", log_file)
134
+ self.messages = []
135
+
136
+ def count_tokens(self) -> Dict[str, int]:
137
+ input_tokens_sequence = 0
138
+ output_tokens_sequence = 0
139
+ input_tokens_total = 0
140
+
141
+ for i, message in enumerate(self.messages):
142
+ message_tokens = len(json.dumps(message)) // 4
143
+
144
+ if message['role'] == 'assistant':
145
+ output_tokens_sequence += message_tokens
146
+ else:
147
+ input_tokens_sequence += message_tokens
148
+
149
+ if message['role'] == 'assistant':
150
+ request_input_tokens = 0
151
+ for j in range(i):
152
+ request_input_tokens += len(json.dumps(self.messages[j])) // 4
153
+ input_tokens_total += request_input_tokens
154
+
155
+ return {
156
+ 'input_tokens_sequence': input_tokens_sequence,
157
+ 'output_tokens_sequence': output_tokens_sequence,
158
+ 'input_tokens_total': input_tokens_total
159
+ }
160
+
161
+ def _invalidate_log_cache():
162
+ """Invalidate the log index cache"""
163
+ global _log_index_cache
164
+ with _log_index_cache['scan_lock']:
165
+ _log_index_cache['last_scan'] = 0
166
+
167
+ def _build_log_index(force_refresh: bool = False) -> None:
168
+ """
169
+ Build an index of all chat logs and their relationships.
170
+ This replaces the inefficient os.walk() calls with a single scan.
171
+ """
172
+ global _log_index_cache
173
+
174
+ with _log_index_cache['scan_lock']:
175
+ current_time = time.time()
176
+
177
+ # Only rebuild if cache is older than 5 minutes or forced
178
+ if not force_refresh and (current_time - _log_index_cache['last_scan']) < 300:
179
+ return
180
+
181
+ print("Building log index cache...")
182
+ start_time = time.time()
183
+
184
+ # Clear existing cache
185
+ _log_index_cache['log_paths'].clear()
186
+ _log_index_cache['parent_children'].clear()
187
+ _log_index_cache['log_metadata'].clear()
188
+
189
+ chat_dir = os.environ.get('CHATLOG_DIR', 'data/chat')
190
+ if not os.path.exists(chat_dir):
191
+ _log_index_cache['last_scan'] = current_time
192
+ return
193
+
194
+ # Single directory walk to build complete index
195
+ for root, dirs, files in os.walk(chat_dir):
196
+ for file in files:
197
+ if file.startswith("chatlog_") and file.endswith(".json"):
198
+ # Extract log_id from filename
199
+ log_id = file[8:-5] # Remove 'chatlog_' prefix and '.json' suffix
200
+ full_path = os.path.join(root, file)
201
+
202
+ try:
203
+ # Get file modification time
204
+ mtime = os.path.getmtime(full_path)
205
+
206
+ # Parse user and agent from path
207
+ rel_path = os.path.relpath(full_path, chat_dir)
208
+ path_parts = rel_path.split(os.sep)
209
+ user = path_parts[0] if len(path_parts) > 0 else 'unknown'
210
+ agent = path_parts[1] if len(path_parts) > 1 else 'unknown'
211
+
212
+ # Store path mapping
213
+ _log_index_cache['log_paths'][log_id] = full_path
214
+
215
+ # Read log data to get parent relationship
216
+ try:
217
+ with open(full_path, 'r') as f:
218
+ log_data = json.load(f)
219
+ parent_log_id = log_data.get('parent_log_id')
220
+
221
+ # Store metadata
222
+ _log_index_cache['log_metadata'][log_id] = {
223
+ 'parent_log_id': parent_log_id,
224
+ 'agent': agent,
225
+ 'user': user,
226
+ 'mtime': mtime
227
+ }
228
+
229
+ # Build parent-child relationships
230
+ if parent_log_id:
231
+ _log_index_cache['parent_children'][parent_log_id].add(log_id)
232
+
233
+ except (json.JSONDecodeError, KeyError) as e:
234
+ print(f"Error reading log file {full_path}: {e}")
235
+ continue
236
+
237
+ except OSError as e:
238
+ print(f"Error accessing file {full_path}: {e}")
239
+ continue
240
+
241
+ _log_index_cache['last_scan'] = current_time
242
+ elapsed = time.time() - start_time
243
+ print(f"Log index built in {elapsed:.2f}s. Found {len(_log_index_cache['log_paths'])} logs.")
244
+
245
+ def find_chatlog_file(log_id: str) -> Optional[str]:
246
+ """
247
+ Find a chatlog file by its log_id using the cached index.
248
+
249
+ Args:
250
+ log_id: The log ID to search for
251
+
252
+ Returns:
253
+ The full path to the chatlog file if found, None otherwise
254
+ """
255
+ _build_log_index()
256
+ return _log_index_cache['log_paths'].get(log_id)
257
+
258
+ def find_child_logs_by_parent_id(parent_log_id: str) -> List[str]:
259
+ """
260
+ Find all chat logs that have the given parent_log_id using the cached index.
261
+
262
+ Args:
263
+ parent_log_id: The parent log ID to search for
264
+
265
+ Returns:
266
+ List of log IDs that have this parent_log_id
267
+ """
268
+ _build_log_index()
269
+ return list(_log_index_cache['parent_children'].get(parent_log_id, set()))
270
+
271
+ def extract_delegate_task_log_ids(messages: List[Dict]) -> List[str]:
272
+ """
273
+ Extract log IDs from delegate_task commands in messages.
274
+
275
+ Args:
276
+ messages: List of chat messages
277
+
278
+ Returns:
279
+ List of log IDs found in delegate_task commands
280
+ """
281
+ log_ids = []
282
+
283
+ for message in messages:
284
+ if message['role'] == 'assistant':
285
+ content = message['content']
286
+ # Handle both string and list content formats
287
+ if isinstance(content, str):
288
+ text = content
289
+ elif isinstance(content, list) and len(content) > 0 and 'text' in content[0]:
290
+ text = content[0]['text']
291
+ else:
292
+ continue
293
+
294
+ # Try to parse as JSON
295
+ try:
296
+ commands = json.loads(text)
297
+ if not isinstance(commands, list):
298
+ commands = [commands]
299
+
300
+ for cmd in commands:
301
+ for key, value in cmd.items():
302
+ if key == 'delegate_task' and 'log_id' in value:
303
+ log_ids.append(value['log_id'])
304
+ except (json.JSONDecodeError, TypeError, KeyError):
305
+ # If not JSON, try regex to find log_ids in delegate_task commands
306
+ matches = re.findall(r'"delegate_task"\s*:\s*{\s*"log_id"\s*:\s*"([^"]+)"', text)
307
+ log_ids.extend(matches)
308
+
309
+ return log_ids
310
+
311
+ def get_cache_dir() -> str:
312
+ """
313
+ Get the directory for token count cache files.
314
+ Creates the directory if it doesn't exist.
315
+ """
316
+ cache_dir = os.environ.get('TOKEN_CACHE_DIR', 'data/token_cache')
317
+ if not os.path.exists(cache_dir):
318
+ os.makedirs(cache_dir)
319
+ return cache_dir
320
+
321
+ def get_cache_path(log_id: str) -> str:
322
+ """
323
+ Get the path to the cache file for a specific log_id.
324
+ """
325
+ cache_dir = get_cache_dir()
326
+ return os.path.join(cache_dir, f"tokens_{log_id}.json")
327
+
328
+ def get_cached_token_counts(log_id: str, log_path: str) -> Dict[str, int]:
329
+ """
330
+ Get cached token counts if available and valid.
331
+
332
+ Args:
333
+ log_id: The log ID
334
+ log_path: Path to the actual log file
335
+
336
+ Returns:
337
+ Cached token counts if valid, None otherwise
338
+ """
339
+ cache_path = get_cache_path(log_id)
340
+
341
+ # If cache doesn't exist, return None
342
+ if not os.path.exists(cache_path):
343
+ return None
344
+
345
+ try:
346
+ # Get modification times
347
+ log_mtime = os.path.getmtime(log_path)
348
+ cache_mtime = os.path.getmtime(cache_path)
349
+ current_time = time.time()
350
+
351
+ # If log was modified after cache was created, cache is invalid
352
+ if log_mtime > cache_mtime:
353
+ return None
354
+
355
+ # Don't recalculate sooner than 3 minutes after last calculation
356
+ if current_time - cache_mtime < 180: # 3 minutes in seconds
357
+ with open(cache_path, 'r') as f:
358
+ return json.load(f)
359
+
360
+ # For logs that haven't been modified in over an hour, consider them "finished"
361
+ # and use the cache regardless of when it was last calculated
362
+ if current_time - log_mtime > 3600: # 1 hour in seconds
363
+ with open(cache_path, 'r') as f:
364
+ return json.load(f)
365
+
366
+ except (json.JSONDecodeError, IOError) as e:
367
+ print(f"Error reading token cache: {e}")
368
+
369
+ return None
370
+
371
+ def save_token_counts_to_cache(log_id: str, token_counts: Dict[str, int]) -> None:
372
+ """
373
+ Save token counts to cache.
374
+ """
375
+ cache_path = get_cache_path(log_id)
376
+ with open(cache_path, 'w') as f:
377
+ json.dump(token_counts, f)
378
+
379
+ def count_tokens_for_log_id_optimized(log_id: str) -> Dict[str, int]:
380
+ """
381
+ Optimized version of count_tokens_for_log_id that uses caching and batch operations.
382
+
383
+ Args:
384
+ log_id: The log ID to count tokens for
385
+
386
+ Returns:
387
+ Dictionary with token counts or None if log not found
388
+ """
389
+ # Find the chatlog file using cached index
390
+ chatlog_path = find_chatlog_file(log_id)
391
+ if not chatlog_path:
392
+ return None
393
+
394
+ # Check cache first
395
+ cached_counts = get_cached_token_counts(log_id, chatlog_path)
396
+ if cached_counts:
397
+ print(f"Using cached token counts for {log_id}")
398
+ return cached_counts
399
+
400
+ print(f"Calculating token counts for {log_id}")
401
+
402
+ # Use batch processing to get all related logs at once
403
+ all_related_logs = _get_all_related_logs_batch(log_id)
404
+
405
+ # Calculate tokens for all logs in batch
406
+ token_results = {}
407
+ for related_log_id, log_data in all_related_logs.items():
408
+ # Create a temporary ChatLog instance to count tokens
409
+ temp_log = ChatLog(log_id=related_log_id, user="system", agent=log_data.get('agent', 'unknown'))
410
+ temp_log.messages = log_data.get('messages', [])
411
+
412
+ # Count tokens for this log
413
+ token_results[related_log_id] = temp_log.count_tokens()
414
+
415
+ # Build the hierarchical token counts
416
+ main_counts = token_results.get(log_id, {
417
+ 'input_tokens_sequence': 0,
418
+ 'output_tokens_sequence': 0,
419
+ 'input_tokens_total': 0
420
+ })
421
+
422
+ # Sum up all child tokens
423
+ combined_counts = {
424
+ 'input_tokens_sequence': main_counts['input_tokens_sequence'],
425
+ 'output_tokens_sequence': main_counts['output_tokens_sequence'],
426
+ 'input_tokens_total': main_counts['input_tokens_total']
427
+ }
428
+
429
+ # Add child log tokens
430
+ for related_log_id, counts in token_results.items():
431
+ if related_log_id != log_id: # Don't double-count the main log
432
+ combined_counts['input_tokens_sequence'] += counts['input_tokens_sequence']
433
+ combined_counts['output_tokens_sequence'] += counts['output_tokens_sequence']
434
+ combined_counts['input_tokens_total'] += counts['input_tokens_total']
435
+
436
+ # Create final result
437
+ final_token_counts = {
438
+ # Parent session only counts
439
+ 'input_tokens_sequence': main_counts['input_tokens_sequence'],
440
+ 'output_tokens_sequence': main_counts['output_tokens_sequence'],
441
+ 'input_tokens_total': main_counts['input_tokens_total'],
442
+ # Combined counts (parent + all subtasks)
443
+ 'combined_input_tokens_sequence': combined_counts['input_tokens_sequence'],
444
+ 'combined_output_tokens_sequence': combined_counts['output_tokens_sequence'],
445
+ 'combined_input_tokens_total': combined_counts['input_tokens_total']
446
+ }
447
+
448
+ # Save to cache
449
+ save_token_counts_to_cache(log_id, final_token_counts)
450
+
451
+ return final_token_counts
452
+
453
+ def _get_all_related_logs_batch(log_id: str) -> Dict[str, Dict]:
454
+ """
455
+ Get all related logs (parent and children) in a single batch operation.
456
+ This avoids multiple directory traversals and file reads.
457
+ """
458
+ # Ensure index is built
459
+ _build_log_index()
460
+
461
+ # Find all related log IDs
462
+ related_log_ids = set([log_id])
463
+ to_process = [log_id]
464
+
465
+ while to_process:
466
+ current_id = to_process.pop()
467
+
468
+ # Add children
469
+ children = _log_index_cache['parent_children'].get(current_id, set())
470
+ for child_id in children:
471
+ if child_id not in related_log_ids:
472
+ related_log_ids.add(child_id)
473
+ to_process.append(child_id)
474
+
475
+ # Also check for delegated tasks in the main log
476
+ main_log_path = _log_index_cache['log_paths'].get(log_id)
477
+ if main_log_path:
478
+ try:
479
+ with open(main_log_path, 'r') as f:
480
+ log_data = json.load(f)
481
+ delegated_ids = extract_delegate_task_log_ids(log_data.get('messages', []))
482
+ for delegated_id in delegated_ids:
483
+ if delegated_id not in related_log_ids:
484
+ related_log_ids.add(delegated_id)
485
+ to_process.append(delegated_id)
486
+ except (json.JSONDecodeError, IOError):
487
+ pass
488
+
489
+ # Batch read all related logs
490
+ result = {}
491
+ for related_id in related_log_ids:
492
+ log_path = _log_index_cache['log_paths'].get(related_id)
493
+ if log_path:
494
+ try:
495
+ with open(log_path, 'r') as f:
496
+ result[related_id] = json.load(f)
497
+ except (json.JSONDecodeError, IOError) as e:
498
+ print(f"Error reading log {related_id}: {e}")
499
+ continue
500
+
501
+ return result
502
+
503
+ # Backward compatibility - keep the original function name but use optimized version
504
+ def count_tokens_for_log_id(log_id: str) -> Dict[str, int]:
505
+ """
506
+ Count tokens for a chat log identified by log_id, including any delegated tasks.
507
+ This is the optimized version that uses caching and batch operations.
508
+ """
509
+ return count_tokens_for_log_id_optimized(log_id)
@@ -62,7 +62,7 @@
62
62
  "source": "core"
63
63
  },
64
64
  "credits": {
65
- "enabled": true,
65
+ "enabled": false,
66
66
  "source": "core"
67
67
  },
68
68
  "startup": {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindroot
3
- Version: 8.10.0
3
+ Version: 8.11.0
4
4
  Summary: MindRoot AI Agent Framework
5
5
  Requires-Python: >=3.9
6
6
  License-File: LICENSE
@@ -6,7 +6,7 @@ mindroot/coreplugins/admin/agent_router.py,sha256=mstYUURNRb9MpfNwqMYC7WY3FOQJ-5
6
6
  mindroot/coreplugins/admin/command_router.py,sha256=nhNkurIb9w7itgQ5nBtDYM-W2q69cYpLAq9HLrK-0Bk,6567
7
7
  mindroot/coreplugins/admin/persona_handler.py,sha256=eRYmmOmupLJuiujWN35SS2nwRgo0lbbXug-chZ_WmjU,2862
8
8
  mindroot/coreplugins/admin/persona_router.py,sha256=tyPxBe-pRnntHo_xyazvnwKZxHmvEI5_Fsu0YkZbrXA,6036
9
- mindroot/coreplugins/admin/plugin_manager.py,sha256=k_buWtxh1JGb7j_NMILxJlZ_ZU5VVuXQpw4_Qp1Zjjo,14587
9
+ mindroot/coreplugins/admin/plugin_manager.py,sha256=ZXzxSNY6Y0_PXvnZSsSK3WDTkgDJNDuDlFP8y-F72h8,15355
10
10
  mindroot/coreplugins/admin/plugin_manager_backup.py,sha256=pUnh-EW4BMOrL2tYUq1X2BC65aoTBhDM-o2-mlDYXfU,24583
11
11
  mindroot/coreplugins/admin/plugin_router.py,sha256=EEDIM0MMPhqeej8Lu8cazYgzaXH1sKZLy_6kBOwrUgY,1020
12
12
  mindroot/coreplugins/admin/router.py,sha256=Wzeri92Rv39YKTYflMZ3E1pnYy959KHWRjS8D4Xgaao,1580
@@ -51,7 +51,7 @@ mindroot/coreplugins/admin/static/js/missing-commands.js,sha256=adNF9GWN981_KX7H
51
51
  mindroot/coreplugins/admin/static/js/model-preferences.js,sha256=J0G7gcGACaPyslWJO42urf5wbZZsqO0LyPicAu-uV_Y,3365
52
52
  mindroot/coreplugins/admin/static/js/notification.js,sha256=296rVCr6MNtzvzdzW3bGiMa231-BnWJtwZZ_sDWX-3c,5633
53
53
  mindroot/coreplugins/admin/static/js/persona-editor.js,sha256=xO2jobJXwqGY7Uajj3vQxyERubsHZovIPF_8FHpvzFE,8604
54
- mindroot/coreplugins/admin/static/js/plugin-advanced-install.js,sha256=nrCpHlQTd9c_SRIqR0MNqsqgocUPL1K7_yfA99f-MlM,7679
54
+ mindroot/coreplugins/admin/static/js/plugin-advanced-install.js,sha256=-HDJ3lVuDwj6R-74TfVUo4dUxB8efl13m3R_sUicnJI,8038
55
55
  mindroot/coreplugins/admin/static/js/plugin-base.js,sha256=KWp5DqueHtyTxYKbuHMoFpoFXrfMbIjzK4M1ulAR9m8,5095
56
56
  mindroot/coreplugins/admin/static/js/plugin-index-browser.js,sha256=P-V4wqlYGxjr7oF2LiD5ti8Is3wtSsKPwpRgRJpT0VI,10028
57
57
  mindroot/coreplugins/admin/static/js/plugin-install-dialog.js,sha256=ty_dZ9dZcpp9El1j3eY4Z6Wp8iZ5WNkf_lHSV-W1IhA,8216
@@ -923,11 +923,6 @@ mindroot/coreplugins/events/mod.py,sha256=MsSFjiLMLJZ7QhUPpVBWKiyDnCzryquRyr329N
923
923
  mindroot/coreplugins/events/router.py,sha256=a-77306_fQPNHPXP5aYtbpfC0gbqMBNRu04aYOh75L4,3587
924
924
  mindroot/coreplugins/events/backup/mod.py,sha256=9QeJpg6WKwxRdjiKVHD1froguTe86FS2-2wWm1B9xa8,1832
925
925
  mindroot/coreplugins/events/backup/router_backup.py,sha256=ImU8xoxdSd45V1yOFVqdtDQ614V6CMsDZQ1gtJj0Mnk,254
926
- mindroot/coreplugins/google_auth/README.md,sha256=9BTSYrU-k22RshkKDsszZwrB40xFu-1JUBi34QD52I8,2801
927
- mindroot/coreplugins/google_auth/__init__.py,sha256=qw8b_7YoN67q1kEdXYXmQkXycF1NaYb3dMbjP-6FsUs,19
928
- mindroot/coreplugins/google_auth/mod.py,sha256=wVhrOK7gw7kTjGRCS3nlhbELHPxydUqKMDQcjdGIUfU,61
929
- mindroot/coreplugins/google_auth/router.py,sha256=15ix01TVTexfhgGRAhkNiQeHZamj-jqNDBNVQm1yqW4,5519
930
- mindroot/coreplugins/google_auth/inject/login.jinja2,sha256=Vlihmk8vgxup0Y6Pvi5nzvbZUQyhUzZH0I5qREW_Vj8,1944
931
926
  mindroot/coreplugins/home/mod.py,sha256=MsSFjiLMLJZ7QhUPpVBWKiyDnCzryquRyr329NoCACI,2
932
927
  mindroot/coreplugins/home/router.py,sha256=kzPg2eIimG_2Qa1bZ0gKCmoo2uzd8GurrePODOO1How,1982
933
928
  mindroot/coreplugins/home/static/css/dark.css,sha256=Q9FHaEsf9xeJjtouyKgr1Su6vTzsN07NHxxqDrDfyx8,14259
@@ -1307,7 +1302,7 @@ mindroot/coreplugins/home/static/js/lit-html/node/directives/when.js.map,sha256=
1307
1302
  mindroot/coreplugins/home/templates/home.jinja2,sha256=wVaXYgaNK3Kn7wmrKEyBT24Az2l4_o7ctVbDhHPXgEw,2088
1308
1303
  mindroot/coreplugins/index/.gitignore,sha256=r_urXBxUYZhSuWyYzpRcAz5rHR0gVTERMrTI_gbBmCM,9
1309
1304
  mindroot/coreplugins/index/__init__.py,sha256=WU5revQym2nu-vAV-o5cdAXpuRlb_Juu0lzc9s6lIFU,74
1310
- mindroot/coreplugins/index/default.json,sha256=E3Rj8gCAcFeppbS4CtXoGwXL94E7jFJQcNflDp4xrJg,2030
1305
+ mindroot/coreplugins/index/default.json,sha256=nFZXpsAFlTCGN_lfqJIewCd_o-fMEpjaUixBbnynWfA,2288
1311
1306
  mindroot/coreplugins/index/mod.py,sha256=F_2PBZIjOOKID36m_DK8fvN92zEkTHQeBm9M6gJ84Tw,554
1312
1307
  mindroot/coreplugins/index/models.py,sha256=WXG4pslFwq-iUld4-7dZlbHTsiFkVq9RghE0AVDzR_M,1044
1313
1308
  mindroot/coreplugins/index/router.py,sha256=z8TlbcN_9i6Q5XgLxcbV6H_PAGqK9H0tJdrQ3oqkCcY,2085
@@ -1315,9 +1310,9 @@ mindroot/coreplugins/index/utils.py,sha256=UF30B_XM3FNHjwn_BnxaW9yEjwZl-TjyDBcUD
1315
1310
  mindroot/coreplugins/index/handlers/__init__.py,sha256=MPoCNJooyIGXwD9lXyIR8RQwUE5MrPj4wFpJVIzPCtM,421
1316
1311
  mindroot/coreplugins/index/handlers/agent_ops.py,sha256=6faXdvDwZfZSsev5BLlKxJkDo4BNskVNAstWt3_T6kA,3849
1317
1312
  mindroot/coreplugins/index/handlers/index_ops.py,sha256=VvGev9qKMtXK0LJnyO4s0fKeJwFLlXIBI3SPgYIGMD8,3898
1318
- mindroot/coreplugins/index/handlers/plugin_ops.py,sha256=lDC4RtSiZ27w_rW6aj_VkoLhq2vIhs1GktyHvDOYdTs,5139
1313
+ mindroot/coreplugins/index/handlers/plugin_ops.py,sha256=KT5Ga8Q42rBfkaqyDd4VPtu3x1Ax7mEApS8a-xIhq2E,5599
1319
1314
  mindroot/coreplugins/index/handlers/publish.py,sha256=J4SiDSvaXpYV7My66nFjD1jHTVeDsV986iIwUAUlLok,4684
1320
- mindroot/coreplugins/index/indices/default/index.json,sha256=zC44bA-WnHfkzx-BCzzycwPtD7QggLCvGzvLPYCWRPI,37844
1315
+ mindroot/coreplugins/index/indices/default/index.json,sha256=2zR869dAVESvpGuRATZXaaQkh7n2oqkjc3DY2VcqXgc,37991
1321
1316
  mindroot/coreplugins/index/indices/default/personas/Assistant/avatar.png,sha256=AsT2_jjGpZvEhzTEwSVhEShSYaIBhcUDrlj_bAG_HVY,1169266
1322
1317
  mindroot/coreplugins/index/indices/default/personas/Assistant/faceref.png,sha256=AsT2_jjGpZvEhzTEwSVhEShSYaIBhcUDrlj_bAG_HVY,1169266
1323
1318
  mindroot/coreplugins/index/indices/default/personas/Assistant/persona.json,sha256=BwVSL8E1VkFtxPEN_6fjULxK2SufaE2Dsi0_Mfs4Mhw,244
@@ -1332,7 +1327,7 @@ mindroot/coreplugins/index/static/js/index-list.js,sha256=7nE7Taeppc1NaxyojOncgv
1332
1327
  mindroot/coreplugins/index/static/js/index-manager.js,sha256=UFTCwT21ZB0I1YRgeJWwRBUiCcKvHWZvuebopHKyTcA,6453
1333
1328
  mindroot/coreplugins/index/static/js/index-metadata.js,sha256=sLFV1P0XLd887PQkwpjZMK25BQUqT5QGF60dvRwAQc8,2640
1334
1329
  mindroot/coreplugins/index/static/js/lit-core.min.js,sha256=wHslYF0J77niIj1xfYyYjSvnU6Mkq-QRiNffiOb6xz8,15443
1335
- mindroot/coreplugins/index/static/js/plugin-section.js,sha256=bXE0w6Npq0Z77sBOPv0kUU7r48-sgdHmyXFWTwPZtew,5333
1330
+ mindroot/coreplugins/index/static/js/plugin-section.js,sha256=bwYiWCuYdWbMdS8lociS72k8_6dBLMS2wyBgG3f6DGA,6551
1336
1331
  mindroot/coreplugins/index/static/js/components/publish-button.js,sha256=REEvOYv0uPe2jPeGkmI9WHscHIwMoAXOQTO67Aux9nU,3367
1337
1332
  mindroot/coreplugins/index/static/js/components/upload-area.js,sha256=MqoVKaTnwm8Gce1_qjqZxF7x3NKVKORHoECBTKGpP0o,4056
1338
1333
  mindroot/coreplugins/index/static/js/lit-html/LICENSE,sha256=K2b5OQr94p7f5DFNr_k_AfsYLhF4BT4igSAXiYfl23U,1518
@@ -1705,7 +1700,7 @@ mindroot/coreplugins/jwt_auth/role_checks.py,sha256=bruZIIBSOvXNWB1YZ2s5btrbbXNf
1705
1700
  mindroot/coreplugins/jwt_auth/router.py,sha256=ecXYao_UG33UjQF15Hi-tf_X0eFsqLEldyqGpt7JNSw,1162
1706
1701
  mindroot/coreplugins/login/mod.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
1707
1702
  mindroot/coreplugins/login/router.py,sha256=vyxBNkrVguRn3SmoR5JTYjkI_Oiv74DMkLbYSSK4ju4,2722
1708
- mindroot/coreplugins/login/templates/login.jinja2,sha256=PZa85CmQeROc8ABQ6s5S9llH9xy2i6JtTWRKLR5FUZs,2513
1703
+ mindroot/coreplugins/login/templates/login.jinja2,sha256=sxZL8G52hv3T364k9pylXz8g6H9xludXfziHGTruKw8,2610
1709
1704
  mindroot/coreplugins/persona/README.md,sha256=YZWBU9O3i8Edvuldd3_9mplAd3hIU7c5ueSO3ssTo8s,826
1710
1705
  mindroot/coreplugins/persona/__init__.py,sha256=qw8b_7YoN67q1kEdXYXmQkXycF1NaYb3dMbjP-6FsUs,19
1711
1706
  mindroot/coreplugins/persona/init_persona.py,sha256=4ZXy9LrmOI8xFgr2gsz4VPvwWBMd1oB04uhAWrRbp8A,410
@@ -1778,6 +1773,7 @@ mindroot/lib/buchatlog2.py,sha256=Va9FteBWePEjWD9OZcw-OtQfEb-IoCVGTmJeMRaX9is,13
1778
1773
  mindroot/lib/butemplates.py,sha256=gfHGPTOjvoEenXsR7xokNuqMjOAPuC2DawheH1Ae4bU,12196
1779
1774
  mindroot/lib/chatcontext.py,sha256=yJQOC0lhS-M7sk0oHet8W3B8urxUZBRwEkvQlDUpsws,11702
1780
1775
  mindroot/lib/chatlog.py,sha256=F5rKxiDotLvJnZVjHlbUrChRLdFkbS4V2gNcAQ-XE-s,16176
1776
+ mindroot/lib/chatlog_optimized.py,sha256=rL7KBP-V4_cGgMLihxPm3HoKcjFEyA1uEtPtqvkOa3A,20011
1781
1777
  mindroot/lib/json_escape.py,sha256=5cAmAdNbnYX2uyfQcnse2fFtNI0CdB-AfZ23RwaDm-k,884
1782
1778
  mindroot/lib/model_selector.py,sha256=Wz-8NZoiclmnhLeCNnI3WCuKFmjsO5HE4bK5F8GpZzU,1397
1783
1779
  mindroot/lib/parent_templates.py,sha256=elcQFFwrFtfAYfQOSTs06aiDDigN1f1R2f8I1V-wj6Q,2731
@@ -1800,7 +1796,7 @@ mindroot/lib/logging/logfiles_prev.py,sha256=Dqya4VlqXh5Wx7HC6uydkwkb5SLpcl6HQ46
1800
1796
  mindroot/lib/pipelines/pipe.py,sha256=zlVI8BTjDaI87UZO61uVvXyZIbs_pPipi-zGVgDoCwk,527
1801
1797
  mindroot/lib/pipelines/pipelines.py,sha256=Uv83Xvnpt34u6J0h0f1mef4M53ifgFco65nMECBJeMM,2654
1802
1798
  mindroot/lib/plugins/__init__.py,sha256=ZpIgOKsGjyvAe9KenqZ_5ppxaQ7br9EmU7RBI_VXaBw,1318
1803
- mindroot/lib/plugins/default_plugin_manifest.json,sha256=vVgFY_vIn5NFsj2dbERg9m_iiaSOOfLdSRlgNfAI5H0,1405
1799
+ mindroot/lib/plugins/default_plugin_manifest.json,sha256=lg3mIQmQPU6nkOKjClstvXGC82tNP9RuV-LLQVIJAIw,1406
1804
1800
  mindroot/lib/plugins/installation.py,sha256=Ju3gD3cv9I5qIY0hrL7-4t5xYeivEtEgUveWIiE1oqM,15360
1805
1801
  mindroot/lib/plugins/loader.py,sha256=OhBzfzHuZCwxVtloVrgkHUJCaLG3_n4k9lDLMIuuZsg,9541
1806
1802
  mindroot/lib/plugins/manifest.py,sha256=DOCpwZRFCbk4fAhXlh67uDunYGMBD-OK6_W4E9tf-_k,3955
@@ -1825,9 +1821,9 @@ mindroot/protocols/services/stream_chat.py,sha256=fMnPfwaB5fdNMBLTEg8BXKAGvrELKH
1825
1821
  mindroot/registry/__init__.py,sha256=40Xy9bmPHsgdIrOzbtBGzf4XMqXVi9P8oZTJhn0r654,151
1826
1822
  mindroot/registry/component_manager.py,sha256=WZFNPg4SNvpqsM5NFiC2DpgmrJQCyR9cNhrCBpp30Qk,995
1827
1823
  mindroot/registry/data_access.py,sha256=NgNMamxIjaKeYxzxnVaQz1Y-Rm0AI51si3788_JHUTM,5316
1828
- mindroot-8.10.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
1829
- mindroot-8.10.0.dist-info/METADATA,sha256=hZ6UII6f1aM5Dhvh2mN7Rst9znemJwqt_CO2suVYO58,892
1830
- mindroot-8.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1831
- mindroot-8.10.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
1832
- mindroot-8.10.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
1833
- mindroot-8.10.0.dist-info/RECORD,,
1824
+ mindroot-8.11.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
1825
+ mindroot-8.11.0.dist-info/METADATA,sha256=wef7mud8dSSLBdP0uNYhmySIvcexiy-Hth-h4Tk471U,892
1826
+ mindroot-8.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1827
+ mindroot-8.11.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
1828
+ mindroot-8.11.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
1829
+ mindroot-8.11.0.dist-info/RECORD,,
@@ -1,76 +0,0 @@
1
- # Google OAuth Authentication Plugin
2
-
3
- This plugin adds Google Sign-In functionality to MindRoot, allowing users to authenticate using their Google accounts.
4
-
5
- ## Setup Instructions
6
-
7
- ### 1. Create Google OAuth Credentials
8
-
9
- 1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
10
- 2. Create a new project or select an existing one
11
- 3. Enable the Google+ API for your project
12
- 4. Go to "Credentials" in the left sidebar
13
- 5. Click "Create Credentials" > "OAuth client ID"
14
- 6. Select "Web application" as the application type
15
- 7. Add your redirect URI (e.g., `http://localhost:8000/google_auth/callback` for local development)
16
- 8. Save your Client ID and Client Secret
17
-
18
- ### 2. Configure Environment Variables
19
-
20
- Add the following to your `.env` file in the MindRoot root directory:
21
-
22
- ```bash
23
- # Google OAuth Configuration
24
- GOOGLE_CLIENT_ID=your-client-id-here
25
- GOOGLE_CLIENT_SECRET=your-client-secret-here
26
- GOOGLE_REDIRECT_URI=http://localhost:8000/google_auth/callback
27
- ```
28
-
29
- For production, update `GOOGLE_REDIRECT_URI` to your actual domain:
30
- ```bash
31
- GOOGLE_REDIRECT_URI=https://yourdomain.com/google_auth/callback
32
- ```
33
-
34
- ### 3. Update Google OAuth Authorized Redirect URIs
35
-
36
- Make sure to add your redirect URI to the authorized redirect URIs in your Google OAuth client settings.
37
-
38
- ## How It Works
39
-
40
- 1. Users click "Sign in with Google" on the login page
41
- 2. They are redirected to Google's OAuth consent screen
42
- 3. After authorization, Google redirects back to `/google_auth/callback`
43
- 4. The plugin:
44
- - Verifies the OAuth response
45
- - Creates a new user account if needed (username: `google_[partial_google_id]`)
46
- - Sets the same JWT token used by the regular login system
47
- - Redirects to the home page
48
-
49
- ## User Data
50
-
51
- When a user signs in with Google:
52
- - A new user account is created with a generated username
53
- - Their Google email is stored
54
- - Email is automatically verified if Google has verified it
55
- - Additional Google profile info is stored in `google_info.json`
56
-
57
- ## Security Notes
58
-
59
- - State tokens are used to prevent CSRF attacks
60
- - Google ID tokens are verified server-side
61
- - Users get the same JWT tokens as regular login
62
- - Passwords for OAuth users are randomly generated and not used
63
-
64
- ## Troubleshooting
65
-
66
- 1. **"Google OAuth not configured" error**: Make sure you've set the environment variables
67
- 2. **Redirect URI mismatch**: Ensure the redirect URI in your `.env` matches exactly what's configured in Google Cloud Console
68
- 3. **Invalid state token**: This is a security feature - just try signing in again
69
-
70
- ## Integration with Existing System
71
-
72
- The plugin integrates seamlessly with MindRoot's existing authentication:
73
- - Uses the same JWT token system
74
- - Compatible with existing middleware
75
- - Users can access all the same features
76
- - Admin can manage Google-authenticated users like any other users
@@ -1 +0,0 @@
1
- from .mod import *
@@ -1,69 +0,0 @@
1
- {% block head_extra %}
2
- <style>
3
- .google-signin-btn {
4
- display: flex;
5
- align-items: center;
6
- justify-content: center;
7
- width: 100%;
8
- padding: 0.5rem;
9
- margin-top: 1rem;
10
- background-color: #4285f4;
11
- color: white;
12
- border: none;
13
- border-radius: 4px;
14
- cursor: pointer;
15
- text-decoration: none;
16
- font-size: 14px;
17
- transition: background-color 0.3s;
18
- }
19
-
20
- .google-signin-btn:hover {
21
- background-color: #357ae8;
22
- }
23
-
24
- .google-signin-btn svg {
25
- width: 18px;
26
- height: 18px;
27
- margin-right: 8px;
28
- }
29
-
30
- .divider {
31
- text-align: center;
32
- margin: 1rem 0;
33
- position: relative;
34
- }
35
-
36
- .divider::before {
37
- content: '';
38
- position: absolute;
39
- top: 50%;
40
- left: 0;
41
- right: 0;
42
- height: 1px;
43
- background-color: #555;
44
- }
45
-
46
- .divider span {
47
- background-color: #2a2a2a;
48
- padding: 0 10px;
49
- position: relative;
50
- color: #888;
51
- font-size: 14px;
52
- }
53
- </style>
54
- {% endblock %}
55
-
56
- {% block content %}
57
- <div class="divider">
58
- <span>OR</span>
59
- </div>
60
- <a href="/google_auth/login" class="google-signin-btn">
61
- <svg viewBox="0 0 24 24" fill="currentColor">
62
- <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
63
- <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
64
- <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
65
- <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
66
- </svg>
67
- Sign in with Google
68
- </a>
69
- {% endblock %}
@@ -1 +0,0 @@
1
- # Google Auth plugin - provides Google OAuth2 authentication
@@ -1,170 +0,0 @@
1
- from fastapi import APIRouter, Request, HTTPException
2
- from fastapi.responses import RedirectResponse, HTMLResponse
3
- from lib.route_decorators import public_route
4
- from lib.providers.services import service_manager
5
- from mindroot.coreplugins.jwt_auth.middleware import create_access_token
6
- from mindroot.coreplugins.user_service.models import UserCreate
7
- from google.auth.transport import requests
8
- from google.oauth2 import id_token
9
- import google_auth_oauthlib.flow
10
- import os
11
- import secrets
12
- import json
13
- from typing import Optional
14
-
15
- router = APIRouter()
16
-
17
- # OAuth 2.0 configuration
18
- CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID')
19
- CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET')
20
- REDIRECT_URI = os.environ.get('GOOGLE_REDIRECT_URI', 'http://localhost:8000/google_auth/callback')
21
-
22
- # OAuth2 flow configuration
23
- CLIENT_CONFIG = {
24
- "web": {
25
- "client_id": CLIENT_ID,
26
- "client_secret": CLIENT_SECRET,
27
- "auth_uri": "https://accounts.google.com/o/oauth2/auth",
28
- "token_uri": "https://oauth2.googleapis.com/token",
29
- "redirect_uris": [REDIRECT_URI]
30
- }
31
- }
32
-
33
- SCOPES = ['openid', 'email', 'profile']
34
-
35
- # Store state tokens temporarily (in production, use Redis or similar)
36
- state_tokens = {}
37
-
38
- @router.get("/google_auth/login")
39
- @public_route()
40
- async def google_login(request: Request):
41
- """Initiate Google OAuth2 login flow"""
42
- if not CLIENT_ID or not CLIENT_SECRET:
43
- raise HTTPException(status_code=500, detail="Google OAuth not configured")
44
-
45
- # Create flow instance
46
- flow = google_auth_oauthlib.flow.Flow.from_client_config(
47
- CLIENT_CONFIG,
48
- scopes=SCOPES
49
- )
50
- flow.redirect_uri = REDIRECT_URI
51
-
52
- # Generate state token for CSRF protection
53
- state = secrets.token_urlsafe(32)
54
- authorization_url, _ = flow.authorization_url(
55
- access_type='offline',
56
- state=state,
57
- prompt='select_account'
58
- )
59
-
60
- # Store state token
61
- state_tokens[state] = True
62
-
63
- return RedirectResponse(url=authorization_url)
64
-
65
- @router.get("/google_auth/callback")
66
- @public_route()
67
- async def google_callback(request: Request, code: str, state: str):
68
- """Handle Google OAuth2 callback"""
69
- # Verify state token
70
- if state not in state_tokens:
71
- return RedirectResponse(url="/login?error=Invalid+state+token")
72
-
73
- # Remove used state token
74
- del state_tokens[state]
75
-
76
- try:
77
- # Create flow instance
78
- flow = google_auth_oauthlib.flow.Flow.from_client_config(
79
- CLIENT_CONFIG,
80
- scopes=SCOPES,
81
- state=state
82
- )
83
- flow.redirect_uri = REDIRECT_URI
84
-
85
- # Exchange authorization code for tokens
86
- flow.fetch_token(code=code)
87
-
88
- # Get user info from ID token
89
- credentials = flow.credentials
90
- request_session = requests.Request()
91
- id_info = id_token.verify_oauth2_token(
92
- credentials.id_token,
93
- request_session,
94
- CLIENT_ID
95
- )
96
-
97
- # Extract user information
98
- google_id = id_info['sub']
99
- email = id_info['email']
100
- name = id_info.get('name', email.split('@')[0])
101
- email_verified = id_info.get('email_verified', False)
102
-
103
- # Create username from email or name
104
- username = f"google_{google_id[:8]}"
105
-
106
- # Check if user exists
107
- existing_user = await service_manager.get_user_data(username)
108
-
109
- if not existing_user:
110
- # Create new user
111
- user_data = UserCreate(
112
- username=username,
113
- email=email,
114
- password=secrets.token_urlsafe(32) # Random password for OAuth users
115
- )
116
-
117
- # Create user with email already verified if Google verified it
118
- await service_manager.create_user(
119
- user_data,
120
- roles=["user"],
121
- skip_verification=email_verified
122
- )
123
-
124
- # Update user metadata with Google info
125
- user_dir = os.path.join("data/users", username)
126
- google_info = {
127
- "google_id": google_id,
128
- "name": name,
129
- "picture": id_info.get('picture', ''),
130
- "locale": id_info.get('locale', ''),
131
- "auth_method": "google_oauth"
132
- }
133
- with open(os.path.join(user_dir, "google_info.json"), 'w') as f:
134
- json.dump(google_info, f, indent=2)
135
-
136
- # Create JWT token
137
- user_data = await service_manager.get_user_data(username)
138
- access_token = create_access_token(data={"sub": username, **user_data.dict()})
139
-
140
- # Create response with redirect
141
- response = RedirectResponse(url="/", status_code=303)
142
-
143
- # Set cookie
144
- response.set_cookie(
145
- key="access_token",
146
- value=access_token,
147
- max_age=604800, # 1 week
148
- httponly=True,
149
- samesite="Lax"
150
- )
151
-
152
- return response
153
-
154
- except Exception as e:
155
- print(f"Google OAuth error: {e}")
156
- import traceback
157
- traceback.print_exc()
158
- return RedirectResponse(
159
- url="/login?error=Google+authentication+failed",
160
- status_code=303
161
- )
162
-
163
- @router.get("/google_auth/config_check")
164
- @public_route()
165
- async def config_check():
166
- """Check if Google OAuth is configured"""
167
- return {
168
- "configured": bool(CLIENT_ID and CLIENT_SECRET),
169
- "redirect_uri": REDIRECT_URI
170
- }