1818import json
1919import logging
2020import re
21- from typing import Any , List , Dict , Optional , Set , TYPE_CHECKING
21+ from typing import Any , List , Dict , Optional , Set , TYPE_CHECKING , Union
2222
2323from .constants import *
2424from ..schema .constants import (
3030 CATALOG_COMPONENTS_KEY ,
3131)
3232from ..schema .validator import (
33- analyze_topology ,
3433 extract_component_ref_fields ,
3534 extract_component_required_fields ,
3635)
3736from ..schema .validator import A2uiValidator
37+ from a2ui .core .validating import analyze_topology
3838from .response_part import ResponsePart
3939from a2ui .core .validating .validator import RELAXED_VALIDATION , STRICT_VALIDATION , ValidationConfig
4040from a2ui .core import A2uiParseError , A2uiIntegrityError
@@ -53,7 +53,7 @@ class A2uiStreamParser:
5353 (V08 or V09) depending on the catalog version.
5454 """
5555
56- def __new__ (cls , catalog : A2uiCatalog ):
56+ def __new__ (cls , catalog : A2uiCatalog ) -> A2uiStreamParser :
5757 if cls is A2uiStreamParser :
5858 version = catalog .version
5959 # Lazy import inside __new__ to prevent circular import errors, as the
@@ -78,7 +78,7 @@ def __init__(self, catalog: A2uiCatalog):
7878 self ._found_delimiter = False
7979 self ._buffer = ""
8080 self ._json_buffer = ""
81- self ._brace_stack : List [ Tuple [str , int ]] = []
81+ self ._brace_stack : list [ tuple [str , int ]] = []
8282 self ._brace_count = 0
8383 self ._in_top_level_list = False
8484 self ._in_string = False
@@ -143,7 +143,7 @@ def surface_id(self) -> Optional[str]:
143143 return self ._surface_id
144144
145145 @surface_id .setter
146- def surface_id (self , value : Optional [str ]):
146+ def surface_id (self , value : Optional [str ]) -> None :
147147 self ._surface_id = value
148148 if value is not None and self ._unbound_root_id is not None :
149149 self ._root_ids [value ] = self ._unbound_root_id
@@ -161,7 +161,7 @@ def root_id(self) -> Optional[str]:
161161 )
162162
163163 @root_id .setter
164- def root_id (self , value : Optional [str ]):
164+ def root_id (self , value : Optional [str ]) -> None :
165165 if self ._surface_id :
166166 if value is not None :
167167 self ._root_ids [self ._surface_id ] = value
@@ -174,7 +174,7 @@ def root_id(self, value: Optional[str]):
174174 def msg_types (self ) -> List [str ]:
175175 return self ._msg_types
176176
177- def add_msg_type (self , msg_type : str ):
177+ def add_msg_type (self , msg_type : str ) -> None :
178178 if msg_type not in self ._msg_types :
179179 self ._msg_types .append (msg_type )
180180 if msg_type in (
@@ -204,6 +204,14 @@ def _get_active_msg_type_for_components(self) -> Optional[str]:
204204 "Subclasses must implement _get_active_msg_type_for_components"
205205 )
206206
207+ def _construct_partial_message (
208+ self , components : List [Dict [str , Any ]], active_msg_type : str
209+ ) -> Dict [str , Any ]:
210+ """Constructs a partial message of the correct type. Subclasses must implement."""
211+ raise NotImplementedError (
212+ "Subclasses must implement _construct_partial_message"
213+ )
214+
207215 def _deduplicate_data_model (self , m : Dict [str , Any ]) -> bool :
208216 """Returns True if message should be yielded, False if skipped."""
209217 return True
@@ -213,7 +221,7 @@ def _yield_messages(
213221 messages_to_yield : List [Dict [str , Any ]],
214222 messages : List [ResponsePart ],
215223 config : ValidationConfig = STRICT_VALIDATION ,
216- ):
224+ ) -> None :
217225 """Validates and appends messages to the final output list."""
218226 for m in messages_to_yield :
219227 if not self ._deduplicate_data_model (m ):
@@ -358,7 +366,7 @@ def process_chunk(self, chunk: str) -> List[ResponsePart]:
358366 )
359367 return messages
360368
361- def _reset_json_state (self ):
369+ def _reset_json_state (self ) -> None :
362370 """Resets the JSON-specific parsing state (e.g., at the end of a block)."""
363371 self ._json_buffer = ""
364372 self ._brace_stack = []
@@ -450,7 +458,7 @@ def _fix_json(self, fragment: str) -> str:
450458
451459 return fixed
452460
453- def _process_json_chunk (self , chunk : str , messages : List [ResponsePart ]):
461+ def _process_json_chunk (self , chunk : str , messages : List [ResponsePart ]) -> None :
454462 for char in chunk :
455463 char_handled = False
456464 if self ._brace_count == 0 :
@@ -670,24 +678,27 @@ def _sniff_partial_data_model(self, messages: List[ResponsePart]) -> None:
670678 or "default"
671679 )
672680 # Deduplicate delta_contents by only keeping the LATEST entry for each dirty key
681+ delta_contents : Union [
682+ List [Dict [str , Any ]], Dict [str , Any ]
683+ ]
673684 if isinstance (raw_contents , list ):
674- delta_contents = []
685+ list_contents : List [ Dict [ str , Any ]] = []
675686 seen_keys = set ()
676687 for entry in reversed (raw_contents ):
677688 if not isinstance (entry , dict ):
678689 continue
679- k = entry .get ("key" )
690+ entry_key = entry .get ("key" )
680691 # Only include entries that have a valid parsed key (cumulative)
681692 if (
682- k
683- and k in contents_dict
684- and k not in seen_keys
693+ entry_key
694+ and entry_key in contents_dict
695+ and entry_key not in seen_keys
685696 ):
686- delta_contents .insert (0 , entry )
687- seen_keys .add (k )
697+ list_contents .insert (0 , entry )
698+ seen_keys .add (entry_key )
688699 delta_contents = (
689700 self ._prune_incomplete_datamodel_entries (
690- delta_contents
701+ list_contents
691702 )
692703 )
693704 else :
@@ -711,7 +722,7 @@ def _sniff_partial_data_model(self, messages: List[ResponsePart]) -> None:
711722 # Update internal model for path resolution
712723 self .update_data_model (dm_obj , messages )
713724
714- def _sniff_partial_component (self , messages : List [ResponsePart ]):
725+ def _sniff_partial_component (self , messages : List [ResponsePart ]) -> None :
715726 """Attempts to parse a partial component from the current buffer."""
716727 # We only care about components if we are inside a "components" array
717728 if f'"{ CATALOG_COMPONENTS_KEY } "' not in self ._json_buffer :
@@ -779,7 +790,7 @@ def _prune_incomplete_datamodel_entries(self, entries: Any) -> Any:
779790
780791 def _handle_partial_component (
781792 self , comp : Dict [str , Any ], messages : List [ResponsePart ]
782- ):
793+ ) -> None :
783794 """Handles a component discovered before its parent message is finished.
784795
785796 When the parser sees a full JSON object that looks like a component
@@ -880,10 +891,10 @@ def _handle_complete_object(
880891
881892 def yield_reachable (
882893 self ,
883- messages : List [Dict [ str , Any ] ],
894+ messages : List [ResponsePart ],
884895 check_root : bool = False ,
885896 raise_on_orphans : bool = False ,
886- ):
897+ ) -> None :
887898 """Yields a partial message containing all reachable and seen components.
888899
889900 This is the core of the streaming logic. Instead of waiting for a UI message
@@ -934,8 +945,8 @@ def yield_reachable(
934945 )
935946
936947 # 1. Process placeholders and partial children
937- processed_components = []
938- extra_components = []
948+ processed_components : List [ Dict [ str , Any ]] = []
949+ extra_components : List [ Dict [ str , Any ]] = []
939950 surface_id = self .surface_id or "unknown"
940951 yielded_for_surface = self ._yielded_ids .get (surface_id , set ())
941952
@@ -1022,7 +1033,7 @@ def _process_component_topology(
10221033 comp : Dict [str , Any ],
10231034 extra_components : List [Dict [str , Any ]],
10241035 inline_resolved : bool = False ,
1025- ):
1036+ ) -> None :
10261037 """Recursively processes path placeholders and child pruning in one pass."""
10271038 comp_id = comp .get ("id" , "unknown" )
10281039
@@ -1033,7 +1044,7 @@ def _process_component_topology(
10331044 else ""
10341045 )
10351046
1036- def traverse (obj , parent_key = None ):
1047+ def traverse (obj : Any , parent_key : Optional [ str ] = None ) -> None :
10371048 if isinstance (obj , dict ):
10381049 # 1. Handle Path Placeholders (from _apply_placeholders)
10391050 if (
0 commit comments