Skip to content

Commit 0e9fa9b

Browse files
committed
Move get_reachable_components to A2uiStreamParserImpl and implement schema-based reference field discovery
1 parent 273143d commit 0e9fa9b

3 files changed

Lines changed: 217 additions & 105 deletions

File tree

agent_sdks/cpp/src/parser/streaming.cc

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -144,106 +144,5 @@ std::string fix_json(const std::string& json_str) {
144144
return fixed;
145145
}
146146

147-
std::set<std::string> get_reachable_components(
148-
const std::string& root_id,
149-
const std::map<std::string, nlohmann::json>& seen_components
150-
) {
151-
std::map<std::string, std::vector<std::string>> adj_list;
152-
for (const auto& pair : seen_components) {
153-
const auto& id = pair.first;
154-
const auto& comp = pair.second;
155-
adj_list[id] = {};
156-
157-
// Extract refs (heuristic)
158-
if (comp.contains("child") && comp["child"].is_string()) {
159-
adj_list[id].push_back(comp["child"].get<std::string>());
160-
}
161-
if (comp.contains("children")) {
162-
const auto& children = comp["children"];
163-
if (children.is_array()) {
164-
for (const auto& item : children) {
165-
if (item.is_string()) adj_list[id].push_back(item.get<std::string>());
166-
}
167-
} else if (children.is_object()) {
168-
if (children.contains("explicitList") && children["explicitList"].is_array()) {
169-
for (const auto& item : children["explicitList"]) {
170-
if (item.is_string()) adj_list[id].push_back(item.get<std::string>());
171-
}
172-
}
173-
if (children.contains("template") && children["template"].is_object()) {
174-
const auto& temp = children["template"];
175-
if (temp.contains("componentId") && temp["componentId"].is_string()) {
176-
adj_list[id].push_back(temp["componentId"].get<std::string>());
177-
}
178-
}
179-
if (children.contains("componentId") && children["componentId"].is_string()) {
180-
adj_list[id].push_back(children["componentId"].get<std::string>());
181-
}
182-
}
183-
}
184-
if (comp.contains("component") && comp["component"].is_object()) {
185-
for (auto it = comp["component"].begin(); it != comp["component"].end(); ++it) {
186-
if (it.value().is_object()) {
187-
auto props = it.value();
188-
if (props.contains("child") && props["child"].is_string()) {
189-
adj_list[id].push_back(props["child"].get<std::string>());
190-
}
191-
if (props.contains("children")) {
192-
const auto& children = props["children"];
193-
if (children.is_array()) {
194-
for (const auto& item : children) {
195-
if (item.is_string()) adj_list[id].push_back(item.get<std::string>());
196-
}
197-
} else if (children.is_object()) {
198-
if (children.contains("explicitList") && children["explicitList"].is_array()) {
199-
for (const auto& item : children["explicitList"]) {
200-
if (item.is_string()) adj_list[id].push_back(item.get<std::string>());
201-
}
202-
}
203-
if (children.contains("template") && children["template"].is_object()) {
204-
const auto& temp = children["template"];
205-
if (temp.contains("componentId") && temp["componentId"].is_string()) {
206-
adj_list[id].push_back(temp["componentId"].get<std::string>());
207-
}
208-
}
209-
if (children.contains("componentId") && children["componentId"].is_string()) {
210-
adj_list[id].push_back(children["componentId"].get<std::string>());
211-
}
212-
}
213-
}
214-
}
215-
}
216-
}
217-
}
218-
219-
std::set<std::string> visited;
220-
std::set<std::string> recursion_stack;
221-
222-
std::function<void(const std::string&)> dfs = [&](const std::string& node_id) {
223-
visited.insert(node_id);
224-
recursion_stack.insert(node_id);
225-
226-
auto it = adj_list.find(node_id);
227-
if (it != adj_list.end()) {
228-
for (const auto& neighbor : it->second) {
229-
if (neighbor == node_id) {
230-
throw std::runtime_error("Self-reference detected");
231-
}
232-
if (visited.find(neighbor) == visited.end()) {
233-
dfs(neighbor);
234-
} else if (recursion_stack.find(neighbor) != recursion_stack.end()) {
235-
throw std::runtime_error("Circular reference detected");
236-
}
237-
}
238-
}
239-
240-
recursion_stack.erase(node_id);
241-
};
242-
243-
if (seen_components.find(root_id) != seen_components.end()) {
244-
dfs(root_id);
245-
}
246-
return visited;
247-
}
248147

249148
} // namespace a2ui

agent_sdks/cpp/src/parser/streaming_impl.cc

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ A2uiStreamParserImpl::A2uiStreamParserImpl(A2uiCatalog catalog)
4545
}
4646
}
4747
}
48+
discover_reference_fields();
4849
}
4950

