base-pmad-ae 0.1.0__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: base-pmad-ae
3
+ Version: 0.1.0
4
+ Summary: Base Action Engine for pMADs — health, metrics, and autoprompt infrastructure flows
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: langgraph>=0.2.60
7
+ Requires-Dist: langchain-core>=0.3.28
8
+ Requires-Dist: prometheus-client>=0.21.1
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "base-pmad-ae"
7
+ version = "0.1.0"
8
+ description = "Base Action Engine for pMADs — health, metrics, and autoprompt infrastructure flows"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "langgraph>=0.2.60",
12
+ "langchain-core>=0.3.28",
13
+ "prometheus-client>=0.21.1",
14
+ ]
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Context Broker AE — infrastructure StateGraph package."""
@@ -0,0 +1,96 @@
1
+ """
2
+ Autoprompter dispatcher — StateGraph flow for Dkron callbacks.
3
+
4
+ When Dkron fires a job, it sends an HTTP POST to the langgraph container.
5
+ This flow reads the referenced runbook file and POSTs its contents as a
6
+ prompt to the Imperator's /v1/chat/completions endpoint.
7
+
8
+ The dispatcher has zero intelligence — it only reads and delivers.
9
+ """
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ import httpx
16
+ from langgraph.graph import END, StateGraph
17
+ from typing_extensions import TypedDict
18
+
19
+ _log = logging.getLogger("pmad_template.flows.autoprompt_dispatcher")
20
+
21
+ RUNBOOK_DIR = Path("/config/runbooks")
22
+
23
+
24
+ class DispatcherState(TypedDict):
25
+ """State for the autoprompter dispatcher flow."""
26
+
27
+ job_name: str
28
+ runbook_path: str
29
+ target_url: str
30
+ runbook_content: Optional[str]
31
+ delivery_status: Optional[str]
32
+ error: Optional[str]
33
+
34
+
35
+ async def load_runbook(state: DispatcherState) -> dict:
36
+ """Read the runbook file from disk."""
37
+ runbook_path = state.get("runbook_path", "")
38
+ if not runbook_path:
39
+ return {"error": "No runbook_path specified"}
40
+
41
+ full_path = RUNBOOK_DIR / runbook_path
42
+ try:
43
+ content = full_path.read_text(encoding="utf-8").strip()
44
+ if not content:
45
+ return {"error": f"Runbook is empty: {runbook_path}"}
46
+ _log.info("Loaded runbook: %s (%d chars)", runbook_path, len(content))
47
+ return {"runbook_content": content}
48
+ except (FileNotFoundError, OSError) as exc:
49
+ _log.error("Failed to load runbook %s: %s", runbook_path, exc)
50
+ return {"error": f"Failed to load runbook: {exc}"}
51
+
52
+
53
+ async def deliver_prompt(state: DispatcherState) -> dict:
54
+ """POST the runbook content to the Imperator's chat endpoint."""
55
+ if state.get("error"):
56
+ return {}
57
+
58
+ content = state.get("runbook_content", "")
59
+ target_url = state.get("target_url", "http://pmad-template-langgraph:8000/v1/chat/completions")
60
+
61
+ payload = {
62
+ "model": "pmad-template",
63
+ "messages": [
64
+ {"role": "user", "content": content},
65
+ ],
66
+ "stream": False,
67
+ }
68
+
69
+ try:
70
+ async with httpx.AsyncClient(timeout=120.0) as client:
71
+ response = await client.post(target_url, json=payload)
72
+ response.raise_for_status()
73
+ _log.info(
74
+ "Autoprompter delivered job '%s' to %s",
75
+ state.get("job_name", "unknown"),
76
+ target_url,
77
+ )
78
+ return {"delivery_status": "delivered"}
79
+ except (httpx.HTTPError, OSError) as exc:
80
+ _log.error(
81
+ "Autoprompter delivery failed for job '%s': %s",
82
+ state.get("job_name", "unknown"),
83
+ exc,
84
+ )
85
+ return {"delivery_status": "failed", "error": str(exc)}
86
+
87
+
88
+ def build_autoprompt_dispatcher_flow() -> StateGraph:
89
+ """Build and compile the autoprompter dispatcher StateGraph."""
90
+ workflow = StateGraph(DispatcherState)
91
+ workflow.add_node("load_runbook", load_runbook)
92
+ workflow.add_node("deliver_prompt", deliver_prompt)
93
+ workflow.set_entry_point("load_runbook")
94
+ workflow.add_edge("load_runbook", "deliver_prompt")
95
+ workflow.add_edge("deliver_prompt", END)
96
+ return workflow.compile()
@@ -0,0 +1,71 @@
1
+ """
2
+ Health Check — LangGraph StateGraph flow.
3
+
4
+ Checks connectivity to backing services (PostgreSQL)
5
+ and returns aggregated health status. Invoked by the /health route.
6
+ """
7
+
8
+ import logging
9
+ from typing import Optional
10
+
11
+ from langgraph.graph import END, StateGraph
12
+ from typing_extensions import TypedDict
13
+
14
+ _log = logging.getLogger("pmad_template.flows.health")
15
+
16
+
17
+ class HealthCheckState(TypedDict):
18
+ """State for the health check flow."""
19
+
20
+ config: dict
21
+
22
+ postgres_ok: bool
23
+ all_healthy: bool
24
+ status_detail: Optional[dict]
25
+ http_status: int
26
+
27
+
28
+ async def check_dependencies(state: HealthCheckState) -> dict:
29
+ """Check connectivity to all backing services."""
30
+ from app.database import get_pg_pool
31
+
32
+ postgres_ok = False
33
+ try:
34
+ pool = get_pg_pool()
35
+ await pool.fetchval("SELECT 1")
36
+ postgres_ok = True
37
+ except (RuntimeError, OSError, Exception) as exc:
38
+ _log.warning("PostgreSQL health check failed: %s", exc)
39
+
40
+ all_healthy = postgres_ok
41
+
42
+ if not all_healthy:
43
+ status_label = "unhealthy"
44
+ http_status = 503
45
+ else:
46
+ status_label = "healthy"
47
+ http_status = 200
48
+
49
+ status_detail = {
50
+ "status": status_label,
51
+ "database": "ok" if postgres_ok else "error",
52
+ }
53
+
54
+ if not all_healthy:
55
+ _log.warning("Health check: unhealthy — %s", status_detail)
56
+
57
+ return {
58
+ "postgres_ok": postgres_ok,
59
+ "all_healthy": all_healthy,
60
+ "status_detail": status_detail,
61
+ "http_status": http_status,
62
+ }
63
+
64
+
65
+ def build_health_check_flow() -> StateGraph:
66
+ """Build and compile the health check StateGraph."""
67
+ workflow = StateGraph(HealthCheckState)
68
+ workflow.add_node("check_dependencies", check_dependencies)
69
+ workflow.set_entry_point("check_dependencies")
70
+ workflow.add_edge("check_dependencies", END)
71
+ return workflow.compile()
@@ -0,0 +1,46 @@
1
+ """
2
+ Metrics collection StateGraph flow.
3
+
4
+ Collects Prometheus metrics inside a StateGraph node,
5
+ as required by REQ §4.8.
6
+ """
7
+
8
+ import logging
9
+ from typing import Optional
10
+
11
+ from langgraph.graph import END, StateGraph
12
+ from prometheus_client import generate_latest, REGISTRY
13
+ from typing_extensions import TypedDict
14
+
15
+ _log = logging.getLogger("pmad_template.flows.metrics")
16
+
17
+
18
+ class MetricsState(TypedDict):
19
+ """State for the metrics collection flow."""
20
+
21
+ action: str
22
+ metrics_output: str
23
+ error: Optional[str]
24
+
25
+
26
+ async def collect_metrics_node(state: MetricsState) -> dict:
27
+ """Collect Prometheus metrics from the registry.
28
+
29
+ Produces metrics output inside the StateGraph as required by REQ §4.8.
30
+ """
31
+ try:
32
+ metrics_bytes = generate_latest(REGISTRY)
33
+ metrics_text = metrics_bytes.decode("utf-8", errors="replace")
34
+ return {"metrics_output": metrics_text, "error": None}
35
+ except (ValueError, OSError) as exc:
36
+ _log.error("Failed to collect metrics: %s", exc)
37
+ return {"metrics_output": "", "error": str(exc)}
38
+
39
+
40
+ def build_metrics_flow() -> StateGraph:
41
+ """Build and compile the metrics collection StateGraph."""
42
+ workflow = StateGraph(MetricsState)
43
+ workflow.add_node("collect_metrics", collect_metrics_node)
44
+ workflow.set_entry_point("collect_metrics")
45
+ workflow.add_edge("collect_metrics", END)
46
+ return workflow.compile()
@@ -0,0 +1,30 @@
1
+ """
2
+ AE — Package registration entry point.
3
+
4
+ Called by the bootstrap kernel's stategraph_registry.scan() when this
5
+ package is discovered via entry_points(group="pmad_template.ae").
6
+
7
+ Returns an AERegistration dict with build type registrations and
8
+ flow builders that the kernel processes to populate its registries.
9
+ """
10
+
11
+
12
+ def register() -> dict:
13
+ """Register the AE's infrastructure StateGraphs.
14
+
15
+ Returns a dict with:
16
+ - build_types: dict of (assembly_builder, retrieval_builder) pairs
17
+ - flows: dict of flow_name -> builder callable
18
+ """
19
+ from base_pmad_ae.health_flow import build_health_check_flow
20
+ from base_pmad_ae.metrics_flow import build_metrics_flow
21
+ from base_pmad_ae.autoprompt_dispatcher import build_autoprompt_dispatcher_flow
22
+
23
+ return {
24
+ "build_types": {},
25
+ "flows": {
26
+ "health_check": build_health_check_flow,
27
+ "metrics": build_metrics_flow,
28
+ "autoprompt_dispatcher": build_autoprompt_dispatcher_flow,
29
+ },
30
+ }
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: base-pmad-ae
3
+ Version: 0.1.0
4
+ Summary: Base Action Engine for pMADs — health, metrics, and autoprompt infrastructure flows
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: langgraph>=0.2.60
7
+ Requires-Dist: langchain-core>=0.3.28
8
+ Requires-Dist: prometheus-client>=0.21.1
@@ -0,0 +1,11 @@
1
+ pyproject.toml
2
+ src/base_pmad_ae/__init__.py
3
+ src/base_pmad_ae/autoprompt_dispatcher.py
4
+ src/base_pmad_ae/health_flow.py
5
+ src/base_pmad_ae/metrics_flow.py
6
+ src/base_pmad_ae/register.py
7
+ src/base_pmad_ae.egg-info/PKG-INFO
8
+ src/base_pmad_ae.egg-info/SOURCES.txt
9
+ src/base_pmad_ae.egg-info/dependency_links.txt
10
+ src/base_pmad_ae.egg-info/requires.txt
11
+ src/base_pmad_ae.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ langgraph>=0.2.60
2
+ langchain-core>=0.3.28
3
+ prometheus-client>=0.21.1
@@ -0,0 +1 @@
1
+ base_pmad_ae