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.
- base_pmad_ae-0.1.0/PKG-INFO +8 -0
- base_pmad_ae-0.1.0/pyproject.toml +17 -0
- base_pmad_ae-0.1.0/setup.cfg +4 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae/__init__.py +1 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae/autoprompt_dispatcher.py +96 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae/health_flow.py +71 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae/metrics_flow.py +46 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae/register.py +30 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae.egg-info/PKG-INFO +8 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae.egg-info/SOURCES.txt +11 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae.egg-info/dependency_links.txt +1 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae.egg-info/requires.txt +3 -0
- base_pmad_ae-0.1.0/src/base_pmad_ae.egg-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
base_pmad_ae
|