5051
std::vector<ResponsePart> A2uiStreamParserImpl::process_chunk(const std::string& chunk) {
@@ -751,4 +752,208 @@ void A2uiStreamParserImpl::yield_messages(const std::vector<nlohmann::json>& mes
751752
}
752753
}
753754

755+
std::set<std::string> A2uiStreamParserImpl::get_reachable_components(
756+
const std::string& root_id,
757+
const std::map<std::string, nlohmann::json>& seen_components
758+
) {
759+
std::map<std::string, std::vector<std::string>> adj_list;
760+
for (const auto& pair : seen_components) {
761+
const auto& id = pair.first;
762+
const auto& comp = pair.second;
763+
adj_list[id] = {};
764+
765+
// Extract refs using schema analysis
766+
std::string comp_type;
767+
if (version_ == VERSION_0_9) {
768+
comp_type = comp.value("component", "");
769+
} else {
770+
if (comp.contains("component") && comp["component"].is_object() && !comp["component"].empty()) {
771+
comp_type = comp["component"].begin().key();
772+
}
773+
}
774+
775+
if (comp_type.empty()) {
776+
throw std::runtime_error("Failed to determine component type during streaming");
777+
}
778+
779+
auto it = component_ref_fields_.find(comp_type);
780+
if (it == component_ref_fields_.end()) {
781+
throw std::runtime_error("Schema analysis failed or component type unknown: " + comp_type);
782+
}
783+
784+
for (const auto& field_name : it->second) {
785+
nlohmann::json props;
786+
if (version_ == VERSION_0_9) {
787+
props = comp;
788+
} else {
789+
if (comp.contains("component") && comp["component"].is_object()) {
790+
props = comp["component"].begin().value();
791+
}
792+
}
793+
794+
if (props.contains(field_name)) {
795+
const auto& val = props[field_name];
796+
if (val.is_string()) {
797+
adj_list[id].push_back(val.get<std::string>());
798+
} else if (val.is_array()) {
799+
for (const auto& item : val) {
800+
if (item.is_string()) adj_list[id].push_back(item.get<std::string>());
801+
}
802+
} else if (val.is_object()) {
803+
if (val.contains("componentId") && val["componentId"].is_string()) {
804+
adj_list[id].push_back(val["componentId"].get<std::string>());
805+
}
806+
if (val.contains("explicitList") && val["explicitList"].is_array()) {
807+
for (const auto& item : val["explicitList"]) {
808+
if (item.is_string()) adj_list[id].push_back(item.get<std::string>());
809+
}
810+
}
811+
if (val.contains("template") && val["template"].is_object()) {
812+
const auto& temp = val["template"];
813+
if (temp.contains("componentId") && temp["componentId"].is_string()) {
814+
adj_list[id].push_back(temp["componentId"].get<std::string>());
815+
}
816+
}
817+
}
818+
819+
}
820+
}
821+
822+
823+
}
824+
825+
std::set<std::string> visited;
826+
std::set<std::string> recursion_stack;
827+
828+
std::function<void(const std::string&)> dfs = [&](const std::string& node_id) {
829+
visited.insert(node_id);
830+
recursion_stack.insert(node_id);
831+
832+
auto it = adj_list.find(node_id);
833+
if (it != adj_list.end()) {
834+
for (const auto& neighbor : it->second) {
835+
if (neighbor == node_id) {
836+
throw std::runtime_error("Self-reference detected");
837+
}
838+
if (visited.find(neighbor) == visited.end()) {
839+
dfs(neighbor);
840+
} else if (recursion_stack.find(neighbor) != recursion_stack.end()) {
841+
throw std::runtime_error("Circular reference detected");
842+
}
843+
}
844+
}
845+
846+
recursion_stack.erase(node_id);
847+
};
848+
849+
if (seen_components.find(root_id) != seen_components.end()) {
850+
dfs(root_id);
851+
}
852+
return visited;
853+
}
854+
855+
void A2uiStreamParserImpl::extract_properties(const nlohmann::json& schema, std::vector<std::string>& ref_fields) const {
856+
if (!schema.is_object()) return;
857+
858+
if (schema.contains("properties") && schema["properties"].is_object()) {
859+
auto props = schema["properties"];
860+
for (auto it = props.begin(); it != props.end(); ++it) {
861+
std::string prop_name = it.key();
862+
if (is_component_id_ref(it.value()) || is_child_list_ref(it.value()) ||
863+
prop_name == "child" || prop_name == "contentChild" || prop_name == "entryPointChild" || prop_name == "children") {
864+
ref_fields.push_back(prop_name);
865+
}
866+
}
867+
}
868+
869+
for (const auto& key : {"allOf", "oneOf", "anyOf"}) {
870+
if (schema.contains(key) && schema[key].is_array()) {
871+
for (const auto& sub : schema[key]) {
872+
extract_properties(sub, ref_fields);
873+
}
874+
}
875+
}
876+
}
877+
878+
879+
void A2uiStreamParserImpl::discover_reference_fields() {
880+
auto catalog_schema = catalog_.catalog_schema();
881+
if (!catalog_schema.contains("components")) return;
882+
883+
auto comps = catalog_schema["components"];
884+
for (auto it = comps.begin(); it != comps.end(); ++it) {
885+
std::string comp_name = it.key();
886+
auto comp_def = it.value();
887+
888+
// Initialize with empty vector so we know it's a valid component type
889+
component_ref_fields_[comp_name] = {};
890+
891+
extract_properties(comp_def, component_ref_fields_[comp_name]);
892+
}
893+
}
894+
895+
896+
bool A2uiStreamParserImpl::is_component_id_ref(const nlohmann::json& schema) const {
897+
if (!schema.is_object()) return false;
898+
899+
if (schema.contains("$ref")) {
900+
std::string ref = schema["$ref"].get<std::string>();
901+
if (ref.rfind("ComponentId") != std::string::npos ||
902+
ref.rfind("child") != std::string::npos ||
903+
ref.find("/child") != std::string::npos) {
904+
return true;
905+
}
906+
}
907+
908+
if (schema.value("type", "") == "string" && schema.value("title", "") == "ComponentId") {
909+
return true;
910+
}
911+
912+
for (const auto& key : {"oneOf", "anyOf", "allOf"}) {
913+
if (schema.contains(key) && schema[key].is_array()) {
914+
for (const auto& item : schema[key]) {
915+
if (is_component_id_ref(item)) return true;
916+
}
917+
}
918+
}
919+
return false;
920+
}
921+
922+
bool A2uiStreamParserImpl::is_child_list_ref(const nlohmann::json& schema) const {
923+
if (!schema.is_object()) return false;
924+
925+
if (schema.contains("$ref")) {
926+
std::string ref = schema["$ref"].get<std::string>();
927+
if (ref.rfind("ChildList") != std::string::npos ||
928+
ref.rfind("children") != std::string::npos ||
929+
ref.find("/children") != std::string::npos) {
930+
return true;
931+
}
932+
}
933+
934+
if (schema.value("type", "") == "object") {
935+
if (schema.contains("properties") && schema["properties"].is_object()) {
936+
auto props = schema["properties"];
937+
if (props.contains("explicitList") || props.contains("template") || props.contains("componentId")) {
938+
return true;
939+
}
940+
}
941+
}
942+
943+
if (schema.value("type", "") == "array") {
944+
if (schema.contains("items")) {
945+
return is_component_id_ref(schema["items"]);
946+
}
947+
}
948+
949+
for (const auto& key : {"oneOf", "anyOf", "allOf"}) {
950+
if (schema.contains(key) && schema[key].is_array()) {
951+
for (const auto& item : schema[key]) {
952+
if (is_child_list_ref(item)) return true;
953+
}
954+
}
955+
}
956+
return false;
957+
}
958+
754959
} // namespace a2ui

