1212 start_onboarding ,
1313 submit_onboarding_message ,
1414)
15+ from agents .kb_builder ._ws_stub import broadcast_stub
1516from aria_mcp .client import mcp_client
1617from core .api_response import created , ok
1718from core .database import get_db
@@ -51,6 +52,45 @@ def _ser_failure(r):
5152 return FailureHistoryOut .model_validate (decode_record (r , JSON_FIELDS )).model_dump (mode = "json" )
5253
5354
55+ # 5 phase labels — kept in sync with issue #22 §2 acceptance criterion
56+ # ("PDF upload emits exactly 5 ui_render events with component kb_progress").
57+ _UPLOAD_PHASES : tuple [str , ...] = (
58+ "Validating PDF" ,
59+ "Reading pages with Opus vision" ,
60+ "Extracting thresholds" ,
61+ "Validating schema" ,
62+ "Saving knowledge base" ,
63+ )
64+
65+
66+ def _phase_status (idx : int , active_idx : int ) -> str :
67+ if idx < active_idx :
68+ return "done"
69+ if idx == active_idx :
70+ return "in_progress"
71+ return "pending"
72+
73+
74+ def _upload_steps (active_idx : int ) -> list [dict [str , str ]]:
75+ return [
76+ {"label" : label , "status" : _phase_status (i , active_idx )}
77+ for i , label in enumerate (_UPLOAD_PHASES )
78+ ]
79+
80+
81+ async def _emit_upload_phase (cell_id : int , active_idx : int ) -> None :
82+ """Stub WS broadcast for one PDF-upload phase. See M3.6 (#22) / M4.1 (#23)."""
83+ await broadcast_stub (
84+ "ui_render" ,
85+ {
86+ "agent" : "kb_builder" ,
87+ "component" : "kb_progress" ,
88+ "props" : {"cell_id" : cell_id , "steps" : _upload_steps (active_idx )},
89+ "turn_id" : None , # set by orchestrator ContextVar after M4.1 (#23)
90+ },
91+ )
92+
93+
5494@router .get ("/equipment" )
5595async def list_kb (conn : asyncpg .Connection = Depends (get_db )):
5696 rows = await KbRepository (conn ).list ()
@@ -95,22 +135,24 @@ async def upload_pdf(
95135 recomputes completeness.
96136 5. Re-read and serialise via ``EquipmentKbOut``.
97137
98- Phase log lines stand in for live progress events until M4.1 (#23) lands
99- the websocket manager — see issue #18 §7.
138+ Phase events are emitted via ``broadcast_stub`` (M3.6 / issue #22). Each
139+ call will become ``ws_manager.broadcast("ui_render", ...)`` once M4.1
140+ (#23) lands; the payload shape is already final, only the transport is
141+ stubbed.
100142 """
101143 if file .content_type not in ("application/pdf" , "application/octet-stream" ):
102144 raise HTTPException (400 , "File must be a PDF" )
103145
104146 lock = _upload_locks .setdefault (cell_id , asyncio .Lock ())
105147 async with lock :
106- log . info ( "kb_upload[cell=%d] phase=Validating PDF" , cell_id )
148+ await _emit_upload_phase ( cell_id , 0 )
107149 pdf_bytes = await file .read ()
108150 if not pdf_bytes :
109151 raise HTTPException (400 , "Uploaded file is empty" )
110152
111- log . info ( "kb_upload[cell=%d] phase=Reading pages with Opus vision" , cell_id )
153+ await _emit_upload_phase ( cell_id , 1 )
112154 try :
113- log . info ( "kb_upload[cell=%d] phase=Extracting thresholds" , cell_id )
155+ await _emit_upload_phase ( cell_id , 2 )
114156 kb , raw_markdown = await extract_from_pdf (pdf_bytes , cell_id )
115157 except ValueError as e :
116158 # ValidationError is a subclass of ValueError, so this single
@@ -122,11 +164,11 @@ async def upload_pdf(
122164 raise HTTPException (413 , msg ) from e
123165 raise HTTPException (422 , f"Extraction failed after retry: { msg } " ) from e
124166
125- log . info ( "kb_upload[cell=%d] phase=Validating schema" , cell_id )
167+ await _emit_upload_phase ( cell_id , 3 )
126168 kb_dict = kb .model_dump (exclude = {"kb_meta" })
127169 kb_dict = await bootstrap_thresholds (cell_id , kb_dict )
128170
129- log . info ( "kb_upload[cell=%d] phase=Saving knowledge base" , cell_id )
171+ await _emit_upload_phase ( cell_id , 4 )
130172 result = await mcp_client .call_tool (
131173 "update_equipment_kb" ,
132174 {
0 commit comments