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.
- mindroot/coreplugins/admin/plugin_manager.py +21 -3
- mindroot/coreplugins/admin/static/js/plugin-advanced-install.js +8 -0
- mindroot/coreplugins/index/default.json +21 -15
- mindroot/coreplugins/index/handlers/plugin_ops.py +13 -0
- mindroot/coreplugins/index/indices/default/index.json +9 -6
- mindroot/coreplugins/index/static/js/plugin-section.js +42 -5
- mindroot/coreplugins/login/templates/login.jinja2 +4 -1
- mindroot/lib/chatlog_optimized.py +509 -0
- mindroot/lib/plugins/default_plugin_manifest.json +1 -1
- {mindroot-8.10.0.dist-info → mindroot-8.11.0.dist-info}/METADATA +1 -1
- {mindroot-8.10.0.dist-info → mindroot-8.11.0.dist-info}/RECORD +15 -19
- mindroot/coreplugins/google_auth/README.md +0 -76
- mindroot/coreplugins/google_auth/__init__.py +0 -1
- mindroot/coreplugins/google_auth/inject/login.jinja2 +0 -69
- mindroot/coreplugins/google_auth/mod.py +0 -1
- mindroot/coreplugins/google_auth/router.py +0 -170
- {mindroot-8.10.0.dist-info → mindroot-8.11.0.dist-info}/WHEEL +0 -0
- {mindroot-8.10.0.dist-info → mindroot-8.11.0.dist-info}/entry_points.txt +0 -0
- {mindroot-8.10.0.dist-info → mindroot-8.11.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-8.10.0.dist-info → mindroot-8.11.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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": "
|
|
20
|
+
"name": "ah_think",
|
|
21
21
|
"version": "0.0.1",
|
|
22
22
|
"description": "Chain of thought reasoning capability",
|
|
23
|
-
"source": "
|
|
24
|
-
"
|
|
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": "
|
|
30
|
+
"name": "ah_look_at",
|
|
31
31
|
"version": "0.0.1",
|
|
32
32
|
"description": "PDF and image examination tools",
|
|
33
|
-
"source": "
|
|
34
|
-
"
|
|
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
|
-
"
|
|
41
|
-
"source": "github",
|
|
42
|
-
"source_path": "runvnc/ah_anthropic",
|
|
40
|
+
"name": "ah_anthropic",
|
|
43
41
|
"version": "0.0.1",
|
|
44
|
-
"
|
|
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
|
-
"
|
|
48
|
-
"source": "github",
|
|
49
|
-
"source_path": "runvnc/ah_shell",
|
|
50
|
+
"name": "ah_shell",
|
|
50
51
|
"version": "0.0.1",
|
|
51
|
-
"
|
|
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
|
-
"
|
|
350
|
-
"
|
|
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
|
-
"
|
|
373
|
-
"
|
|
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
|
-
"
|
|
385
|
-
"
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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)
|
|
@@ -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=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
1829
|
-
mindroot-8.
|
|
1830
|
-
mindroot-8.
|
|
1831
|
-
mindroot-8.
|
|
1832
|
-
mindroot-8.
|
|
1833
|
-
mindroot-8.
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|