mindroot 10.2.0__py3-none-any.whl → 10.4.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.

@@ -0,0 +1,287 @@
1
+ from fastapi import APIRouter, HTTPException, Request, Response
2
+ from fastapi.responses import HTMLResponse
3
+ from pydantic import BaseModel
4
+ from typing import Optional, List
5
+ from .widget_manager import widget_manager
6
+ from lib.auth.api_key import verify_api_key
7
+ from lib.providers.services import service_manager
8
+ from lib.route_decorators import public_route
9
+ import nanoid
10
+ from .services import init_chat_session
11
+ from lib.session_files import save_session_data
12
+ import traceback
13
+
14
+ router = APIRouter()
15
+
16
+ class WidgetTokenCreate(BaseModel):
17
+ api_key: str
18
+ agent_name: str
19
+ base_url: str
20
+ description: Optional[str] = ""
21
+ styling: Optional[dict] = None
22
+
23
+ class WidgetTokenResponse(BaseModel):
24
+ token: str
25
+ agent_name: str
26
+ base_url: str
27
+ description: str
28
+ created_at: str
29
+ created_by: str
30
+ styling: dict
31
+
32
+ @router.post("/widgets/create")
33
+ async def create_widget_token(request: Request, widget_request: WidgetTokenCreate):
34
+ """Create a new widget token."""
35
+ try:
36
+ # Verify the API key is valid
37
+ api_key_data = await verify_api_key(widget_request.api_key)
38
+ if not api_key_data:
39
+ raise HTTPException(status_code=400, detail="Invalid API key")
40
+
41
+ # Verify the agent exists
42
+ try:
43
+ agent_data = await service_manager.get_agent_data(widget_request.agent_name)
44
+ if not agent_data:
45
+ raise HTTPException(status_code=400, detail="Agent not found")
46
+ except Exception:
47
+ raise HTTPException(status_code=400, detail="Agent not found")
48
+
49
+ # Get the current user
50
+ user = request.state.user
51
+
52
+ # Create the widget token
53
+ token = widget_manager.create_widget_token(
54
+ api_key=widget_request.api_key,
55
+ agent_name=widget_request.agent_name,
56
+ base_url=widget_request.base_url,
57
+ created_by=user.username,
58
+ description=widget_request.description,
59
+ styling=widget_request.styling
60
+ )
61
+
62
+ return {
63
+ "success": True,
64
+ "token": token,
65
+ "embed_url": f"{widget_request.base_url}/chat/embed/{token}"
66
+ }
67
+
68
+ except HTTPException:
69
+ raise
70
+ except Exception as e:
71
+ raise HTTPException(status_code=500, detail=str(e))
72
+
73
+ @router.get("/widgets/list")
74
+ async def list_widget_tokens(request: Request):
75
+ """List widget tokens for the current user."""
76
+ try:
77
+ user = request.state.user
78
+ widgets = widget_manager.list_widget_tokens(created_by=user.username)
79
+ return {"success": True, "data": widgets}
80
+ except Exception as e:
81
+ raise HTTPException(status_code=500, detail=str(e))
82
+
83
+ @router.delete("/widgets/delete/{token}")
84
+ async def delete_widget_token(request: Request, token: str):
85
+ """Delete a widget token."""
86
+ try:
87
+ # Verify the widget exists and belongs to the user
88
+ widget_config = widget_manager.get_widget_config(token)
89
+ if not widget_config:
90
+ raise HTTPException(status_code=404, detail="Widget token not found")
91
+
92
+ user = request.state.user
93
+ if widget_config.get("created_by") != user.username:
94
+ raise HTTPException(status_code=403, detail="Not authorized to delete this widget")
95
+
96
+ success = widget_manager.delete_widget_token(token)
97
+ if success:
98
+ return {"success": True, "message": "Widget token deleted successfully"}
99
+ else:
100
+ raise HTTPException(status_code=500, detail="Failed to delete widget token")
101
+
102
+ except HTTPException:
103
+ raise
104
+ except Exception as e:
105
+ raise HTTPException(status_code=500, detail=str(e))
106
+
107
+ @router.get("/chat/embed/{token}")
108
+ @public_route()
109
+ async def get_embed_script(token: str):
110
+ """Generate the secure embed script for a widget token."""
111
+ try:
112
+ # Validate the widget token
113
+ widget_config = widget_manager.get_widget_config(token)
114
+ if not widget_config:
115
+ raise HTTPException(status_code=404, detail="Widget token not found")
116
+
117
+ # Generate the embed JavaScript
118
+ base_url = widget_config["base_url"]
119
+ styling = widget_config.get("styling", {})
120
+
121
+ # Create the embed script that doesn't expose the API key
122
+ embed_script = f'''
123
+ (function() {{
124
+ const config = {{
125
+ baseUrl: "{base_url}",
126
+ token: "{token}",
127
+ position: "{styling.get('position', 'bottom-right')}",
128
+ width: "{styling.get('width', '400px')}",
129
+ height: "{styling.get('height', '600px')}",
130
+ theme: "{styling.get('theme', 'dark')}"
131
+ }};
132
+
133
+ let chatContainer = null;
134
+ let chatIcon = null;
135
+ let isLoaded = false;
136
+
137
+ function createChatIcon() {{
138
+ if (chatIcon) return;
139
+
140
+ chatIcon = document.createElement("div");
141
+ chatIcon.id = "mindroot-chat-icon-" + config.token;
142
+ chatIcon.innerHTML = "💬";
143
+
144
+ const iconStyles = {{
145
+ position: "fixed",
146
+ bottom: "20px",
147
+ right: config.position.includes("left") ? "auto" : "20px",
148
+ left: config.position.includes("left") ? "20px" : "auto",
149
+ width: "60px",
150
+ height: "60px",
151
+ background: "#2196F3",
152
+ borderRadius: "50%",
153
+ display: "flex",
154
+ alignItems: "center",
155
+ justifyContent: "center",
156
+ cursor: "pointer",
157
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
158
+ zIndex: "10000",
159
+ fontSize: "24px",
160
+ color: "white",
161
+ transition: "all 0.3s ease"
162
+ }};
163
+
164
+ Object.assign(chatIcon.style, iconStyles);
165
+ chatIcon.addEventListener("click", toggleChat);
166
+ document.body.appendChild(chatIcon);
167
+ }}
168
+
169
+ function createChatContainer() {{
170
+ if (chatContainer) return;
171
+
172
+ chatContainer = document.createElement("div");
173
+ chatContainer.id = "mindroot-chat-container-" + config.token;
174
+
175
+ const containerStyles = {{
176
+ position: "fixed",
177
+ bottom: "90px",
178
+ right: config.position.includes("left") ? "auto" : "20px",
179
+ left: config.position.includes("left") ? "20px" : "auto",
180
+ width: config.width,
181
+ height: config.height,
182
+ background: "white",
183
+ borderRadius: "12px",
184
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
185
+ zIndex: "10001",
186
+ display: "none",
187
+ overflow: "hidden"
188
+ }};
189
+
190
+ Object.assign(chatContainer.style, containerStyles);
191
+ document.body.appendChild(chatContainer);
192
+ }}
193
+
194
+ function toggleChat() {{
195
+ if (!chatContainer) createChatContainer();
196
+
197
+ const isVisible = chatContainer.style.display !== "none";
198
+
199
+ if (isVisible) {{
200
+ chatContainer.style.display = "none";
201
+ }} else {{
202
+ if (!isLoaded) {{
203
+ // Create iframe and load the secure session
204
+ const iframe = document.createElement("iframe");
205
+ iframe.style.cssText = "width: 100%; height: 100%; border: none; border-radius: 12px;";
206
+ iframe.src = config.baseUrl + "/chat/widget/" + config.token + "/session";
207
+ chatContainer.appendChild(iframe);
208
+ isLoaded = true;
209
+ }}
210
+ chatContainer.style.display = "block";
211
+ }}
212
+ }}
213
+
214
+ function init() {{
215
+ if (document.readyState === "loading") {{
216
+ document.addEventListener("DOMContentLoaded", function() {{
217
+ createChatIcon();
218
+ }});
219
+ }} else {{
220
+ createChatIcon();
221
+ }}
222
+ }}
223
+
224
+ init();
225
+ }})();
226
+ '''
227
+
228
+ return Response(
229
+ content=embed_script,
230
+ media_type="application/javascript",
231
+ headers={
232
+ "Cache-Control": "no-cache, no-store, must-revalidate",
233
+ "Pragma": "no-cache",
234
+ "Expires": "0"
235
+ }
236
+ )
237
+
238
+ except HTTPException:
239
+ raise
240
+ except Exception as e:
241
+ raise HTTPException(status_code=500, detail=str(e))
242
+
243
+
244
+ @router.get("/chat/widget/{token}/session")
245
+ @public_route()
246
+ async def create_widget_session(token: str):
247
+ """Create a secure chat session for a widget token."""
248
+ try:
249
+ # Validate the widget token
250
+ widget_config = widget_manager.get_widget_config(token)
251
+ if not widget_config:
252
+ raise HTTPException(status_code=404, detail="Widget token not found")
253
+
254
+ # Verify the API key from the widget config
255
+ api_key_data = await verify_api_key(widget_config["api_key"])
256
+ if not api_key_data:
257
+ raise HTTPException(status_code=401, detail="Invalid API key in widget configuration")
258
+
259
+ # Create mock user and generate session ID
260
+ class MockUser:
261
+ def __init__(self, username):
262
+ self.username = username
263
+ user = MockUser(api_key_data['username'])
264
+ session_id = nanoid.generate()
265
+ agent_name = widget_config["agent_name"]
266
+
267
+ # Initialize the chat session (this was missing!)
268
+ await init_chat_session(user, agent_name, session_id)
269
+
270
+ # Create and save access token for authentication
271
+ from coreplugins.jwt_auth.middleware import create_access_token
272
+ token_data = create_access_token({"sub": api_key_data['username']})
273
+ await save_session_data(session_id, "access_token", token_data)
274
+
275
+ # Now redirect to the session WITHOUT exposing the API key
276
+ redirect_url = f"/session/{agent_name}/{session_id}?embed=true"
277
+
278
+ return Response(
279
+ status_code=302,
280
+ headers={"Location": redirect_url}
281
+ )
282
+
283
+ except Exception as e:
284
+ trace = traceback.format_exc()
285
+ print(e, trace)
286
+ raise HTTPException(status_code=500, detail=str(e))
287
+
@@ -22,6 +22,7 @@ from io import BytesIO
22
22
  import base64
