fastworkflow 2.17.9__py3-none-any.whl → 2.17.10__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.
@@ -40,11 +40,12 @@ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
40
40
 
41
41
  from .mcp_specific import setup_mcp
42
42
  from .utils import (
43
- get_userconversations_dir,
44
- UserSessionManager,
43
+ get_channelconversations_dir,
44
+ ChannelSessionManager,
45
45
  save_conversation_incremental,
46
46
  InitializationRequest,
47
47
  TokenResponse,
48
+ InitializeResponse,
48
49
  SessionData,
49
50
  InvokeRequest,
50
51
  PerformActionRequest,
@@ -54,6 +55,7 @@ from .utils import (
54
55
  GenerateMCPTokenRequest,
55
56
  wait_for_command_output,
56
57
  collect_trace_events,
58
+ collect_trace_events_async,
57
59
  get_session_from_jwt,
58
60
  ensure_user_runtime_exists
59
61
  )
@@ -79,7 +81,7 @@ from .conversation_store import (
79
81
  # ============================================================================
80
82
 
81
83
  # Global session manager
82
- session_manager = UserSessionManager()
84
+ session_manager = ChannelSessionManager()
83
85
 
84
86
 
85
87
  # ============================================================================
@@ -114,14 +116,14 @@ async def get_session_and_ensure_runtime(
114
116
  ```python
115
117
  @app.post("/endpoint")
116
118
  async def endpoint(session: SessionData = Depends(get_session_and_ensure_runtime)):
117
- # session.user_id can now safely be used with session_manager
118
- runtime = await session_manager.get_session(session.user_id)
119
+ # session.channel_id can now safely be used with session_manager
120
+ runtime = await session_manager.get_session(session.channel_id)
119
121
  # runtime is guaranteed to exist
120
122
  ```
121
123
  """
122
124
  # Ensure the user runtime exists (creates if missing)
123
125
  await ensure_user_runtime_exists(
124
- user_id=session.user_id,
126
+ channel_id=session.channel_id,
125
127
  session_manager=session_manager,
126
128
  workflow_path=ARGS.workflow_path,
127
129
  context=json.loads(ARGS.context) if ARGS.context else None,
@@ -154,31 +156,31 @@ async def lifespan(_app: FastAPI):
154
156
  # Configure JWT verification mode based on CLI parameter
155
157
  set_jwt_verification_mode(ARGS.expect_encrypted_jwt)
156
158
 
157
- async def _active_turn_user_ids() -> list[str]:
159
+ async def _active_turn_channel_ids() -> list[str]:
158
160
  active: list[str] = []
159
- for user_id in list(session_manager._sessions.keys()):
160
- rt = await session_manager.get_session(user_id)
161
+ for channel_id in list(session_manager._sessions.keys()):
162
+ rt = await session_manager.get_session(channel_id)
161
163
  if rt and rt.lock.locked():
162
- active.append(user_id)
164
+ active.append(channel_id)
163
165
  return active
164
166
 
165
167
  async def wait_for_active_turns_to_complete(max_wait_seconds: int) -> None:
166
168
  logger.info(f"Waiting up to {max_wait_seconds}s for active turns to complete...")
167
169
  start_time = time.time()
168
170
  while time.time() - start_time < max_wait_seconds:
169
- active_turns = await _active_turn_user_ids()
171
+ active_turns = await _active_turn_channel_ids()
170
172
  if not active_turns:
171
173
  logger.info("All turns completed, shutting down gracefully")
172
174
  return
173
175
  logger.debug(f"Waiting for {len(active_turns)} active turns: {active_turns}")
174
176
  await asyncio.sleep(0.5)
175
- remaining = await _active_turn_user_ids()
177
+ remaining = await _active_turn_channel_ids()
176
178
  logger.warning(f"Shutdown timeout reached with {len(remaining)} turns still active")
177
179
 
178
180
  async def finalize_conversations_on_shutdown() -> None:
179
181
  logger.info("Finalizing conversations with topic and summary...")
180
- for user_id in list(session_manager._sessions.keys()):
181
- runtime = await session_manager.get_session(user_id)
182
+ for channel_id in list(session_manager._sessions.keys()):
183
+ runtime = await session_manager.get_session(channel_id)
182
184
  if not runtime:
183
185
  continue
184
186
  if turns := extract_turns_from_history(runtime.chat_session.conversation_history):
@@ -188,17 +190,17 @@ async def lifespan(_app: FastAPI):
188
190
  runtime.conversation_store.update_conversation_topic_summary(
189
191
  runtime.active_conversation_id, topic, summary
190
192
  )
191
- logger.info(f"Finalized conversation {runtime.active_conversation_id} for user {user_id} during shutdown")
193
+ logger.info(f"Finalized conversation {runtime.active_conversation_id} for user {channel_id} during shutdown")
192
194
  else:
193
- logger.warning(f"Conversation history exists but no active_conversation_id for user {user_id} during shutdown")
195
+ logger.warning(f"Conversation history exists but no active_conversation_id for user {channel_id} during shutdown")
194
196
  conv_id = runtime.conversation_store.save_conversation(topic, summary, turns)
195
- logger.info(f"Created conversation {conv_id} for user {user_id} during shutdown")
197
+ logger.info(f"Created conversation {conv_id} for user {channel_id} during shutdown")
196
198
  except Exception as e:
197
- logger.error(f"Failed to finalize conversation for user {user_id} during shutdown: {e}")
199
+ logger.error(f"Failed to finalize conversation for user {channel_id} during shutdown: {e}")
198
200
 
199
201
  async def stop_all_chat_sessions() -> None:
200
- for user_id in list(session_manager._sessions.keys()):
201
- runtime = await session_manager.get_session(user_id)
202
+ for channel_id in list(session_manager._sessions.keys()):
203
+ runtime = await session_manager.get_session(channel_id)
202
204
  if runtime:
203
205
  runtime.chat_session.stop_workflow()
204
206
 
@@ -311,76 +313,146 @@ async def root():
311
313
  @app.post(
312
314
  "/initialize",
313
315
  operation_id="rest_initialize",
314
- response_model=TokenResponse,
316
+ response_model=InitializeResponse,
315
317
  status_code=status.HTTP_200_OK,
316
318
  responses={
317
- 200: {"description": "Session successfully initialized, JWT tokens returned"},
318
- 400: {"description": "Both startup_command and startup_action provided"},
319
+ 200: {"description": "Session successfully initialized, JWT tokens returned with optional startup output"},
320
+ 400: {"description": "Both startup_command and startup_action provided, or user_id missing when startup provided"},
319
321
  422: {"description": "Invalid paths or missing env vars"},
320
322
  500: {"description": "Internal error during initialization"}
321
323
  }
322
324
  )
323
- async def initialize(request: InitializationRequest) -> TokenResponse:
325
+ async def initialize(request: InitializationRequest) -> InitializeResponse:
324
326
  """
325
- Initialize a FastWorkflow session for a user.
327
+ Initialize a FastWorkflow session for a channel.
326
328
  Creates or resumes a ChatSession and starts the workflow.
329
+ Optionally executes a startup command/action and returns its output.
327
330
  """
328
331
  try:
332
+ channel_id = request.channel_id
329
333
  user_id = request.user_id
330
- logger.info(f"Initializing session for user_id: {user_id}")
334
+ logger.info(f"Initializing session for channel_id: {channel_id}, user_id: {user_id}")
335
+
336
+ # Validate XOR: can't have both startup_command and startup_action
337
+ if request.startup_command and request.startup_action:
338
+ raise HTTPException(
339
+ status_code=status.HTTP_400_BAD_REQUEST,
340
+ detail="Cannot provide both startup_command and startup_action. Choose one or neither."
341
+ )
342
+
343
+ # Validate: if startup provided, user_id is required
344
+ if (request.startup_command or request.startup_action) and not user_id:
345
+ raise HTTPException(
346
+ status_code=status.HTTP_400_BAD_REQUEST,
347
+ detail="user_id is required when startup_command or startup_action is provided"
348
+ )
331
349
 
332
350
  # Check if user already has an active session
333
- existing_runtime = await session_manager.get_session(user_id)
351
+ existing_runtime = await session_manager.get_session(channel_id)
334
352
  if existing_runtime:
335
- logger.info(f"Session for user_id {user_id} already exists, generating new tokens")
353
+ logger.info(f"Session for channel_id {channel_id} already exists, generating new tokens")
336
354
 
337
355
  # Generate new JWT tokens for existing session
338
- access_token = create_access_token(user_id)
339
- refresh_token = create_refresh_token(user_id)
356
+ access_token = create_access_token(channel_id, user_id)
357
+ refresh_token = create_refresh_token(channel_id, user_id)
340
358
 
341
- return TokenResponse(
359
+ return InitializeResponse(
342
360
  access_token=access_token,
343
361
  refresh_token=refresh_token,
344
362
  token_type="bearer",
345
363
  expires_in=JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60 # Convert to seconds
346
364
  )
347
365
 
348
- # Prepare startup action if provided via CLI args
366
+ # Prepare startup action if provided in request (takes precedence over CLI args)
349
367
  startup_action = None
350
- if ARGS.startup_action:
368
+ startup_command_str = request.startup_command or ARGS.startup_command
369
+
370
+ if request.startup_action:
371
+ startup_action = fastworkflow.Action(**request.startup_action)
372
+ elif ARGS.startup_action:
351
373
  startup_action = fastworkflow.Action(**json.loads(ARGS.startup_action))
352
374
 
353
375
  # Use the modular helper function to create the session
354
- # This ensures consistent session creation logic across the application
355
376
  await ensure_user_runtime_exists(
356
- user_id=user_id,
377
+ channel_id=channel_id,
357
378
  session_manager=session_manager,
358
379
  workflow_path=ARGS.workflow_path,
359
380
  context=json.loads(ARGS.context) if ARGS.context else None,
360
- startup_command=ARGS.startup_command,
361
- startup_action=startup_action,
381
+ startup_command=None, # Don't execute during session creation
382
+ startup_action=None, # Don't execute during session creation
362
383
  stream_format=(request.stream_format if request.stream_format in ("ndjson", "sse") else "ndjson")
363
384
  )
364
385
 
386
+ # Execute startup if provided
387
+ startup_output = None
388
+ if startup_command_str or startup_action:
389
+ runtime = await session_manager.get_session(channel_id)
390
+ if not runtime:
391
+ raise HTTPException(
392
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
393
+ detail=f"Runtime not found after creation for channel_id: {channel_id}"
394
+ )
395
+
396
+ chat_session = runtime.chat_session
397
+
398
+ # Execute startup action or command
399
+ if startup_action:
400
+ # Execute action directly (like perform_action)
401
+ logger.info(f"Executing startup action for channel_id {channel_id}: {startup_action.command_name}")
402
+ chat_session.user_message_queue.put(startup_action)
403
+ else:
404
+ # Execute command via assistant path (deterministic) - needs / prefix
405
+ assistant_command = f"/{startup_command_str.lstrip('/')}"
406
+ logger.info(f"Executing startup command for channel_id {channel_id}: {assistant_command}")
407
+ chat_session.user_message_queue.put(assistant_command)
408
+
409
+ # Wait for output
410
+ try:
411
+ startup_output = await wait_for_command_output(
412
+ runtime=runtime,
413
+ timeout_seconds=60
414
+ )
415
+
416
+ # Collect traces
417
+ traces = await collect_trace_events_async(
418
+ trace_queue=chat_session.command_trace_queue,
419
+ user_id=user_id
420
+ )
421
+
422
+ # Persist the startup turn to conversation store
423
+ if startup_output:
424
+ # Save turn incrementally using existing conversation store in runtime
425
+ save_conversation_incremental(runtime, extract_turns_from_history, logger)
426
+
427
+ logger.info(f"Startup execution completed and persisted for channel_id: {channel_id}")
428
+
429
+ except asyncio.TimeoutError:
430
+ logger.error(f"Startup execution timed out for channel_id: {channel_id}")
431
+ raise HTTPException(
432
+ status_code=status.HTTP_504_GATEWAY_TIMEOUT,
433
+ detail=f"Startup execution timed out for channel_id: {channel_id}"
434
+ )
435
+
365
436
  # Generate JWT tokens
366
- access_token = create_access_token(user_id)
367
- refresh_token = create_refresh_token(user_id)
437
+ access_token = create_access_token(channel_id, user_id)
438
+ refresh_token = create_refresh_token(channel_id, user_id)
368
439
 
369
- return TokenResponse(
440
+ return InitializeResponse(
370
441
  access_token=access_token,
371
442
  refresh_token=refresh_token,
372
443
  token_type="bearer",
373
- expires_in=JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60 # Convert to seconds
444
+ expires_in=JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60, # Convert to seconds
445
+ startup_output=startup_output
374
446
  )
375
447
 
376
448
  except HTTPException:
377
449
  raise
378
450
  except Exception as e:
379
- logger.error(f"Error initializing session for user_id: {request.user_id}: {e}")
451
+ logger.error(f"Error initializing session for channel_id: {request.channel_id}: {e}")
380
452
  traceback.print_exc()
381
453
  raise HTTPException(
382
454
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
383
- detail=f"Internal error in initialize() for user_id: {request.user_id}",
455
+ detail=f"Internal error in initialize() for channel_id: {request.channel_id}",
384
456
  ) from e
385
457
 
386
458
 
@@ -426,22 +498,23 @@ async def refresh_token(
426
498
  headers={"WWW-Authenticate": "Bearer"},
427
499
  ) from e
428
500
 
429
- # Extract user_id from payload
430
- user_id = payload["sub"]
501
+ # Extract channel_id and optional user_id from payload
502
+ channel_id = payload["sub"]
503
+ user_id = payload.get("uid")
431
504
 
432
505
  # Verify session still exists
433
- runtime = await session_manager.get_session(user_id)
506
+ runtime = await session_manager.get_session(channel_id)
434
507
  if not runtime:
435
508
  raise HTTPException(
436
509
  status_code=status.HTTP_404_NOT_FOUND,
437
- detail=f"User session not found: {user_id} (may have been cleaned up)"
510
+ detail=f"User session not found: {channel_id} (may have been cleaned up)"
438
511
  )
439
512
 
440
- # Generate new tokens
441
- new_access_token = create_access_token(user_id)
442
- new_refresh_token = create_refresh_token(user_id)
513
+ # Generate new tokens with same user_id
514
+ new_access_token = create_access_token(channel_id, user_id)
515
+ new_refresh_token = create_refresh_token(channel_id, user_id)
443
516
 
444
- logger.info(f"Refreshed tokens for user_id: {user_id}")
517
+ logger.info(f"Refreshed tokens for channel_id: {channel_id}, user_id: {user_id}")
445
518
 
446
519
  return TokenResponse(
447
520
  access_token=new_access_token,
@@ -484,20 +557,21 @@ async def invoke_agent(
484
557
 
485
558
  Requires a valid JWT access token in the Authorization header (Bearer token format).
486
559
  """
560
+ channel_id = session.channel_id
487
561
  user_id = session.user_id
488
562
  try:
489
- runtime = await session_manager.get_session(user_id)
563
+ runtime = await session_manager.get_session(channel_id)
490
564
  if not runtime:
491
565
  raise HTTPException(
492
566
  status_code=status.HTTP_404_NOT_FOUND,
493
- detail=f"User session not found: {user_id}"
567
+ detail=f"User session not found: {channel_id}"
494
568
  )
495
569
 
496
570
  # Serialize turns per user
497
571
  if runtime.lock.locked():
498
572
  raise HTTPException(
499
573
  status_code=status.HTTP_409_CONFLICT,
500
- detail=f"A turn is already in progress for user: {user_id}"
574
+ detail=f"A turn is already in progress for user: {channel_id}"
501
575
  )
502
576
 
503
577
  async with runtime.lock:
@@ -513,7 +587,7 @@ async def invoke_agent(
513
587
  # Incrementally save conversation turns (without generating topic/summary)
514
588
  save_conversation_incremental(runtime, extract_turns_from_history, logger)
515
589
 
516
- traces = collect_trace_events(runtime)
590
+ traces = collect_trace_events(runtime, user_id=user_id)
517
591
  # Build response with traces
518
592
  response_data = command_output.model_dump()
519
593
  if traces:
@@ -524,11 +598,11 @@ async def invoke_agent(
524
598
  except HTTPException:
525
599
  raise
526
600
  except Exception as e:
527
- logger.error(f"Error in invoke_agent for user {user_id}: {e}")
601
+ logger.error(f"Error in invoke_agent for user {channel_id}: {e}")
528
602
  traceback.print_exc()
529
603
  raise HTTPException(
530
604
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
531
- detail=f"Internal error in invoke_agent() for user_id: {user_id}",
605
+ detail=f"Internal error in invoke_agent() for channel_id: {channel_id}",
532
606
  ) from e
533
607
 
534
608
 
@@ -563,20 +637,21 @@ async def invoke_agent_stream(
563
637
  Requires a valid JWT access token in the Authorization header (Bearer token format).
564
638
  Exposed as 'invoke_agent' tool for MCP clients (who don't need JWT auth).
565
639
  """
640
+ channel_id = session.channel_id
566
641
  user_id = session.user_id
567
642
 
568
643
  # Get runtime and validate session exists
569
- runtime = await session_manager.get_session(user_id)
644
+ runtime = await session_manager.get_session(channel_id)
570
645
  if not runtime:
571
646
  return JSONResponse(
572
647
  status_code=status.HTTP_404_NOT_FOUND,
573
- content={"detail": f"User session not found: {user_id}"}
648
+ content={"detail": f"User session not found: {channel_id}"}
574
649
  )
575
650
 
576
651
  async def ndjson_stream():
577
652
  try:
578
653
  if runtime.lock.locked():
579
- yield {"type": "error", "data": {"detail": f"A turn is already in progress for user: {user_id}"}}
654
+ yield {"type": "error", "data": {"detail": f"A turn is already in progress for user: {channel_id}"}}
580
655
  return
581
656
 
582
657
  async with runtime.lock:
@@ -597,6 +672,8 @@ async def invoke_agent_stream(
597
672
  "success": evt.success,
598
673
  "timestamp_ms": evt.timestamp_ms,
599
674
  }
675
+ if user_id is not None:
676
+ trace_json["user_id"] = user_id
600
677
  yield {"type": "trace", "data": trace_json}
601
678
  except queue.Empty:
602
679
  break
@@ -621,6 +698,8 @@ async def invoke_agent_stream(
621
698
  "success": evt.success,
622
699
  "timestamp_ms": evt.timestamp_ms,
623
700
  }
701
+ if user_id is not None:
702
+ trace_json["user_id"] = user_id
624
703
  yield {"type": "trace", "data": trace_json}
625
704
  except queue.Empty:
626
705
  break
@@ -633,14 +712,14 @@ async def invoke_agent_stream(
633
712
  yield {"type": "output", "data": command_output.model_dump()}
634
713
 
635
714
  except Exception as e:
636
- logger.error(f"Error in invoke_agent_stream for user {user_id}: {e}")
715
+ logger.error(f"Error in invoke_agent_stream for user {channel_id}: {e}")
637
716
  traceback.print_exc()
638
- yield {"type": "error", "data": {"detail": f"Internal error in invoke_agent_stream() for user_id: {user_id}"}}
717
+ yield {"type": "error", "data": {"detail": f"Internal error in invoke_agent_stream() for channel_id: {channel_id}"}}
639
718
 
640
719
  async def sse_stream():
641
720
  try:
642
721
  if runtime.lock.locked():
643
- yield "event: error\n" + f"data: {json.dumps({'detail': f'A turn is already in progress for user: {user_id}'})}\n\n"
722
+ yield "event: error\n" + f"data: {json.dumps({'detail': f'A turn is already in progress for user: {channel_id}'})}\n\n"
644
723
  return
645
724
 
646
725
  async with runtime.lock:
@@ -656,6 +735,8 @@ async def invoke_agent_stream(
656
735
  "success": evt.success,
657
736
  "timestamp_ms": evt.timestamp_ms,
658
737
  }
738
+ if user_id is not None:
739
+ trace_data["user_id"] = user_id
659
740
  return f"event: trace\ndata: {json.dumps(trace_data)}\n\n"
660
741
 
661
742
  start_time = time.time()
@@ -692,9 +773,9 @@ async def invoke_agent_stream(
692
773
  yield "event: output\n" + f"data: {json.dumps(command_output.model_dump())}\n\n"
693
774
 
694
775
  except Exception as e:
695
- logger.error(f"Error in invoke_agent_stream SSE for user {user_id}: {e}")
776
+ logger.error(f"Error in invoke_agent_stream SSE for user {channel_id}: {e}")
696
777
  traceback.print_exc()
697
- yield "event: error\n" + f"data: {json.dumps({'detail': f'Internal error in invoke_agent_stream() for user_id: {user_id}'})}\n\n"
778
+ yield "event: error\n" + f"data: {json.dumps({'detail': f'Internal error in invoke_agent_stream() for channel_id: {channel_id}'})}\n\n"
698
779
 
699
780
  # Route to appropriate stream format
700
781
  if runtime.stream_format == "sse":
@@ -735,19 +816,20 @@ async def invoke_assistant(
735
816
 
736
817
  Requires a valid JWT access token in the Authorization header (Bearer token format).
737
818
  """
819
+ channel_id = session.channel_id
738
820
  user_id = session.user_id
739
821
  try:
740
- runtime = await session_manager.get_session(user_id)
822
+ runtime = await session_manager.get_session(channel_id)
741
823
  if not runtime:
742
824
  raise HTTPException(
743
825
  status_code=status.HTTP_404_NOT_FOUND,
744
- detail=f"User session not found: {user_id}"
826
+ detail=f"User session not found: {channel_id}"
745
827
  )
746
828
 
747
829
  if runtime.lock.locked():
748
830
  raise HTTPException(
749
831
  status_code=status.HTTP_409_CONFLICT,
750
- detail=f"A turn is already in progress for user: {user_id}"
832
+ detail=f"A turn is already in progress for user: {channel_id}"
751
833
  )
752
834
 
753
835
  async with runtime.lock:
@@ -769,7 +851,7 @@ async def invoke_assistant(
769
851
  # Incrementally save conversation turns (without generating topic/summary)
770
852
  save_conversation_incremental(runtime, extract_turns_from_history, logger)
771
853
 
772
- traces = collect_trace_events(runtime)
854
+ traces = collect_trace_events(runtime, user_id=user_id)
773
855
  response_data = command_output.model_dump()
774
856
  if traces:
775
857
  response_data["traces"] = traces
@@ -779,11 +861,11 @@ async def invoke_assistant(
779
861
  except HTTPException:
780
862
  raise
781
863
  except Exception as e:
782
- logger.error(f"Error in invoke_assistant for session {user_id}: {e}")
864
+ logger.error(f"Error in invoke_assistant for session {channel_id}: {e}")
783
865
  traceback.print_exc()
784
866
  raise HTTPException(
785
867
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
786
- detail=f"Internal error in invoke_assistant() for user_id: {user_id}",
868
+ detail=f"Internal error in invoke_assistant() for channel_id: {channel_id}",
787
869
  ) from e
788
870
 
789
871
 
@@ -810,19 +892,20 @@ async def perform_action(
810
892
 
811
893
  Requires a valid JWT access token in the Authorization header (Bearer token format).
812
894
  """
895
+ channel_id = session.channel_id
813
896
  user_id = session.user_id
814
897
  try:
815
- runtime = await session_manager.get_session(user_id)
898
+ runtime = await session_manager.get_session(channel_id)
816
899
  if not runtime:
817
900
  raise HTTPException(
818
901
  status_code=status.HTTP_404_NOT_FOUND,
819
- detail=f"User session not found: {user_id}"
902
+ detail=f"User session not found: {channel_id}"
820
903
  )
821
904
 
822
905
  if runtime.lock.locked():
823
906
  raise HTTPException(
824
907
  status_code=status.HTTP_409_CONFLICT,
825
- detail=f"A turn is already in progress for user: {user_id}"
908
+ detail=f"A turn is already in progress for user: {channel_id}"
826
909
  )
827
910
 
828
911
  async with runtime.lock:
@@ -839,7 +922,7 @@ async def perform_action(
839
922
  # This executes synchronously in the current thread (not via queue)
840
923
  command_output = runtime.chat_session._process_action(action)
841
924
 
842
- traces = collect_trace_events(runtime)
925
+ traces = collect_trace_events(runtime, user_id=user_id)
843
926
  response_data = command_output.model_dump()
844
927
  if traces:
845
928
  response_data["traces"] = traces
@@ -849,11 +932,11 @@ async def perform_action(
849
932
  except HTTPException:
850
933
  raise
851
934
  except Exception as e:
852
- logger.error(f"Error in perform_action for session {user_id}: {e}")
935
+ logger.error(f"Error in perform_action for session {channel_id}: {e}")
853
936
  traceback.print_exc()
854
937
  raise HTTPException(
855
938
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
856
- detail=f"Internal error in perform_action() for user_id: {user_id}",
939
+ detail=f"Internal error in perform_action() for channel_id: {channel_id}",
857
940
  ) from e
858
941
 
859
942
 
@@ -877,13 +960,13 @@ async def new_conversation(
877
960
 
878
961
  Requires a valid JWT access token in the Authorization header (Bearer token format).
879
962
  """
880
- user_id = session.user_id
963
+ channel_id = session.channel_id
881
964
  try:
882
- runtime = await session_manager.get_session(user_id)
965
+ runtime = await session_manager.get_session(channel_id)
883
966
  if not runtime:
884
967
  raise HTTPException(
885
968
  status_code=status.HTTP_404_NOT_FOUND,
886
- detail=f"User session not found: {user_id}"
969
+ detail=f"User session not found: {channel_id}"
887
970
  )
888
971
 
889
972
  # Extract turns from chat_session conversation history
@@ -897,34 +980,34 @@ async def new_conversation(
897
980
  runtime.conversation_store.update_conversation_topic_summary(
898
981
  conv_id, topic, summary
899
982
  )
900
- logger.info(f"Finalized conversation {conv_id} with topic and summary for session {user_id}")
983
+ logger.info(f"Finalized conversation {conv_id} with topic and summary for session {channel_id}")
901
984
  else:
902
985
  # Edge case: conversation history exists but no active ID (shouldn't happen with incremental saves)
903
- logger.warning(f"Conversation history exists but no active_conversation_id for session {user_id}")
986
+ logger.warning(f"Conversation history exists but no active_conversation_id for session {channel_id}")
904
987
  conv_id = runtime.conversation_store.save_conversation(topic, summary, turns)
905
- logger.info(f"Created conversation {conv_id} for session {user_id}")
988
+ logger.info(f"Created conversation {conv_id} for session {channel_id}")
906
989
 
907
990
  # Reserve next conversation ID for the next conversation
908
991
  next_id = runtime.conversation_store.reserve_next_conversation_id()
909
992
  runtime.active_conversation_id = next_id
910
993
  runtime.chat_session.clear_conversation_history()
911
994
 
912
- logger.info(f"Ready for new conversation {runtime.active_conversation_id} for session {user_id}")
995
+ logger.info(f"Ready for new conversation {runtime.active_conversation_id} for session {channel_id}")
913
996
  return {"status": "ok"}
914
997
  else:
915
998
  # No turns to save, just clear history and start fresh
916
999
  runtime.chat_session.clear_conversation_history()
917
- logger.info(f"No turns to save for session {user_id}, cleared history")
1000
+ logger.info(f"No turns to save for session {channel_id}, cleared history")
918
1001
  return {"status": "ok", "message": "No turns to save"}
919
1002
 
920
1003
  except HTTPException:
921
1004
  raise
922
1005
  except Exception as e:
923
- logger.error(f"Error in new_conversation for session {user_id}: {e}")
1006
+ logger.error(f"Error in new_conversation for session {channel_id}: {e}")
924
1007
  traceback.print_exc()
925
1008
  raise HTTPException(
926
1009
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
927
- detail=f"Internal error in new_conversation() for user_id: {user_id}",
1010
+ detail=f"Internal error in new_conversation() for channel_id: {channel_id}",
928
1011
  ) from e
929
1012
 
930
1013
 
@@ -949,23 +1032,23 @@ async def list_conversations(
949
1032
 
950
1033
  Requires a valid JWT access token in the Authorization header (Bearer token format).
951
1034
  """
952
- user_id = session.user_id
1035
+ channel_id = session.channel_id
953
1036
  try:
954
- runtime = await session_manager.get_session(user_id)
1037
+ runtime = await session_manager.get_session(channel_id)
955
1038
  if not runtime:
956
1039
  raise HTTPException(
957
1040
  status_code=status.HTTP_404_NOT_FOUND,
958
- detail=f"User session not found: {user_id}"
1041
+ detail=f"User session not found: {channel_id}"
959
1042
  )
960
1043
  return runtime.conversation_store.list_conversations(limit)
961
1044
  except HTTPException:
962
1045
  raise
963
1046
  except Exception as e:
964
- logger.error(f"Error in list_conversations for session {user_id}: {e}")
1047
+ logger.error(f"Error in list_conversations for session {channel_id}: {e}")
965
1048
  traceback.print_exc()
966
1049
  raise HTTPException(
967
1050
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
968
- detail=f"Internal error in list_conversations() for user_id: {user_id}",
1051
+ detail=f"Internal error in list_conversations() for channel_id: {channel_id}",
969
1052
  ) from e
970
1053
 
971
1054
 
@@ -992,20 +1075,20 @@ async def post_feedback(
992
1075
 
993
1076
  Requires a valid JWT access token in the Authorization header (Bearer token format).
994
1077
  """
995
- user_id = session.user_id
1078
+ channel_id = session.channel_id
996
1079
  try:
997
- runtime = await session_manager.get_session(user_id)
1080
+ runtime = await session_manager.get_session(channel_id)
998
1081
  if not runtime:
999
1082
  raise HTTPException(
1000
1083
  status_code=status.HTTP_404_NOT_FOUND,
1001
- detail=f"User session not found: {user_id}"
1084
+ detail=f"User session not found: {channel_id}"
1002
1085
  )
1003
1086
 
1004
1087
  # Check if there are any in-memory turns to give feedback on
1005
1088
  if not runtime.chat_session.conversation_history.messages:
1006
1089
  raise HTTPException(
1007
1090
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1008
- detail=f"No turns available to give feedback on for user: {user_id}"
1091
+ detail=f"No turns available to give feedback on for user: {channel_id}"
1009
1092
  )
1010
1093
 
1011
1094
  # Update feedback on the last turn in the in-memory conversation history
@@ -1019,17 +1102,17 @@ async def post_feedback(
1019
1102
  # Incrementally save the updated turns with feedback
1020
1103
  save_conversation_incremental(runtime, extract_turns_from_history, logger)
1021
1104
 
1022
- logger.info(f"Added feedback to latest turn for session {user_id}")
1105
+ logger.info(f"Added feedback to latest turn for session {channel_id}")
1023
1106
  return {"status": "ok"}
1024
1107
 
1025
1108
  except HTTPException:
1026
1109
  raise
1027
1110
  except Exception as e:
1028
- logger.error(f"Error in post_feedback for session {user_id}: {e}")
1111
+ logger.error(f"Error in post_feedback for session {channel_id}: {e}")
1029
1112
  traceback.print_exc()
1030
1113
  raise HTTPException(
1031
1114
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1032
- detail=f"Internal error in post_feedback() for user_id: {user_id}",
1115
+ detail=f"Internal error in post_feedback() for channel_id: {channel_id}",
1033
1116
  ) from e
1034
1117
 
1035
1118
 
@@ -1052,13 +1135,13 @@ async def activate_conversation(
1052
1135
 
1053
1136
  Requires a valid JWT access token in the Authorization header (Bearer token format).
1054
1137
  """
1055
- user_id = session.user_id
1138
+ channel_id = session.channel_id
1056
1139
  try:
1057
- runtime = await session_manager.get_session(user_id)
1140
+ runtime = await session_manager.get_session(channel_id)
1058
1141
  if not runtime:
1059
1142
  raise HTTPException(
1060
1143
  status_code=status.HTTP_404_NOT_FOUND,
1061
- detail=f"User session not found: {user_id}"
1144
+ detail=f"User session not found: {channel_id}"
1062
1145
  )
1063
1146
 
1064
1147
  # Get conversation by ID
@@ -1074,18 +1157,18 @@ async def activate_conversation(
1074
1157
  # Restore conversation history to chat_session
1075
1158
  restored_history = restore_history_from_turns(conv["turns"])
1076
1159
  runtime.chat_session._conversation_history = restored_history
1077
- logger.info(f"Activated conversation {request.conversation_id} for session {user_id}")
1160
+ logger.info(f"Activated conversation {request.conversation_id} for session {channel_id}")
1078
1161
 
1079
1162
  return {"status": "ok"}
1080
1163
 
1081
1164
  except HTTPException:
1082
1165
  raise
1083
1166
  except Exception as e:
1084
- logger.error(f"Error in activate_conversation for session {user_id}: {e}")
1167
+ logger.error(f"Error in activate_conversation for session {channel_id}: {e}")
1085
1168
  traceback.print_exc()
1086
1169
  raise HTTPException(
1087
1170
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1088
- detail=f"Internal error in activate_conversation() for user_id: {user_id}",
1171
+ detail=f"Internal error in activate_conversation() for channel_id: {channel_id}",
1089
1172
  ) from e
1090
1173
 
1091
1174
 
@@ -1108,8 +1191,8 @@ async def dump_all_conversations(request: DumpConversationsRequest) -> dict[str,
1108
1191
  timestamp = int(time.time())
1109
1192
  output_file = os.path.join(request.output_folder, f"all_conversations_{timestamp}.jsonl")
1110
1193
 
1111
- # Resolve base folder using SPEEDDICT_FOLDERNAME/user_conversations
1112
- base_folder = get_userconversations_dir()
1194
+ # Resolve base folder using SPEEDDICT_FOLDERNAME/channel_conversations
1195
+ base_folder = get_channelconversations_dir()
1113
1196
 
1114
1197
  all_conversations = []
1115
1198
  session_count = 0
@@ -1118,11 +1201,11 @@ async def dump_all_conversations(request: DumpConversationsRequest) -> dict[str,
1118
1201
  if os.path.isdir(base_folder):
1119
1202
  for filename in os.listdir(base_folder):
1120
1203
  if filename.endswith('.rdb'):
1121
- # Extract user_id from filename (format: <user_id>.rdb)
1122
- user_id = filename[:-4] # Remove .rdb extension
1204
+ # Extract channel_id from filename (format: <channel_id>.rdb)
1205
+ channel_id = filename[:-4] # Remove .rdb extension
1123
1206
 
1124
1207
  # Create temporary ConversationStore for this user
1125
- store = ConversationStore(user_id, base_folder)
1208
+ store = ConversationStore(channel_id, base_folder)
1126
1209
  user_convs = store.get_all_conversations_for_dump()
1127
1210
  all_conversations.extend(user_convs)
1128
1211
  session_count += 1
@@ -1162,7 +1245,7 @@ async def generate_mcp_token(request: GenerateMCPTokenRequest) -> TokenResponse:
1162
1245
  and have extended expiration times (default 365 days) since they can't be easily refreshed.
1163
1246
 
1164
1247
  Args:
1165
- user_id: Identifier for the MCP user/client
1248
+ channel_id: Identifier for the MCP user/client
1166
1249
  expires_days: Token expiration in days (default: 365 days / 1 year)
1167
1250
 
1168
1251
  Returns:
@@ -1171,10 +1254,10 @@ async def generate_mcp_token(request: GenerateMCPTokenRequest) -> TokenResponse:
1171
1254
  Note: This endpoint should be restricted to administrators only in production.
1172
1255
  """
1173
1256
  try:
1174
- # Generate long-lived access token
1175
- access_token = create_access_token(request.user_id, expires_days=request.expires_days)
1257
+ # Generate long-lived access token with optional user_id
1258
+ access_token = create_access_token(request.channel_id, user_id=request.user_id, expires_days=request.expires_days)
1176
1259
 
1177
- logger.info(f"Generated MCP token for user_id: {request.user_id}, expires in {request.expires_days} days")
1260
+ logger.info(f"Generated MCP token for channel_id: {request.channel_id}, user_id: {request.user_id}, expires in {request.expires_days} days")
1178
1261
 
1179
1262
  return TokenResponse(
1180
1263
  access_token=access_token,