agent_sdks/cpp/src/parser/streaming_impl.h

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ namespace a2ui {
4040

4141
// Helper functions
4242
std::string fix_json(const std::string& json_str);
43-
std::set<std::string> get_reachable_components(
44-
const std::string& root_id,
45-
const std::map<std::string, nlohmann::json>& seen_components
46-
);
4743

4844
class A2uiStreamParserImpl : public A2uiStreamParser {
4945
protected:
@@ -79,6 +75,7 @@ class A2uiStreamParserImpl : public A2uiStreamParser {
7975
bool found_valid_json_in_block_ = false;
8076

8177
std::map<std::string, std::set<std::string>> required_fields_map_;
78+
std::map<std::string, std::vector<std::string>> component_ref_fields_;
8279
std::unique_ptr<A2uiValidator> validator_;
8380

8481
public:
@@ -106,6 +103,17 @@ class A2uiStreamParserImpl : public A2uiStreamParser {
106103
void yield_reachable(std::vector<ResponsePart>& messages, std::string_view msg_type = "", bool check_root = false, bool raise_on_orphans = false);
107104
void process_component_topology(nlohmann::json& comp, std::vector<nlohmann::json>& extra_components, bool inline_resolved);
108105
void yield_messages(const std::vector<nlohmann::json>& messages_to_yield, std::vector<ResponsePart>& messages, bool strict_integrity = true);
106+
107+
std::set<std::string> get_reachable_components(
108+
const std::string& root_id,
109+
const std::map<std::string, nlohmann::json>& seen_components
110+
);
111+
112+
void discover_reference_fields();
113+
void extract_properties(const nlohmann::json& schema, std::vector<std::string>& ref_fields) const;
114+
bool is_component_id_ref(const nlohmann::json& schema) const;
115+
116+
bool is_child_list_ref(const nlohmann::json& schema) const;
109117
};
110118

111119
} // namespace a2ui

0 commit comments

Comments
 (0)