mindroot 10.4.0__py3-none-any.whl → 10.6.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.

@@ -658,7 +658,14 @@ class AgentForm extends BaseEl {
658
658
  }
659
659
  console.log('before',this.agent)
660
660
  // Handle all other inputs
661
- const inputValue = type === 'checkbox' ? checked : value;
661
+ let inputValue;
662
+ if (type === 'checkbox') {
663
+ inputValue = checked;
664
+ } else if (type === 'number') {
665
+ inputValue = value === '' ? null : Number(value);
666
+ } else {
667
+ inputValue = value;
668
+ }
662
669
  this.agent = { ...this.agent, [name]: inputValue };
663
670
  console.log('after', this.agent)
664
671
  }
@@ -1273,6 +1280,24 @@ class AgentForm extends BaseEl {
1273
1280
  </details>
1274
1281
  </div>
1275
1282
 
1283
+ <div class="form-group commands-section">
1284
+ <details>
1285
+ <summary>Max Tokens</summary>
1286
+ <div class="commands-category">
1287
+ <div class="form-group">
1288
+ <label>Maximum Tokens:</label>
1289
+ <input
1290
+ type="number"
1291
+ name="max_tokens"
1292
+ .value=${agentForRender.max_tokens || ''}
1293
+ placeholder="Leave empty for model default"
1294
+ @input=${this.handleInputChange}
1295
+ >
1296
+ </div>
1297
+ </div>
1298
+ </details>
1299
+ </div>
1300
+
1276
1301
  <div class="form-group commands-section">
1277
1302
  <details>
1278
1303
  <summary>Recommended Plugins</summary>
@@ -10,7 +10,8 @@ class PersonaEditor extends BaseEl {
10
10
  personas: { type: Array, reflect: true },
11
11
  newPersona: { type: Boolean, reflect: true },
12
12
  facerefFileName: { type: String, reflect: true },
13
- avatarFileName: { type: String, reflect: true }
13
+ avatarFileName: { type: String, reflect: true },
14
+ voiceId: { type: String, reflect: true }
14
15
  };
15
16
 
16
17
  static styles = [
@@ -130,6 +131,7 @@ class PersonaEditor extends BaseEl {
130
131
  this.newPersona = false;
131
132
  this.facerefFileName = '';
132
133
  this.avatarFileName = '';
134
+ this.voiceId = '';
133
135
  this.fetchPersonas();
134
136
  }
135
137
 
@@ -168,13 +170,16 @@ class PersonaEditor extends BaseEl {
168
170
  if (this.scope === 'registry') {
169
171
  // For registry personas, use the full path format
170
172
  const response = await fetch(`/personas/registry/${this.name}`);
173
+ this.voiceId = this.persona.voice_id || '';
171
174
  this.persona = await response.json();
172
175
  } else {
173
176
  const response = await fetch(`/personas/${this.scope}/${this.name}`);
174
177
  this.persona = await response.json();
178
+ this.voiceId = this.persona.voice_id || '';
175
179
  }
176
180
  } else {
177
181
  this.persona = {};
182
+ this.voiceId = '';
178
183
  }
179
184
  }
180
185
 
@@ -194,11 +199,15 @@ class PersonaEditor extends BaseEl {
194
199
  this.persona = {};
195
200
  this.facerefFileName = '';
196
201
  this.avatarFileName = '';
202
+ this.voiceId = '';
197
203
  }
198
204
 
199
205
  handleInputChange(event) {
200
206
  const { name, value, type, checked } = event.target;
201
207
  const inputValue = type === 'checkbox' ? checked : value;
208
+ if (name === 'voice_id') {
209
+ this.voiceId = value;
210
+ }
202
211
  this.persona = { ...this.persona, [name]: inputValue };
203
212
  }
204
213
 
@@ -280,6 +289,10 @@ class PersonaEditor extends BaseEl {
280
289
  <label>Negative Appearance:</label>
281
290
  <textarea class="text_lg" name="negative_appearance" .value=${this.persona.negative_appearance || ''} @input=${this.handleInputChange}></textarea>
282
291
  </div>
292
+ <div class="form-group">
293
+ <label>Voice ID:</label>
294
+ <input class="text_inp" type="text" name="voice_id" .value=${this.voiceId || ''} @input=${this.handleInputChange} placeholder="Optional voice identifier for TTS" />
295
+ </div>
283
296
  <div class="form-group">
284
297
  <label>
285
298
  Moderated:
@@ -340,7 +340,8 @@ class Agent:
340
340
  logger.debug(f"Processing command: {cmd}")
341
341
  await context.partial_command(cmd_name, json.dumps(cmd_args), cmd_args)
342
342
 
343
- # Create a task for the command so it can be cancelled
343
+ self.handle_cmds(cmd_name, cmd_args, json_cmd=json.dumps(cmd), context=context)
344
+
344
345
  cmd_task = asyncio.create_task(
345
346
  self.handle_cmds(cmd_name, cmd_args, json_cmd=json.dumps(cmd), context=context)
346
347
  )
@@ -355,6 +356,14 @@ class Agent:
355
356
  await context.command_result(cmd_name, result)
356
357
  sys_header = "Note: tool command results follow, not user replies"
357
358
  sys_header = ""
359
+
360
+ if result == "SYSTEM: WARNING - Command interrupted!\n\n":
361
+ logger.warning("Command was interrupted. Skipping any extra commands in list.")
362
+ await context.chat_log.drop_last('assistant')
363
+ return results, full_cmds
364
+ break
365
+
366
+
358
367
  full_cmds.append({ "SYSTEM": sys_header, "cmd": cmd_name, "args": cmd_args, "result": result})
359
368
  if result is not None:
360
369
  results.append({"SYSTEM": sys_header, "cmd": cmd_name, "args": { "omitted": "(see command msg.)"}, "result": result})
@@ -473,6 +482,7 @@ class Agent:
473
482
  tmp_data = { "messages": new_messages }
474
483
  debug_box("Filtering messages")
475
484
  #debug_box(tmp_data)
485
+
476
486
  tmp_data = await pipeline_manager.filter_messages(tmp_data, context=context)
477
487
  new_messages = tmp_data['messages']
478
488
  except Exception as e:
@@ -487,6 +497,12 @@ class Agent:
487
497
  if not isinstance(context.agent, dict):
488
498
  context.agent = await get_agent_data(context.agent, context=context)
489
499
 
500
+ if 'max_tokens' in context.agent and context.agent['max_tokens'] is not None and context.agent['max_tokens'] != '':
501
+ logger.info(f"Using agent max tokens {max_tokens}")
502
+ max_tokens = context.agent['max_tokens']
503
+ else:
504
+ logger.info(f"Using default max tokens {max_tokens}")
505
+
490
506
  if model is None:
491
507
  if 'service_models' in context.agent and context.agent['service_models'] is not None:
492
508
  if context.agent['service_models'].get('stream_chat', None) is None:
@@ -0,0 +1,495 @@
1
+ from fastapi import APIRouter, HTTPException, Request, Response, Depends, Query
2
+ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
3
+ from fastapi import File, UploadFile, Form
4
+ from sse_starlette.sse import EventSourceResponse
5
+ from .models import MessageParts
6
+ from lib.providers.services import service, service_manager
7
+ from .services import init_chat_session, send_message_to_agent, subscribe_to_agent_messages, get_chat_history, run_task
8
+ from lib.templates import render
9
+ from lib.auth.auth import require_user
10
+ from lib.plugins import list_enabled
11
+ import nanoid
12
+ from lib.providers.commands import *
13
+ import asyncio
14
+ from lib.chatcontext import get_context, ChatContext
15
+ from typing import List
16
+ from lib.providers.services import service, service_manager
17
+ from lib.providers.commands import command_manager
18
+ from lib.utils.debug import debug_box
19
+ from lib.session_files import load_session_data, save_session_data
20
+ import os
21
+ import json
22
+ from lib.chatcontext import ChatContext
23
+ import shutil
24
+ from pydantic import BaseModel
25
+ from lib.auth.api_key import verify_api_key
26
+ from .services import active_send_tasks, cancel_active_send_task, get_active_send_tasks, cleanup_completed_tasks
27
+
28
+ router = APIRouter()
29
+
30
+ # Global dictionary to store tasks
31
+ tasks = {}
32
+
33
+ @router.post("/chat/{log_id}/{task_id}/cancel")
34
+ async def cancel_chat(request: Request, log_id: str, task_id: str):
35
+ debug_box("cancel_chat")
36
+ print("Trying to cancel task", task_id)
37
+ user = request.state.user.username
38
+ context = await get_context(log_id, user)
39
+ debug_box(str(context))
40
+
41
+ # Cancel the active send_message_to_agent task if it exists
42
+ if log_id in active_send_tasks:
43
+ task_info = active_send_tasks[log_id]
44
+ task = task_info['task']
45
+
46
+ if not task.done():
47
+ print(f"Cancelling active send_message_to_agent task for session {log_id}")
48
+ task.cancel()
49
+ context.data['task_cancelled'] = True
50
+ context.data['cancel_current_turn'] = True
51
+
52
+ # Also cancel the router task if it exists
53
+ if task_id in tasks:
54
+ tasks[task_id].cancel()
55
+ del tasks[task_id]
56
+
57
+ return {"status": "ok", "message": "Task cancelled successfully"}
58
+ else:
59
+ return {"status": "error", "message": "No active task found"}
60
+ else:
61
+ return {"status": "error", "message": "No active task found for this session"}
62
+
63
+
64
+ @router.get("/context1/{log_id}")
65
+ async def context1(request: Request, log_id: str):
66
+ user = request.state.user.username
67
+ context = await get_context(log_id, user)
68
+ print(context)
69
+ return "ok"
70
+
71
+
72
+ # need to serve persona images from ./personas/local/[persona_path]/avatar.png
73
+ @router.get("/chat/personas/{persona_path:path}/avatar.png")
74
+ async def get_persona_avatar(persona_path: str):
75
+ # Check if this is a registry persona with deduplicated assets
76
+ if persona_path.startswith("registry/"):
77
+ persona_json_path = f"personas/{persona_path}/persona.json"
78
+ if os.path.exists(persona_json_path):
79
+ try:
80
+ with open(persona_json_path, "r") as f:
81
+ persona_data = json.load(f)
82
+
83
+ # Check if persona has asset hashes (deduplicated storage)
84
+ asset_hashes = persona_data.get("asset_hashes", {})
85
+ if "avatar" in asset_hashes:
86
+ # Redirect to deduplicated asset endpoint
87
+ return RedirectResponse(f"/assets/{asset_hashes['avatar']}")
88
+ except Exception as e:
89
+ print(f"Error checking for deduplicated assets: {e}")
90
+
91
+ # Handle registry personas: registry/owner/name
92
+ if persona_path.startswith('registry/'):
93
+ file_path = f"personas/{persona_path}/avatar.png"
94
+ else:
95
+ # Legacy support: check local first, then shared
96
+ file_path = f"personas/local/{persona_path}/avatar.png"
97
+ if not os.path.exists(file_path):
98
+ file_path = f"personas/registry/{persona_path}/avatar.png"
99
+
100
+ if not os.path.exists(file_path):
101
+ resolved = os.path.realpath(file_path)
102
+ return {"error": "File not found: " + resolved}
103
+
104
+ with open(file_path, "rb") as f:
105
+ image_bytes = f.read()
106
+
107
+ return Response(
108
+ content=image_bytes,
109
+ media_type="image/png",
110
+ headers={
111
+ "Cache-Control": "max-age=3600",
112
+ "Content-Disposition": "inline; filename=avatar.png"
113
+ }
114
+ )
115
+
116
+ @router.get("/chat/personas/{persona_path:path}/faceref.png")
117
+ async def get_persona_faceref(persona_path: str):
118
+ # Check if this is a registry persona with deduplicated assets
119
+ if persona_path.startswith("registry/"):
120
+ persona_json_path = f"personas/{persona_path}/persona.json"
121
+ if os.path.exists(persona_json_path):
122
+ try:
123
+ with open(persona_json_path, "r") as f:
124
+ persona_data = json.load(f)
125
+
126
+ # Check if persona has asset hashes (deduplicated storage)
127
+ asset_hashes = persona_data.get("asset_hashes", {})
128
+ if "faceref" in asset_hashes:
129
+ # Redirect to deduplicated asset endpoint
130
+ return RedirectResponse(f"/assets/{asset_hashes['faceref']}")
131
+ except Exception as e:
132
+ print(f"Error checking for deduplicated assets: {e}")
133
+
134
+ # Handle registry personas: registry/owner/name
135
+ if persona_path.startswith('registry/'):
136
+ file_path = f"personas/{persona_path}/faceref.png"
137
+ else:
138
+ # Legacy support: check local first, then shared
139
+ file_path = f"personas/local/{persona_path}/faceref.png"
140
+ if not os.path.exists(file_path):
141
+ file_path = f"personas/registry/{persona_path}/faceref.png"
142
+
143
+ if not os.path.exists(file_path):
144
+ # Fallback to avatar if faceref doesn't exist
145
+ return RedirectResponse(f"/chat/personas/{persona_path}/avatar.png")
146
+
147
+ with open(file_path, "rb") as f:
148
+ image_bytes = f.read()
149
+
150
+ return Response(
151
+ content=image_bytes,
152
+ media_type="image/png",
153
+ headers={
154
+ "Cache-Control": "max-age=3600",
155
+ "Content-Disposition": "inline; filename=faceref.png"
156
+ }
157
+ )
158
+
159
+ @router.get("/chat/{log_id}/events")
160
+ async def chat_events(log_id: str):
161
+ return EventSourceResponse(await subscribe_to_agent_messages(log_id))
162
+
163
+
164
+ @router.post("/chat/{log_id}/send")
165
+ async def send_message(request: Request, log_id: str, message_parts: List[MessageParts] ):
166
+ user = request.state.user
167
+ debug_box("send_message")
168
+
169
+
170
+ context = await get_context(log_id, user.username)
171
+ debug_box(str(context))
172
+ #context = ChatContext(command_manager, service_manager, user=user.user)
173
+ task = asyncio.create_task(send_message_to_agent(log_id, message_parts, context=context, user=user))
174
+ #task = asyncio.create_task(send_message_to_agent(log_id, message_parts, user=user))
175
+
176
+ task_id = nanoid.generate()
177
+
178
+
179
+ # Check if there's already an active task for this session and cancel it
180
+ if log_id in active_send_tasks:
181
+ existing_task_info = active_send_tasks[log_id]
182
+ existing_task = existing_task_info['task']
183
+ if not existing_task.done():
184
+ print(f"Cancelling existing task for session {log_id} before starting new one")
185
+ existing_task.cancel()
186
+
187
+ tasks[task_id] = task
188
+
189
+ return {"status": "ok", "task_id": task_id}
190
+
191
+ @router.get("/chat/{log_id}/active_tasks")
192
+ async def get_active_tasks(request: Request, log_id: str):
193
+ """
194
+ Get information about active tasks for a session (for debugging)
195
+ """
196
+ user = request.state.user.username
197
+
198
+ active_info = {}
199
+ if log_id in active_send_tasks:
200
+ task_info = active_send_tasks[log_id]
201
+ task = task_info['task']
202
+ active_info = {
203
+ "session_id": log_id,
204
+ "task_active": not task.done(),
205
+ "task_cancelled": task.cancelled() if task else False,
206
+ "created_at": task_info.get('created_at'),
207
+ "router_tasks": [tid for tid, t in tasks.items() if not t.done()]
208
+ }
209
+
210
+ return {"status": "ok", "active_tasks": active_info}
211
+
212
+ @router.post("/chat/{log_id}/cancel_send_task")
213
+ async def cancel_send_task_endpoint(request: Request, log_id: str):
214
+ """
215
+ Cancel the active send_message_to_agent task for a session.
216
+ """
217
+ user = request.state.user.username
218
+ context = await get_context(log_id, user)
219
+
220
+ result = await cancel_active_send_task(log_id, context=context)
221
+ return result
222
+
223
+ @router.get("/chat/active_send_tasks")
224
+ async def get_all_active_send_tasks(request: Request):
225
+ """
226
+ Get information about all active send_message_to_agent tasks.
227
+ """
228
+ result = await get_active_send_tasks()
229
+ return result
230
+
231
+ @router.post("/chat/cleanup_completed_tasks")
232
+ async def cleanup_completed_tasks_endpoint(request: Request):
233
+ """
234
+ Clean up completed tasks from memory.
235
+ """
236
+ result = await cleanup_completed_tasks()
237
+ return result
238
+
239
+ @router.get("/agent/{agent_name}", response_class=HTMLResponse)
240
+ async def get_chat_html(request: Request, agent_name: str, api_key: str = Query(None), embed: bool = Query(False)):
241
+ # Handle API key authentication if provided
242
+ if api_key:
243
+ try:
244
+ user_data = await verify_api_key(api_key)
245
+ if not user_data:
246
+ raise HTTPException(status_code=401, detail="Invalid API key")
247
+ # Create a mock user object for API key users
248
+ class MockUser:
249
+ def __init__(self, username):
250
+ self.username = username
251
+ user = MockUser(user_data['username'])
252
+ except Exception as e:
253
+ raise HTTPException(status_code=401, detail="Invalid API key")
254
+ else:
255
+ # Use regular authentication
256
+ if not hasattr(request.state, "user"):
257
+ return RedirectResponse("/login")
258
+ user = request.state.user
259
+
260
+ log_id = nanoid.generate()
261
+ plugins = list_enabled()
262
+ print("Init chat with user", user)
263
+ print(f"Init chat with {agent_name}")
264
+ await init_chat_session(user, agent_name, log_id)
265
+
266
+ if hasattr(request.state, "access_token"):
267
+ debug_box("Access token found in request state, saving to session file")
268
+ access_token = request.state.access_token
269
+ await save_session_data(log_id, "access_token", access_token)
270
+ print("..")
271
+ debug_box("Access token saved to session file")
272
+ else:
273
+ debug_box("No access token found in request state")
274
+
275
+ # If embed mode is requested, redirect to embed session
276
+ if embed:
277
+ return RedirectResponse(f"/session/{agent_name}/{log_id}?embed=true")
278
+
279
+ # Regular redirect
280
+ return RedirectResponse(f"/session/{agent_name}/{log_id}")
281
+
282
+ @router.get("/makesession/{agent_name}")
283
+ async def make_session(request: Request, agent_name: str):
284
+ """
285
+ Create a new chat session for the specified agent.
286
+ Returns a redirect to the chat session page.
287
+ """
288
+ if not hasattr(request.state, "user"):
289
+ return RedirectResponse("/login")
290
+ user = request.state.user
291
+ log_id = nanoid.generate()
292
+
293
+ await init_chat_session(user, agent_name, log_id)
294
+ return JSONResponse({ "log_id": log_id })
295
+
296
+ @router.get("/history/{agent_name}/{log_id}")
297
+ async def chat_history(request: Request, agent_name: str, log_id: str):
298
+ user = request.state.user.username
299
+ history = await get_chat_history(agent_name, log_id, user)
300
+ if history is None or len(history) == 0:
301
+ try:
302
+ print("trying to load from system session")
303
+ history = await get_chat_history(agent_name, log_id, "system")
304
+ except Exception as e:
305
+ print("Error loading from system session:", e)
306
+ history = []
307
+ pass
308
+ return history
309
+
310
+ @router.get("/session/{agent_name}/{log_id}")
311
+ async def chat_session(request: Request, agent_name: str, log_id: str, embed: bool = Query(False)):
312
+ # Check authentication (API key or regular user)
313
+ plugins = list_enabled()
314
+ if not hasattr(request.state, "user"):
315
+ return RedirectResponse("/login")
316
+
317
+ user = request.state.user
318
+ agent = await service_manager.get_agent_data(agent_name)
319
+ persona = agent['persona']['name']
320
+ print("persona is:", persona)
321
+ auth_token = None
322
+ try:
323
+ auth_token = await load_session_data(log_id, "access_token")
324
+ except:
325
+ pass
326
+ chat_data = {"log_id": log_id, "agent_name": agent_name, "user": user, "persona": persona }
327
+
328
+ if auth_token is not None:
329
+ chat_data["access_token"] = auth_token
330
+
331
+ # Add embed mode flag
332
+ if embed:
333
+ chat_data["embed_mode"] = True
334
+
335
+ html = await render('chat', chat_data)
336
+ return HTMLResponse(html)
337
+
338
+ # use starlette staticfiles to mount ./imgs
339
+ app.mount("/published", StaticFiles(directory=str(published_dir)), name="published_indices")
340
+
341
+ class TaskRequest(BaseModel):
342
+ instructions: str
343
+
344
+ @router.post("/task/{agent_name}")
345
+ async def run_task_route(request: Request, agent_name: str, task_request: TaskRequest = None):
346
+ """
347
+ Run a task for an agent with the given instructions.
348
+ This endpoint allows programmatic interaction with agents without a full chat session.
349
+
350
+ Parameters:
351
+ - agent_name: The name of the agent to run the task
352
+ - instructions: The instructions/prompt to send to the agent
353
+
354
+ Returns:
355
+ - JSON with results and log_id for tracking
356
+ """
357
+
358
+ user = request.state.user.username
359
+
360
+ instructions = None
361
+ if task_request is not None:
362
+ instructions = task_request.instructions
363
+
364
+ if not instructions:
365
+ return {"status": "error", "message": "No instructions provided"}
366
+
367
+ task_result, full_results, log_id = await run_task(instructions=instructions, agent_name=agent_name, user=user)
368
+ print(task_result)
369
+ print(full_results)
370
+ print(log_id)
371
+ return {"status": "ok", "results": task_result, "full_results": full_results, "log_id": log_id}
372
+
373
+
374
+ @router.post("/chat/{log_id}/upload")
375
+ async def upload_file(request: Request, log_id: str, file: UploadFile = File(...)):
376
+ """
377
+ Upload a file and store it in a user-specific directory.
378
+ Returns the file path that can be used in messages.
379
+ """
380
+ user = request.state.user.username
381
+
382
+ # Create user uploads directory if it doesn't exist
383
+ user_upload_dir = f"data/users/{user}/uploads/{log_id}"
384
+ os.makedirs(user_upload_dir, exist_ok=True)
385
+
386
+ # Generate a safe filename to prevent path traversal
387
+ filename = os.path.basename(file.filename)
388
+ file_path = os.path.join(user_upload_dir, filename)
389
+
390
+ # Save the file
391
+ with open(file_path, "wb") as buffer:
392
+ shutil.copyfileobj(file.file, buffer)
393
+
394
+ # Return the file information
395
+ return {
396
+ "status": "ok",
397
+ "filename": filename,
398
+ "path": file_path,
399
+ "mime_type": file.content_type
400
+ }
401
+
402
+
403
+ from lib.chatlog import count_tokens_for_log_id
404
+
405
+ @router.get("/chat/{log_id}/tokens")
406
+ async def get_token_count(request: Request, log_id: str):
407
+ """
408
+ Get token counts for a chat log identified by log_id, including any delegated tasks.
409
+
410
+ Parameters:
411
+ - log_id: The log ID to count tokens for
412
+
413
+ Returns:
414
+ - JSON with token counts or error message if log not found
415
+ """
416
+ token_counts = await count_tokens_for_log_id(log_id)
417
+
418
+ if token_counts is None:
419
+ return {"status": "error", "message": f"Chat log with ID {log_id} not found"}
420
+
421
+
422
+ return {"status": "ok", "token_counts": token_counts}
423
+
424
+ @router.get("/chat/del_session/{log_id}")
425
+ async def delete_chat_session(request: Request, log_id: str, user=Depends(require_user)):
426
+ """
427
+ Delete a chat session by log_id, including chat logs, context files, and all child sessions.
428
+
429
+ Parameters:
430
+ - log_id: The log ID of the session to delete
431
+
432
+ Returns:
433
+ - JSON with success status and message
434
+ """
435
+ try:
436
+ # Try to determine the agent name from the context file first
437
+ agent_name = "unknown"
438
+ context_dir = os.environ.get('CHATCONTEXT_DIR', 'data/context')
439
+ context_file_path = f"{context_dir}/{user.username}/context_{log_id}.json"
440
+
441
+ if os.path.exists(context_file_path):
442
+ try:
443
+ with open(context_file_path, 'r') as f:
444
+ context_data = json.load(f)
445
+ agent_name = context_data.get('agent_name', 'unknown')
446
+ print(f"Found agent name '{agent_name}' from context file for log_id {log_id}")
447
+ except Exception as e:
448
+ print(f"Error reading context file {context_file_path}: {e}")
449
+
450
+ # If we still don't have the agent name, try to find the chatlog file
451
+ if agent_name == "unknown":
452
+ from lib.chatlog import find_chatlog_file
453
+ chatlog_path = find_chatlog_file(log_id)
454
+ if chatlog_path:
455
+ # Extract agent from path: data/chat/{user}/{agent}/chatlog_{log_id}.json
456
+ path_parts = chatlog_path.split(os.sep)
457
+ if len(path_parts) >= 3:
458
+ agent_name = path_parts[-2] # Agent is the second-to-last part
459
+ print(f"Found agent name '{agent_name}' from chatlog file path for log_id {log_id}")
460
+
461
+ await ChatContext.delete_session_by_id(log_id=log_id, user=user.username, agent=agent_name, cascade=True)
462
+
463
+ return {"status": "ok", "message": f"Chat session {log_id} deleted successfully"}
464
+ except Exception as e:
465
+ print(f"Error deleting chat session {log_id}: {e}")
466
+ raise HTTPException(status_code=500, detail=f"Error deleting chat session: {str(e)}")
467
+
468
+
469
+ @router.get("/chat/{log_id}/tokens")
470
+ async def get_token_count_alt(request: Request, log_id: str):
471
+ """
472
+ Alternative token count endpoint using token_counter module.
473
+
474
+ Parameters:
475
+ - log_id: The log ID to count tokens for
476
+
477
+ Returns:
478
+ - JSON with token counts or error message if log not found
479
+ """
480
+ from lib.token_counter import count_tokens_for_log_id
481
+
482
+ token_counts = await count_tokens_for_log_id(log_id)
483
+
484
+ if token_counts is None:
485
+ return {"status": "error", "message": f"Chat log with ID {log_id} not found"}
486
+
487
+
488
+ return {"status": "ok", "token_counts": token_counts}
489
+
490
+ # Include widget routes
491
+ try:
492
+ from .widget_routes import router as widget_router
493
+ router.include_router(widget_router)
494
+ except ImportError as e:
495
+ print(f"Warning: Could not load widget routes: {e}")
@@ -217,8 +217,25 @@ def process_result(result, formatted_results):
217
217
 
218
218
  return formatted_results
219
219
 
220
+ in_progress = {}
221
+
220
222
  @service()
221
223
  async def send_message_to_agent(session_id: str, message: str | List[MessageParts], max_iterations=35, context=None, user=None):
224
+ global in_progress
225
+ existing_session = in_progress.get(session_id, False)
226
+ print(existing_session)
227
+
228
+ if existing_session:
229
+ context_ = await get_context(session_id, user)
230
+ context.data['finished_conversation'] = True
231
+ await asyncio.sleep(0.4)
232
+ else:
233
+ print('starting')
234
+
235
+ print('ok')
236
+ in_progress[session_id] = True
237
+
238
+ print('b')
222
239
  if os.environ.get("MR_MAX_ITERATIONS") is not None:
223
240
  max_iterations = int(os.environ.get("MR_MAX_ITERATIONS"))
224
241
  if not user:
@@ -399,10 +416,12 @@ async def send_message_to_agent(session_id: str, message: str | List[MessagePart
399
416
  print("Exiting send_message_to_agent: ", session_id, message, max_iterations)
400
417
 
401
418
  await context.finished_chat()
419
+ in_progress.pop(session_id, None)
402
420
  return [results, full_results]
403
421
  except Exception as e:
404
422
  print("Error in send_message_to_agent: ", e)
405
423
  print(traceback.format_exc())
424
+ in_progress.pop(session_id, None)
406
425
  return []
407
426
 
408
427
  @pipe(name='process_results', priority=5)
@@ -548,4 +567,4 @@ async def cancel_active_response(log_id: str, context=None):
548
567
  await context.save_context()
549
568
 
550
569
  print(f"Cancelled active response for session {log_id}")
551
- return {"status": "cancelled", "log_id": log_id}
570
+ return {"status": "cancelled", "log_id": log_id}
@@ -72,6 +72,10 @@
72
72
  </div>
73
73
  {% endblock %}
74
74
 
75
+ {% block post_content %}
76
+
77
+ {% endblock %}
78
+
75
79
  </div> <!-- page-container -->
76
80
 
77
81
  <script type="module" src="/chat/static/js/code-copy-button.js"></script>
@@ -241,7 +241,7 @@ async def get_embed_script(token: str):
241
241
  iframe.style.cssText = "width: 100%; height: 100%; border: none; border-radius: 12px;";
242
242
  }}
243
243
  iframe.src = config.baseUrl + "/chat/widget/" + config.token + "/session";
244
- iframe.allow = "microphone autoplay"
244
+ iframe.setAttribute('allow', 'microphone');
245
245
  chatContainer.appendChild(iframe);
246
246
  isLoaded = true;
247
247
  }}
