deploycraft-ai / rules /deployment_rules.py
Upshivam
Initial release of AI MLOps Architecture Designer
4b9217e
Raw
History Blame Contribute Delete
4.72 kB
"""
rules/deployment_rules.py
--------------------------
Decides *which* services are active and how they relate to each other,
based on a user's InfraConfig. This module contains no rendering logic
(no Jinja2, no YAML, no Mermaid syntax) — it only answers questions like
"what does the request path look like?" and "what should the security
report flag?". Generators consume these decisions and render them into
their respective formats.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal
from utils.config import InfraConfig
NodeKind = Literal["entry", "network", "compute", "cache", "database", "vector", "monitoring", "cicd"]
@dataclass(frozen=True)
class ServiceNode:
"""A single box in the architecture graph."""
id: str
label: str
kind: NodeKind
@dataclass(frozen=True)
class ServiceEdge:
"""A connection between two ServiceNodes."""
source: str
target: str
style: Literal["solid", "dashed"] = "solid"
label: str = ""
@dataclass(frozen=True)
class ArchitectureGraph:
nodes: list[ServiceNode] = field(default_factory=list)
edges: list[ServiceEdge] = field(default_factory=list)
def build_architecture_graph(config: InfraConfig) -> ArchitectureGraph:
"""
Build the full architecture graph (nodes + edges) for a given config.
Main request path (always present, top to bottom):
Client -> Load Balancer -> Deployment Target -> App Framework
Then, only if the user selected them (i.e. not "None"), each data
service connects DIRECTLY from App Framework — as independent,
parallel dependencies, not a chain:
App Framework -> Cache
App Framework -> Database
App Framework -> Vector Database
(A prior version chained these off each other — App -> Cache ->
Database -> Vector Database — which is topologically wrong: it
implies, for example, that the vector database sits between the app
and the relational database. Since this graph feeds both the Mermaid
diagram and the ArchitectureAdvisor's prompt, that wrong shape was
visible in the diagram and could mislead the AI's reasoning about the
actual request path.)
Side branches (dashed, since they're not on the live request path):
App Framework -> Monitoring
CI/CD -> Deployment Target (deploy pipeline, not user traffic)
"""
nodes: list[ServiceNode] = [
ServiceNode("client", "Client", "entry"),
ServiceNode("lb", "Load Balancer", "network"),
ServiceNode("deploy_target", config.deployment_target, "compute"),
ServiceNode("app", config.app_framework, "compute"),
]
edges: list[ServiceEdge] = [
ServiceEdge("client", "lb"),
ServiceEdge("lb", "deploy_target"),
ServiceEdge("deploy_target", "app"),
]
# Data-layer dependencies — each is an independent, parallel child of
# "app", only wired in if the user actually chose one. Order in this
# tuple only affects node/edge list ordering (cosmetic), not topology.
for field_value, node_id, kind in (
(config.cache, "cache", "cache"),
(config.database, "database", "database"),
(config.vector_db, "vector_db", "vector"),
):
if field_value and field_value != "None":
nodes.append(ServiceNode(node_id, field_value, kind))
edges.append(ServiceEdge("app", node_id))
# Side branches
if config.monitoring and config.monitoring != "None":
nodes.append(ServiceNode("monitoring", config.monitoring, "monitoring"))
edges.append(ServiceEdge("app", "monitoring", style="dashed", label="metrics"))
if config.cicd and config.cicd != "None":
nodes.append(ServiceNode("cicd", config.cicd, "cicd"))
edges.append(ServiceEdge("cicd", "deploy_target", style="dashed", label="deploy"))
return ArchitectureGraph(nodes=nodes, edges=edges)
def active_data_services(config: InfraConfig) -> list[str]:
"""
Convenience helper for generators that just need the list of active
data-layer services (used by docker_generator / kubernetes_generator
in later steps to decide which containers/manifests to emit).
"""
services = []
if config.cache and config.cache != "None":
services.append(config.cache)
if config.database and config.database != "None":
services.append(config.database)
if config.vector_db and config.vector_db != "None":
services.append(config.vector_db)
return services
def high_availability_requested(config: InfraConfig) -> bool:
"""Single source of truth for HA branching across generators."""
return config.high_availability