dtSpark 1.0.4__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.
Files changed (96) hide show
  1. dtSpark/__init__.py +0 -0
  2. dtSpark/_description.txt +1 -0
  3. dtSpark/_full_name.txt +1 -0
  4. dtSpark/_licence.txt +21 -0
  5. dtSpark/_metadata.yaml +6 -0
  6. dtSpark/_name.txt +1 -0
  7. dtSpark/_version.txt +1 -0
  8. dtSpark/aws/__init__.py +7 -0
  9. dtSpark/aws/authentication.py +296 -0
  10. dtSpark/aws/bedrock.py +578 -0
  11. dtSpark/aws/costs.py +318 -0
  12. dtSpark/aws/pricing.py +580 -0
  13. dtSpark/cli_interface.py +2645 -0
  14. dtSpark/conversation_manager.py +3050 -0
  15. dtSpark/core/__init__.py +12 -0
  16. dtSpark/core/application.py +3355 -0
  17. dtSpark/core/context_compaction.py +735 -0
  18. dtSpark/daemon/__init__.py +104 -0
  19. dtSpark/daemon/__main__.py +10 -0
  20. dtSpark/daemon/action_monitor.py +213 -0
  21. dtSpark/daemon/daemon_app.py +730 -0
  22. dtSpark/daemon/daemon_manager.py +289 -0
  23. dtSpark/daemon/execution_coordinator.py +194 -0
  24. dtSpark/daemon/pid_file.py +169 -0
  25. dtSpark/database/__init__.py +482 -0
  26. dtSpark/database/autonomous_actions.py +1191 -0
  27. dtSpark/database/backends.py +329 -0
  28. dtSpark/database/connection.py +122 -0
  29. dtSpark/database/conversations.py +520 -0
  30. dtSpark/database/credential_prompt.py +218 -0
  31. dtSpark/database/files.py +205 -0
  32. dtSpark/database/mcp_ops.py +355 -0
  33. dtSpark/database/messages.py +161 -0
  34. dtSpark/database/schema.py +673 -0
  35. dtSpark/database/tool_permissions.py +186 -0
  36. dtSpark/database/usage.py +167 -0
  37. dtSpark/files/__init__.py +4 -0
  38. dtSpark/files/manager.py +322 -0
  39. dtSpark/launch.py +39 -0
  40. dtSpark/limits/__init__.py +10 -0
  41. dtSpark/limits/costs.py +296 -0
  42. dtSpark/limits/tokens.py +342 -0
  43. dtSpark/llm/__init__.py +17 -0
  44. dtSpark/llm/anthropic_direct.py +446 -0
  45. dtSpark/llm/base.py +146 -0
  46. dtSpark/llm/context_limits.py +438 -0
  47. dtSpark/llm/manager.py +177 -0
  48. dtSpark/llm/ollama.py +578 -0
  49. dtSpark/mcp_integration/__init__.py +5 -0
  50. dtSpark/mcp_integration/manager.py +653 -0
  51. dtSpark/mcp_integration/tool_selector.py +225 -0
  52. dtSpark/resources/config.yaml.template +631 -0
  53. dtSpark/safety/__init__.py +22 -0
  54. dtSpark/safety/llm_service.py +111 -0
  55. dtSpark/safety/patterns.py +229 -0
  56. dtSpark/safety/prompt_inspector.py +442 -0
  57. dtSpark/safety/violation_logger.py +346 -0
  58. dtSpark/scheduler/__init__.py +20 -0
  59. dtSpark/scheduler/creation_tools.py +599 -0
  60. dtSpark/scheduler/execution_queue.py +159 -0
  61. dtSpark/scheduler/executor.py +1152 -0
  62. dtSpark/scheduler/manager.py +395 -0
  63. dtSpark/tools/__init__.py +4 -0
  64. dtSpark/tools/builtin.py +833 -0
  65. dtSpark/web/__init__.py +20 -0
  66. dtSpark/web/auth.py +152 -0
  67. dtSpark/web/dependencies.py +37 -0
  68. dtSpark/web/endpoints/__init__.py +17 -0
  69. dtSpark/web/endpoints/autonomous_actions.py +1125 -0
  70. dtSpark/web/endpoints/chat.py +621 -0
  71. dtSpark/web/endpoints/conversations.py +353 -0
  72. dtSpark/web/endpoints/main_menu.py +547 -0
  73. dtSpark/web/endpoints/streaming.py +421 -0
  74. dtSpark/web/server.py +578 -0
  75. dtSpark/web/session.py +167 -0
  76. dtSpark/web/ssl_utils.py +195 -0
  77. dtSpark/web/static/css/dark-theme.css +427 -0
  78. dtSpark/web/static/js/actions.js +1101 -0
  79. dtSpark/web/static/js/chat.js +614 -0
  80. dtSpark/web/static/js/main.js +496 -0
  81. dtSpark/web/static/js/sse-client.js +242 -0
  82. dtSpark/web/templates/actions.html +408 -0
  83. dtSpark/web/templates/base.html +93 -0
  84. dtSpark/web/templates/chat.html +814 -0
  85. dtSpark/web/templates/conversations.html +350 -0
  86. dtSpark/web/templates/goodbye.html +81 -0
  87. dtSpark/web/templates/login.html +90 -0
  88. dtSpark/web/templates/main_menu.html +983 -0
  89. dtSpark/web/templates/new_conversation.html +191 -0
  90. dtSpark/web/web_interface.py +137 -0
  91. dtspark-1.0.4.dist-info/METADATA +187 -0
  92. dtspark-1.0.4.dist-info/RECORD +96 -0
  93. dtspark-1.0.4.dist-info/WHEEL +5 -0
  94. dtspark-1.0.4.dist-info/entry_points.txt +3 -0
  95. dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
  96. dtspark-1.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,621 @@