mindroot/lib/chatlog.py CHANGED
@@ -133,6 +133,7 @@ class ChatLog:
133
133
  debug_box("5")
134
134
  self.messages.append(message)
135
135
  self._save_log_sync()
136
+
136
137
  async def add_message_async(self, message: Dict[str, str]) -> None:
137
138
  """Async version for new code that needs non-blocking operations"""
138
139
  should_save = self._add_message_impl(message)
@@ -145,6 +146,13 @@ class ChatLog:
145
146
  len(self.messages[-1]['content']) > 0 and
146
147
  self.messages[-1]['content'][0].get('type') == 'image'):
147
148
  await self.save_log()
149
+
150
+ async def drop_last(self, role) -> None:
151
+ if len(self.messages) == 0:
152
+ return
153
+ if self.messages[-1]['role'] == role:
154
+ self.messages = self.messages[:-1]
155
+ await self.save_log()
148
156
 
149
157
  def get_history(self) -> List[Dict[str, str]]:
150
158
  return self.messages
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindroot
3
- Version: 10.4.0
3
+ Version: 10.6.0
4
4
  Summary: MindRoot AI Agent Framework
5
5
  Requires-Python: >=3.9
6
6
  License-File: LICENSE
@@ -36,7 +36,7 @@ mindroot/coreplugins/admin/static/css/reset.css,sha256=pN9wuf7laZeIt-QjbxqJXDfu7
36
36
  mindroot/coreplugins/admin/static/css/update.css,sha256=J1lchkEY6WST1LLoyPhXjjCZ4-oqMu7ooWjJNduRmMs,3008
