frontend-perception-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. frontend_perception_engine-0.1.0.dist-info/METADATA +235 -0
  2. frontend_perception_engine-0.1.0.dist-info/RECORD +50 -0
  3. frontend_perception_engine-0.1.0.dist-info/WHEEL +5 -0
  4. frontend_perception_engine-0.1.0.dist-info/entry_points.txt +2 -0
  5. frontend_perception_engine-0.1.0.dist-info/top_level.txt +1 -0
  6. navigation/__init__.py +1 -0
  7. navigation/browser_use/__init__.py +17 -0
  8. navigation/browser_use/agent_runner.py +165 -0
  9. navigation/browser_use/hints.py +155 -0
  10. navigation/browser_use/integration.py +46 -0
  11. navigation/browser_use/llm.py +70 -0
  12. navigation/codeGraph/__init__.py +4 -0
  13. navigation/codeGraph/crg_impl.py +170 -0
  14. navigation/codeGraph/factory.py +24 -0
  15. navigation/codeGraph/interface.py +78 -0
  16. navigation/codeGraph/null_impl.py +47 -0
  17. navigation/mcp/__init__.py +4 -0
  18. navigation/mcp/__main__.py +4 -0
  19. navigation/mcp/diff.py +28 -0
  20. navigation/mcp/envelope.py +50 -0
  21. navigation/mcp/handlers.py +791 -0
  22. navigation/mcp/instructions.py +36 -0
  23. navigation/mcp/resources.py +82 -0
  24. navigation/mcp/scan_registry.py +47 -0
  25. navigation/mcp/server.py +229 -0
  26. navigation/mcp/session_store.py +87 -0
  27. navigation/mcp/tools.py +332 -0
  28. navigation/perception/__init__.py +76 -0
  29. navigation/perception/artifacts.py +19 -0
  30. navigation/perception/auth_gate.py +57 -0
  31. navigation/perception/budget.py +55 -0
  32. navigation/perception/cdp_hub.py +122 -0
  33. navigation/perception/dev_insights.py +625 -0
  34. navigation/perception/exploration.py +99 -0
  35. navigation/perception/feature_flags.py +83 -0
  36. navigation/perception/file_upload.py +84 -0
  37. navigation/perception/flow_graph.py +121 -0
  38. navigation/perception/form_probe.py +148 -0
  39. navigation/perception/iframe_context.py +76 -0
  40. navigation/perception/observation.py +97 -0
  41. navigation/perception/preflight.py +91 -0
  42. navigation/perception/rich_editors.py +90 -0
  43. navigation/perception/route_guards.py +136 -0
  44. navigation/perception/runner.py +138 -0
  45. navigation/perception/scan.py +74 -0
  46. navigation/perception/scripted_actions.py +67 -0
  47. navigation/perception/state_manager.py +114 -0
  48. navigation/perception/verification.py +117 -0
  49. navigation/perception/virtual_scroll.py +84 -0
  50. navigation/perception/websocket_observer.py +79 -0
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+
7
+ def get_bedrock_config() -> dict[str, Any]:
8
+ return {
9
+ "model": os.getenv("BEDROCK_MODEL", "amazon.nova-pro-v1:0"),
10
+ "region": os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION", "us-east-1"),
11
+ "aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"),
12
+ "aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
13
+ "aws_session_token": os.getenv("AWS_SESSION_TOKEN"),
14
+ "aws_sso_auth": os.getenv("AWS_SSO_AUTH", "").lower() in {"1", "true", "yes"},
15
+ }
16
+
17
+
18
+ def create_bedrock_llm(
19
+ model: str | None = None,
20
+ region: str | None = None,
21
+ temperature: float = 0.2,
22
+ ):
23
+ """Create Browser Use ChatAWSBedrock for Nova / other Bedrock models."""
24
+ cfg = get_bedrock_config()
25
+ model = model or cfg["model"]
26
+ region = region or cfg["region"]
27
+
28
+ try:
29
+ from browser_use.llm import ChatAWSBedrock
30
+ except ImportError as exc:
31
+ raise RuntimeError("browser-use is not installed") from exc
32
+
33
+ kwargs: dict[str, Any] = {
34
+ "model": model,
35
+ "aws_region": region,
36
+ "temperature": temperature,
37
+ "aws_sso_auth": cfg["aws_sso_auth"],
38
+ }
39
+ if cfg["aws_access_key_id"] and cfg["aws_secret_access_key"]:
40
+ kwargs["aws_access_key_id"] = cfg["aws_access_key_id"]
41
+ kwargs["aws_secret_access_key"] = cfg["aws_secret_access_key"]
42
+ if cfg["aws_session_token"]:
43
+ kwargs["aws_session_token"] = cfg["aws_session_token"]
44
+ else:
45
+ # Fall back to boto3 default credential chain (~/.aws/credentials, SSO, etc.)
46
+ try:
47
+ import boto3
48
+
49
+ session = boto3.Session(region_name=region)
50
+ if session.get_credentials() is not None:
51
+ kwargs["session"] = session
52
+ kwargs["aws_sso_auth"] = True
53
+ except ImportError:
54
+ pass
55
+
56
+ return ChatAWSBedrock(**kwargs)
57
+
58
+
59
+ def credentials_available() -> bool:
60
+ cfg = get_bedrock_config()
61
+ if cfg["aws_sso_auth"]:
62
+ return True
63
+ if cfg["aws_access_key_id"] and cfg["aws_secret_access_key"]:
64
+ return True
65
+ try:
66
+ import boto3
67
+
68
+ return boto3.Session().get_credentials() is not None
69
+ except Exception:
70
+ return False
@@ -0,0 +1,4 @@
1
+ from .factory import create_code_graph
2
+ from .interface import GraphQueryResult, ICodeGraph
3
+
4
+ __all__ = ["ICodeGraph", "GraphQueryResult", "create_code_graph"]
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Callable
5
+
6
+ from .interface import GraphQueryResult, ICodeGraph
7
+
8
+ CRGError = Exception
9
+ _build_or_update_graph: Callable[..., dict[str, Any]] | None = None
10
+ _query_graph: Callable[..., dict[str, Any]] | None = None
11
+ _semantic_search_nodes: Callable[..., dict[str, Any]] | None = None
12
+ _get_impact_radius: Callable[..., dict[str, Any]] | None = None
13
+ _list_graph_stats: Callable[..., dict[str, Any]] | None = None
14
+ _traverse_graph_func: Callable[..., dict[str, Any]] | None = None
15
+
16
+ try:
17
+ from code_review_graph.tools.build import build_or_update_graph as _build_or_update_graph
18
+ from code_review_graph.tools.query import (
19
+ get_impact_radius as _get_impact_radius,
20
+ list_graph_stats as _list_graph_stats,
21
+ query_graph as _query_graph,
22
+ semantic_search_nodes as _semantic_search_nodes,
23
+ traverse_graph_func as _traverse_graph_func,
24
+ )
25
+ except Exception as exc: # pragma: no cover - defensive import
26
+ CRGError = type(exc)
27
+
28
+
29
+ class CRGCodeGraph(ICodeGraph):
30
+ """CRG-backed code graph adapter hidden behind ICodeGraph."""
31
+
32
+ def __init__(self, repo_root: str | Path) -> None:
33
+ self.repo_root = str(Path(repo_root))
34
+
35
+ def _ok(self, summary: str, payload: dict[str, Any]) -> GraphQueryResult:
36
+ return GraphQueryResult(ok=True, source="crg", summary=summary, payload=payload)
37
+
38
+ def _err(self, action: str, exc: Exception) -> GraphQueryResult:
39
+ return GraphQueryResult(
40
+ ok=False,
41
+ source="crg",
42
+ summary=f"CRG {action} failed.",
43
+ error=str(exc),
44
+ )
45
+
46
+ @staticmethod
47
+ def _ensure_available() -> None:
48
+ required = [
49
+ _build_or_update_graph,
50
+ _query_graph,
51
+ _semantic_search_nodes,
52
+ _get_impact_radius,
53
+ _list_graph_stats,
54
+ _traverse_graph_func,
55
+ ]
56
+ if any(fn is None for fn in required):
57
+ raise RuntimeError("code-review-graph is not available")
58
+
59
+ def initialize(self) -> GraphQueryResult:
60
+ try:
61
+ self._ensure_available()
62
+ payload = _build_or_update_graph(repo_root=self.repo_root, full_rebuild=False, postprocess="minimal")
63
+ return self._ok(payload.get("summary", "Graph initialized"), payload)
64
+ except Exception as exc:
65
+ return self._err("initialize", exc)
66
+
67
+ def refresh(self) -> GraphQueryResult:
68
+ try:
69
+ self._ensure_available()
70
+ payload = _build_or_update_graph(repo_root=self.repo_root, full_rebuild=False)
71
+ return self._ok(payload.get("summary", "Graph refreshed"), payload)
72
+ except Exception as exc:
73
+ return self._err("refresh", exc)
74
+
75
+ def rebuild(self) -> GraphQueryResult:
76
+ try:
77
+ self._ensure_available()
78
+ payload = _build_or_update_graph(repo_root=self.repo_root, full_rebuild=True)
79
+ return self._ok(payload.get("summary", "Graph rebuilt"), payload)
80
+ except Exception as exc:
81
+ return self._err("rebuild", exc)
82
+
83
+ def search(self, query: str, *, kind: str | None = None, limit: int = 20) -> GraphQueryResult:
84
+ try:
85
+ self._ensure_available()
86
+ payload = _semantic_search_nodes(query=query, kind=kind, limit=limit, repo_root=self.repo_root)
87
+ return self._ok(payload.get("summary", f"Search for {query}"), payload)
88
+ except Exception as exc:
89
+ return self._err("search", exc)
90
+
91
+ def shortest_path(self, query: str, *, depth: int = 3, mode: str = "bfs") -> GraphQueryResult:
92
+ try:
93
+ self._ensure_available()
94
+ payload = _traverse_graph_func(query=query, depth=depth, mode=mode, repo_root=self.repo_root)
95
+ return self._ok(payload.get("summary", f"Traversal for {query}"), payload)
96
+ except Exception as exc:
97
+ return self._err("shortest_path", exc)
98
+
99
+ def get_neighbors(self, target: str, *, relation: str = "callees_of") -> GraphQueryResult:
100
+ try:
101
+ self._ensure_available()
102
+ payload = _query_graph(pattern=relation, target=target, repo_root=self.repo_root)
103
+ return self._ok(payload.get("summary", f"{relation} for {target}"), payload)
104
+ except Exception as exc:
105
+ return self._err("get_neighbors", exc)
106
+
107
+ def get_component(self, name: str) -> GraphQueryResult:
108
+ return self.search(name, kind="Class", limit=10)
109
+
110
+ def get_file(self, file_path: str) -> GraphQueryResult:
111
+ try:
112
+ self._ensure_available()
113
+ payload = _query_graph(pattern="file_summary", target=file_path, repo_root=self.repo_root)
114
+ return self._ok(payload.get("summary", f"File summary for {file_path}"), payload)
115
+ except Exception as exc:
116
+ return self._err("get_file", exc)
117
+
118
+ def get_route(self, changed_files: list[str] | None = None, *, max_depth: int = 2) -> GraphQueryResult:
119
+ try:
120
+ self._ensure_available()
121
+ payload = _get_impact_radius(
122
+ changed_files=changed_files,
123
+ max_depth=max_depth,
124
+ repo_root=self.repo_root,
125
+ )
126
+ return self._ok(payload.get("summary", "Impact route"), payload)
127
+ except Exception as exc:
128
+ return self._err("get_route", exc)
129
+
130
+ def query(self, query_type: str, **kwargs: Any) -> GraphQueryResult:
131
+ # Public, graph-agnostic API contract for current + future methods.
132
+ if query_type == "search":
133
+ return self.search(kwargs["query"], kind=kwargs.get("kind"), limit=kwargs.get("limit", 20))
134
+ if query_type == "neighbors":
135
+ return self.get_neighbors(kwargs["target"], relation=kwargs.get("relation", "callees_of"))
136
+ if query_type == "component":
137
+ return self.get_component(kwargs["name"])
138
+ if query_type == "file":
139
+ return self.get_file(kwargs["file_path"])
140
+ if query_type == "route":
141
+ return self.get_route(kwargs.get("changed_files"), max_depth=kwargs.get("max_depth", 2))
142
+ if query_type == "shortest_path":
143
+ return self.shortest_path(kwargs["query"], depth=kwargs.get("depth", 3), mode=kwargs.get("mode", "bfs"))
144
+ if query_type == "stats":
145
+ try:
146
+ self._ensure_available()
147
+ payload = _list_graph_stats(repo_root=self.repo_root)
148
+ return self._ok(payload.get("summary", "Graph stats"), payload)
149
+ except Exception as exc:
150
+ return self._err("stats", exc)
151
+
152
+ # Future API shape examples (defer to search/traversal today):
153
+ if query_type in {
154
+ "find_navigation_hint",
155
+ "find_relevant_components",
156
+ "find_likely_route",
157
+ "find_related_files",
158
+ "find_button_candidates",
159
+ "find_component_hierarchy",
160
+ "find_entry_point",
161
+ }:
162
+ prompt = " ".join(str(v) for v in kwargs.values())
163
+ return self.search(prompt, limit=10)
164
+
165
+ return GraphQueryResult(
166
+ ok=False,
167
+ source="crg",
168
+ summary=f"Unknown query type: {query_type}",
169
+ error="unknown_query_type",
170
+ )
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .crg_impl import CRGCodeGraph
6
+ from .interface import ICodeGraph
7
+ from .null_impl import NullCodeGraph
8
+
9
+
10
+ def create_code_graph(repo_root: str | Path, enabled: bool = True) -> ICodeGraph:
11
+ if not enabled:
12
+ return NullCodeGraph()
13
+ graph = CRGCodeGraph(repo_root=repo_root)
14
+ init = graph.initialize()
15
+ if not init.ok:
16
+ return NullCodeGraph()
17
+ # First-time sandbox: incremental init may succeed with 0 nodes — force full build.
18
+ stats = graph.query("stats")
19
+ node_count = (stats.payload or {}).get("node_count", 0)
20
+ if node_count == 0:
21
+ rebuild = graph.rebuild()
22
+ if not rebuild.ok:
23
+ return NullCodeGraph()
24
+ return graph
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class GraphQueryResult:
10
+ ok: bool
11
+ source: str
12
+ summary: str
13
+ payload: dict[str, Any] = field(default_factory=dict)
14
+ error: str | None = None
15
+
16
+
17
+ class ICodeGraph(ABC):
18
+ @abstractmethod
19
+ def initialize(self) -> GraphQueryResult:
20
+ raise NotImplementedError
21
+
22
+ @abstractmethod
23
+ def refresh(self) -> GraphQueryResult:
24
+ raise NotImplementedError
25
+
26
+ @abstractmethod
27
+ def rebuild(self) -> GraphQueryResult:
28
+ raise NotImplementedError
29
+
30
+ @abstractmethod
31
+ def search(self, query: str, *, kind: str | None = None, limit: int = 20) -> GraphQueryResult:
32
+ raise NotImplementedError
33
+
34
+ @abstractmethod
35
+ def shortest_path(self, query: str, *, depth: int = 3, mode: str = "bfs") -> GraphQueryResult:
36
+ raise NotImplementedError
37
+
38
+ @abstractmethod
39
+ def get_neighbors(self, target: str, *, relation: str = "callees_of") -> GraphQueryResult:
40
+ raise NotImplementedError
41
+
42
+ @abstractmethod
43
+ def get_component(self, name: str) -> GraphQueryResult:
44
+ raise NotImplementedError
45
+
46
+ @abstractmethod
47
+ def get_file(self, file_path: str) -> GraphQueryResult:
48
+ raise NotImplementedError
49
+
50
+ @abstractmethod
51
+ def get_route(self, changed_files: list[str] | None = None, *, max_depth: int = 2) -> GraphQueryResult:
52
+ raise NotImplementedError
53
+
54
+ @abstractmethod
55
+ def query(self, query_type: str, **kwargs: Any) -> GraphQueryResult:
56
+ raise NotImplementedError
57
+
58
+ # Future-facing methods (designed now, can remain no-op in implementations)
59
+ def find_navigation_hint(self, goal: str, **kwargs: Any) -> GraphQueryResult:
60
+ return self.query("find_navigation_hint", goal=goal, **kwargs)
61
+
62
+ def find_relevant_components(self, intent: str, **kwargs: Any) -> GraphQueryResult:
63
+ return self.query("find_relevant_components", intent=intent, **kwargs)
64
+
65
+ def find_likely_route(self, intent: str, **kwargs: Any) -> GraphQueryResult:
66
+ return self.query("find_likely_route", intent=intent, **kwargs)
67
+
68
+ def find_related_files(self, symbol: str, **kwargs: Any) -> GraphQueryResult:
69
+ return self.query("find_related_files", symbol=symbol, **kwargs)
70
+
71
+ def find_button_candidates(self, label_hint: str, **kwargs: Any) -> GraphQueryResult:
72
+ return self.query("find_button_candidates", label_hint=label_hint, **kwargs)
73
+
74
+ def find_component_hierarchy(self, component: str, **kwargs: Any) -> GraphQueryResult:
75
+ return self.query("find_component_hierarchy", component=component, **kwargs)
76
+
77
+ def find_entry_point(self, feature: str, **kwargs: Any) -> GraphQueryResult:
78
+ return self.query("find_entry_point", feature=feature, **kwargs)
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .interface import GraphQueryResult, ICodeGraph
6
+
7
+
8
+ class NullCodeGraph(ICodeGraph):
9
+ """Safe fallback when CRG is unavailable."""
10
+
11
+ def _disabled(self, action: str) -> GraphQueryResult:
12
+ return GraphQueryResult(
13
+ ok=False,
14
+ source="null",
15
+ summary=f"Code graph unavailable: {action} skipped.",
16
+ error="code_graph_unavailable",
17
+ )
18
+
19
+ def initialize(self) -> GraphQueryResult:
20
+ return self._disabled("initialize")
21
+
22
+ def refresh(self) -> GraphQueryResult:
23
+ return self._disabled("refresh")
24
+
25
+ def rebuild(self) -> GraphQueryResult:
26
+ return self._disabled("rebuild")
27
+
28
+ def search(self, query: str, *, kind: str | None = None, limit: int = 20) -> GraphQueryResult:
29
+ return self._disabled("search")
30
+
31
+ def shortest_path(self, query: str, *, depth: int = 3, mode: str = "bfs") -> GraphQueryResult:
32
+ return self._disabled("shortest_path")
33
+
34
+ def get_neighbors(self, target: str, *, relation: str = "callees_of") -> GraphQueryResult:
35
+ return self._disabled("get_neighbors")
36
+
37
+ def get_component(self, name: str) -> GraphQueryResult:
38
+ return self._disabled("get_component")
39
+
40
+ def get_file(self, file_path: str) -> GraphQueryResult:
41
+ return self._disabled("get_file")
42
+
43
+ def get_route(self, changed_files: list[str] | None = None, *, max_depth: int = 2) -> GraphQueryResult:
44
+ return self._disabled("get_route")
45
+
46
+ def query(self, query_type: str, **kwargs: Any) -> GraphQueryResult:
47
+ return self._disabled(query_type)
@@ -0,0 +1,4 @@
1
+ """Frontend Perception MCP package."""
2
+ from .server import PerceptionMCPServer, main
3
+
4
+ __all__ = ["PerceptionMCPServer", "main"]
@@ -0,0 +1,4 @@
1
+ from navigation.mcp.server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
navigation/mcp/diff.py ADDED
@@ -0,0 +1,28 @@
1
+ """Compare two observation snapshots by scan_id."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ def _blocking_issues(obs: dict[str, Any]) -> list[str]:
8
+ di = obs.get("dev_insights") or {}
9
+ summary = di.get("summary") or {}
10
+ return list(summary.get("blocking_issues") or [])
11
+
12
+
13
+ def diff_observations(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
14
+ b_block = set(_blocking_issues(before))
15
+ a_block = set(_blocking_issues(after))
16
+ b_dom = str(before.get("dom_text") or "")
17
+ a_dom = str(after.get("dom_text") or "")
18
+ excerpt_len = 500
19
+ return {
20
+ "url_before": before.get("url") or "",
21
+ "url_after": after.get("url") or "",
22
+ "url_changed": (before.get("url") or "") != (after.get("url") or ""),
23
+ "dom_text_changed": b_dom != a_dom,
24
+ "new_blocking_issues": sorted(a_block - b_block),
25
+ "removed_blocking_issues": sorted(b_block - a_block),
26
+ "dom_excerpt_before": b_dom[:excerpt_len],
27
+ "dom_excerpt_after": a_dom[:excerpt_len],
28
+ }
@@ -0,0 +1,50 @@
1
+ """Standard MCP tool response envelope (contract v1.0)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ CONTRACT_VERSION = "1.0"
8
+
9
+
10
+ def make_envelope(
11
+ tool: str,
12
+ *,
13
+ ok: bool = True,
14
+ session_id: str | None = None,
15
+ run_id: str | None = None,
16
+ scan_id: str | None = None,
17
+ url: str = "",
18
+ error: str | None = None,
19
+ degraded: list[str] | None = None,
20
+ data: dict[str, Any] | None = None,
21
+ ) -> dict[str, Any]:
22
+ return {
23
+ "contract_version": CONTRACT_VERSION,
24
+ "tool": tool,
25
+ "ok": ok,
26
+ "session_id": session_id,
27
+ "run_id": run_id,
28
+ "scan_id": scan_id,
29
+ "url": url,
30
+ "error": error,
31
+ "degraded": list(degraded or []),
32
+ "data": data or {},
33
+ }
34
+
35
+
36
+ def envelope_json(**kwargs: Any) -> str:
37
+ return json.dumps(make_envelope(**kwargs), indent=2, default=str)
38
+
39
+
40
+ def agent_summary_from_observation(obs_dict: dict[str, Any]) -> dict[str, Any]:
41
+ """Compact summary for host agent reasoning (no planning hints)."""
42
+ di = obs_dict.get("dev_insights") or {}
43
+ summary = di.get("summary") or {}
44
+ page_meta = di.get("page_meta")
45
+ return {
46
+ "blocking": summary.get("blocking_issues") or [],
47
+ "advisory": summary.get("advisory_issues") or [],
48
+ "page_meta": page_meta,
49
+ "degraded": list(obs_dict.get("degraded") or []) + list(di.get("degraded") or []),
50
+ }