1
+ """
2
+ Chat API endpoints.
3
+
4
+ Provides REST API for chat operations:
5
+ - Load conversation
6
+ - Send message
7
+ - Execute commands (history, info, attach, etc.)
8
+ - Export conversation
9
+
10
+
11
+ """
12
+
13
+ import logging
14
+ import tempfile
15
+ import os
16
+ from typing import Optional, List
17
+ from datetime import datetime
18
+
19
+ from fastapi import APIRouter, Depends, Request, HTTPException, UploadFile, File, Form
20
+ from pydantic import BaseModel
21
+
22
+ from ..dependencies import get_current_session
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ router = APIRouter()
28
+
29
+
30
+ class Message(BaseModel):
31
+ """Chat message."""
32
+ role: str # 'user' or 'assistant'
33
+ content: str
34
+ timestamp: datetime
35
+ tokens: Optional[int] = None
36
+
37
+
38
+ class ChatHistory(BaseModel):
39
+ """Chat history for a conversation."""
40
+ conversation_id: int
41
+ conversation_name: str
42
+ messages: List[Message]
43
+
44
+
45
+ class CommandResponse(BaseModel):
46
+ """Response from a chat command."""
47
+ command: str
48
+ status: str
49
+ data: Optional[dict] = None
50
+ message: Optional[str] = None
51
+
52
+
53
+ @router.get("/chat/{conversation_id}/history")
54
+ async def get_chat_history(
55
+ conversation_id: int,
56
+ request: Request,
57
+ session_id: str = Depends(get_current_session),
58
+ ) -> ChatHistory:
59
+ """
60
+ Get chat history for a conversation.
61
+
62
+ Args:
63
+ conversation_id: ID of the conversation
64
+
65
+ Returns:
66
+ ChatHistory with all messages
67
+ """
68
+ try:
69
+ app_instance = request.app.state.app_instance
70
+ conversation_manager = app_instance.conversation_manager
71
+ database = app_instance.database
72
+
73
+ # Load the conversation (will set it as current)
74
+ conversation_manager.load_conversation(conversation_id)
75
+
76
+ # Get conversation name from database
77
+ conv = database.get_conversation(conversation_id)
78
+ if not conv:
79
+ raise HTTPException(status_code=404, detail="Conversation not found")
80
+ conv_name = conv['name']
81
+
82
+ # Set the model from the conversation and update service references
83
+ model_id = conv['model_id']
84
+ app_instance.llm_manager.set_model(model_id)
85
+ app_instance.bedrock_service = app_instance.llm_manager.get_active_service()
86
+ conversation_manager.update_service(app_instance.bedrock_service)
87
+
88
+ # Get conversation history from conversation manager
89
+ history = conversation_manager.get_conversation_history(include_rolled_up=False)
90
+
91
+ # Format messages
92
+ messages = []
93
+ for msg in history:
94
+ messages.append(
95
+ Message(
96
+ role=msg['role'],
97
+ content=msg['content'],
98
+ timestamp=msg['timestamp'],
99
+ tokens=msg.get('tokens'),
100
+ )
101
+ )
102
+
103
+ return ChatHistory(
104
+ conversation_id=conversation_id,
105
+ conversation_name=conv_name,
106
+ messages=messages,
107
+ )
108
+
109
+ except HTTPException:
110
+ raise
111
+ except Exception as e:
112
+ logger.error(f"Error getting chat history for conversation {conversation_id}: {e}")
113
+ raise HTTPException(status_code=500, detail=str(e))
114
+
115
+
116
+ @router.post("/chat/{conversation_id}/message")
117
+ async def send_message(
118
+ conversation_id: int,
119
+ request: Request,
120
+ message: str = Form(...),
121
+ session_id: str = Depends(get_current_session),
122
+ ) -> dict:
123
+ """
124
+ Send a message in a conversation.
125
+
126
+ Args:
127
+ conversation_id: ID of the conversation
128
+ message: Message text to send
129
+
130
+ Returns:
131
+ Response with assistant's reply
132
+ """
133
+ try:
134
+ app_instance = request.app.state.app_instance
135
+ conversation_manager = app_instance.conversation_manager
136
+
137
+ # Load conversation in conversation manager
138
+ conversation_manager.load_conversation(conversation_id)
139
+
140
+ # Set the model from the conversation and update service references
141
+ conv_info = conversation_manager.get_current_conversation_info()
142
+ model_id = conv_info['model_id']
143
+ app_instance.llm_manager.set_model(model_id)
144
+ app_instance.bedrock_service = app_instance.llm_manager.get_active_service()
145
+ conversation_manager.update_service(app_instance.bedrock_service)
146
+
147
+ # Send message (this will handle tool use, streaming, etc.)
148
+ response = conversation_manager.send_message(message)
149
+
150
+ return {
151
+ "status": "success",
152
+ "response": response,
153
+ }
154
+
155
+ except Exception as e:
156
+ logger.error(f"Error sending message in conversation {conversation_id}: {e}")
157
+ raise HTTPException(status_code=500, detail=str(e))
158
+
159
+
160
+ @router.post("/chat/{conversation_id}/command/info")
161
+ async def command_info(
162
+ conversation_id: int,
163
+ request: Request,
164
+ session_id: str = Depends(get_current_session),
165
+ ) -> CommandResponse:
166
+ """
167
+ Execute 'info' command to get conversation information.
168
+
169
+ Args:
170
+ conversation_id: ID of the conversation
171
+
172
+ Returns:
173
+ CommandResponse with conversation info
174
+ """
175
+ try:
176
+ app_instance = request.app.state.app_instance
177
+ database = app_instance.database
178
+ conversation_manager = app_instance.conversation_manager
179
+
180
+ # Get conversation
181
+ conv = database.get_conversation(conversation_id)
182
+ if not conv:
183
+ raise HTTPException(status_code=404, detail="Conversation not found")
184
+
185
+ # Get model usage breakdown
186
+ model_usage = database.get_model_usage_breakdown(conversation_id)
187
+
188
+ # Get attached files
189
+ files = database.get_conversation_files(conversation_id)
190
+
191
+ # Get MCP server states (if available)
192
+ mcp_servers = []
193
+ if app_instance.mcp_manager:
194
+ # Load conversation to set current_conversation_id
195
+ conversation_manager.load_conversation(conversation_id)
196
+ mcp_servers = conversation_manager.get_mcp_server_states()
197
+
198
+ # Handle created_at - could be datetime or string from database
199
+ created_at = conv['created_at']
200
+ if hasattr(created_at, 'isoformat'):
201
+ created_at_str = created_at.isoformat()
202
+ else:
203
+ created_at_str = str(created_at)
204
+
205
+ # Get rollup/compaction settings
206
+ rollup_info = {}
207
+ try:
208
+ model_id = conv.get('model_id', '')
209
+ provider = app_instance.llm_manager.get_active_provider() if app_instance.llm_manager else 'unknown'
210
+
211
+ # Get context limits from resolver
212
+ context_limits = conversation_manager.context_limit_resolver.get_context_limits(model_id, provider)
213
+ context_window = context_limits.get('context_window', 8192)
214
+ max_output = context_limits.get('max_output', 4096)
215
+
216
+ # Get compaction thresholds from compactor
217
+ compaction_threshold = conversation_manager.context_compactor.compaction_threshold
218
+ emergency_threshold = conversation_manager.context_compactor.emergency_threshold
219
+
220
+ # Calculate threshold token counts
221
+ compaction_trigger_tokens = int(context_window * compaction_threshold)
222
+ emergency_trigger_tokens = int(context_window * emergency_threshold)
223
+
224
+ # Get current token count
225
+ current_tokens = conv.get('tokens_sent', 0) + conv.get('tokens_received', 0)
226
+ context_usage_percent = (current_tokens / context_window * 100) if context_window > 0 else 0
227
+
228
+ rollup_info = {
229
+ "context_window": context_window,
230
+ "max_output": max_output,
231
+ "compaction_threshold": compaction_threshold,
232
+ "compaction_trigger_tokens": compaction_trigger_tokens,
233
+ "emergency_threshold": emergency_threshold,
234
+ "emergency_trigger_tokens": emergency_trigger_tokens,
235
+ "current_tokens": current_tokens,
236
+ "context_usage_percent": round(context_usage_percent, 1),
237
+ "provider": provider,
238
+ }
239
+ except Exception as e:
240
+ logger.warning(f"Could not get rollup info: {e}")
241
+ rollup_info = {"error": str(e)}
242
+
243
+ return CommandResponse(
244
+ command="info",
245
+ status="success",
246
+ data={
247
+ "conversation": {
248
+ "id": conv['id'],
249
+ "name": conv['name'],
250
+ "model_id": conv['model_id'],
251
+ "created_at": created_at_str,
252
+ "tokens_sent": conv.get('tokens_sent', 0),
253
+ "tokens_received": conv.get('tokens_received', 0),
254
+ "total_tokens": conv.get('tokens_sent', 0) + conv.get('tokens_received', 0),
255
+ },
256
+ "model_usage": model_usage,
257
+ "files": [f['filename'] for f in files],
258
+ "mcp_servers": mcp_servers,
259
+ "rollup_settings": rollup_info,
260
+ },
261
+ )
262
+
263
+ except HTTPException:
264
+ raise
265
+ except Exception as e:
266
+ logger.error(f"Error getting info for conversation {conversation_id}: {e}")
267
+ raise HTTPException(status_code=500, detail=str(e))
268
+
269
+
270
+ @router.post("/chat/{conversation_id}/command/attach")
271
+ async def command_attach(
272
+ conversation_id: int,
273
+ request: Request,
274
+ files: List[UploadFile] = File(...),
275
+ session_id: str = Depends(get_current_session),
276
+ ) -> CommandResponse:
277
+ """
278
+ Execute 'attach' command to attach files to conversation.
279
+
280
+ Args:
281
+ conversation_id: ID of the conversation
282
+ files: Files to attach
283
+
284
+ Returns:
285
+ CommandResponse with status
286
+ """
287
+ try:
288
+ app_instance = request.app.state.app_instance
289
+ conversation_manager = app_instance.conversation_manager
290
+ database = app_instance.database
291
+
292
+ # Load conversation
293
+ conversation_manager.load_conversation(conversation_id)
294
+
295
+ # Set the model from the conversation and update service references
296
+ conv = database.get_conversation(conversation_id)
297
+ if conv:
298
+ app_instance.llm_manager.set_model(conv['model_id'])
299
+ app_instance.bedrock_service = app_instance.llm_manager.get_active_service()
300
+ conversation_manager.update_service(app_instance.bedrock_service)
301
+
302
+ # Save uploaded files to temporary locations and attach
303
+ temp_files = []
304
+ attached_filenames = []
305
+
306
+ try:
307
+ # Save uploaded files to temporary locations
308
+ for upload_file in files:
309
+ # Create temporary file
310
+ suffix = os.path.splitext(upload_file.filename)[1]
311
+ temp_fd, temp_path = tempfile.mkstemp(suffix=suffix)
312
+
313
+ # Write uploaded content to temp file
314
+ with os.fdopen(temp_fd, 'wb') as f:
315
+ content = await upload_file.read()
316
+ f.write(content)
317
+
318
+ temp_files.append(temp_path)
319
+ attached_filenames.append(upload_file.filename)
320
+ logger.info(f"Saved uploaded file {upload_file.filename} to {temp_path}")
321
+
322
+ # Attach files using conversation manager
323
+ if temp_files:
324
+ success = conversation_manager.attach_files(temp_files)
325
+
326
+ if success:
327
+ logger.info(f"Successfully attached {len(temp_files)} file(s) to conversation {conversation_id}")
328
+ return CommandResponse(
329
+ command="attach",
330
+ status="success",
331
+ message=f"Attached {len(attached_filenames)} file(s)",
332
+ data={"files": attached_filenames},
333
+ )
334
+ else:
335
+ logger.warning("Some files failed to attach")
336
+ return CommandResponse(
337
+ command="attach",
338
+ status="partial",
339
+ message="Some files failed to attach",
340
+ data={"files": attached_filenames},
341
+ )
342
+ else:
343
+ return CommandResponse(
344
+ command="attach",
345
+ status="error",
346
+ message="No files provided",
347
+ )
348
+
349
+ except Exception as e:
350
+ logger.error(f"Error processing file uploads: {e}")
351
+ raise
352
+
353
+ finally:
354
+ # Clean up temporary files
355
+ for temp_path in temp_files:
356
+ try:
357
+ if os.path.exists(temp_path):
358
+ os.unlink(temp_path)
359
+ except:
360
+ pass
361
+
362
+ except Exception as e:
363
+ logger.error(f"Error attaching files to conversation {conversation_id}: {e}")
364
+ raise HTTPException(status_code=500, detail=str(e))
365
+
366
+
367
+ @router.post("/chat/{conversation_id}/command/export")
368
+ async def command_export(
369
+ conversation_id: int,
370
+ request: Request,
371
+ format: str = Form(...), # 'markdown', 'html', or 'csv'
372
+ include_tools: bool = Form(True),
373
+ session_id: str = Depends(get_current_session),
374
+ ) -> dict:
375
+ """
376
+ Execute 'export' command to export conversation.
377
+
378
+ Args:
379
+ conversation_id: ID of the conversation
380
+ format: Export format ('markdown', 'html', or 'csv')
381
+ include_tools: Whether to include tool use details
382
+
383
+ Returns:
384
+ Export data
385
+ """
386
+ try:
387
+ app_instance = request.app.state.app_instance
388
+
389
+ # Load conversation
390
+ app_instance.conversation_manager.load_conversation(conversation_id)
391
+
392
+ # Export conversation
393
+ if format == 'markdown':
394
+ content = app_instance.conversation_manager.export_to_markdown(
395
+ include_tool_details=include_tools
396
+ )
397
+ elif format == 'html':
398
+ content = app_instance.conversation_manager.export_to_html(
399
+ include_tool_details=include_tools
400
+ )
401
+ elif format == 'csv':
402
+ content = app_instance.conversation_manager.export_to_csv(
403
+ include_tool_details=include_tools
404
+ )
405
+ else:
406
+ raise HTTPException(status_code=400, detail="Invalid export format")
407
+
408
+ return {
409
+ "status": "success",
410
+ "format": format,
411
+ "content": content,
412
+ }
413
+
414
+ except HTTPException:
415
+ raise
416
+ except Exception as e:
417
+ logger.error(f"Error exporting conversation {conversation_id}: {e}")
418
+ raise HTTPException(status_code=500, detail=str(e))
419
+
420
+
421
+ @router.post("/chat/{conversation_id}/command/changemodel")
422
+ async def command_change_model(
423
+ conversation_id: int,
424
+ request: Request,
425
+ model_id: str = Form(...),
426
+ session_id: str = Depends(get_current_session),
427
+ ) -> CommandResponse:
428
+ """
429
+ Execute 'changemodel' command to change conversation model.
430
+
431
+ Args:
432
+ conversation_id: ID of the conversation
433
+ model_id: New model ID
434
+
435
+ Returns:
436
+ CommandResponse with status
437
+ """
438
+ try:
439
+ app_instance = request.app.state.app_instance
440
+
441
+ # Load conversation
442
+ app_instance.conversation_manager.load_conversation(conversation_id)
443
+
444
+ # Change model
445
+ app_instance.conversation_manager.change_model(model_id)
446
+
447
+ return CommandResponse(
448
+ command="changemodel",
449
+ status="success",
450
+ message=f"Model changed to {model_id}",
451
+ data={"model_id": model_id},
452
+ )
453
+
454
+ except Exception as e:
455
+ logger.error(f"Error changing model for conversation {conversation_id}: {e}")
456
+ raise HTTPException(status_code=500, detail=str(e))
457
+
458
+
459
+ @router.get("/chat/{conversation_id}/command/mcpaudit")
460
+ async def command_mcp_audit(
461
+ conversation_id: int,
462
+ request: Request,
463
+ session_id: str = Depends(get_current_session),
464
+ ) -> CommandResponse:
465
+ """
466
+ Execute 'mcpaudit' command to get MCP transaction audit log.
467
+
468
+ Args:
469
+ conversation_id: ID of the conversation
470
+
471
+ Returns:
472
+ CommandResponse with audit log
473
+ """
474
+ try:
475
+ app_instance = request.app.state.app_instance
476
+ database = app_instance.database
477
+
478
+ # Get MCP transactions for this conversation
479
+ transactions = database.get_mcp_transactions(conversation_id=conversation_id)
480
+
481
+ return CommandResponse(
482
+ command="mcpaudit",
483
+ status="success",
484
+ data={"transactions": transactions},
485
+ )
486
+
487
+ except Exception as e:
488
+ logger.error(f"Error getting MCP audit for conversation {conversation_id}: {e}")
489
+ raise HTTPException(status_code=500, detail=str(e))
490
+
491
+
492
+ @router.get("/chat/{conversation_id}/command/mcpservers")
493
+ async def command_mcp_servers(
494
+ conversation_id: int,
495
+ request: Request,
496
+ session_id: str = Depends(get_current_session),
497
+ ) -> CommandResponse:
498
+ """
499
+ Execute 'mcpservers' command to get MCP server states.
500
+
501
+ Args:
502
+ conversation_id: ID of the conversation
503
+
504
+ Returns:
505
+ CommandResponse with MCP server states
506
+ """
507
+ try:
508
+ app_instance = request.app.state.app_instance
509
+ database = app_instance.database
510
+
511
+ # Get all server names from MCP manager
512
+ all_server_names = []
513
+ if app_instance.mcp_manager and hasattr(app_instance.mcp_manager, 'clients'):
514
+ all_server_names = list(app_instance.mcp_manager.clients.keys())
515
+
516
+ # Get MCP server states for this conversation
517
+ servers = database.get_all_mcp_server_states(conversation_id, all_server_names)
518
+
519
+ return CommandResponse(
520
+ command="mcpservers",
521
+ status="success",
522
+ data={"servers": servers},
523
+ )
524
+
525
+ except Exception as e:
526
+ logger.error(f"Error getting MCP servers for conversation {conversation_id}: {e}")
527
+ raise HTTPException(status_code=500, detail=str(e))
528
+
529
+
530
+ @router.post("/chat/{conversation_id}/command/mcpservers/toggle")
531
+ async def toggle_mcp_server(
532
+ conversation_id: int,
533
+ request: Request,
534
+ server_name: str = Form(...),
535
+ enabled: bool = Form(...),
536
+ session_id: str = Depends(get_current_session),
537
+ ) -> CommandResponse:
538
+ """
539
+ Toggle MCP server enabled/disabled state.
540
+
541
+ Args:
542
+ conversation_id: ID of the conversation
543
+ server_name: Name of the server to toggle
544
+ enabled: New enabled state
545
+
546
+ Returns:
547
+ CommandResponse with status
548
+ """
549
+ try:
550
+ app_instance = request.app.state.app_instance
551
+
552
+ # Load conversation
553
+ app_instance.conversation_manager.load_conversation(conversation_id)
554
+
555
+ # Toggle server
556
+ app_instance.conversation_manager.set_mcp_server_enabled(server_name, enabled)
557
+
558
+ return CommandResponse(
559
+ command="mcpservers",
560
+ status="success",
561
+ message=f"Server '{server_name}' {'enabled' if enabled else 'disabled'}",
562
+ data={
563
+ "server_name": server_name,
564
+ "enabled": enabled,
565
+ },
566
+ )
567
+
568
+ except Exception as e:
569
+ logger.error(f"Error toggling MCP server for conversation {conversation_id}: {e}")
570
+ raise HTTPException(status_code=500, detail=str(e))
571
+
572
+
573
+ @router.post("/chat/permission/respond")
574
+ async def respond_to_permission_request(
575
+ request: Request,
576
+ request_id: str = Form(...),
577
+ response: str = Form(...),
578
+ session_id: str = Depends(get_current_session)
579
+ ):
580
+ """
581
+ Submit a response to a tool permission request.
582
+
583
+ Args:
584
+ request: FastAPI request
585
+ request_id: The permission request ID
586
+ response: User's response ('once', 'allowed', 'denied', or 'cancel')
587
+ session_id: Current session ID
588
+
589
+ Returns:
590
+ CommandResponse with status
591
+ """
592
+ try:
593
+ app_instance = request.app.state.app_instance
594
+
595
+ # Check if web interface is available
596
+ if not hasattr(app_instance.conversation_manager, 'web_interface') or not app_instance.conversation_manager.web_interface:
597
+ raise HTTPException(status_code=400, detail="Web interface not initialized")
598
+
599
+ # Submit the response
600
+ success = app_instance.conversation_manager.web_interface.submit_permission_response(
601
+ request_id, None if response == 'cancel' else response
602
+ )
603
+
604
+ if not success:
605
+ raise HTTPException(status_code=404, detail="Permission request not found")
606
+
607
+ return CommandResponse(
608
+ command="permission_response",
609
+ status="success",
610
+ message=f"Permission response submitted: {response}",
611
+ data={
612
+ "request_id": request_id,
613
+ "response": response,
614
+ },
615
+ )
616
+
617
+ except HTTPException:
618
+ raise
619
+ except Exception as e:
620
+ logger.error(f"Error submitting permission response: {e}")
621
+ raise HTTPException(status_code=500, detail=str(e))