37
37
  mindroot/coreplugins/admin/static/js/about-info.js,sha256=CRyI5xGX3huycwseT5c80zsqQkElcdZsKMofyzyCimI,9230
38
38
  mindroot/coreplugins/admin/static/js/agent-editor.js,sha256=ZCJBNQ-l4kJj-ZufYuzEg45ZpqxwliNmxuqUa2GNiqY,2825
39
- mindroot/coreplugins/admin/static/js/agent-form.js,sha256=eSd7I5-fl11QsAIV0QU2JavqHNrkj8rEc4UTu49Rkos,40611
39
+ mindroot/coreplugins/admin/static/js/agent-form.js,sha256=cFyIAS6dVxvzqJgJra-a5aFtS6zau0roveDSgpUF63A,41403
40
40
  mindroot/coreplugins/admin/static/js/agent-list.js,sha256=86mqFyHspx9EnzJpgUDyeAgEq-jcqQ4v96CrgfUXoGM,2239
41
41
  mindroot/coreplugins/admin/static/js/api-key-script.js,sha256=By80j-cwgxRT96MUBiZbVJuRvr6OAhd-GPmAUdZDZ4E,11199
42
42
  mindroot/coreplugins/admin/static/js/base.js,sha256=xGCA6ngMapQ_jqMgHXg__CS3R46qprycOjkKTFDCMlA,1307