23
23
  import nanoid
24
24
  sse_clients = {}
25
+ from lib.chatcontext import get_context
25
26
 
26
27
  @service()
27
28
  async def prompt(model: str, instructions: str, temperature=0, max_tokens=400, json=False, context=None):
@@ -485,3 +486,66 @@ async def command_result(command: str, result, context=None):
485
486
  agent_ = context.agent
486
487
  await context.agent_output("command_result", { "command": command, "result": result, "persona": agent_['persona']['name'] })
487
488
 
489
+ @service()
490
+ async def backend_user_message(message: str, context=None):
491
+ """
492
+ Insert a user message from the backend and signal the frontend to display it.
493
+ This allows backend processes to inject messages into the chat without user interaction.
494
+ """
495
+ agent_ = context.agent
496
+ persona = 'user'
497
+ await context.agent_output("backend_user_message", {
498
+ "content": message,
499
+ "sender": "user",
500
+ "persona": persona
501
+ })
502
+
503
+ @service()
504
+ async def cancel_active_response(log_id: str, context=None):
505
+ """
506
+ Cancel active AI response for eager end of turn processing.
507
+ Sets the finished_conversation flag to stop the agent processing loop.
508
+ """
509
+ if context is None:
510
+ # Get context from log_id - we need the username, but for SIP calls it might be 'system'
511
+ # Try to load context, fallback to system user if needed
512
+ try:
513
+ context = await get_context(log_id, 'system')
514
+ except Exception as e:
515
+ print(f"Error getting context for cancellation: {e}")
516
+ return {"status": "error", "message": f"Could not load context: {e}"}
517
+
518
+ # Set flag to stop current processing loop iteration
519
+ # But don't permanently mark conversation as finished - just this turn
520
+ context.data['cancel_current_turn'] = True
521
+
522
+ # DEBUG TRACE
523
+ print("\033[91;107m[DEBUG TRACE 5/6] Core cancel_active_response service executed.\033[0m")
524
+
525
+ # Cancel any active TTS streams (ElevenLabs)
526
+ try:
527
+ # Import here to avoid circular dependency
528
+ from mr_eleven_stream.mod import _active_tts_streams
529
+ for stream_id, stop_event in list(_active_tts_streams.items()):
530
+ stop_event.set()
531
+ logger.info(f"Cancelled TTS stream {stream_id}")
532
+ print("\033[91;107m[DEBUG TRACE 5.5/6] Cancelled active TTS stream.\033[0m")
533
+ except ImportError:
534
+ logger.debug("ElevenLabs TTS plugin not available for cancellation")
535
+
536
+ # Also, cancel any active command task (like speak())
537
+ if 'active_command_task' in context.data:
538
+ active_task = context.data['active_command_task']
539
+ if active_task and not active_task.done():
540
+ try:
541
+ active_task.cancel()
542
+ # DEBUG TRACE
543
+ print("\033[91;107m[DEBUG TRACE 6/6] Active command task found and cancelled.\033[0m")
544
+ print(f"Cancelled active command task for session {log_id}")
545
+ except Exception as e:
546
+ print(f"Error cancelling active command task: {e}")
547
+
548
+ await context.save_context()
549
+
550
+ print(f"Cancelled active response for session {log_id}")
551
+ return {"status": "cancelled", "log_id": log_id}
@@ -1,4 +1,5 @@
1
1
  /* Mobile-specific styles */
