redhairedshanks1 commited on
Commit
ea1e68e
Β·
1 Parent(s): 307eb51

sessions history, chat/unified/stream

Browse files
Files changed (1) hide show
  1. api_routes_v2.py +266 -264
api_routes_v2.py CHANGED
@@ -505,6 +505,26 @@ def _normalize_history_for_api(chat_id: str) -> List[Message]:
505
  return history
506
 
507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  def _assistant_response_payload(
509
  chat_id: str,
510
  friendly_response: str,
@@ -513,7 +533,7 @@ def _assistant_response_payload(
513
  state: str
514
  ) -> ChatResponse:
515
  # Persist assistant message
516
- session_manager.add_message(chat_id, "assistant", friendly_response)
517
  # Return full history in role/content shape
518
  history = _normalize_history_for_api(chat_id)
519
  return ChatResponse(
@@ -610,7 +630,7 @@ def upload_stream_to_s3(chat_id: str, file: UploadFile) -> str:
610
 
611
  s3_uri = f"s3://{S3_BUCKET}/{key}"
612
  session_manager.update_session(chat_id, {"current_file": s3_uri, "state": "initial"})
613
- session_manager.add_message(chat_id, "system", f"File uploaded to S3: {s3_uri}")
614
  return s3_uri
615
 
616
 
@@ -651,7 +671,7 @@ async def chat_unified(
651
  # If JSON included a file_path (e.g., s3://...), attach it
652
  if file_path_from_json:
653
  session_manager.update_session(chat_id, {"current_file": file_path_from_json})
654
- session_manager.add_message(chat_id, "system", f"File attached: {file_path_from_json}")
655
  session = _get_session_or_init(chat_id)
656
 
657
  # If a file is included in the form, upload to S3 and attach it
@@ -674,7 +694,7 @@ async def chat_unified(
674
  return _assistant_response_payload(chat_id, friendly, {"intent": "missing_message"}, api_data, session.get("state", "initial"))
675
 
676
  # Add user message
677
- session_manager.add_message(chat_id, "user", message)
678
 
679
  # Classify intent
680
  intent_data = intent_classifier.classify_intent(message)
@@ -907,7 +927,7 @@ async def chat_unified(
907
  # ========================
908
 
909
  @router.post("/chat/unified/stream")
910
- def chat_unified_stream(
911
  request: Request,
912
  chat_id: Optional[str] = Form(None),
913
  message: Optional[str] = Form(None),
@@ -921,277 +941,258 @@ def chat_unified_stream(
921
  - On approval, streams execution progress and final result.
922
  """
923
 
924
- async def prepare():
925
- # Parse JSON if needed
926
- content_type = (request.headers.get("content-type") or "").lower()
927
- file_path_from_json = None
928
- _chat_id, _message, _prefer_bedrock, _file = chat_id, message, prefer_bedrock, file
929
- if "application/json" in content_type:
930
- body = await request.json()
931
- _chat_id = body.get("chat_id") or _chat_id
932
- _message = body.get("message") if "message" in body else _message
933
- _prefer_bedrock = body.get("prefer_bedrock", True) if "prefer_bedrock" in body else _prefer_bedrock
934
- file_path_from_json = body.get("file_path")
935
-
936
- _chat_id = _ensure_chat(_chat_id)
937
- _session = _get_session_or_init(_chat_id)
938
-
939
- # Attach JSON file path if provided
940
- if file_path_from_json:
941
- session_manager.update_session(_chat_id, {"current_file": file_path_from_json})
942
- session_manager.add_message(_chat_id, "system", f"File attached: {file_path_from_json}")
943
- _session = _get_session_or_init(_chat_id)
944
-
945
- # Upload file if provided (form)
946
- uploaded_file_info = None
947
- if _file is not None:
948
- s3_uri = upload_stream_to_s3(_chat_id, _file)
949
- uploaded_file_info = {"bucket": S3_BUCKET, "key": s3_uri.split(f"s3://{S3_BUCKET}/", 1)[1], "s3_uri": s3_uri}
950
- _session = _get_session_or_init(_chat_id)
951
-
952
- return _chat_id, _message, _prefer_bedrock, uploaded_file_info
953
-
954
- def make_stream(chat_id_local: str, msg: Optional[str], prefer_bedrock_local: bool, uploaded_file_info: Optional[Dict[str, Any]]):
955
- def emit(obj: Dict[str, Any]):
956
- obj.setdefault("chat_id", chat_id_local)
957
- obj.setdefault("state", session_manager.get_session(chat_id_local).get("state", "initial"))
958
- line = json.dumps(obj, ensure_ascii=False).encode("utf-8") + b"\n"
959
- return line
960
 
961
- def gen() -> Generator[bytes, None, None]:
962
- session = _get_session_or_init(chat_id_local)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
963
 
964
- # If only a file was uploaded and no message, acknowledge
965
- if (msg is None or str(msg).strip() == "") and uploaded_file_info:
966
- friendly = "πŸ“ File uploaded successfully. Tell me what you'd like to do with it (e.g., extract text, get tables, summarize)."
967
- session_manager.add_message(chat_id_local, "assistant", friendly)
968
- yield emit({"type": "assistant_final", "content": friendly, "file": uploaded_file_info, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
 
 
 
 
 
969
  return
970
 
971
- # If no message at all, nudge
972
- if msg is None or str(msg).strip() == "":
973
- friendly = "Please provide a message (e.g., 'extract text', 'get tables', 'summarize')."
974
- session_manager.add_message(chat_id_local, "assistant", friendly)
975
- yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
976
  return
977
 
978
- # Add user message
979
- session_manager.add_message(chat_id_local, "user", msg)
 
 
 
 
 
 
980
 
981
- # Classify
982
- intent_data = intent_classifier.classify_intent(msg)
983
- current_state = session.get("state", "initial")
984
 
985
- # Casual / question / unclear at initial
986
- if intent_data["intent"] in {"casual_chat", "question", "unclear"} and current_state == "initial":
987
- friendly = intent_classifier.get_friendly_response(intent_data["intent"], msg)
988
- session_manager.add_message(chat_id_local, "assistant", friendly)
989
- yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
 
 
 
 
 
 
 
 
 
 
990
  return
991
 
992
- # Initial: pipeline request or nudge
993
- if current_state == "initial":
994
- if not intent_data.get("requires_pipeline", False):
995
- friendly = (
996
- "I'm here to help process documents! Please tell me what you'd like to do with your document.\n\n"
997
- "For example:\n- 'extract text and summarize'\n- 'get tables from pages 2-5'\n- 'translate to Spanish'\n\n"
998
- "Type 'help' to see all capabilities!"
999
- )
1000
- session_manager.add_message(chat_id_local, "assistant", friendly)
1001
- yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1002
- return
1003
 
1004
- if not session.get("current_file"):
1005
- friendly = "πŸ“ Please upload a document first before I can process it!\n\nClick 'Upload Document' to get started."
1006
- session_manager.add_message(chat_id_local, "assistant", friendly)
1007
- yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1008
- return
1009
 
1010
- # Generate pipeline (no need to download file)
1011
- yield emit({"type": "status", "message": "Analyzing request and creating a pipeline..."})
1012
  try:
1013
- pipeline = generate_pipeline(
1014
- user_input=msg,
1015
- file_path=session.get("current_file"),
1016
- prefer_bedrock=bool(prefer_bedrock_local)
1017
- )
1018
- session_manager.update_session(chat_id_local, {"proposed_pipeline": pipeline, "state": "pipeline_proposed"})
1019
-
1020
- pipeline_name = pipeline.get("pipeline_name", "Document Processing")
1021
- steps_list = pipeline.get("pipeline_steps", [])
1022
- steps_summary = "\n".join([f" {i+1}. {step.get('tool', 'Unknown')}" for i, step in enumerate(steps_list)])
1023
-
1024
- friendly = (
1025
- f"🎯 **Pipeline Created: {pipeline_name}**\n"
1026
- f"Here's what I'll do:\n{steps_summary}\n"
1027
- f"**Ready to proceed?**\n"
1028
- f"- Type 'approve' or 'yes' to execute\n"
1029
- f"- Type 'reject' or 'no' to cancel\n"
1030
- f"- Describe changes to modify the plan"
1031
- )
1032
 
1033
- session_manager.add_message(chat_id_local, "assistant", friendly)
1034
- yield emit({"type": "assistant_final", "content": friendly, "pipeline": pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1035
- return
1036
- except Exception as e:
1037
- friendly = f"❌ Error generating pipeline: {str(e)}"
1038
- session_manager.add_message(chat_id_local, "assistant", friendly)
1039
- yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1040
- return
1041
 
1042
- # Pipeline proposed: handle approval, rejection, or edit
1043
- if current_state == "pipeline_proposed":
1044
- if intent_data["intent"] == "approval":
1045
- session_manager.update_session(chat_id_local, {"state": "executing"})
1046
- plan = session.get("proposed_pipeline", {})
1047
- initial = (
1048
- f"βœ… Approved! Starting execution of: **{plan.get('pipeline_name', 'pipeline')}**\n\n"
1049
- f"πŸš€ Processing, please wait...\n_(Using {plan.get('_generator', 'AI')} - {plan.get('_model', 'model')})_"
1050
- )
1051
- yield emit({"type": "assistant_delta", "content": initial})
 
 
 
 
 
 
 
 
 
 
 
1052
 
1053
- steps_completed = []
1054
- final_payload = None
1055
- executor_used = "unknown"
1056
- accumulated = initial
1057
 
1058
- file_ref = session.get("current_file")
1059
- local_path, cleanup = download_to_temp_file(file_ref)
 
1060
 
1061
- try:
1062
- for event in execute_pipeline_streaming(
1063
- pipeline=plan,
1064
- file_path=local_path,
1065
- session_id=chat_id_local,
1066
- prefer_bedrock=bool(prefer_bedrock_local)
1067
- ):
1068
- etype = event.get("type")
1069
-
1070
- if etype == "info":
1071
- msg2 = f"ℹ️ {event.get('message')} _(Executor: {event.get('executor', 'unknown')})_"
1072
- accumulated += "\n\n" + msg2
1073
- yield emit({"type": "assistant_delta", "content": accumulated})
1074
-
1075
- elif etype == "step":
1076
- step_num = event.get("step", 0)
1077
- tool_name = event.get("tool", "processing")
1078
- status = event.get("status", "running")
1079
- if status == "completed" and "observation" in event:
1080
- obs_preview = str(event.get("observation"))[:80]
1081
- step_msg = f"βœ… Step {step_num}: {tool_name} - Completed!\n Preview: {obs_preview}..."
1082
- elif status == "executing":
1083
- step_msg = f"⏳ Step {step_num}: {tool_name} - Processing..."
1084
- else:
1085
- step_msg = f"πŸ“ Step {step_num}: {tool_name}"
1086
-
1087
- steps_completed.append({
1088
- "step": step_num,
1089
- "tool": tool_name,
1090
- "status": status,
1091
- "executor": event.get("executor", "unknown"),
1092
- "observation": event.get("observation"),
1093
- "input": event.get("input"),
1094
- })
1095
- executor_used = event.get("executor", executor_used)
1096
-
1097
- accumulated += "\n\n" + step_msg
1098
- yield emit({"type": "assistant_delta", "content": accumulated})
1099
-
1100
- elif etype == "final":
1101
- final_payload = event.get("data")
1102
- executor_used = event.get("executor", executor_used)
1103
-
1104
- elif etype == "error":
1105
- err = event.get("error", "Unknown error")
1106
- friendly_err = f"❌ Pipeline Failed\n\nError: {err}\n\nCompleted {len(steps_completed)} step(s) before failure."
1107
- session_manager.update_session(chat_id_local, {"state": "initial"})
1108
- session_manager.add_message(chat_id_local, "assistant", friendly_err)
1109
- yield emit({"type": "assistant_final", "content": friendly_err, "error": err, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1110
- return
1111
-
1112
- # Finalize
1113
- if final_payload:
1114
- session_manager.update_session(chat_id_local, {"pipeline_result": final_payload, "state": "initial"})
1115
- session_manager.save_pipeline_execution(
1116
- session_id=chat_id_local,
1117
- pipeline=plan,
1118
- result=final_payload,
1119
- file_path=file_ref,
1120
- executor=executor_used
1121
- )
1122
- success_count = len([s for s in steps_completed if s.get("status") == "completed"])
1123
- friendly_final = (
1124
- f"πŸŽ‰ Pipeline Completed Successfully!\n"
1125
- f"- Pipeline: {plan.get('pipeline_name', 'Document Processing')}\n"
1126
- f"- Total Steps: {len(steps_completed)}\n"
1127
- f"- Successful: {success_count}\n"
1128
- f"- Executor: {executor_used}\n"
1129
- f"βœ… All done! What else would you like me to help you with?"
1130
- )
1131
- session_manager.add_message(chat_id_local, "assistant", friendly_final)
1132
- yield emit({"type": "assistant_final", "content": friendly_final, "result": final_payload, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1133
- return
1134
- else:
1135
- done = f"βœ… Pipeline Completed! Executed {len(steps_completed)} steps using {executor_used}."
1136
- session_manager.update_session(chat_id_local, {"state": "initial"})
1137
- session_manager.add_message(chat_id_local, "assistant", done)
1138
- yield emit({"type": "assistant_final", "content": done, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1139
  return
1140
 
1141
- except Exception as e:
1142
- friendly_err = f"❌ Pipeline Execution Failed\n\nError: {str(e)}\n\nCompleted {len(steps_completed)} step(s) before failure."
1143
- session_manager.update_session(chat_id_local, {"state": "initial"})
1144
- session_manager.add_message(chat_id_local, "assistant", friendly_err)
1145
- yield emit({"type": "assistant_final", "content": friendly_err, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1146
- return
1147
- finally:
1148
- try:
1149
- cleanup()
1150
- except Exception:
1151
- pass
1152
-
1153
- elif intent_data["intent"] == "rejection":
1154
- session_manager.update_session(chat_id_local, {"state": "initial", "proposed_pipeline": None})
1155
- friendly = "πŸ‘ No problem! Pipeline cancelled. What else would you like me to help you with?"
1156
- session_manager.add_message(chat_id_local, "assistant", friendly)
1157
- yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1158
- return
1159
-
1160
- else:
1161
- # Treat as edit/modify
1162
- try:
1163
- original_plan = session.get("proposed_pipeline", {})
1164
- edit_context = f"Original: {original_plan.get('pipeline_name')}. User wants: {msg}"
1165
- new_pipeline = generate_pipeline(
1166
- user_input=edit_context,
1167
- file_path=session.get("current_file"),
1168
- prefer_bedrock=bool(prefer_bedrock_local)
1169
  )
1170
- session_manager.update_session(chat_id_local, {"proposed_pipeline": new_pipeline, "state": "pipeline_proposed"})
1171
- formatted = format_pipeline_for_display(new_pipeline)
1172
- friendly = formatted + f"\n\n```json\n{json.dumps(new_pipeline, indent=2)}\n```"
1173
- session_manager.add_message(chat_id_local, "assistant", friendly)
1174
- yield emit({"type": "assistant_final", "content": friendly, "pipeline": new_pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
1175
  return
1176
- except Exception as e:
1177
- friendly = f"❌ Edit failed: {str(e)}"
1178
- session_manager.add_message(chat_id_local, "assistant", friendly)
1179
- yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
 
1180
  return
1181
 
1182
- # Default
1183
- friendly = "Please upload a document and tell me what you'd like me to do (e.g., extract text, summarize, translate)."
1184
- session_manager.add_message(chat_id_local, "assistant", friendly)
1185
- yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id_local)]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1186
 
1187
- return gen()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
 
1189
- async def stream_wrapper():
1190
- chat_id_local, msg_local, prefer_local, uploaded_info = await prepare()
1191
- return StreamingResponse(make_stream(chat_id_local, msg_local, bool(prefer_local), uploaded_info), media_type="application/x-ndjson")
 
1192
 
1193
- # Note: for FastAPI sync path, we return the awaitable response
1194
- return stream_wrapper()
1195
 
1196
 
1197
  # ========================
@@ -1211,7 +1212,7 @@ async def smart_chat(request: ChatRequest):
1211
  session_manager.update_session(chat_id, {"current_file": request.file_path})
1212
  session = _get_session_or_init(chat_id)
1213
 
1214
- session_manager.add_message(chat_id, "user", request.message)
1215
 
1216
  intent_data = intent_classifier.classify_intent(request.message)
1217
  current_state = session.get("state", "initial")
@@ -1422,7 +1423,7 @@ def smart_chat_stream(request: ChatRequest):
1422
  session_manager.update_session(chat_id, {"current_file": request.file_path})
1423
  session = _get_session_or_init(chat_id)
1424
 
1425
- session_manager.add_message(chat_id, "user", request.message)
1426
 
1427
  intent_data = intent_classifier.classify_intent(request.message)
1428
  current_state = session.get("state", "initial")
@@ -1435,7 +1436,7 @@ def smart_chat_stream(request: ChatRequest):
1435
 
1436
  if intent_data["intent"] in {"casual_chat", "question", "unclear"} and current_state == "initial":
1437
  friendly = intent_classifier.get_friendly_response(intent_data["intent"], request.message)
1438
- session_manager.add_message(chat_id, "assistant", friendly)
1439
  yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1440
  return
1441
 
@@ -1446,13 +1447,13 @@ def smart_chat_stream(request: ChatRequest):
1446
  "For example:\n- 'extract text and summarize'\n- 'get tables from pages 2-5'\n- 'translate to Spanish'\n\n"
1447
  "Type 'help' to see all capabilities!"
1448
  )
1449
- session_manager.add_message(chat_id, "assistant", friendly)
1450
  yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1451
  return
1452
 
1453
  if not session.get("current_file"):
1454
  friendly = "πŸ“ Please upload a document first before I can process it!\n\nClick 'Upload Document' to get started."
1455
- session_manager.add_message(chat_id, "assistant", friendly)
1456
  yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1457
  return
1458
 
@@ -1478,12 +1479,12 @@ def smart_chat_stream(request: ChatRequest):
1478
  f"- Describe changes to modify the plan"
1479
  )
1480
 
1481
- session_manager.add_message(chat_id, "assistant", friendly)
1482
  yield emit({"type": "assistant_final", "content": friendly, "pipeline": pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1483
  return
1484
  except Exception as e:
1485
  friendly = f"❌ Error generating pipeline: {str(e)}"
1486
- session_manager.add_message(chat_id, "assistant", friendly)
1487
  yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1488
  return
1489
 
@@ -1552,7 +1553,7 @@ def smart_chat_stream(request: ChatRequest):
1552
  err = event.get("error", "Unknown error")
1553
  friendly_err = f"❌ Pipeline Failed\n\nError: {err}\n\nCompleted {len(steps_completed)} step(s) before failure."
1554
  session_manager.update_session(chat_id, {"state": "initial"})
1555
- session_manager.add_message(chat_id, "assistant", friendly_err)
1556
  yield emit({"type": "assistant_final", "content": friendly_err, "error": err, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1557
  return
1558
 
@@ -1574,20 +1575,20 @@ def smart_chat_stream(request: ChatRequest):
1574
  f"- Executor: {executor_used}\n"
1575
  f"βœ… All done! What else would you like me to help you with?"
1576
  )
1577
- session_manager.add_message(chat_id, "assistant", friendly_final)
1578
  yield emit({"type": "assistant_final", "content": friendly_final, "result": final_payload, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1579
  return
1580
  else:
1581
  done = f"βœ… Pipeline Completed! Executed {len(steps_completed)} steps using {executor_used}."
1582
  session_manager.update_session(chat_id, {"state": "initial"})
1583
- session_manager.add_message(chat_id, "assistant", done)
1584
  yield emit({"type": "assistant_final", "content": done, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1585
  return
1586
 
1587
  except Exception as e:
1588
  friendly_err = f"❌ Pipeline Execution Failed\n\nError: {str(e)}\n\nCompleted {len(steps_completed)} step(s) before failure."
1589
  session_manager.update_session(chat_id, {"state": "initial"})
1590
- session_manager.add_message(chat_id, "assistant", friendly_err)
1591
  yield emit({"type": "assistant_final", "content": friendly_err, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1592
  return
1593
  finally:
@@ -1599,7 +1600,7 @@ def smart_chat_stream(request: ChatRequest):
1599
  elif intent_data["intent"] == "rejection":
1600
  session_manager.update_session(chat_id, {"state": "initial", "proposed_pipeline": None})
1601
  friendly = "πŸ‘ No problem! Pipeline cancelled. What else would you like me to help you with?"
1602
- session_manager.add_message(chat_id, "assistant", friendly)
1603
  yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1604
  return
1605
 
@@ -1615,17 +1616,17 @@ def smart_chat_stream(request: ChatRequest):
1615
  session_manager.update_session(chat_id, {"proposed_pipeline": new_pipeline, "state": "pipeline_proposed"})
1616
  formatted = format_pipeline_for_display(new_pipeline)
1617
  friendly = formatted + f"\n\n```json\n{json.dumps(new_pipeline, indent=2)}\n```"
1618
- session_manager.add_message(chat_id, "assistant", friendly)
1619
  yield emit({"type": "assistant_final", "content": friendly, "pipeline": new_pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1620
  return
1621
  except Exception as e:
1622
  friendly = f"❌ Edit failed: {str(e)}"
1623
- session_manager.add_message(chat_id, "assistant", friendly)
1624
  yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1625
  return
1626
 
1627
  friendly = "Please upload a document and tell me what you'd like me to do (e.g., extract text, summarize, translate)."
1628
- session_manager.add_message(chat_id, "assistant", friendly)
1629
  yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1630
 
1631
  return StreamingResponse(gen(), media_type="application/x-ndjson")
@@ -1799,6 +1800,7 @@ def download_chat_file(chat_id: str):
1799
  yield chunk
1800
 
1801
  media_type = obj.get("ContentType", "application/octet-stream")
 
1802
  return StreamingResponse(stream(), media_type=media_type, headers={
1803
  "Content-Disposition": f'attachment; filename="{os.path.basename(key)}"'
1804
  })
 
505
  return history
506
 
507
 
508
+ def _add_and_mirror_message(chat_id: str, role: str, content: str):
509
+ """
510
+ Add a message via session_manager AND mirror it into the session doc
511
+ so history is always available in responses.
512
+ """
513
+ session_manager.add_message(chat_id, role, content)
514
+ try:
515
+ s = session_manager.get_session(chat_id) or {}
516
+ msgs = list(s.get("messages", []))
517
+ msgs.append({
518
+ "role": role,
519
+ "content": content if isinstance(content, str) else json.dumps(content, ensure_ascii=False),
520
+ "timestamp": datetime.utcnow().isoformat() + "Z",
521
+ })
522
+ session_manager.update_session(chat_id, {"messages": msgs})
523
+ except Exception:
524
+ # Don't block the flow on mirror issues
525
+ pass
526
+
527
+
528
  def _assistant_response_payload(
529
  chat_id: str,
530
  friendly_response: str,
 
533
  state: str
534
  ) -> ChatResponse:
535
  # Persist assistant message
536
+ _add_and_mirror_message(chat_id, "assistant", friendly_response)
537
  # Return full history in role/content shape
538
  history = _normalize_history_for_api(chat_id)
539
  return ChatResponse(
 
630
 
631
  s3_uri = f"s3://{S3_BUCKET}/{key}"
632
  session_manager.update_session(chat_id, {"current_file": s3_uri, "state": "initial"})
633
+ _add_and_mirror_message(chat_id, "system", f"File uploaded to S3: {s3_uri}")
634
  return s3_uri
635
 
636
 
 
671
  # If JSON included a file_path (e.g., s3://...), attach it
672
  if file_path_from_json:
673
  session_manager.update_session(chat_id, {"current_file": file_path_from_json})
674
+ _add_and_mirror_message(chat_id, "system", f"File attached: {file_path_from_json}")
675
  session = _get_session_or_init(chat_id)
676
 
677
  # If a file is included in the form, upload to S3 and attach it
 
694
  return _assistant_response_payload(chat_id, friendly, {"intent": "missing_message"}, api_data, session.get("state", "initial"))
695
 
696
  # Add user message
697
+ _add_and_mirror_message(chat_id, "user", message)
698
 
699
  # Classify intent
700
  intent_data = intent_classifier.classify_intent(message)
 
927
  # ========================
928
 
929
  @router.post("/chat/unified/stream")
930
+ async def chat_unified_stream(
931
  request: Request,
932
  chat_id: Optional[str] = Form(None),
933
  message: Optional[str] = Form(None),
 
941
  - On approval, streams execution progress and final result.
942
  """
943
 
944
+ # Parse JSON if needed
945
+ content_type = (request.headers.get("content-type") or "").lower()
946
+ file_path_from_json = None
947
+ if "application/json" in content_type:
948
+ body = await request.json()
949
+ chat_id = body.get("chat_id") or chat_id
950
+ message = body.get("message") if "message" in body else message
951
+ prefer_bedrock = body.get("prefer_bedrock", True) if "prefer_bedrock" in body else prefer_bedrock
952
+ file_path_from_json = body.get("file_path")
953
+
954
+ chat_id = _ensure_chat(chat_id)
955
+ session = _get_session_or_init(chat_id)
956
+
957
+ # Attach JSON file path if provided
958
+ if file_path_from_json:
959
+ session_manager.update_session(chat_id, {"current_file": file_path_from_json})
960
+ _add_and_mirror_message(chat_id, "system", f"File attached: {file_path_from_json}")
961
+ session = _get_session_or_init(chat_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
962
 
963
+ # Upload file if provided
964
+ uploaded_file_info = None
965
+ if file is not None:
966
+ s3_uri = upload_stream_to_s3(chat_id, file)
967
+ uploaded_file_info = {"bucket": S3_BUCKET, "key": s3_uri.split(f"s3://{S3_BUCKET}/", 1)[1], "s3_uri": s3_uri}
968
+ session = _get_session_or_init(chat_id)
969
+
970
+ def emit(obj: Dict[str, Any]) -> bytes:
971
+ obj.setdefault("chat_id", chat_id)
972
+ obj.setdefault("state", session_manager.get_session(chat_id).get("state", "initial"))
973
+ return (json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8")
974
+
975
+ def stream_gen() -> Generator[bytes, None, None]:
976
+ session_local = _get_session_or_init(chat_id)
977
+
978
+ # Only-file case
979
+ if (message is None or str(message).strip() == "") and uploaded_file_info:
980
+ friendly = "πŸ“ File uploaded successfully. Tell me what you'd like to do with it (e.g., extract text, get tables, summarize)."
981
+ _add_and_mirror_message(chat_id, "assistant", friendly)
982
+ yield emit({"type": "assistant_final", "content": friendly, "file": uploaded_file_info, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
983
+ return
984
+
985
+ # No message
986
+ if message is None or str(message).strip() == "":
987
+ friendly = "Please provide a message (e.g., 'extract text', 'get tables', 'summarize')."
988
+ _add_and_mirror_message(chat_id, "assistant", friendly)
989
+ yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
990
+ return
991
+
992
+ # Add user message
993
+ _add_and_mirror_message(chat_id, "user", message)
994
+
995
+ # Classify
996
+ intent_data = intent_classifier.classify_intent(message)
997
+ current_state = session_local.get("state", "initial")
998
+
999
+ # Casual / question / unclear
1000
+ if intent_data["intent"] in {"casual_chat", "question", "unclear"} and current_state == "initial":
1001
+ friendly = intent_classifier.get_friendly_response(intent_data["intent"], message)
1002
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1003
+ yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1004
+ return
1005
 
1006
+ # Initial: nudge or generate plan
1007
+ if current_state == "initial":
1008
+ if not intent_data.get("requires_pipeline", False):
1009
+ friendly = (
1010
+ "I'm here to help process documents! Please tell me what you'd like to do with your document.\n\n"
1011
+ "For example:\n- 'extract text and summarize'\n- 'get tables from pages 2-5'\n- 'translate to Spanish'\n\n"
1012
+ "Type 'help' to see all capabilities!"
1013
+ )
1014
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1015
+ yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1016
  return
1017
 
1018
+ if not session_local.get("current_file"):
1019
+ friendly = "πŸ“ Please upload a document first before I can process it!"
1020
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1021
+ yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
 
1022
  return
1023
 
1024
+ yield emit({"type": "status", "message": "Analyzing request and creating a pipeline..."})
1025
+ try:
1026
+ pipeline = generate_pipeline(
1027
+ user_input=message,
1028
+ file_path=session_local.get("current_file"),
1029
+ prefer_bedrock=bool(prefer_bedrock),
1030
+ )
1031
+ session_manager.update_session(chat_id, {"proposed_pipeline": pipeline, "state": "pipeline_proposed"})
1032
 
1033
+ pipeline_name = pipeline.get("pipeline_name", "Document Processing")
1034
+ steps_list = pipeline.get("pipeline_steps", [])
1035
+ steps_summary = "\n".join([f" {i+1}. {step.get('tool', 'Unknown')}" for i, step in enumerate(steps_list)])
1036
 
1037
+ friendly = (
1038
+ f"🎯 **Pipeline Created: {pipeline_name}**\n"
1039
+ f"Here's what I'll do:\n{steps_summary}\n"
1040
+ f"**Ready to proceed?**\n"
1041
+ f"- Type 'approve' or 'yes' to execute\n"
1042
+ f"- Type 'reject' or 'no' to cancel\n"
1043
+ f"- Describe changes to modify the plan"
1044
+ )
1045
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1046
+ yield emit({"type": "assistant_final", "content": friendly, "pipeline": pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1047
+ return
1048
+ except Exception as e:
1049
+ friendly = f"❌ Error generating pipeline: {str(e)}"
1050
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1051
+ yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1052
  return
1053
 
1054
+ # Pipeline proposed
1055
+ if current_state == "pipeline_proposed":
1056
+ if intent_data["intent"] == "approval":
1057
+ session_manager.update_session(chat_id, {"state": "executing"})
1058
+ plan = session_local.get("proposed_pipeline", {})
1059
+ initial = (
1060
+ f"βœ… Approved! Starting execution of: **{plan.get('pipeline_name', 'pipeline')}**\n\n"
1061
+ f"πŸš€ Processing, please wait...\n_(Using {plan.get('_generator', 'AI')} - {plan.get('_model', 'model')})_"
1062
+ )
1063
+ yield emit({"type": "assistant_delta", "content": initial})
 
1064
 
1065
+ steps_completed, final_payload, executor_used = [], None, "unknown"
1066
+ accumulated = initial
1067
+ file_ref = session_local.get("current_file")
1068
+ local_path, cleanup = download_to_temp_file(file_ref)
 
1069
 
 
 
1070
  try:
1071
+ for event in execute_pipeline_streaming(
1072
+ pipeline=plan,
1073
+ file_path=local_path,
1074
+ session_id=chat_id,
1075
+ prefer_bedrock=bool(prefer_bedrock)
1076
+ ):
1077
+ etype = event.get("type")
 
 
 
 
 
 
 
 
 
 
 
 
1078
 
1079
+ if etype == "info":
1080
+ msg2 = f"ℹ️ {event.get('message')} _(Executor: {event.get('executor', 'unknown')})_"
1081
+ accumulated += "\n\n" + msg2
1082
+ yield emit({"type": "assistant_delta", "content": accumulated})
 
 
 
 
1083
 
1084
+ elif etype == "step":
1085
+ step_num = event.get("step", 0)
1086
+ tool_name = event.get("tool", "processing")
1087
+ status = event.get("status", "running")
1088
+ if status == "completed" and "observation" in event:
1089
+ obs_preview = str(event.get("observation"))[:80]
1090
+ step_msg = f"βœ… Step {step_num}: {tool_name} - Completed!\n Preview: {obs_preview}..."
1091
+ elif status == "executing":
1092
+ step_msg = f"⏳ Step {step_num}: {tool_name} - Processing..."
1093
+ else:
1094
+ step_msg = f"πŸ“ Step {step_num}: {tool_name}"
1095
+
1096
+ steps_completed.append({
1097
+ "step": step_num,
1098
+ "tool": tool_name,
1099
+ "status": status,
1100
+ "executor": event.get("executor", "unknown"),
1101
+ "observation": event.get("observation"),
1102
+ "input": event.get("input"),
1103
+ })
1104
+ executor_used = event.get("executor", executor_used)
1105
 
1106
+ accumulated += "\n\n" + step_msg
1107
+ yield emit({"type": "assistant_delta", "content": accumulated})
 
 
1108
 
1109
+ elif etype == "final":
1110
+ final_payload = event.get("data")
1111
+ executor_used = event.get("executor", executor_used)
1112
 
1113
+ elif etype == "error":
1114
+ err = event.get("error", "Unknown error")
1115
+ friendly_err = f"❌ Pipeline Failed\n\nError: {err}\n\nCompleted {len(steps_completed)} step(s) before failure."
1116
+ session_manager.update_session(chat_id, {"state": "initial"})
1117
+ _add_and_mirror_message(chat_id, "assistant", friendly_err)
1118
+ yield emit({"type": "assistant_final", "content": friendly_err, "error": err, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1119
  return
1120
 
1121
+ if final_payload:
1122
+ session_manager.update_session(chat_id, {"pipeline_result": final_payload, "state": "initial"})
1123
+ session_manager.save_pipeline_execution(
1124
+ session_id=chat_id,
1125
+ pipeline=plan,
1126
+ result=final_payload,
1127
+ file_path=file_ref,
1128
+ executor=executor_used
1129
+ )
1130
+ success_count = len([s for s in steps_completed if s.get("status") == "completed"])
1131
+ friendly_final = (
1132
+ f"πŸŽ‰ Pipeline Completed Successfully!\n"
1133
+ f"- Pipeline: {plan.get('pipeline_name', 'Document Processing')}\n"
1134
+ f"- Total Steps: {len(steps_completed)}\n"
1135
+ f"- Successful: {success_count}\n"
1136
+ f"- Executor: {executor_used}\n"
1137
+ f"βœ… All done! What else would you like me to help you with?"
 
 
 
 
 
 
 
 
 
 
 
1138
  )
1139
+ _add_and_mirror_message(chat_id, "assistant", friendly_final)
1140
+ yield emit({"type": "assistant_final", "content": friendly_final, "result": final_payload, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
 
 
 
1141
  return
1142
+ else:
1143
+ done = f"βœ… Pipeline Completed! Executed {len(steps_completed)} steps."
1144
+ session_manager.update_session(chat_id, {"state": "initial"})
1145
+ _add_and_mirror_message(chat_id, "assistant", done)
1146
+ yield emit({"type": "assistant_final", "content": done, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1147
  return
1148
 
1149
+ except Exception as e:
1150
+ friendly_err = f"❌ Pipeline Execution Failed\n\nError: {str(e)}"
1151
+ session_manager.update_session(chat_id, {"state": "initial"})
1152
+ _add_and_mirror_message(chat_id, "assistant", friendly_err)
1153
+ yield emit({"type": "assistant_final", "content": friendly_err, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1154
+ return
1155
+ finally:
1156
+ try:
1157
+ cleanup()
1158
+ except Exception:
1159
+ pass
1160
+
1161
+ elif intent_data["intent"] == "rejection":
1162
+ session_manager.update_session(chat_id, {"state": "initial", "proposed_pipeline": None})
1163
+ friendly = "πŸ‘ No problem! Pipeline cancelled. What else would you like me to help you with?"
1164
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1165
+ yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1166
+ return
1167
 
1168
+ else:
1169
+ try:
1170
+ original_plan = session_local.get("proposed_pipeline", {})
1171
+ edit_context = f"Original: {original_plan.get('pipeline_name')}. User wants: {message}"
1172
+ new_pipeline = generate_pipeline(
1173
+ user_input=edit_context,
1174
+ file_path=session_local.get("current_file"),
1175
+ prefer_bedrock=bool(prefer_bedrock)
1176
+ )
1177
+ session_manager.update_session(chat_id, {"proposed_pipeline": new_pipeline, "state": "pipeline_proposed"})
1178
+ formatted = format_pipeline_for_display(new_pipeline)
1179
+ friendly = formatted + f"\n\n```json\n{json.dumps(new_pipeline, indent=2)}\n```"
1180
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1181
+ yield emit({"type": "assistant_final", "content": friendly, "pipeline": new_pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1182
+ return
1183
+ except Exception as e:
1184
+ friendly = f"❌ Edit failed: {str(e)}"
1185
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1186
+ yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1187
+ return
1188
 
1189
+ # Default
1190
+ friendly = "Please upload a document and tell me what you'd like me to do (e.g., extract text, summarize, translate)."
1191
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1192
+ yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1193
 
1194
+ # Return a real StreamingResponse
1195
+ return StreamingResponse(stream_gen(), media_type="application/x-ndjson")
1196
 
1197
 
1198
  # ========================
 
1212
  session_manager.update_session(chat_id, {"current_file": request.file_path})
1213
  session = _get_session_or_init(chat_id)
1214
 
1215
+ _add_and_mirror_message(chat_id, "user", request.message)
1216
 
1217
  intent_data = intent_classifier.classify_intent(request.message)
1218
  current_state = session.get("state", "initial")
 
1423
  session_manager.update_session(chat_id, {"current_file": request.file_path})
1424
  session = _get_session_or_init(chat_id)
1425
 
1426
+ _add_and_mirror_message(chat_id, "user", request.message)
1427
 
1428
  intent_data = intent_classifier.classify_intent(request.message)
1429
  current_state = session.get("state", "initial")
 
1436
 
1437
  if intent_data["intent"] in {"casual_chat", "question", "unclear"} and current_state == "initial":
1438
  friendly = intent_classifier.get_friendly_response(intent_data["intent"], request.message)
1439
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1440
  yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1441
  return
1442
 
 
1447
  "For example:\n- 'extract text and summarize'\n- 'get tables from pages 2-5'\n- 'translate to Spanish'\n\n"
1448
  "Type 'help' to see all capabilities!"
1449
  )
1450
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1451
  yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1452
  return
1453
 
1454
  if not session.get("current_file"):
1455
  friendly = "πŸ“ Please upload a document first before I can process it!\n\nClick 'Upload Document' to get started."
1456
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1457
  yield emit({"type": "assistant_final", "content": friendly, "intent": intent_data, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1458
  return
1459
 
 
1479
  f"- Describe changes to modify the plan"
1480
  )
1481
 
1482
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1483
  yield emit({"type": "assistant_final", "content": friendly, "pipeline": pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1484
  return
1485
  except Exception as e:
1486
  friendly = f"❌ Error generating pipeline: {str(e)}"
1487
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1488
  yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1489
  return
1490
 
 
1553
  err = event.get("error", "Unknown error")
1554
  friendly_err = f"❌ Pipeline Failed\n\nError: {err}\n\nCompleted {len(steps_completed)} step(s) before failure."
1555
  session_manager.update_session(chat_id, {"state": "initial"})
1556
+ _add_and_mirror_message(chat_id, "assistant", friendly_err)
1557
  yield emit({"type": "assistant_final", "content": friendly_err, "error": err, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1558
  return
1559
 
 
1575
  f"- Executor: {executor_used}\n"
1576
  f"βœ… All done! What else would you like me to help you with?"
1577
  )
1578
+ _add_and_mirror_message(chat_id, "assistant", friendly_final)
1579
  yield emit({"type": "assistant_final", "content": friendly_final, "result": final_payload, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1580
  return
1581
  else:
1582
  done = f"βœ… Pipeline Completed! Executed {len(steps_completed)} steps using {executor_used}."
1583
  session_manager.update_session(chat_id, {"state": "initial"})
1584
+ _add_and_mirror_message(chat_id, "assistant", done)
1585
  yield emit({"type": "assistant_final", "content": done, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1586
  return
1587
 
1588
  except Exception as e:
1589
  friendly_err = f"❌ Pipeline Execution Failed\n\nError: {str(e)}\n\nCompleted {len(steps_completed)} step(s) before failure."
1590
  session_manager.update_session(chat_id, {"state": "initial"})
1591
+ _add_and_mirror_message(chat_id, "assistant", friendly_err)
1592
  yield emit({"type": "assistant_final", "content": friendly_err, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1593
  return
1594
  finally:
 
1600
  elif intent_data["intent"] == "rejection":
1601
  session_manager.update_session(chat_id, {"state": "initial", "proposed_pipeline": None})
1602
  friendly = "πŸ‘ No problem! Pipeline cancelled. What else would you like me to help you with?"
1603
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1604
  yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1605
  return
1606
 
 
1616
  session_manager.update_session(chat_id, {"proposed_pipeline": new_pipeline, "state": "pipeline_proposed"})
1617
  formatted = format_pipeline_for_display(new_pipeline)
1618
  friendly = formatted + f"\n\n```json\n{json.dumps(new_pipeline, indent=2)}\n```"
1619
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1620
  yield emit({"type": "assistant_final", "content": friendly, "pipeline": new_pipeline, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1621
  return
1622
  except Exception as e:
1623
  friendly = f"❌ Edit failed: {str(e)}"
1624
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1625
  yield emit({"type": "assistant_final", "content": friendly, "error": str(e), "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1626
  return
1627
 
1628
  friendly = "Please upload a document and tell me what you'd like me to do (e.g., extract text, summarize, translate)."
1629
+ _add_and_mirror_message(chat_id, "assistant", friendly)
1630
  yield emit({"type": "assistant_final", "content": friendly, "history": [m.dict() for m in _normalize_history_for_api(chat_id)]})
1631
 
1632
  return StreamingResponse(gen(), media_type="application/x-ndjson")
 
1800
  yield chunk
1801
 
1802
  media_type = obj.get("ContentType", "application/octet-stream")
1803
+ # Note: os.path.basename used below requires os import (already imported at top)
1804
  return StreamingResponse(stream(), media_type=media_type, headers={
1805
  "Content-Disposition": f'attachment; filename="{os.path.basename(key)}"'
1806
  })