@@ -52,7 +52,7 @@ mindroot/coreplugins/admin/static/js/missing-commands.js,sha256=adNF9GWN981_KX7H
52
52
  mindroot/coreplugins/admin/static/js/model-preferences-v2.js,sha256=4SFZBia959kRrSZXzIdWB0dscg4WtVIGYuLFlLpqBBI,15669
53
53
  mindroot/coreplugins/admin/static/js/model-preferences.js,sha256=J0G7gcGACaPyslWJO42urf5wbZZsqO0LyPicAu-uV_Y,3365
54
54
  mindroot/coreplugins/admin/static/js/notification.js,sha256=296rVCr6MNtzvzdzW3bGiMa231-BnWJtwZZ_sDWX-3c,5633
55
- mindroot/coreplugins/admin/static/js/persona-editor.js,sha256=bVp-d1y3HPocmVkHpqsVh0HiRUf77vJ43kargj1iPHk,9830
55
+ mindroot/coreplugins/admin/static/js/persona-editor.js,sha256=emJSPBXDk54ztWDYOcs-bHAf1qQSIpq11RjV9bt5adA,10382
56
56
  mindroot/coreplugins/admin/static/js/plugin-advanced-install.js,sha256=-HDJ3lVuDwj6R-74TfVUo4dUxB8efl13m3R_sUicnJI,8038