2
+
2
3
  @media screen and (max-width: 768px) {
3
4
  .page-container {
4
5
  position: relative;
@@ -132,6 +133,42 @@
132
133
  padding: 8px 12px;
133
134
  }
134
135
 
136
+ /* Fix for mobile chat layout - ensure chat form is visible */
137
+ html, body {
138
+ height: 100%;
139
+ overflow: hidden;
140
+ }
141
+
142
+ .page-container {
143
+ height: 100%;
144
+ display: flex;
145
+ flex-direction: column;
146
+ }
147
+
148
+ .main {
149
+ height: 100%;
150
+ display: flex;
151
+ flex-direction: column;
152
+ overflow: hidden;
153
+ }
154
+
155
+ chat-ai {
156
+ height: 100%;
157
+ display: flex;
158
+ flex-direction: column;
159
+ padding-top: 60px; /* Space for hamburger menu */
160
+ padding-bottom: 10px;
161
+ }
162
+
163
+ .chat-log {
164
+ flex: 1;
165
+ overflow-y: auto;
166
+ }
167
+
168
+ chat-form {
169
+ flex-shrink: 0;
170
+ }
171
+
135
172
  /* Ensure images in markdown content are also responsive */
136
173
  .message p img {
137
174
  max-width: 100%;
@@ -117,6 +117,7 @@ class Chat extends BaseEl {
117
117
  this.sse.addEventListener('command_result', e => thisResult(e).catch(console.error));
118
118
  this.sse.addEventListener('finished_chat', e => thisFinished(e).catch(console.error));
119
119
  this.sse.addEventListener('system_error', e=> thisError(e).catch(console.error));
120
+ this.sse.addEventListener('backend_user_message', this._backendUserMessage.bind(this));
120
121
 
121
122
  // when the user scrolls in the chat log, stop auto-scrolling to the bottom
122
123
  const chatLog = this.shadowRoot.querySelector('.chat-log');
@@ -144,6 +145,27 @@ class Chat extends BaseEl {
144
145
  showNotification('error', data.error);
145
146
  }
146
147
 
148
+ _backendUserMessage(event) {
149
+ console.log('Backend user message received:', event);
150
+ const data = JSON.parse(event.data);
151
+ const { content, sender, persona } = data;
152
+
153
+ // Parse the content as markdown
154
+ const parsed = tryParse(content);
155
+
156
+ // Add the message to the chat log
157
+ this.messages = [...this.messages, {
158
+ content: parsed,
159
+ spinning: 'no',
160
+ sender: sender || 'user',
161
+ persona: persona || 'user'
162
+ }];
163
+
164
+ // Scroll to show the new message
165
+ setTimeout(() => {
166
+ this._scrollToBottom();
167
+ }, 100);
168
+ }
147
169
 
148
170
 
149
171
  _addMessage(event) {
@@ -36,7 +36,7 @@
36
36
 
37
37
  {% block head_extra %}{% endblock %}
38
38
  </head>
39
- <body>
39
+ <body{% if embed_mode %} class="embedded"{% endif %}>
40
40
  {% block body_init %}
41
41
  <script>
42
42
  // print a big colorful header annoucing data init section
@@ -133,6 +133,12 @@ async def get_embed_script(token: str):
133
133
  let chatContainer = null;
134
134
  let chatIcon = null;
135
135
  let isLoaded = false;
136
+ let isMobile = false;
137
+
138
+ function detectMobile() {{
139
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera || "";
140
+ return /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent.toLowerCase());
141
+ }}
136
142
 
137
143
  function createChatIcon() {{
138
144
  if (chatIcon) return;
@@ -141,13 +147,15 @@ async def get_embed_script(token: str):
141
147
  chatIcon.id = "mindroot-chat-icon-" + config.token;
142
148
  chatIcon.innerHTML = "💬";
143
149
 
150
+ const iconSize = isMobile ? 80 : 60;
151
+ const fontSize = isMobile ? 28 : 24;
144
152
  const iconStyles = {{
145
153
  position: "fixed",
146
- bottom: "20px",
154
+ bottom: isMobile ? "30px" : "20px",
147
155
  right: config.position.includes("left") ? "auto" : "20px",
148
156
  left: config.position.includes("left") ? "20px" : "auto",
149
- width: "60px",
150
- height: "60px",
157
+ width: iconSize + "px",
158
+ height: iconSize + "px",
151
159
  background: "#2196F3",
152
160
  borderRadius: "50%",
153
161
  display: "flex",
@@ -156,7 +164,7 @@ async def get_embed_script(token: str):
156
164
  cursor: "pointer",
157
165
  boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
158
166
  zIndex: "10000",
159
- fontSize: "24px",
167
+ fontSize: fontSize + "px",
160
168
  color: "white",
161
169
  transition: "all 0.3s ease"
162
170
  }};
@@ -172,7 +180,22 @@ async def get_embed_script(token: str):
172
180
  chatContainer = document.createElement("div");
173
181
  chatContainer.id = "mindroot-chat-container-" + config.token;
174
182
 
175
- const containerStyles = {{
183
+ const containerStyles = isMobile ? {{
184
+ position: "fixed",
185
+ top: "0px",
186
+ left: "0",
187
+ right: "0",
188
+ bottom: "0",
189
+ width: "100vw",
190
+ height: "100vh",
191
+ background: "white",
192
+ borderRadius: "0",
193
+ boxShadow: "none",
194
+ zIndex: "10001",
195
+ display: "none",
196
+ flexDirection: "column",
197
+ overflow: "hidden"
198
+ }} : {{
176
199
  position: "fixed",
177
200
  bottom: "90px",
178
201
  right: config.position.includes("left") ? "auto" : "20px",
@@ -198,26 +221,47 @@ async def get_embed_script(token: str):
198
221
 
199
222
  if (isVisible) {{
200
223
  chatContainer.style.display = "none";
224
+ if (isMobile) {{
225
+ document.body.style.removeProperty("overflow");
226
+ }}
201
227
  }} else {{
202
228
  if (!isLoaded) {{
203
229
  // Create iframe and load the secure session
204
230
  const iframe = document.createElement("iframe");
205
- iframe.style.cssText = "width: 100%; height: 100%; border: none; border-radius: 12px;";
231
+ if (isMobile) {{
232
+ iframe.style.cssText = "width: 100%; height: 100%; border: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;";
233
+ // Add viewport meta tag if not present
234
+ if (!document.querySelector('meta[name="viewport"]')) {{
235
+ const meta = document.createElement('meta');
236
+ meta.name = 'viewport';
237
+ meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
238
+ document.head.appendChild(meta);
239
+ }}
240
+ }} else {{
241
+ iframe.style.cssText = "width: 100%; height: 100%; border: none; border-radius: 12px;";
242
+ }}
206
243
  iframe.src = config.baseUrl + "/chat/widget/" + config.token + "/session";
244
+ iframe.allow = "microphone autoplay"
207
245
  chatContainer.appendChild(iframe);
208
246
  isLoaded = true;
209
247
  }}
210
248
  chatContainer.style.display = "block";
249
+ if (isMobile) {{
250
+ document.body.style.overflow = "hidden";
251
+ }}
211
252
  }}
212
253
  }}
213
254
 
214
255
  function init() {{
256
+ const boot = () => {{
257
+ isMobile = detectMobile();
258
+ createChatIcon();
259
+ }};
260
+
215
261
  if (document.readyState === "loading") {{
216
- document.addEventListener("DOMContentLoaded", function() {{
217
- createChatIcon();
218
- }});
262
+ document.addEventListener("DOMContentLoaded", boot);
219
263
  }} else {{
220
- createChatIcon();
264
+ boot();
221
265
  }}
222
266
  }}
223
267
 
@@ -593,3 +593,55 @@ You are working on {task_context}."""
593
593
 
594
594
  return await command_manager.delegate_task(instructions, agent_name, context=context)
595
595
 
596
+
597
+
598
+ @command()
599
+ async def create_checklist(tasks, title=None, replace=True, context=None):
600
+ """
601
+ Create a new checklist dynamically from a list of task descriptions.
602
+
603
+ Parameters:
604
+ - tasks: Required. List of task description strings
605
+ - title: Optional. Title for the checklist (for display purposes)
606
+ - replace: Optional. Whether to replace existing checklist (default: True)
607
+
608
+ Example:
609
+ { "create_checklist": {
610
+ "tasks": [
611
+ "Research the topic",
612
+ "Create outline",
613
+ "Write first draft",
614
+ "Review and edit"
615
+ ],
616
+ "title": "Article Writing Process"
617
+ }}
618
+ """
619
+ if context is None:
620
+ return "_Context is required._"
621
+
622
+ if not tasks or not isinstance(tasks, list):
623
+ return "_Tasks parameter must be a non-empty list of task descriptions._"
624
+
625
+ # Check if we should replace existing checklist
626
+ st = _state(context)
627
+ if not replace and st["tasks"]:
628
+ return "_Checklist already exists. Use replace=True to overwrite it._"
629
+
630
+ # Convert task list to markdown checklist format
631
+ markdown_lines = []
632
+ if title:
633
+ markdown_lines.append(f"# {title}\n")
634
+
635
+ for task_desc in tasks:
636
+ if not isinstance(task_desc, str):
637
+ return "_All tasks must be strings._"
638
+ markdown_lines.append(f"- [ ] {task_desc.strip()}")
639
+
640
+ markdown_text = "\n".join(markdown_lines)
641
+
642
+ # Use existing load_checklist function to parse and store
643
+ load_checklist(markdown_text, context)
644
+
645
+ # Build response
646
+ title_text = f" '{title}'" if title else ""
647
+ return f"Created checklist{title_text} with {len(tasks)} tasks.\n\n{_format_checklist_status(context)}"