From e0d3ce36038cc62e2683790dd561f0515f96dfc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:11:16 +0000 Subject: [PATCH 1/4] Initial plan From 1128fa3739f1ac1ca267a470842767ea0521d01e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:32:45 +0000 Subject: [PATCH 2/4] feat: add stable agent API and structured machine-facing DSL ergonomics Agent-Logs-Url: https://github.com/SkBlaz/py3plex/sessions/d3b9524e-566e-4164-98f8-c710dc0c94ea Co-authored-by: SkBlaz <10035780+SkBlaz@users.noreply.github.com> --- py3plex/__init__.py | 19 +++ py3plex/agent.py | 216 ++++++++++++++++++++++++++++++++++ py3plex/dsl/ast.py | 1 + py3plex/dsl/builder.py | 162 +++++++++++++++++++++++++- py3plex/dsl/executor.py | 15 +++ py3plex/dsl/result.py | 60 ++++++++++ py3plex/dsl/warnings.py | 121 +++++++++++++++++++ tests/test_agent_api.py | 250 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 py3plex/agent.py create mode 100644 tests/test_agent_api.py diff --git a/py3plex/__init__.py b/py3plex/__init__.py index 1ba3794c3..bb3b26e6a 100644 --- a/py3plex/__init__.py +++ b/py3plex/__init__.py @@ -198,6 +198,16 @@ uncertainty_enabled, estimate_uncertainty, ) +from py3plex.agent import ( + load_network_from_path, + top_hubs_by_layer, + uncertainty_centrality, + community_detection_with_uq, + temporal_slice, + reproducible_export_bundle, + compare_networks, + summarize_result, +) def save_to_arrow(network, path, **kwargs): @@ -361,4 +371,13 @@ def load_network_from_parquet(path, **kwargs): "capabilities", "capabilities_flat", "capabilities_fingerprint", + # Agent-facing stable API + "load_network_from_path", + "top_hubs_by_layer", + "uncertainty_centrality", + "community_detection_with_uq", + "temporal_slice", + "reproducible_export_bundle", + "compare_networks", + "summarize_result", ] diff --git a/py3plex/agent.py b/py3plex/agent.py new file mode 100644 index 000000000..3149b153f --- /dev/null +++ b/py3plex/agent.py @@ -0,0 +1,216 @@ +"""Stable machine-facing agent API for py3plex workflows.""" + +from pathlib import Path +from typing import Any, Dict, List, Optional + +from py3plex.core import multinet +from py3plex.dsl.builder import L, Q +from py3plex.dsl.result import QueryResult + + +def _extract_replay_payload(result: QueryResult) -> Dict[str, Any]: + prov = result.provenance or {} + query_info = prov.get("query", {}) if isinstance(prov, dict) else {} + return { + "is_replayable": result.is_replayable, + "seed": (prov.get("randomness", {}) or {}).get("seed") + if isinstance(prov, dict) + else None, + "ast_hash": query_info.get("ast_hash"), + "network_fingerprint": prov.get("network_fingerprint") if isinstance(prov, dict) else None, + "network_version": prov.get("network_version") if isinstance(prov, dict) else None, + } + + +def _as_agent_response( + *, + result: QueryResult, + assumptions: Optional[List[str]] = None, + warnings: Optional[List[Dict[str, Any]]] = None, + export_paths: Optional[List[str]] = None, +) -> Dict[str, Any]: + return { + "status": "ok", + "assumptions": assumptions or [], + "warnings": warnings if warnings is not None else result.meta.get("warnings", []), + "result": result.canonical_export_dict(), + "provenance": result.meta.get("provenance", {}), + "replay": _extract_replay_payload(result), + "export_paths": export_paths or [], + } + + +def load_network_from_path( + path: str, *, input_type: str = "edgelist", directed: bool = False +) -> Dict[str, Any]: + """Load a network from path and return stable machine-facing payload.""" + net = multinet.multi_layer_network(directed=directed) + net.load_network(path, input_type=input_type) + layers = list(net.get_layers()) + return { + "status": "ok", + "assumptions": ["Network path is trusted and readable."], + "warnings": [], + "result": { + "network": net, + "network_stats": { + "node_replicas": len(list(net.get_nodes())), + "edges": len(list(net.get_edges())), + "layers": layers, + "layer_count": len(layers), + }, + }, + "provenance": {"source_path": path, "input_type": input_type, "directed": directed}, + "replay": {}, + "export_paths": [], + } + + +def top_hubs_by_layer( + network: Any, + *, + top_k: int = 10, + measure: str = "degree", + seed: Optional[int] = 42, +) -> Dict[str, Any]: + """Compute per-layer hubs with reproducible defaults.""" + query = ( + Q.nodes() + .from_layers(L["*"]) + .compute(measure, kind="aggregate") + .per_layer() + .top_k(top_k, measure) + .provenance(mode="replayable", seed=seed) + ) + result = query.execute(network) + return _as_agent_response( + result=result, + assumptions=[ + "Top-k is applied independently per layer.", + "Degree kind defaults to aggregate unless explicitly overridden.", + ], + ) + + +def uncertainty_centrality( + network: Any, + *, + measures: Optional[List[str]] = None, + method: str = "bootstrap", + n_samples: int = 50, + ci: float = 0.95, + seed: Optional[int] = 42, +) -> Dict[str, Any]: + """Compute uncertainty-aware centrality with structured output.""" + measures = measures or ["pagerank"] + query = ( + Q.nodes() + .compute(*measures) + .uq(method=method, n_samples=n_samples, ci=ci, seed=seed) + .provenance(mode="replayable", seed=seed) + ) + result = query.execute(network) + return _as_agent_response( + result=result, + assumptions=["Uncertainty summaries use canonical mean/std/CI schema."], + ) + + +def community_detection_with_uq( + network: Any, + *, + method: str = "leiden", + n_samples: int = 20, + seed: Optional[int] = 42, +) -> Dict[str, Any]: + """Run reproducible community detection with UQ.""" + query = ( + Q.nodes() + .community(method=method, random_state=seed) + .uq(method="seed", n_samples=n_samples, seed=seed) + .provenance(mode="replayable", seed=seed) + ) + result = query.execute(network) + return _as_agent_response( + result=result, + assumptions=["Community uncertainty is estimated via seed ensemble."], + ) + + +def temporal_slice( + network: Any, + *, + t_start: Optional[float] = None, + t_end: Optional[float] = None, + at: Optional[float] = None, +) -> Dict[str, Any]: + """Run temporal slice query with structured result contract.""" + query = Q.edges() + assumptions = [] + if at is not None: + query = query.at(float(at)) + assumptions.append("Temporal query uses point-in-time snapshot semantics.") + else: + if t_start is None or t_end is None: + raise ValueError("Provide either 'at' or both 't_start' and 't_end'.") + query = query.during(float(t_start), float(t_end)) + assumptions.append("Temporal query uses inclusive time window semantics.") + result = query.execute(network) + return _as_agent_response(result=result, assumptions=assumptions) + + +def reproducible_export_bundle( + result: QueryResult, *, path: str, compress: bool = True +) -> Dict[str, Any]: + """Export replay bundle and return stable path metadata.""" + out_path = str(Path(path)) + result.export_bundle(out_path, compress=compress) + return _as_agent_response( + result=result, + assumptions=["Bundle includes provenance metadata for replay."], + export_paths=[out_path], + ) + + +def compare_networks( + network_a: Any, + network_b: Any, + *, + measure: str = "degree", +) -> Dict[str, Any]: + """Compare two networks via shared machine-facing summary metric.""" + res_a = Q.nodes().compute(measure).execute(network_a) + res_b = Q.nodes().compute(measure).execute(network_b) + vals_a = res_a.attributes.get(measure, {}) + vals_b = res_b.attributes.get(measure, {}) + mean_a = sum(vals_a.values()) / len(vals_a) if vals_a else 0.0 + mean_b = sum(vals_b.values()) / len(vals_b) if vals_b else 0.0 + return { + "status": "ok", + "assumptions": ["Comparison uses simple mean of selected metric over node replicas."], + "warnings": [], + "result": { + "metric": measure, + "mean_a": mean_a, + "mean_b": mean_b, + "delta": mean_b - mean_a, + "count_a": len(vals_a), + "count_b": len(vals_b), + }, + "provenance": {}, + "replay": {}, + "export_paths": [], + } + + +def summarize_result(result: QueryResult) -> Dict[str, Any]: + """Return compact structured summary for a QueryResult.""" + return { + "status": "ok", + "assumptions": [], + "warnings": result.meta.get("warnings", []), + "result": result.summary_dict(), + "provenance": result.meta.get("provenance", {}), + "replay": _extract_replay_payload(result), + "export_paths": [], + } diff --git a/py3plex/dsl/ast.py b/py3plex/dsl/ast.py index a2d3d353e..58ce6f373 100644 --- a/py3plex/dsl/ast.py +++ b/py3plex/dsl/ast.py @@ -292,6 +292,7 @@ class ComputeItem: null_model: Optional[str] = None random_state: Optional[int] = None approx: Optional["ApproximationSpec"] = None + kind: Optional[str] = None @property def result_name(self) -> str: diff --git a/py3plex/dsl/builder.py b/py3plex/dsl/builder.py index 5d7a36db9..1ac9ddf85 100644 --- a/py3plex/dsl/builder.py +++ b/py3plex/dsl/builder.py @@ -66,6 +66,7 @@ DistanceSpec, ) from .result import QueryResult +from .warnings import build_structured_warning from py3plex.uncertainty import ( get_uncertainty_config, @@ -676,6 +677,7 @@ def compute( *measures: str, alias: Optional[str] = None, aliases: Optional[Dict[str, str]] = None, + kind: Optional[str] = None, uncertainty: Optional[bool] = None, method: Optional[str] = None, n_samples: Optional[int] = None, @@ -698,6 +700,7 @@ def compute( *measures: Measure names to compute alias: Alias for single measure aliases: Dictionary mapping measure names to aliases + kind: Optional metric semantic kind (e.g., degree kind: aggregate|intra|inter) uncertainty: Whether to compute uncertainty for these measures. If None, uses Q.uncertainty defaults or the global uncertainty context. method: Uncertainty estimation method ('bootstrap', 'perturbation', 'seed', 'null_model') @@ -889,6 +892,7 @@ def compute( null_model=null_model, random_state=random_state, approx=measure_approx_spec, + kind=kind, ) ) elif alias and len(measures) == 1: @@ -918,6 +922,7 @@ def compute( null_model=null_model, random_state=random_state, approx=measure_approx_spec, + kind=kind, ) ) else: @@ -947,12 +952,146 @@ def compute( null_model=null_model, random_state=random_state, approx=measure_approx_spec, + kind=kind, ) ) self._select.compute.extend(items) return self + def validate(self) -> Dict[str, Any]: + """Return structured machine-readable validation diagnostics.""" + select = self._select + selected_layers: List[str] = [] + if getattr(select, "layer_set", None) is not None: + layer_set_obj = select.layer_set + try: + selected_layers = list(layer_set_obj.layers) # type: ignore[attr-defined] + except Exception: + selected_layers = [str(layer_set_obj)] + elif getattr(select, "layer_expr", None) is not None: + try: + selected_layers = [term.name for term in select.layer_expr.terms] + except Exception: + selected_layers = [] + + computed_measures = [item.name for item in select.compute] + grouping_mode: Optional[str] = None + if select.group_by: + if select.group_by == ["layer"]: + grouping_mode = "per_layer" + elif select.group_by == ["src_layer", "dst_layer"]: + grouping_mode = "per_layer_pair" + else: + grouping_mode = "group_by" + + uq_cfg = getattr(select, "uq_config", None) + uq_summary = { + "enabled": bool(uq_cfg and uq_cfg.method), + "method": getattr(uq_cfg, "method", None), + "n_samples": getattr(uq_cfg, "n_samples", None), + "ci": getattr(uq_cfg, "ci", None), + "seed": getattr(uq_cfg, "seed", None), + "mode": getattr(uq_cfg, "mode", None), + } + + provenance_cfg = getattr(select, "provenance_config", None) or {} + provenance_mode = provenance_cfg.get("mode") + + ambiguity_flags: List[str] = [] + performance_risk_flags: List[str] = [] + missing_prerequisites: List[Dict[str, Any]] = [] + recommended_fixes: List[str] = [] + warnings_structured: List[Dict[str, Any]] = [] + + if select.target == Target.NODES and not selected_layers: + ambiguity_flags.append("replica_vs_physical_node_ambiguity") + warnings_structured.append( + build_structured_warning( + "replica_vs_physical_node_ambiguity", + message="Node results are replica-based unless layer scope is explicit.", + ) + ) + + if any(item.name == "degree" for item in select.compute): + has_kind = any(bool(getattr(item, "kind", None)) for item in select.compute) + if select.target == Target.NODES and not has_kind: + ambiguity_flags.append("aggregate_degree_ambiguity") + warnings_structured.append( + build_structured_warning( + "aggregate_degree_ambiguity", + message="Degree kind is not explicit for multilayer analysis.", + ) + ) + + for order_item in select.order_by: + key = order_item.key.lstrip("-") + if key not in computed_measures: + missing_prerequisites.append( + { + "code": "missing_metric", + "detail": f"order_by references '{key}' before it is computed.", + } + ) + recommended_fixes.append(f"Add .compute('{key}') before .order_by(...).") + + if select.coverage_mode and not select.group_by: + missing_prerequisites.append( + { + "code": "active_grouping_required", + "detail": "coverage requires active grouping.", + } + ) + recommended_fixes.append( + "Call per_layer()/per_layer_pair()/group_by(...) before coverage(...)." + ) + + if uq_summary["enabled"] and isinstance(uq_summary["n_samples"], int): + if uq_summary["n_samples"] > 200: + performance_risk_flags.append("high_uq_cost") + recommended_fixes.append("Reduce n_samples or use fast UQ mode.") + + expensive = {"betweenness_centrality", "closeness_centrality", "eigenvector_centrality"} + if any(m in expensive for m in computed_measures): + performance_risk_flags.append("expensive_centrality") + recommended_fixes.append( + "Use per_layer()/from_layers() or approx=True for expensive centralities." + ) + + return { + "target": select.target.value if hasattr(select.target, "value") else str(select.target), + "selected_layers": selected_layers, + "computed_measures": computed_measures, + "grouping_mode": grouping_mode, + "uncertainty_config": uq_summary, + "provenance_mode": provenance_mode, + "ambiguity_flags": sorted(set(ambiguity_flags)), + "performance_risk_flags": sorted(set(performance_risk_flags)), + "missing_prerequisites": missing_prerequisites, + "recommended_fixes": sorted(set(recommended_fixes)), + "warnings": warnings_structured, + "status": "ok" if not missing_prerequisites else "needs_attention", + } + + def explain_plan_json(self) -> Dict[str, Any]: + """Return machine-readable plan/introspection information.""" + return { + "machine": True, + "validation": self.validate(), + "query": { + "dsl": self.to_dsl(), + "target": self._select.target.value + if hasattr(self._select.target, "value") + else str(self._select.target), + }, + "planner": { + "enabled": True, + "compute_policy": getattr(self, "_planner_config", {}).get("compute_policy", "explicit"), + "enable_cache": getattr(self, "_planner_config", {}).get("enable_cache", True), + }, + "next_action": "Call .explain_plan().execute(network) to attach full planned stages in result.meta['plan'].", + } + def order_by(self, *keys: str, desc: bool = False) -> "QueryBuilder": """Add ORDER BY clause. @@ -1836,6 +1975,27 @@ def top_k(self, k: int, key: Optional[str] = None) -> "QueryBuilder": self._select.limit_per_group = int(k) return self + def top_k_global(self, k: int, key: str) -> "QueryBuilder": + """Explicit global top-k helper for machine-facing clarity.""" + self._select.limit_per_group = None + self._select.group_by = [] + return self.order_by(f"-{key}").limit(int(k)) + + def top_k_per_layer(self, k: int, key: str) -> "QueryBuilder": + """Explicit per-layer top-k helper for machine-facing clarity.""" + return self.per_layer().top_k(int(k), key) + + def top_k_across_layers( + self, k: int, key: str, *, coverage_mode: str = "at_least", coverage_k: int = 2 + ) -> "QueryBuilder": + """Explicit across-layer top-k helper with coverage semantics.""" + return ( + self.per_layer() + .top_k(int(k), key) + .end_grouping() + .coverage(mode=coverage_mode, k=coverage_k) + ) + def end_grouping(self) -> "QueryBuilder": """Marker for the end of grouping configuration. @@ -3601,7 +3761,7 @@ def explain_plan(self) -> "QueryBuilder": showing stage order, costs, and optimization rewrites. Returns: - Self for chaining + Self for chaining. Example: >>> result = Q.nodes().compute("degree").explain_plan().execute(net) diff --git a/py3plex/dsl/executor.py b/py3plex/dsl/executor.py index ff598aee3..cea918262 100644 --- a/py3plex/dsl/executor.py +++ b/py3plex/dsl/executor.py @@ -52,6 +52,7 @@ UnknownAttributeError, GroupingError, ) +from .warnings import build_structured_warning # Import requirements system for algorithm compatibility checking from py3plex.requirements import check_compat, AlgorithmCompatibilityError @@ -2828,6 +2829,20 @@ def _record_timing(stage_name: str, duration_ms: float): result = QueryResult( target=select.target.value, items=items, attributes=attributes, meta=meta_dict ) + + diagnostics: List[Dict[str, Any]] = [] + if select.target == Target.NODES and not ( + select.layer_set is not None or select.layer_expr is not None + ): + diagnostics.append(build_structured_warning("replica_vs_physical_node_ambiguity")) + layer_count = len(list(network.get_layers())) if hasattr(network, "get_layers") else 1 + if layer_count > 1 and any( + ci.name == "degree" and not getattr(ci, "kind", None) for ci in select.compute + ): + diagnostics.append(build_structured_warning("aggregate_degree_ambiguity")) + if diagnostics: + result.meta["warnings"] = diagnostics + _record_timing("materialize", (time.monotonic() - stage_start) * 1000) # Step 6.7: Compute embeddings if requested diff --git a/py3plex/dsl/result.py b/py3plex/dsl/result.py index f81fb752d..cf53f2607 100644 --- a/py3plex/dsl/result.py +++ b/py3plex/dsl/result.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Union, Tuple import math +import json # Tolerance for matching quantile keys when finding CI bounds @@ -1100,6 +1101,65 @@ def to_dict(self) -> Dict[str, Any]: "meta": self.meta, } + def replica_nodes(self) -> List[Any]: + """Return replica nodes (node, layer pairs) for node results.""" + if self.target != "nodes": + raise ValueError("replica_nodes() is only available for node results.") + return list(self.items) + + def physical_nodes(self) -> List[Any]: + """Return unique physical nodes for node results.""" + if self.target != "nodes": + raise ValueError("physical_nodes() is only available for node results.") + ordered: Dict[Any, bool] = {} + for item in self.items: + if isinstance(item, tuple) and len(item) >= 1: + node_id = item[0] + else: + node_id = item + ordered[node_id] = True + return list(ordered.keys()) + + def summary_dict(self) -> Dict[str, Any]: + """Return a compact, machine-readable summary for agent workflows.""" + prov = self.meta.get("provenance", {}) if isinstance(self.meta, dict) else {} + query_prov = prov.get("query", {}) if isinstance(prov, dict) else {} + random_prov = prov.get("randomness", {}) if isinstance(prov, dict) else {} + return { + "target": self.target, + "count": len(self.items), + "replica_count": len(self.items) if self.target == "nodes" else None, + "physical_count": len(self.physical_nodes()) if self.target == "nodes" else None, + "attributes": sorted(list(self.attributes.keys())), + "grouping": self.meta.get("grouping") if isinstance(self.meta, dict) else None, + "warnings": self.meta.get("warnings", []) if isinstance(self.meta, dict) else [], + "provenance": { + "seed": random_prov.get("seed"), + "ast_hash": query_prov.get("ast_hash"), + "network_fingerprint": prov.get("network_fingerprint"), + "network_version": prov.get("network_version"), + "replayable": self.is_replayable, + }, + } + + def inspect_json(self) -> str: + """Return stable JSON payload suitable for machine handoff.""" + return json.dumps(self.summary_dict(), sort_keys=True, ensure_ascii=False) + + def canonical_export_dict(self) -> Dict[str, Any]: + """Return canonical, stable serialization for agent handoff.""" + return { + "target": self.target, + "items": self.items, + "attributes": self.attributes, + "meta": { + "grouping": self.meta.get("grouping") if isinstance(self.meta, dict) else None, + "warnings": self.meta.get("warnings", []) if isinstance(self.meta, dict) else [], + "provenance": self.meta.get("provenance") if isinstance(self.meta, dict) else None, + }, + "summary": self.summary_dict(), + } + def record_as_experiment( self, *, diff --git a/py3plex/dsl/warnings.py b/py3plex/dsl/warnings.py index 2443c12b7..eacb171b5 100644 --- a/py3plex/dsl/warnings.py +++ b/py3plex/dsl/warnings.py @@ -37,6 +37,127 @@ class MultilayerSemanticWarning(Py3plexWarning): pass +# Stable machine-readable warning catalog used by validation/execution surfaces. +# +# Schema per warning code: +# - severity: "error" | "warning" | "info" +# - cause: concise root-cause explanation +# - likely_intent: what an agent/user likely meant +# - suggested_fixes: list of actionable repair steps +# - autofixable: whether a deterministic autofix is usually possible +STRUCTURED_WARNING_CATALOG: Dict[str, Dict[str, Any]] = { + "execute_too_early": { + "severity": "error", + "cause": "Query building method used after execution result creation.", + "likely_intent": "Filter or transform before execute().", + "suggested_fixes": [ + "Move where()/compute()/grouping methods before execute().", + "Use result.to_pandas() filtering for post-execution filtering.", + ], + "autofixable": True, + }, + "replica_vs_physical_node_ambiguity": { + "severity": "warning", + "cause": "Multilayer results return node replicas by default.", + "likely_intent": "Count or rank physical nodes rather than replicas.", + "suggested_fixes": [ + "Use QueryResult.physical_nodes() for physical node counts.", + "Use per-layer grouping to make replica semantics explicit.", + ], + "autofixable": True, + }, + "aggregate_degree_ambiguity": { + "severity": "warning", + "cause": "Degree in multilayer graphs can mean aggregate, intra-layer, or inter-layer.", + "likely_intent": "Compute a specific degree variant explicitly.", + "suggested_fixes": [ + "Use compute('degree', kind='aggregate|intra|inter').", + "Use from_layers()/per_layer() to constrain semantics.", + ], + "autofixable": True, + }, + "expensive_centrality": { + "severity": "warning", + "cause": "Expensive centrality requested on large graph.", + "likely_intent": "Complete analysis faster while preserving intent.", + "suggested_fixes": [ + "Use per_layer() or from_layers() to reduce workload.", + "Use approximate mode in compute(..., approx=True).", + ], + "autofixable": True, + }, + "high_uq_cost": { + "severity": "warning", + "cause": "High uncertainty sample count relative to graph size.", + "likely_intent": "Estimate uncertainty with lower runtime.", + "suggested_fixes": [ + "Reduce n_samples.", + "Use stratified_perturbation or seed method for quick checks.", + ], + "autofixable": True, + }, + "missing_metric": { + "severity": "error", + "cause": "Query references a metric that has not been computed.", + "likely_intent": "Use metric in where/order/coverage logic.", + "suggested_fixes": [ + "Add .compute('') before referencing the metric.", + "Enable autocompute if available.", + ], + "autofixable": True, + }, + "active_grouping_required": { + "severity": "error", + "cause": "Operation requires grouped context.", + "likely_intent": "Apply operation per layer or per group.", + "suggested_fixes": [ + "Call per_layer(), per_layer_pair(), or group_by(...) first.", + ], + "autofixable": True, + }, + "grouping_not_ended": { + "severity": "error", + "cause": "Coverage or global operation called while grouping still active.", + "likely_intent": "Flatten grouped result before coverage/global operation.", + "suggested_fixes": [ + "Call end_grouping() before coverage()/global operations.", + ], + "autofixable": True, + }, +} + + +def build_structured_warning( + code: str, + *, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """Build a machine-readable warning dictionary from a stable warning code.""" + template = STRUCTURED_WARNING_CATALOG.get( + code, + { + "severity": "warning", + "cause": "Unknown warning.", + "likely_intent": "Inspect warning details.", + "suggested_fixes": [], + "autofixable": False, + }, + ) + payload = { + "code": code, + "severity": template["severity"], + "message": message or code.replace("_", " "), + "cause": template["cause"], + "likely_intent": template["likely_intent"], + "suggested_fixes": list(template["suggested_fixes"]), + "autofixable": bool(template["autofixable"]), + } + if details: + payload["details"] = details + return payload + + @contextmanager def suppress_warnings(*warning_types: str): """Context manager to suppress specific warning types. diff --git a/tests/test_agent_api.py b/tests/test_agent_api.py new file mode 100644 index 000000000..6ea85e66b --- /dev/null +++ b/tests/test_agent_api.py @@ -0,0 +1,250 @@ +"""Regression tests for stable machine-facing agent API.""" + +import json +from pathlib import Path + +from py3plex.agent import ( + load_network_from_path, + top_hubs_by_layer, + uncertainty_centrality, + summarize_result, + compare_networks, + reproducible_export_bundle, + community_detection_with_uq, + temporal_slice, +) +from py3plex.core import multinet +from py3plex.dsl import Q + + +def _build_small_network(): + net = multinet.multi_layer_network(directed=False) + net.add_nodes( + [ + {"source": "A", "type": "social"}, + {"source": "B", "type": "social"}, + {"source": "A", "type": "work"}, + {"source": "B", "type": "work"}, + ] + ) + net.add_edges( + [ + { + "source": "A", + "target": "B", + "source_type": "social", + "target_type": "social", + }, + { + "source": "A", + "target": "B", + "source_type": "work", + "target_type": "work", + }, + ] + ) + return net + + +def test_top_hubs_by_layer_structured_contract(): + net = _build_small_network() + payload = top_hubs_by_layer(net, top_k=1, measure="degree", seed=42) + assert payload["status"] == "ok" + assert set(payload.keys()) == { + "status", + "assumptions", + "warnings", + "result", + "provenance", + "replay", + "export_paths", + } + assert payload["replay"]["is_replayable"] is True + assert payload["result"]["summary"]["target"] == "nodes" + + +def test_load_network_from_path_contract(tmp_path): + net = _build_small_network() + edge_path = tmp_path / "net.edgelist" + net.save_network(str(edge_path), output_type="edgelist") + + payload = load_network_from_path(str(edge_path), input_type="edgelist") + assert payload["status"] == "ok" + assert "network" in payload["result"] + assert payload["result"]["network_stats"]["layer_count"] >= 1 + + +def test_uncertainty_centrality_deterministic_with_seed(): + net = _build_small_network() + p1 = uncertainty_centrality(net, measures=["degree"], n_samples=10, seed=7) + p2 = uncertainty_centrality(net, measures=["degree"], n_samples=10, seed=7) + assert p1["result"]["summary"] == p2["result"]["summary"] + assert p1["result"]["attributes"]["degree"] == p2["result"]["attributes"]["degree"] + + +def test_summarize_result_machine_payload(): + net = _build_small_network() + result = Q.nodes().compute("degree").execute(net) + payload = summarize_result(result) + assert payload["status"] == "ok" + assert payload["result"]["target"] == "nodes" + assert "provenance" in payload + + +def test_compare_networks_contract(): + net_a = _build_small_network() + net_b = _build_small_network() + payload = compare_networks(net_a, net_b, measure="degree") + assert payload["status"] == "ok" + assert payload["result"]["metric"] == "degree" + assert payload["result"]["delta"] == 0.0 + + +def test_query_builder_validate_and_machine_plan(): + q = Q.nodes().compute("degree").order_by("degree") + v = q.validate() + assert v["target"] == "nodes" + assert "computed_measures" in v + assert "warnings" in v + + plan = q.explain_plan_json() + assert plan["machine"] is True + assert "validation" in plan + assert "planner" in plan + + +def test_query_builder_validate_missing_metric(): + q = Q.nodes().order_by("pagerank") + v = q.validate() + assert v["status"] == "needs_attention" + codes = [m["code"] for m in v["missing_prerequisites"]] + assert "missing_metric" in codes + + +def test_top_k_explicit_helpers(): + q1 = Q.nodes().top_k_global(3, "degree") + q2 = Q.nodes().top_k_per_layer(3, "degree") + q3 = Q.nodes().top_k_across_layers(3, "degree", coverage_mode="at_least", coverage_k=2) + assert q1._select.limit == 3 + assert q1._select.group_by == [] + assert q2._select.group_by == ["layer"] + assert q3._select.group_by == ["layer"] + assert q3._select.coverage_mode == "at_least" + assert q3._select.coverage_k == 2 + + +def test_query_result_summary_and_inspect_json_and_counts(): + net = _build_small_network() + result = Q.nodes().compute("degree").execute(net) + summary = result.summary_dict() + assert summary["target"] == "nodes" + assert summary["replica_count"] == len(result.items) + assert summary["physical_count"] == 2 + as_json = result.inspect_json() + parsed = json.loads(as_json) + assert parsed["target"] == "nodes" + assert set(result.physical_nodes()) == {"A", "B"} + assert len(result.replica_nodes()) == len(result.items) + + +def test_compute_degree_kind_survives_ast(): + q = Q.nodes().compute("degree", kind="intra") + assert q.to_ast().select.compute[0].kind == "intra" + + +def test_structured_warning_catalog_and_builder(): + from py3plex.dsl.warnings import STRUCTURED_WARNING_CATALOG, build_structured_warning + + assert "expensive_centrality" in STRUCTURED_WARNING_CATALOG + w = build_structured_warning("expensive_centrality") + assert w["code"] == "expensive_centrality" + assert "autofixable" in w + + +def test_explain_plan_chainable_flag(): + q = Q.nodes().compute("degree").explain_plan() + assert hasattr(q, "_explain_plan_flag") + assert q._explain_plan_flag is True + + +def test_reproducible_export_bundle_contract(tmp_path): + net = _build_small_network() + result = Q.nodes().provenance(mode="replayable", seed=11).compute("degree").execute(net) + out = tmp_path / "bundle.json.gz" + payload = reproducible_export_bundle(result, path=str(out), compress=True) + assert payload["status"] == "ok" + assert str(out) in payload["export_paths"] + assert Path(str(out)).exists() + + +def test_temporal_slice_requires_valid_mode(): + net = _build_small_network() + try: + temporal_slice(net) + except ValueError as exc: + assert "either 'at' or both 't_start' and 't_end'" in str(exc) + else: + raise AssertionError("Expected ValueError for missing temporal parameters.") + + +def test_temporal_slice_at_mode_with_mock(monkeypatch): + net = _build_small_network() + captured = {} + + class _DummyResult: + meta = {"warnings": [], "provenance": {}} + provenance = {} + is_replayable = False + + def canonical_export_dict(self): + return {"summary": {"target": "edges"}} + + def _fake_execute(self, network, *args, **kwargs): + captured["network"] = network + return _DummyResult() + + monkeypatch.setattr(type(Q.edges()), "execute", _fake_execute, raising=False) + payload = temporal_slice(net, at=1.5) + assert payload["status"] == "ok" + assert payload["result"]["summary"]["target"] == "edges" + assert captured["network"] is net + + +def test_temporal_slice_during_mode_with_mock(monkeypatch): + net = _build_small_network() + + class _DummyResult: + meta = {"warnings": [], "provenance": {}} + provenance = {} + is_replayable = False + + def canonical_export_dict(self): + return {"summary": {"target": "edges"}} + + def _fake_execute(self, network, *args, **kwargs): + return _DummyResult() + + monkeypatch.setattr(type(Q.edges()), "execute", _fake_execute, raising=False) + payload = temporal_slice(net, t_start=0.0, t_end=2.0) + assert payload["status"] == "ok" + assert payload["result"]["summary"]["target"] == "edges" + + +def test_community_detection_with_uq_with_mock(monkeypatch): + net = _build_small_network() + + class _DummyResult: + meta = {"warnings": [], "provenance": {}} + provenance = {} + is_replayable = False + + def canonical_export_dict(self): + return {"summary": {"target": "nodes"}} + + def _fake_execute(self, network, *args, **kwargs): + return _DummyResult() + + monkeypatch.setattr(type(Q.nodes()), "execute", _fake_execute, raising=False) + payload = community_detection_with_uq(net, method="leiden", n_samples=5, seed=3) + assert payload["status"] == "ok" + assert payload["result"]["summary"]["target"] == "nodes" From 383624a98e482cf281ab52a75a1d9dd93b0973fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:23:35 +0000 Subject: [PATCH 3/4] chore: refresh generated documentation outputs Agent-Logs-Url: https://github.com/SkBlaz/py3plex/sessions/f81350e5-7f10-4dca-9dfd-003c23e5e9c1 Co-authored-by: SkBlaz <10035780+SkBlaz@users.noreply.github.com> --- examples/docs_outputs/01_basic_query.err | 6 +- examples/docs_outputs/01_basic_query.txt | 30 ++++ .../dsl_patterns_quick_reference.err | 9 ++ .../dsl_patterns_quick_reference.txt | 135 +++++++++++++++++ .../docs_outputs/example_ergonomics_demo.err | 7 + .../docs_outputs/example_ergonomics_demo.txt | 70 +++++++++ examples/docs_outputs/manifest.json | 8 +- .../docs_outputs/quick_start_ergonomics.err | 1 + .../docs_outputs/quick_start_ergonomics.txt | 10 ++ .../docs_outputs/user_journey_simulation.err | 10 ++ .../docs_outputs/user_journey_simulation.txt | 140 ++++++++++++++++++ 11 files changed, 419 insertions(+), 7 deletions(-) create mode 100644 examples/docs_outputs/dsl_patterns_quick_reference.err create mode 100644 examples/docs_outputs/example_ergonomics_demo.err create mode 100644 examples/docs_outputs/user_journey_simulation.err diff --git a/examples/docs_outputs/01_basic_query.err b/examples/docs_outputs/01_basic_query.err index 56e65f1ea..102d2eb19 100644 --- a/examples/docs_outputs/01_basic_query.err +++ b/examples/docs_outputs/01_basic_query.err @@ -1,3 +1,3 @@ -2026-02-22 11:28:19 - py3plex.core.converters - INFO - Finished with layout.. -2026-02-22 11:28:19 - py3plex.core.converters - INFO - Finished with layout.. -2026-02-22 11:28:19 - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. diff --git a/examples/docs_outputs/01_basic_query.txt b/examples/docs_outputs/01_basic_query.txt index 261301e84..ff06d7a45 100644 --- a/examples/docs_outputs/01_basic_query.txt +++ b/examples/docs_outputs/01_basic_query.txt @@ -4,11 +4,41 @@ Added 5 edges Network Statistics: ======================================== +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Total node instances: 6 +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Total edges: 5 Node Degrees: ======================================== +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds 0 (social ): 2 1 (social ): 2 2 (social ): 2 diff --git a/examples/docs_outputs/dsl_patterns_quick_reference.err b/examples/docs_outputs/dsl_patterns_quick_reference.err new file mode 100644 index 000000000..e0555136a --- /dev/null +++ b/examples/docs_outputs/dsl_patterns_quick_reference.err @@ -0,0 +1,9 @@ + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. diff --git a/examples/docs_outputs/dsl_patterns_quick_reference.txt b/examples/docs_outputs/dsl_patterns_quick_reference.txt index 05f5118bf..523bd4fb5 100644 --- a/examples/docs_outputs/dsl_patterns_quick_reference.txt +++ b/examples/docs_outputs/dsl_patterns_quick_reference.txt @@ -26,6 +26,21 @@ Code: .execute(net) ) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Result: Found 2 high-degree nodes in social layer @@ -51,6 +66,21 @@ Code: .execute(net) ) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Result: Found 8 cross-layer hubs @@ -78,6 +108,21 @@ Code: df = result.to_pandas(expand_uncertainty=True) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Result: Computed PageRank with confidence intervals for 18 nodes @@ -109,9 +154,54 @@ Code: .execute(net) ) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Social | Work: 12 node replicas +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds All - Hobby: 12 node replicas +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Social only: 6 node replicas ====================================================================== @@ -130,6 +220,21 @@ Code: .execute(net) ) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Result: Created 2 derived columns for 18 nodes @@ -165,6 +270,21 @@ Code: .execute(net) ) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Result: Computed per-layer statistics @@ -193,6 +313,21 @@ Code: # CSV file df.to_csv("results.csv", index=False) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Pandas DataFrame: 18 rows x 3 columns OK NetworkX graph: 18 nodes, 0 edges diff --git a/examples/docs_outputs/example_ergonomics_demo.err b/examples/docs_outputs/example_ergonomics_demo.err new file mode 100644 index 000000000..48315adc9 --- /dev/null +++ b/examples/docs_outputs/example_ergonomics_demo.err @@ -0,0 +1,7 @@ + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. diff --git a/examples/docs_outputs/example_ergonomics_demo.txt b/examples/docs_outputs/example_ergonomics_demo.txt index bb7e7003e..d96fba9b8 100644 --- a/examples/docs_outputs/example_ergonomics_demo.txt +++ b/examples/docs_outputs/example_ergonomics_demo.txt @@ -126,12 +126,32 @@ Demo 1: Interactive Query Building with .hint() Example: Q.nodes().where(...).compute(...).order_by(...).limit(...).execute(net) ============================================================ +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Query executed successfully, got 4 results ============================================================ Demo 2: Enhanced QueryResult Introspection ============================================================ +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds QueryResult representation shows full context: QueryResult( @@ -156,15 +176,45 @@ Known measures: betweenness, betweenness_centrality, closeness, closeness_centra Note: Error message includes suggestions and examples 2. Unknown layer error: +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds ============================================================ Demo 4: Performance and Semantic Warnings ============================================================ 1. Multilayer context (may show warnings): +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Computed degree for 8 node replicas 2. Suppress warnings: +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Query executed without warnings: 8 results ============================================================ @@ -172,11 +222,31 @@ Demo 5: Multilayer Semantics Awareness ============================================================ 1. Understanding node replicas: +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Total node replicas: 8 Physical nodes: 4 Physical node names: ['Alice', 'Bob', 'Charlie', 'David'] 2. Per-layer operations: +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Computed per-layer metrics Grouping mode: unknown diff --git a/examples/docs_outputs/manifest.json b/examples/docs_outputs/manifest.json index ca2860186..79dea2c07 100644 --- a/examples/docs_outputs/manifest.json +++ b/examples/docs_outputs/manifest.json @@ -1,7 +1,7 @@ { "examples": { "01_basic_query": { - "has_stderr": false, + "has_stderr": true, "output_file": "01_basic_query.txt", "success": true }, @@ -11,7 +11,7 @@ "success": true }, "dsl_patterns_quick_reference": { - "has_stderr": false, + "has_stderr": true, "output_file": "dsl_patterns_quick_reference.txt", "success": true }, @@ -21,7 +21,7 @@ "success": true }, "example_ergonomics_demo": { - "has_stderr": false, + "has_stderr": true, "output_file": "example_ergonomics_demo.txt", "success": true }, @@ -79,7 +79,7 @@ "success": true }, "user_journey_simulation": { - "has_stderr": false, + "has_stderr": true, "output_file": "user_journey_simulation.txt", "success": true } diff --git a/examples/docs_outputs/quick_start_ergonomics.err b/examples/docs_outputs/quick_start_ergonomics.err index 276f21844..102d2eb19 100644 --- a/examples/docs_outputs/quick_start_ergonomics.err +++ b/examples/docs_outputs/quick_start_ergonomics.err @@ -1,2 +1,3 @@ - py3plex.core.converters - INFO - Finished with layout.. - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. diff --git a/examples/docs_outputs/quick_start_ergonomics.txt b/examples/docs_outputs/quick_start_ergonomics.txt index 0d463eecb..bb3595069 100644 --- a/examples/docs_outputs/quick_start_ergonomics.txt +++ b/examples/docs_outputs/quick_start_ergonomics.txt @@ -50,6 +50,16 @@ Repulsion forces took 0.00 seconds Gravitational forces took 0.00 seconds Attraction forces took 0.00 seconds AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Analysis complete: 5 nodes analyzed Network: 8 nodes, 5 edges, 3 layers diff --git a/examples/docs_outputs/user_journey_simulation.err b/examples/docs_outputs/user_journey_simulation.err new file mode 100644 index 000000000..930f57696 --- /dev/null +++ b/examples/docs_outputs/user_journey_simulation.err @@ -0,0 +1,10 @@ + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. + - py3plex.core.converters - INFO - Finished with layout.. diff --git a/examples/docs_outputs/user_journey_simulation.txt b/examples/docs_outputs/user_journey_simulation.txt index 028a298a9..af7fdc44d 100644 --- a/examples/docs_outputs/user_journey_simulation.txt +++ b/examples/docs_outputs/user_journey_simulation.txt @@ -36,6 +36,16 @@ OK Added 5 edges >>> Step 4: Exploring the network structure ---------------------------------------------------------------------- Code: result = Q.nodes().execute(net) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Query completed: 8 node replicas found Physical nodes: 4 Ergonomic win: Clear distinction between replicas and physical nodes @@ -43,6 +53,16 @@ OK Query completed: 8 node replicas found >>> Step 5: Computing basic network metrics ---------------------------------------------------------------------- Code: result = Q.nodes().compute('degree').execute(net) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.00 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Computed degree for 8 nodes Top 5 by degree: @@ -83,6 +103,21 @@ OK Network loaded: >> Step 3: Analyzing each layer independently ---------------------------------------------------------------------- Code: Q.nodes().per_layer().compute('degree').top_k(3, 'degree') +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Found top hubs per layer Top nodes by layer: @@ -128,6 +178,21 @@ OK Found 10 communities ---------------------------------------------------------------------- Code: Q.nodes().per_layer().top_k(5, 'degree') .end_grouping().coverage(mode='at_least', k=2) +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.00 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Found 12 cross-layer hubs Nodes: ['Person_4', 'Person_5', 'Person_6', 'Person_7', 'Person_8'] Ergonomic win: Complex cross-layer analysis made simple @@ -153,20 +218,95 @@ OK Network created: >> Step 3: Building reusable, parameterized queries ---------------------------------------------------------------------- Code: Using threshold parameter for different analyses +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Threshold 2: 30 nodes with degree > 2 +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Threshold 3: 20 nodes with degree > 3 +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds Threshold 4: 5 nodes with degree > 4 Ergonomic win: Same query template, different parameters >>> Step 4: Exporting results in multiple formats ---------------------------------------------------------------------- Code: result.to_pandas(), result.to_networkx() +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.01 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took 0.01 seconds +Repulsion forces took 0.02 seconds +Gravitational forces took 0.00 seconds +Attraction forces took 0.00 seconds +AdjustSpeedAndApplyForces step took 0.00 seconds OK Pandas DataFrame: 45 rows, 4 columns OK NetworkX graph: 45 nodes, 0 edges Ergonomic win: Multiple export formats for different use cases From d382c102fefdbd660522b56526e4b68a290a0c3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:00:19 +0000 Subject: [PATCH 4/4] stabilize docs output generation for CI divergence Agent-Logs-Url: https://github.com/SkBlaz/py3plex/sessions/417604dc-7c0e-4e77-b692-b8c28f48bd51 Co-authored-by: SkBlaz <10035780+SkBlaz@users.noreply.github.com> --- examples/docs_outputs/01_basic_query.txt | 60 ++-- .../dsl_patterns_quick_reference.txt | 270 ++++++++--------- .../docs_outputs/example_ergonomics_demo.txt | 140 ++++----- .../docs_outputs/quick_start_ergonomics.txt | 60 ++-- .../docs_outputs/user_journey_simulation.txt | 280 +++++++++--------- scripts/generate_docs_outputs.py | 11 + 6 files changed, 416 insertions(+), 405 deletions(-) diff --git a/examples/docs_outputs/01_basic_query.txt b/examples/docs_outputs/01_basic_query.txt index ff06d7a45..614310fe4 100644 --- a/examples/docs_outputs/01_basic_query.txt +++ b/examples/docs_outputs/01_basic_query.txt @@ -4,41 +4,41 @@ Added 5 edges Network Statistics: ======================================== -BarnesHut Approximation took 0.00 seconds -Repulsion forces took 0.00 seconds -Gravitational forces took 0.00 seconds -Attraction forces took 0.00 seconds -AdjustSpeedAndApplyForces step took 0.00 seconds -BarnesHut Approximation took 0.00 seconds -Repulsion forces took 0.00 seconds -Gravitational forces took 0.00 seconds -Attraction forces took 0.00 seconds -AdjustSpeedAndApplyForces step took 0.00 seconds +BarnesHut Approximation took