57
57
  mindroot/coreplugins/admin/static/js/plugin-base.js,sha256=KWp5DqueHtyTxYKbuHMoFpoFXrfMbIjzK4M1ulAR9m8,5095
58
58
  mindroot/coreplugins/admin/static/js/plugin-index-browser.js,sha256=P-V4wqlYGxjr7oF2LiD5ti8Is3wtSsKPwpRgRJpT0VI,10028
@@ -439,7 +439,7 @@ mindroot/coreplugins/admin/static/js/lit-html/node/directives/when.js,sha256=NLe
439
439
  mindroot/coreplugins/admin/static/js/lit-html/node/directives/when.js.map,sha256=tOonih_-EaqrunhNGshA9xN--WIVdGikjg8MkVp0itQ,1534
440
440
  mindroot/coreplugins/admin/templates/admin.jinja2,sha256=H_oDqoWWk0Da0Jre67LIKvB3h30fmjcZz2T5knUyz0k,13272
441
441
  mindroot/coreplugins/admin/templates/model-preferences-v2.jinja2,sha256=5J3rXYmtp_yMTFCk85SYN03gc4lbidF0Nip6YcqcIW4,891
442
- mindroot/coreplugins/agent/agent.py,sha256=gPw82QbQYlWvJxy-FEg1kcVu1da04MhDYCZGv9YMEbk,21629
442
+ mindroot/coreplugins/agent/agent.py,sha256=k3G3VaRjGwd1TNYQloytE4NV5cXyVxiuUl1GaMfdTEE,22335
443
443
  mindroot/coreplugins/agent/agent.py.bak,sha256=X-EmtrpEpdfo-iUw9gj7mLveRVzAApsDWPTwMAuv7Ww,20715
444
444
  mindroot/coreplugins/agent/cmd_start_example.py,sha256=Mdcd9st6viI6-M7a0-zqkw3IxR9FAxIiZ_8G-tLdIJk,1416
445
445
  mindroot/coreplugins/agent/command_parser.py,sha256=dgMqtVLPQWE2BU7iyjqwKGy5Gh74jcZkiy1JDs07t4E,13166
@@ -463,15 +463,16 @@ mindroot/coreplugins/api_keys/static/js/api-key-manager.js,sha256=imqlhd85Z-1e7u
463
463
  mindroot/coreplugins/chat/__init__.py,sha256=qVdTF1fHZJHwY_ChnPvNFx2Nlg07FHvK0V_JmzfWzdw,230
464
464
  mindroot/coreplugins/chat/buwidget_routes.py,sha256=MtwaPX2vEGDylifWOqcx7EAhDw0y1Y3Y91z58EJaLsc,9982
465
465
  mindroot/coreplugins/chat/commands.py,sha256=vlgGOvwvjpCbSsW25x4HaeFzeRNWXoEKrdqNpwX_EGg,17077
466
+ mindroot/coreplugins/chat/edit_router.py,sha256=25BStzAIvGhULb_07bZw2aNxnpqabKV6CZHl9xrYdbQ,18629
466
467
  mindroot/coreplugins/chat/format_result_msgs.py,sha256=daEdpEyAJIa8b2VkCqSKcw8PaExcB6Qro80XNes_sHA,2
467
468
  mindroot/coreplugins/chat/mod.py,sha256=Xydjv3feKJJRbwdiB7raqiQnWtaS_2GcdC9bXYQX3nE,425
468
469
  mindroot/coreplugins/chat/models.py,sha256=GRcRuDUAJFpyWERPMxkxUaZ21igNlWeeamriruEKiEQ,692
469
470
  mindroot/coreplugins/chat/router.py,sha256=vq9UwYoFoGQMVmDC0TkjH7TWivFwkIe6OU0Scu-dtHg,15998
470
471
  mindroot/coreplugins/chat/router_dedup_patch.py,sha256=lDSpVMSd26pXJwrrdirUmR1Jv_N5VHiDzTS8t3XswDU,918
471
- mindroot/coreplugins/chat/services.py,sha256=UJl4Ri2Aj_F0Pzvh-Ztde0CaRPhbyA8g2KcONckpACk,21842
472
+ mindroot/coreplugins/chat/services.py,sha256=cODRoNjE0lrraYBR5fwzMAwB-Y-GBQTr8VCMTzsejlY,22325
472
473
  mindroot/coreplugins/chat/utils.py,sha256=BiE14PpsAcQSO5vbU88klHGm8cAXJDXxgVgva-EXybU,155
473
474
  mindroot/coreplugins/chat/widget_manager.py,sha256=LrMbZlHqpxbLwdN4XZ4GkLxORwxa1o6IVCrlUDBmGQs,4786
474
- mindroot/coreplugins/chat/widget_routes.py,sha256=q_gD7Wxc0WnTU7S159dwe9-IJ2EYG4CC5Z6JbKpIrWU,11795
475
+ mindroot/coreplugins/chat/widget_routes.py,sha256=iV3OwLFnvLDsMHdckJnmVXcUgyyng-zIPNXyK2LAUjc,11802
475
476
  mindroot/coreplugins/chat/static/assistant.png,sha256=oAt1ctkFKLSPBoAZGNnSixooW9ANVIk1GwniauVWDXo,215190
476
477
  mindroot/coreplugins/chat/static/mindgen.png,sha256=fN3E3oOFvAGYjJq-Pvg2f75jIMv7kg5WRU0EeEbxCWg,235353
477
478
  mindroot/coreplugins/chat/static/mindroot_logo.png,sha256=ZfPlCqCjU0_TXte5gvEx81zRKge--l_z2y0AArKl0as,17823
@@ -887,7 +888,7 @@ mindroot/coreplugins/chat/static/js/lit-html/node/directives/until.js,sha256=j1W
887
888
  mindroot/coreplugins/chat/static/js/lit-html/node/directives/until.js.map,sha256=7xiwSZ7_fGtr5XwW-10Dzs8n9QE2VUfXaZ0Sd6d82L0,6567
888
889
  mindroot/coreplugins/chat/static/js/lit-html/node/directives/when.js,sha256=NLe0NJ-6jqjVDUrT_DzmSpREsRaLo1yarzdYcV_5xHY,181
889
890
  mindroot/coreplugins/chat/static/js/lit-html/node/directives/when.js.map,sha256=tOonih_-EaqrunhNGshA9xN--WIVdGikjg8MkVp0itQ,1534
890
- mindroot/coreplugins/chat/templates/chat.jinja2,sha256=6mPBxZRcTCfCybhQvVIuRfcnkLiVd4Us3UbsMyoTMxM,3206
891
+ mindroot/coreplugins/chat/templates/chat.jinja2,sha256=54yUJ7v-fE2r8iuXYleqznChApXKNQSuEJgIlZgyrCk,3256
891
892
  mindroot/coreplugins/chat_avatar/__init__.py,sha256=MsSFjiLMLJZ7QhUPpVBWKiyDnCzryquRyr329NoCACI,2
892
893
  mindroot/coreplugins/chat_avatar/inject/chat.jinja2,sha256=TDSSt_SdOOW4EJMQK7fA_L2W5GNbDICRmXyqSsw0wuE,1093
893
894
  mindroot/coreplugins/check_list/__init__.py,sha256=SaaGvnpz37xRM7DjGWBz5CD27Jh2UVdPLGoVUAFrUSY,77
@@ -2196,7 +2197,7 @@ mindroot/lib/buchatlog2.py,sha256=Va9FteBWePEjWD9OZcw-OtQfEb-IoCVGTmJeMRaX9is,13
2196
2197
  mindroot/lib/buchatlog3.py,sha256=SAvcK2m_CW0Jw8p1pqnbrTexcx24PotrsJTqvQ_D290,24573
2197
2198
  mindroot/lib/butemplates.py,sha256=gfHGPTOjvoEenXsR7xokNuqMjOAPuC2DawheH1Ae4bU,12196
2198
2199
  mindroot/lib/chatcontext.py,sha256=CXk-pX-7RG3NiRFsAZWERWxnuFJOHH7FHtOLm-kGRXE,12437
2199
- mindroot/lib/chatlog.py,sha256=JuUffRUhs966d7MhE_xt8iSviZCULSRpwCtvnpjNd4Y,26139
2200
+ mindroot/lib/chatlog.py,sha256=ACdixKTn_GlVVfB00fNlphNPMFnRrum_za_mQfGPQoQ,26372
2200
2201
  mindroot/lib/chatlog_optimized.py,sha256=rL7KBP-V4_cGgMLihxPm3HoKcjFEyA1uEtPtqvkOa3A,20011
2201
2202
  mindroot/lib/json_escape.py,sha256=5cAmAdNbnYX2uyfQcnse2fFtNI0CdB-AfZ23RwaDm-k,884
2202
2203
  mindroot/lib/model_selector.py,sha256=Wz-8NZoiclmnhLeCNnI3WCuKFmjsO5HE4bK5F8GpZzU,1397
@@ -2250,9 +2251,9 @@ mindroot/protocols/services/stream_chat.py,sha256=fMnPfwaB5fdNMBLTEg8BXKAGvrELKH
2250
2251
  mindroot/registry/__init__.py,sha256=40Xy9bmPHsgdIrOzbtBGzf4XMqXVi9P8oZTJhn0r654,151
2251
2252
  mindroot/registry/component_manager.py,sha256=WZFNPg4SNvpqsM5NFiC2DpgmrJQCyR9cNhrCBpp30Qk,995
2252
2253
  mindroot/registry/data_access.py,sha256=81In5TwETpaqnnY1_-tBQM7rfWvUxZUZkG7lEelRUfU,5321
2253
- mindroot-10.4.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
2254
- mindroot-10.4.0.dist-info/METADATA,sha256=OBn4gxCPRrMB6oyPLB8dGQxp0nIUhIlCG0NqiKETVTo,1035
2255
- mindroot-10.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
2256
- mindroot-10.4.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
2257
- mindroot-10.4.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
2258
- mindroot-10.4.0.dist-info/RECORD,,
2254
+ mindroot-10.6.0.dist-info/licenses/LICENSE,sha256=8plAmZh8y9ccuuqFFz4kp7G-cO_qsPgAOoHNvabSB4U,1070
2255
+ mindroot-10.6.0.dist-info/METADATA,sha256=B_jgoqAFx9YKO_ddBGR0_rVBewpVyTae8JJhUxQI_LY,1035
2256
+ mindroot-10.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
2257
+ mindroot-10.6.0.dist-info/entry_points.txt,sha256=0bpyjMccLttx6VcjDp6zfJPN0Kk0rffor6IdIbP0j4c,50
2258
+ mindroot-10.6.0.dist-info/top_level.txt,sha256=gwKm7DmNjhdrCJTYCrxa9Szne4lLpCtrEBltfsX-Mm8,9
2259
+ mindroot-10.6.0.dist-info/RECORD,,